controller_widget_builder.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import 'dart:async';
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
  5. import 'package:flutter_ijkplayer/src/helper/time_helper.dart';
  6. import 'package:flutter_ijkplayer/src/logutil.dart';
  7. import 'package:flutter_ijkplayer/src/widget/progress_bar.dart';
  8. /// Using mediaController to Construct a Controller UI
  9. typedef Widget ControllerWidgetBuilder(IjkMediaController controller);
  10. /// default create IJK Controller UI
  11. Widget defaultBuildIjkControllerWidget(IjkMediaController controller) {
  12. return DefaultControllerWidget(
  13. controller: controller,
  14. // verticalGesture: false,
  15. // horizontalGesture: false,
  16. );
  17. }
  18. /// Default Controller Widget
  19. ///
  20. /// see [IjkPlayer] and [ControllerWidgetBuilder]
  21. class DefaultControllerWidget extends StatefulWidget {
  22. final IjkMediaController controller;
  23. /// If [doubleTapPlay] is true, can double tap to play or pause media.
  24. final bool doubleTapPlay;
  25. /// If [verticalGesture] is false, vertical gesture will be ignored.
  26. final bool verticalGesture;
  27. /// If [horizontalGesture] is false, horizontal gesture will be ignored.
  28. final bool horizontalGesture;
  29. /// Controlling [verticalGesture] is controlling system volume or media volume.
  30. final VolumeType volumeType;
  31. /// The UI of the controller.
  32. const DefaultControllerWidget({
  33. @required this.controller,
  34. this.doubleTapPlay = false,
  35. this.verticalGesture = true,
  36. this.horizontalGesture = true,
  37. this.volumeType = VolumeType.system,
  38. });
  39. @override
  40. _DefaultControllerWidgetState createState() =>
  41. _DefaultControllerWidgetState();
  42. }
  43. class _DefaultControllerWidgetState extends State<DefaultControllerWidget>
  44. implements TooltipDelegate {
  45. IjkMediaController get controller => widget.controller;
  46. bool _isShow = true;
  47. set isShow(bool value) {
  48. _isShow = value;
  49. setState(() {});
  50. if (value == true) {
  51. controller.refreshVideoInfo();
  52. }
  53. }
  54. bool get isShow => _isShow;
  55. Timer progressTimer;
  56. StreamSubscription controllerSubscription;
  57. @override
  58. void initState() {
  59. super.initState();
  60. startTimer();
  61. controllerSubscription = controller.textureIdStream.listen(_onTextIdChange);
  62. }
  63. void _onTextIdChange(int textId) {
  64. LogUtils.log("onTextChange");
  65. if (textId != null) {
  66. startTimer();
  67. } else {
  68. stopTimer();
  69. }
  70. }
  71. @override
  72. void deactivate() {
  73. super.deactivate();
  74. }
  75. @override
  76. void dispose() {
  77. controllerSubscription.cancel();
  78. stopTimer();
  79. super.dispose();
  80. }
  81. void startTimer() {
  82. if (controller.textureId == null) {
  83. return;
  84. }
  85. progressTimer?.cancel();
  86. progressTimer = Timer.periodic(Duration(milliseconds: 400), (timer) {
  87. LogUtils.log("will refresh info");
  88. controller.refreshVideoInfo();
  89. });
  90. }
  91. void stopTimer() {
  92. progressTimer?.cancel();
  93. }
  94. @override
  95. Widget build(BuildContext context) {
  96. return GestureDetector(
  97. behavior: HitTestBehavior.opaque,
  98. child: buildContent(),
  99. onDoubleTap: onDoubleTap(),
  100. onHorizontalDragStart: wrapHorizontalGesture(_onHorizontalDragStart),
  101. onHorizontalDragUpdate: wrapHorizontalGesture(_onHorizontalDragUpdate),
  102. onHorizontalDragEnd: wrapHorizontalGesture(_onHorizontalDragEnd),
  103. onVerticalDragStart: wrapVerticalGesture(_onVerticalDragStart),
  104. onVerticalDragUpdate: wrapVerticalGesture(_onVerticalDragUpdate),
  105. onVerticalDragEnd: wrapVerticalGesture(_onVerticalDragEnd),
  106. onTap: onTap,
  107. );
  108. }
  109. Widget buildContent() {
  110. if (!isShow) {
  111. return Container();
  112. }
  113. return StreamBuilder<VideoInfo>(
  114. stream: controller.videoInfoStream,
  115. builder: (context, snapshot) {
  116. var info = snapshot.data;
  117. if (info == null || !info.hasData) {
  118. return Container();
  119. }
  120. return buildPortrait(info);
  121. },
  122. );
  123. }
  124. Widget buildPortrait(VideoInfo info) {
  125. return PortraitController(
  126. controller: controller,
  127. info: info,
  128. tooltipDelegate: this,
  129. );
  130. }
  131. OverlayEntry _tipOverlay;
  132. Widget createTooltipWidgetWrapper(Widget widget) {
  133. var typography = Typography(platform: TargetPlatform.android);
  134. var theme = typography.white;
  135. const style = const TextStyle(
  136. fontSize: 15.0,
  137. color: Colors.white,
  138. fontWeight: FontWeight.normal,
  139. );
  140. var mergedTextStyle = theme.body2.merge(style);
  141. return Container(
  142. decoration: BoxDecoration(
  143. color: Colors.black.withOpacity(0.5),
  144. borderRadius: BorderRadius.circular(20.0),
  145. ),
  146. height: 100.0,
  147. width: 100.0,
  148. child: DefaultTextStyle(
  149. child: widget,
  150. style: mergedTextStyle,
  151. ),
  152. );
  153. }
  154. void showTooltip(Widget widget) {
  155. hideTooltip();
  156. _tipOverlay = OverlayEntry(
  157. builder: (BuildContext context) {
  158. return IgnorePointer(
  159. child: Center(
  160. child: widget,
  161. ),
  162. );
  163. },
  164. );
  165. Overlay.of(context).insert(_tipOverlay);
  166. }
  167. void hideTooltip() {
  168. _tipOverlay?.remove();
  169. _tipOverlay = null;
  170. }
  171. _ProgressCalculator _calculator;
  172. onTap() => isShow = !isShow;
  173. Function onDoubleTap() {
  174. return widget.doubleTapPlay
  175. ? () {
  176. LogUtils.log("ondouble tap");
  177. controller.playOrPause();
  178. }
  179. : null;
  180. }
  181. Function wrapHorizontalGesture(Function function) =>
  182. widget.horizontalGesture == true ? function : null;
  183. Function wrapVerticalGesture(Function function) =>
  184. widget.verticalGesture == true ? function : null;
  185. void _onHorizontalDragStart(DragStartDetails details) async {
  186. var videoInfo = await controller.getVideoInfo();
  187. _calculator = _ProgressCalculator(details, videoInfo);
  188. }
  189. void _onHorizontalDragUpdate(DragUpdateDetails details) {
  190. if (_calculator == null || details == null) {
  191. return;
  192. }
  193. var updateText = _calculator.calcUpdate(details);
  194. var offsetPosition = _calculator.getOffsetPosition();
  195. IconData iconData =
  196. offsetPosition > 0 ? Icons.fast_forward : Icons.fast_rewind;
  197. var w = Column(
  198. mainAxisAlignment: MainAxisAlignment.center,
  199. children: <Widget>[
  200. Icon(
  201. iconData,
  202. color: Colors.white,
  203. size: 40.0,
  204. ),
  205. Text(
  206. updateText,
  207. textAlign: TextAlign.center,
  208. ),
  209. ],
  210. );
  211. showTooltip(createTooltipWidgetWrapper(w));
  212. }
  213. void _onHorizontalDragEnd(DragEndDetails details) async {
  214. hideTooltip();
  215. var targetSeek = _calculator.getTargetSeek(details);
  216. _calculator = null;
  217. await controller.seekTo(targetSeek);
  218. var videoInfo = await controller.getVideoInfo();
  219. if (targetSeek < videoInfo.duration) await controller.play();
  220. }
  221. void _onVerticalDragStart(DragStartDetails details) {}
  222. void _onVerticalDragUpdate(DragUpdateDetails details) async {
  223. if (details.delta.dy > 0) {
  224. volumeDown();
  225. } else if (details.delta.dy < 0) {
  226. volumeUp();
  227. }
  228. var currentVolume = await getVolume();
  229. var column = Column(
  230. mainAxisAlignment: MainAxisAlignment.center,
  231. children: <Widget>[
  232. Icon(
  233. Icons.volume_up,
  234. color: Colors.white,
  235. size: 25.0,
  236. ),
  237. Padding(
  238. padding: const EdgeInsets.only(top: 10.0),
  239. child: Text(currentVolume.toString()),
  240. ),
  241. ],
  242. );
  243. showTooltip(createTooltipWidgetWrapper(column));
  244. }
  245. void _onVerticalDragEnd(DragEndDetails details) {
  246. hideTooltip();
  247. }
  248. Future<int> getVolume() async {
  249. switch (widget.volumeType) {
  250. case VolumeType.media:
  251. return controller.volume;
  252. case VolumeType.system:
  253. return controller.getSystemVolume();
  254. }
  255. return 0;
  256. }
  257. Future<void> volumeUp() async {
  258. var volume = await getVolume();
  259. volume++;
  260. switch (widget.volumeType) {
  261. case VolumeType.media:
  262. controller.volume = volume;
  263. break;
  264. case VolumeType.system:
  265. await IjkManager.systemVolumeUp();
  266. break;
  267. }
  268. }
  269. Future<void> volumeDown() async {
  270. var volume = await getVolume();
  271. volume--;
  272. switch (widget.volumeType) {
  273. case VolumeType.media:
  274. controller.volume = volume;
  275. break;
  276. case VolumeType.system:
  277. await IjkManager.systemVolumeDown();
  278. break;
  279. }
  280. }
  281. }
  282. class _ProgressCalculator {
  283. DragStartDetails startDetails;
  284. VideoInfo info;
  285. double dx;
  286. _ProgressCalculator(this.startDetails, this.info);
  287. String calcUpdate(DragUpdateDetails details) {
  288. dx = details.globalPosition.dx - startDetails.globalPosition.dx;
  289. var f = dx > 0 ? "+" : "-";
  290. var offset = getOffsetPosition().round().abs();
  291. return "$f${offset}s";
  292. }
  293. double getTargetSeek(DragEndDetails details) {
  294. var target = info.currentPosition + getOffsetPosition();
  295. if (target < 0) {
  296. target = 0;
  297. } else if (target > info.duration) {
  298. target = info.duration;
  299. }
  300. return target;
  301. }
  302. double getOffsetPosition() {
  303. return dx / 10;
  304. }
  305. }
  306. class PortraitController extends StatelessWidget {
  307. final IjkMediaController controller;
  308. final VideoInfo info;
  309. final TooltipDelegate tooltipDelegate;
  310. const PortraitController({
  311. Key key,
  312. this.controller,
  313. this.info,
  314. this.tooltipDelegate,
  315. }) : super(key: key);
  316. bool get haveTime {
  317. return info.hasData && info.duration > 0;
  318. }
  319. @override
  320. Widget build(BuildContext context) {
  321. if (!info.hasData) {
  322. return Container();
  323. }
  324. Widget bottomBar = buildBottomBar(context);
  325. return Column(
  326. children: <Widget>[
  327. Expanded(
  328. child: Container(),
  329. ),
  330. bottomBar,
  331. ],
  332. );
  333. }
  334. Widget buildBottomBar(BuildContext context) {
  335. var currentTime = buildCurrentText();
  336. var maxTime = buildMaxTimeText();
  337. var progress = buildProgress(info);
  338. var playButton = buildPlayButton(context);
  339. Widget widget = Row(
  340. children: <Widget>[
  341. playButton,
  342. Padding(
  343. padding: const EdgeInsets.all(8.0),
  344. child: currentTime,
  345. ),
  346. Expanded(child: progress),
  347. Padding(
  348. padding: const EdgeInsets.all(8.0),
  349. child: maxTime,
  350. ),
  351. ],
  352. );
  353. widget = DefaultTextStyle(
  354. style: const TextStyle(
  355. color: Colors.white,
  356. ),
  357. child: widget,
  358. );
  359. widget = Container(
  360. color: Colors.black.withOpacity(0.12),
  361. child: widget,
  362. );
  363. return widget;
  364. }
  365. Widget buildProgress(VideoInfo info) {
  366. if (!info.hasData || info.duration == 0) {
  367. return Container();
  368. }
  369. return Container(
  370. height: 22,
  371. child: ProgressBar(
  372. current: info.currentPosition,
  373. max: info.duration,
  374. changeProgressHandler: (progress) async {
  375. await controller.seekToProgress(progress);
  376. tooltipDelegate?.hideTooltip();
  377. },
  378. tapProgressHandler: (progress) {
  379. showProgressTooltip(info, progress);
  380. },
  381. ),
  382. );
  383. }
  384. buildCurrentText() {
  385. return haveTime
  386. ? Text(
  387. TimeHelper.getTimeText(info.currentPosition),
  388. )
  389. : Container();
  390. }
  391. buildMaxTimeText() {
  392. return haveTime
  393. ? Text(
  394. TimeHelper.getTimeText(info.duration),
  395. )
  396. : Container();
  397. }
  398. buildPlayButton(BuildContext context) {
  399. return IconButton(
  400. onPressed: () {
  401. controller.playOrPause();
  402. },
  403. color: Colors.white,
  404. icon: Icon(info.isPlaying ? Icons.pause : Icons.play_arrow),
  405. iconSize: 25.0,
  406. );
  407. }
  408. void showProgressTooltip(VideoInfo info, double progress) {
  409. var target = info.duration * progress;
  410. var diff = info.currentPosition - target;
  411. String diffString;
  412. if (diff < 1 && diff > -1) {
  413. diffString = "0s";
  414. } else if (diff < 0) {
  415. diffString = "+${TimeHelper.getTimeText(diff.abs())}";
  416. } else if (diff > 0) {
  417. diffString = "-${TimeHelper.getTimeText(diff.abs())}";
  418. } else {
  419. diffString = "0s";
  420. }
  421. Widget text = Container(
  422. alignment: Alignment.center,
  423. child: Column(
  424. mainAxisAlignment: MainAxisAlignment.center,
  425. children: <Widget>[
  426. Text(
  427. TimeHelper.getTimeText(target),
  428. style: TextStyle(fontSize: 20),
  429. ),
  430. Container(
  431. height: 10,
  432. ),
  433. Text(diffString),
  434. ],
  435. ),
  436. );
  437. var tooltip = tooltipDelegate?.createTooltipWidgetWrapper(text);
  438. tooltipDelegate?.showTooltip(tooltip);
  439. }
  440. }
  441. abstract class TooltipDelegate {
  442. void showTooltip(Widget widget);
  443. Widget createTooltipWidgetWrapper(Widget widget);
  444. void hideTooltip();
  445. }
  446. enum VolumeType {
  447. system,
  448. media,
  449. }