controller_widget_builder.dart 11 KB

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