controller_widget_builder.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. import 'dart:async';
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
  6. import 'package:flutter_ijkplayer/src/helper/logutil.dart';
  7. import 'package:flutter_ijkplayer/src/helper/time_helper.dart';
  8. import 'package:flutter_ijkplayer/src/helper/ui_helper.dart';
  9. import 'package:flutter_ijkplayer/src/route/fullscreen_route.dart';
  10. import 'package:flutter_ijkplayer/src/widget/progress_bar.dart';
  11. /// Using mediaController to Construct a Controller UI
  12. typedef Widget IJKControllerWidgetBuilder(IjkMediaController controller);
  13. /// default create IJK Controller UI
  14. Widget defaultBuildIjkControllerWidget(IjkMediaController controller) {
  15. return DefaultIJKControllerWidget(
  16. controller: controller,
  17. fullscreenControllerWidgetBuilder: (ctl) =>
  18. buildFullscreenMediaController(ctl),
  19. );
  20. }
  21. /// Default Controller Widget
  22. ///
  23. /// see [IjkPlayer] and [IJKControllerWidgetBuilder]
  24. class DefaultIJKControllerWidget extends StatefulWidget {
  25. final IjkMediaController controller;
  26. /// If [doubleTapPlay] is true, can double tap to play or pause media.
  27. final bool doubleTapPlay;
  28. /// If [verticalGesture] is false, vertical gesture will be ignored.
  29. final bool verticalGesture;
  30. /// If [horizontalGesture] is false, horizontal gesture will be ignored.
  31. final bool horizontalGesture;
  32. /// Controlling [verticalGesture] is controlling system volume or media volume.
  33. final VolumeType volumeType;
  34. final bool playWillPauseOther;
  35. /// Control whether there is a full-screen button.
  36. final bool showFullScreenButton;
  37. /// The current full-screen button style should not be changed by users.
  38. final bool currentFullScreenState;
  39. final IJKControllerWidgetBuilder fullscreenControllerWidgetBuilder;
  40. /// The UI of the controller.
  41. const DefaultIJKControllerWidget({
  42. @required this.controller,
  43. this.doubleTapPlay = false,
  44. this.verticalGesture = true,
  45. this.horizontalGesture = true,
  46. this.volumeType = VolumeType.system,
  47. this.playWillPauseOther = true,
  48. this.currentFullScreenState = false,
  49. this.showFullScreenButton = true,
  50. this.fullscreenControllerWidgetBuilder,
  51. Key key,
  52. }) : super(key: key);
  53. @override
  54. _DefaultIJKControllerWidgetState createState() =>
  55. _DefaultIJKControllerWidgetState();
  56. DefaultIJKControllerWidget copyWith({
  57. IjkMediaController controller,
  58. bool doubleTapPlay,
  59. bool verticalGesture,
  60. bool horizontalGesture,
  61. VolumeType volumeType,
  62. bool playWillPauseOther,
  63. bool currentFullScreenState,
  64. bool showFullScreenButton,
  65. IJKControllerWidgetBuilder fullscreenControllerWidgetBuilder,
  66. Key key,
  67. }) {
  68. return DefaultIJKControllerWidget(
  69. controller: controller ?? this.controller,
  70. doubleTapPlay: doubleTapPlay ?? this.doubleTapPlay,
  71. fullscreenControllerWidgetBuilder: fullscreenControllerWidgetBuilder ??
  72. this.fullscreenControllerWidgetBuilder,
  73. horizontalGesture: horizontalGesture ?? this.horizontalGesture,
  74. currentFullScreenState:
  75. currentFullScreenState ?? this.currentFullScreenState,
  76. key: key,
  77. volumeType: volumeType ?? this.volumeType,
  78. playWillPauseOther: playWillPauseOther ?? this.playWillPauseOther,
  79. showFullScreenButton: showFullScreenButton ?? this.showFullScreenButton,
  80. verticalGesture: verticalGesture ?? this.verticalGesture,
  81. );
  82. }
  83. }
  84. class _DefaultIJKControllerWidgetState extends State<DefaultIJKControllerWidget>
  85. implements TooltipDelegate {
  86. IjkMediaController get controller => widget.controller;
  87. GlobalKey currentKey = GlobalKey();
  88. bool _isShow = true;
  89. set isShow(bool value) {
  90. _isShow = value;
  91. setState(() {});
  92. if (value == true) {
  93. controller.refreshVideoInfo();
  94. }
  95. }
  96. bool get isShow => _isShow;
  97. Timer progressTimer;
  98. StreamSubscription controllerSubscription;
  99. @override
  100. void initState() {
  101. super.initState();
  102. startTimer();
  103. controllerSubscription =
  104. controller.textureIdStream.listen(_onTextureIdChange);
  105. }
  106. void _onTextureIdChange(int textureId) {
  107. LogUtils.debug("onTextureChange $textureId");
  108. if (textureId != null) {
  109. startTimer();
  110. } else {
  111. stopTimer();
  112. }
  113. }
  114. @override
  115. void deactivate() {
  116. super.deactivate();
  117. }
  118. @override
  119. void dispose() {
  120. controllerSubscription.cancel();
  121. stopTimer();
  122. IjkManager.resetBrightness();
  123. super.dispose();
  124. }
  125. void startTimer() {
  126. if (controller.textureId == null) {
  127. return;
  128. }
  129. progressTimer?.cancel();
  130. progressTimer = Timer.periodic(Duration(milliseconds: 350), (timer) {
  131. LogUtils.verbose("timer will call refresh info");
  132. controller.refreshVideoInfo();
  133. });
  134. }
  135. void stopTimer() {
  136. progressTimer?.cancel();
  137. }
  138. @override
  139. Widget build(BuildContext context) {
  140. return GestureDetector(
  141. behavior: HitTestBehavior.opaque,
  142. child: buildContent(),
  143. onDoubleTap: onDoubleTap(),
  144. onHorizontalDragStart: wrapHorizontalGesture(_onHorizontalDragStart),
  145. onHorizontalDragUpdate: wrapHorizontalGesture(_onHorizontalDragUpdate),
  146. onHorizontalDragEnd: wrapHorizontalGesture(_onHorizontalDragEnd),
  147. onVerticalDragStart: wrapVerticalGesture(_onVerticalDragStart),
  148. onVerticalDragUpdate: wrapVerticalGesture(_onVerticalDragUpdate),
  149. onVerticalDragEnd: wrapVerticalGesture(_onVerticalDragEnd),
  150. onTap: onTap,
  151. key: currentKey,
  152. );
  153. }
  154. Widget buildContent() {
  155. if (!isShow) {
  156. return Container();
  157. }
  158. return StreamBuilder<VideoInfo>(
  159. stream: controller.videoInfoStream,
  160. builder: (context, snapshot) {
  161. var info = snapshot.data;
  162. if (info == null || !info.hasData) {
  163. return Container();
  164. }
  165. return buildPortrait(info);
  166. },
  167. );
  168. }
  169. Widget _buildFullScreenButton() {
  170. if (widget.showFullScreenButton != true) {
  171. return Container();
  172. }
  173. var isFull = widget.currentFullScreenState;
  174. IJKControllerWidgetBuilder fullscreenBuilder =
  175. widget.fullscreenControllerWidgetBuilder ??
  176. (ctx) => widget.copyWith(currentFullScreenState: true);
  177. return IconButton(
  178. color: Colors.white,
  179. icon: Icon(isFull ? Icons.fullscreen_exit : Icons.fullscreen),
  180. onPressed: () {
  181. if (isFull) {
  182. Navigator.pop(context);
  183. } else {
  184. showFullScreenIJKPlayer(context, controller,
  185. fullscreenControllerWidgetBuilder: fullscreenBuilder);
  186. }
  187. },
  188. );
  189. }
  190. Widget buildPortrait(VideoInfo info) {
  191. return PortraitController(
  192. controller: controller,
  193. info: info,
  194. tooltipDelegate: this,
  195. playWillPauseOther: widget.playWillPauseOther,
  196. fullScreenWidget: _buildFullScreenButton(),
  197. );
  198. }
  199. OverlayEntry _tipOverlay;
  200. Widget createTooltipWidgetWrapper(Widget widget) {
  201. var typography = Typography(platform: TargetPlatform.android);
  202. var theme = typography.white;
  203. const style = const TextStyle(
  204. fontSize: 15.0,
  205. color: Colors.white,
  206. fontWeight: FontWeight.normal,
  207. );
  208. var mergedTextStyle = theme.body2.merge(style);
  209. return Container(
  210. decoration: BoxDecoration(
  211. color: Colors.black.withOpacity(0.5),
  212. borderRadius: BorderRadius.circular(20.0),
  213. ),
  214. height: 100.0,
  215. width: 100.0,
  216. child: DefaultTextStyle(
  217. child: widget,
  218. style: mergedTextStyle,
  219. ),
  220. );
  221. }
  222. void showTooltip(Widget widget) {
  223. hideTooltip();
  224. _tipOverlay = OverlayEntry(
  225. builder: (BuildContext context) {
  226. return IgnorePointer(
  227. child: Center(
  228. child: widget,
  229. ),
  230. );
  231. },
  232. );
  233. Overlay.of(context).insert(_tipOverlay);
  234. }
  235. void hideTooltip() {
  236. _tipOverlay?.remove();
  237. _tipOverlay = null;
  238. }
  239. _ProgressCalculator _calculator;
  240. onTap() => isShow = !isShow;
  241. Function onDoubleTap() {
  242. return widget.doubleTapPlay
  243. ? () {
  244. LogUtils.debug("ondouble tap");
  245. controller.playOrPause();
  246. }
  247. : null;
  248. }
  249. Function wrapHorizontalGesture(Function function) =>
  250. widget.horizontalGesture == true ? function : null;
  251. Function wrapVerticalGesture(Function function) =>
  252. widget.verticalGesture == true ? function : null;
  253. void _onHorizontalDragStart(DragStartDetails details) async {
  254. var videoInfo = await controller.getVideoInfo();
  255. _calculator = _ProgressCalculator(details, videoInfo);
  256. }
  257. void _onHorizontalDragUpdate(DragUpdateDetails details) {
  258. if (_calculator == null || details == null) {
  259. return;
  260. }
  261. var updateText = _calculator.calcUpdate(details);
  262. var offsetPosition = _calculator.getOffsetPosition();
  263. IconData iconData =
  264. offsetPosition > 0 ? Icons.fast_forward : Icons.fast_rewind;
  265. var w = Column(
  266. mainAxisAlignment: MainAxisAlignment.center,
  267. children: <Widget>[
  268. Icon(
  269. iconData,
  270. color: Colors.white,
  271. size: 40.0,
  272. ),
  273. Text(
  274. updateText,
  275. textAlign: TextAlign.center,
  276. ),
  277. ],
  278. );
  279. showTooltip(createTooltipWidgetWrapper(w));
  280. }
  281. void _onHorizontalDragEnd(DragEndDetails details) async {
  282. hideTooltip();
  283. var targetSeek = _calculator.getTargetSeek(details);
  284. _calculator = null;
  285. await controller.seekTo(targetSeek);
  286. var videoInfo = await controller.getVideoInfo();
  287. if (targetSeek < videoInfo.duration) await controller.play();
  288. }
  289. bool verticalDragging = false;
  290. bool leftVerticalDrag;
  291. void _onVerticalDragStart(DragStartDetails details) {
  292. verticalDragging = true;
  293. var width = UIHelper.findGlobalRect(currentKey).width;
  294. var dx =
  295. UIHelper.globalOffsetToLocal(currentKey, details.globalPosition).dx;
  296. leftVerticalDrag = dx / width <= 0.5;
  297. }
  298. void _onVerticalDragUpdate(DragUpdateDetails details) async {
  299. if (verticalDragging == false) return;
  300. String text = "";
  301. IconData iconData = Icons.volume_up;
  302. if (leftVerticalDrag == false) {
  303. if (details.delta.dy > 0) {
  304. await volumeDown();
  305. } else if (details.delta.dy < 0) {
  306. await volumeUp();
  307. }
  308. var currentVolume = await getVolume();
  309. if (currentVolume <= 0) {
  310. iconData = Icons.volume_mute;
  311. } else if (currentVolume < 50) {
  312. iconData = Icons.volume_down;
  313. } else {
  314. iconData = Icons.volume_up;
  315. }
  316. text = currentVolume.toString();
  317. } else if (leftVerticalDrag == true) {
  318. var currentBright = await IjkManager.getSystemBrightness();
  319. double target;
  320. if (details.delta.dy > 0) {
  321. target = currentBright - 0.03;
  322. } else {
  323. target = currentBright + 0.03;
  324. }
  325. if (target > 1) {
  326. target = 1;
  327. } else if (target < 0) {
  328. target = 0;
  329. }
  330. await IjkManager.setSystemBrightness(target);
  331. if (target >= 0.66) {
  332. iconData = Icons.brightness_high;
  333. } else if (target < 0.66 && target > 0.33) {
  334. iconData = Icons.brightness_medium;
  335. } else {
  336. iconData = Icons.brightness_low;
  337. }
  338. text = (target * 100).toStringAsFixed(0);
  339. } else {
  340. return;
  341. }
  342. var column = Column(
  343. mainAxisAlignment: MainAxisAlignment.center,
  344. children: <Widget>[
  345. Icon(
  346. iconData,
  347. color: Colors.white,
  348. size: 25.0,
  349. ),
  350. Padding(
  351. padding: const EdgeInsets.only(top: 10.0),
  352. child: Text(text),
  353. ),
  354. ],
  355. );
  356. showTooltip(createTooltipWidgetWrapper(column));
  357. }
  358. void _onVerticalDragEnd(DragEndDetails details) async {
  359. verticalDragging = false;
  360. leftVerticalDrag = null;
  361. hideTooltip();
  362. Future.delayed(const Duration(milliseconds: 2000), () {
  363. hideTooltip();
  364. });
  365. }
  366. Future<int> getVolume() async {
  367. switch (widget.volumeType) {
  368. case VolumeType.media:
  369. return controller.volume;
  370. case VolumeType.system:
  371. return controller.getSystemVolume();
  372. }
  373. return 0;
  374. }
  375. Future<void> volumeUp() async {
  376. var volume = await getVolume();
  377. volume++;
  378. switch (widget.volumeType) {
  379. case VolumeType.media:
  380. controller.volume = volume;
  381. break;
  382. case VolumeType.system:
  383. await IjkManager.systemVolumeUp();
  384. break;
  385. }
  386. }
  387. Future<void> volumeDown() async {
  388. var volume = await getVolume();
  389. volume--;
  390. switch (widget.volumeType) {
  391. case VolumeType.media:
  392. controller.volume = volume;
  393. break;
  394. case VolumeType.system:
  395. await IjkManager.systemVolumeDown();
  396. break;
  397. }
  398. }
  399. }
  400. class _ProgressCalculator {
  401. DragStartDetails startDetails;
  402. VideoInfo info;
  403. double dx;
  404. _ProgressCalculator(this.startDetails, this.info);
  405. String calcUpdate(DragUpdateDetails details) {
  406. dx = details.globalPosition.dx - startDetails.globalPosition.dx;
  407. var f = dx > 0 ? "+" : "-";
  408. var offset = getOffsetPosition().round().abs();
  409. return "$f${offset}s";
  410. }
  411. double getTargetSeek(DragEndDetails details) {
  412. var target = info.currentPosition + getOffsetPosition();
  413. if (target < 0) {
  414. target = 0;
  415. } else if (target > info.duration) {
  416. target = info.duration;
  417. }
  418. return target;
  419. }
  420. double getOffsetPosition() {
  421. return dx / 10;
  422. }
  423. }
  424. class PortraitController extends StatelessWidget {
  425. final IjkMediaController controller;
  426. final VideoInfo info;
  427. final TooltipDelegate tooltipDelegate;
  428. final bool playWillPauseOther;
  429. final Widget fullScreenWidget;
  430. const PortraitController({
  431. Key key,
  432. this.controller,
  433. this.info,
  434. this.tooltipDelegate,
  435. this.playWillPauseOther = true,
  436. this.fullScreenWidget,
  437. }) : super(key: key);
  438. bool get haveTime {
  439. return info.hasData && info.duration > 0;
  440. }
  441. @override
  442. Widget build(BuildContext context) {
  443. if (!info.hasData) {
  444. return Container();
  445. }
  446. Widget bottomBar = buildBottomBar(context);
  447. return Column(
  448. children: <Widget>[
  449. Expanded(
  450. child: Container(),
  451. ),
  452. bottomBar,
  453. ],
  454. );
  455. }
  456. Widget buildBottomBar(BuildContext context) {
  457. var currentTime = buildCurrentText();
  458. var maxTime = buildMaxTimeText();
  459. var progress = buildProgress(info);
  460. var playButton = buildPlayButton(context);
  461. var fullScreenButton = buildFullScreenButton();
  462. Widget widget = Row(
  463. children: <Widget>[
  464. playButton,
  465. Padding(
  466. padding: const EdgeInsets.all(8.0),
  467. child: currentTime,
  468. ),
  469. Expanded(child: progress),
  470. Padding(
  471. padding: const EdgeInsets.all(8.0),
  472. child: maxTime,
  473. ),
  474. fullScreenButton,
  475. ],
  476. );
  477. widget = DefaultTextStyle(
  478. style: const TextStyle(
  479. color: Colors.white,
  480. ),
  481. child: widget,
  482. );
  483. widget = Container(
  484. color: Colors.black.withOpacity(0.12),
  485. child: widget,
  486. );
  487. return widget;
  488. }
  489. Widget buildProgress(VideoInfo info) {
  490. if (!info.hasData || info.duration == 0) {
  491. return Container();
  492. }
  493. return Container(
  494. height: 22,
  495. child: ProgressBar(
  496. current: info.currentPosition,
  497. max: info.duration,
  498. changeProgressHandler: (progress) async {
  499. await controller.seekToProgress(progress);
  500. tooltipDelegate?.hideTooltip();
  501. },
  502. tapProgressHandler: (progress) {
  503. showProgressTooltip(info, progress);
  504. },
  505. ),
  506. );
  507. }
  508. buildCurrentText() {
  509. return haveTime
  510. ? Text(
  511. TimeHelper.getTimeText(info.currentPosition),
  512. )
  513. : Container();
  514. }
  515. buildMaxTimeText() {
  516. return haveTime
  517. ? Text(
  518. TimeHelper.getTimeText(info.duration),
  519. )
  520. : Container();
  521. }
  522. buildPlayButton(BuildContext context) {
  523. return IconButton(
  524. onPressed: () {
  525. controller.playOrPause(pauseOther: playWillPauseOther);
  526. },
  527. color: Colors.white,
  528. icon: Icon(info.isPlaying ? Icons.pause : Icons.play_arrow),
  529. iconSize: 25.0,
  530. );
  531. }
  532. void showProgressTooltip(VideoInfo info, double progress) {
  533. var target = info.duration * progress;
  534. var diff = info.currentPosition - target;
  535. String diffString;
  536. if (diff < 1 && diff > -1) {
  537. diffString = "0s";
  538. } else if (diff < 0) {
  539. diffString = "+${TimeHelper.getTimeText(diff.abs())}";
  540. } else if (diff > 0) {
  541. diffString = "-${TimeHelper.getTimeText(diff.abs())}";
  542. } else {
  543. diffString = "0s";
  544. }
  545. Widget text = Container(
  546. alignment: Alignment.center,
  547. child: Column(
  548. mainAxisAlignment: MainAxisAlignment.center,
  549. children: <Widget>[
  550. Text(
  551. TimeHelper.getTimeText(target),
  552. style: TextStyle(fontSize: 20),
  553. ),
  554. Container(
  555. height: 10,
  556. ),
  557. Text(diffString),
  558. ],
  559. ),
  560. );
  561. var tooltip = tooltipDelegate?.createTooltipWidgetWrapper(text);
  562. tooltipDelegate?.showTooltip(tooltip);
  563. }
  564. Widget buildFullScreenButton() {
  565. return fullScreenWidget ?? Container();
  566. }
  567. }
  568. abstract class TooltipDelegate {
  569. void showTooltip(Widget widget);
  570. Widget createTooltipWidgetWrapper(Widget widget);
  571. void hideTooltip();
  572. }
  573. enum VolumeType {
  574. system,
  575. media,
  576. }
  577. showFullScreenIJKPlayer(
  578. BuildContext context,
  579. IjkMediaController controller, {
  580. IJKControllerWidgetBuilder fullscreenControllerWidgetBuilder,
  581. }) async {
  582. Navigator.push(
  583. context,
  584. FullScreenRoute(
  585. builder: (c) {
  586. return IjkPlayer(
  587. mediaController: controller,
  588. controllerWidgetBuilder: (ctl) =>
  589. fullscreenControllerWidgetBuilder(ctl),
  590. );
  591. },
  592. ),
  593. ).then((_) {
  594. IjkManager.unlockOrientation();
  595. IjkManager.setCurrentOrientation(DeviceOrientation.portraitUp);
  596. });
  597. var info = await controller.getVideoInfo();
  598. Axis axis;
  599. if (info.width == 0 || info.height == 0) {
  600. axis = Axis.horizontal;
  601. } else if (info.width > info.height) {
  602. if (info.degree == 90 || info.degree == 270) {
  603. axis = Axis.vertical;
  604. } else {
  605. axis = Axis.horizontal;
  606. }
  607. } else {
  608. if (info.degree == 90 || info.degree == 270) {
  609. axis = Axis.horizontal;
  610. } else {
  611. axis = Axis.vertical;
  612. }
  613. }
  614. if (axis == Axis.horizontal) {
  615. IjkManager.setLandScape();
  616. } else {
  617. IjkManager.setPortrait();
  618. }
  619. }
  620. Widget _buildFullScreenMediaController(
  621. IjkMediaController controller, bool fullScreen) {
  622. return DefaultIJKControllerWidget(
  623. controller: controller,
  624. currentFullScreenState: true,
  625. );
  626. }
  627. Widget buildFullscreenMediaController(IjkMediaController controller) {
  628. return _buildFullScreenMediaController(controller, true);
  629. }