my_video_player.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:orientation/orientation.dart';
  5. import 'package:video_player/video_player.dart'; // 引入官方插件
  6. class MyVideo extends StatefulWidget {
  7. MyVideo({
  8. @required this.url, // 当前需要播放的地址
  9. @required this.width, // 播放器尺寸(大于等于视频播放区域)
  10. @required this.height,
  11. this.title = '', // 视频需要显示的标题
  12. });
  13. // 视频地址
  14. final String url;
  15. // 视频尺寸比例
  16. final double width;
  17. final double height;
  18. // 视频标题
  19. final String title;
  20. @override
  21. State<MyVideo> createState() {
  22. return _MyVideoState();
  23. }
  24. }
  25. class _MyVideoState extends State<MyVideo> {
  26. // 指示video资源是否加载完成,加载完成后会获得总时长和视频长宽比等信息
  27. bool _videoInit = false;
  28. // video控件管理器
  29. VideoPlayerController _controller;
  30. // 记录video播放进度
  31. Duration _position = Duration(seconds: 0);
  32. // 记录播放控件ui是否显示(进度条,播放按钮,全屏按钮等等)
  33. Timer _timer; // 计时器,用于延迟隐藏控件ui
  34. bool _hidePlayControl = true; // 控制是否隐藏控件ui
  35. double _playControlOpacity = 0; // 通过透明度动画显示/隐藏控件ui
  36. // 记录是否全屏
  37. bool get _isFullScreen =>
  38. MediaQuery.of(context).orientation == Orientation.landscape;
  39. @override
  40. Widget build(BuildContext context) {
  41. return Container(
  42. width: widget.width,
  43. height: widget.height,
  44. color: Colors.black,
  45. child: widget.url != null
  46. ? Stack(
  47. // 因为控件ui和视频是重叠的,所以要用定位了
  48. children: <Widget>[
  49. GestureDetector(
  50. // 手势组件
  51. onTap: () {
  52. // 点击显示/隐藏控件ui
  53. _togglePlayControl();
  54. },
  55. child: _videoInit
  56. ? Center(
  57. child: AspectRatio(
  58. // 加载url成功时,根据视频比例渲染播放器
  59. aspectRatio: _controller.value.aspectRatio,
  60. child: VideoPlayer(_controller),
  61. ),
  62. )
  63. : Center(
  64. // 没加载完成时显示转圈圈loading
  65. child: SizedBox(
  66. width: 20,
  67. height: 20,
  68. child: CircularProgressIndicator(),
  69. ),
  70. ),
  71. ),
  72. Positioned(
  73. // 需要定位
  74. left: 0,
  75. bottom: 0,
  76. child: Offstage(
  77. // 控制是否隐藏
  78. offstage: _hidePlayControl,
  79. child: AnimatedOpacity(
  80. // 加入透明度动画
  81. opacity: _playControlOpacity,
  82. duration: Duration(milliseconds: 300),
  83. child: Container(
  84. // 底部控件的容器
  85. width: widget.width,
  86. height: 40,
  87. decoration: BoxDecoration(
  88. gradient: LinearGradient(
  89. // 来点黑色到透明的渐变优雅一下
  90. begin: Alignment.bottomCenter,
  91. end: Alignment.topCenter,
  92. colors: [
  93. Color.fromRGBO(0, 0, 0, .7),
  94. Color.fromRGBO(0, 0, 0, .1)
  95. ],
  96. ),
  97. ),
  98. child: _videoInit
  99. ? Row(
  100. // 加载完成时才渲染,flex布局
  101. children: <Widget>[
  102. IconButton(
  103. // 播放按钮
  104. padding: EdgeInsets.zero,
  105. iconSize: 26,
  106. icon: Icon(
  107. // 根据控制器动态变化播放图标还是暂停
  108. _controller.value.isPlaying
  109. ? Icons.pause
  110. : Icons.play_arrow,
  111. color: Colors.white,
  112. ),
  113. onPressed: () {
  114. setState(() {
  115. // 同样的,点击动态播放或者暂停
  116. _controller.value.isPlaying
  117. ? _controller.pause()
  118. : _controller.play();
  119. _startPlayControlTimer(); // 操作控件后,重置延迟隐藏控件的timer
  120. });
  121. },
  122. ),
  123. Flexible(
  124. // 相当于前端的flex: 1
  125. child: VideoProgressIndicator(
  126. // 嘻嘻,这是video_player编写好的进度条,直接用就是了~~
  127. _controller,
  128. allowScrubbing: true, // 允许手势操作进度条
  129. padding: EdgeInsets.all(0),
  130. colors: VideoProgressColors(
  131. // 配置进度条颜色,也是video_player现成的,直接用
  132. playedColor: Theme.of(context)
  133. .primaryColor, // 已播放的颜色
  134. bufferedColor: Color.fromRGBO(
  135. 255, 255, 255, .5), // 缓存中的颜色
  136. backgroundColor: Color.fromRGBO(
  137. 255, 255, 255, .2), // 为缓存的颜色
  138. ),
  139. ),
  140. ),
  141. Container(
  142. // 播放时间
  143. margin: EdgeInsets.only(left: 10),
  144. child: Text(
  145. // durationToTime是通过Duration转成hh:mm:ss的格式,自己实现。
  146. "A",
  147. style: TextStyle(color: Colors.white),
  148. ),
  149. ),
  150. IconButton(
  151. // 全屏/横屏按钮
  152. padding: EdgeInsets.zero,
  153. iconSize: 26,
  154. icon: Icon(
  155. // 根据当前屏幕方向切换图标
  156. _isFullScreen
  157. ? Icons.fullscreen_exit
  158. : Icons.fullscreen,
  159. color: Colors.white,
  160. ),
  161. onPressed: () {
  162. // 点击切换是否全屏
  163. _toggleFullScreen();
  164. },
  165. ),
  166. ],
  167. )
  168. : Container(),
  169. ),
  170. ),
  171. ),
  172. )
  173. ],
  174. )
  175. : Center(
  176. // 判断是否传入了url,没有的话显示"暂无视频信息"
  177. child: Text(
  178. '暂无视频信息',
  179. style: TextStyle(color: Colors.white),
  180. ),
  181. ),
  182. );
  183. }
  184. @override
  185. void initState() {
  186. _urlChange(); // 初始进行一次url加载
  187. super.initState();
  188. }
  189. @override
  190. void didUpdateWidget(MyVideo oldWidget) {
  191. if (oldWidget.url != widget.url) {
  192. _urlChange(); // url变化时重新执行一次url加载
  193. }
  194. super.didUpdateWidget(oldWidget);
  195. }
  196. @override
  197. void dispose() {
  198. if (_controller != null) {
  199. // 惯例。组件销毁时清理下
  200. _controller.removeListener(_videoListener);
  201. _controller.dispose();
  202. }
  203. super.dispose();
  204. }
  205. void _urlChange() {
  206. if (widget.url == null || widget.url == '') return;
  207. if (_controller != null) {
  208. // 如果控制器存在,清理掉重新创建
  209. _controller.removeListener(_videoListener);
  210. _controller.dispose();
  211. }
  212. setState(() {
  213. // 重置组件参数
  214. _hidePlayControl = true;
  215. _videoInit = false;
  216. _position = Duration(seconds: 0);
  217. });
  218. // 加载network的url,也支持本地文件,自行阅览官方api
  219. _controller = VideoPlayerController.network(widget.url)
  220. ..initialize().then((_) {
  221. // 加载资源完成时,监听播放进度,并且标记_videoInit=true加载完成
  222. _controller.addListener(_videoListener);
  223. setState(() {
  224. _videoInit = true;
  225. });
  226. });
  227. }
  228. void _videoListener() async {
  229. Duration res = await _controller.position;
  230. if (res >= _controller.value.duration) {
  231. _controller.pause();
  232. _controller.seekTo(Duration(seconds: 0));
  233. }
  234. setState(() {
  235. _position = res;
  236. });
  237. }
  238. void _togglePlayControl() {
  239. setState(() {
  240. if (_hidePlayControl) {
  241. // 如果隐藏则显示
  242. _hidePlayControl = false;
  243. _playControlOpacity = 1;
  244. _startPlayControlTimer(); // 开始计时器,计时后隐藏
  245. } else {
  246. // 如果显示就隐藏
  247. if (_timer != null) _timer.cancel(); // 有计时器先移除计时器
  248. _playControlOpacity = 0;
  249. Future.delayed(Duration(milliseconds: 300)).whenComplete(() {
  250. _hidePlayControl = true; // 延迟300ms(透明度动画结束)后,隐藏
  251. });
  252. }
  253. });
  254. }
  255. void _startPlayControlTimer() {
  256. // 计时器,用法和前端js的大同小异
  257. if (_timer != null) _timer.cancel();
  258. // _hidePlayControl = true;
  259. _timer = Timer(Duration(seconds: 3), () {
  260. // 延迟3s后隐藏
  261. setState(() {
  262. _playControlOpacity = 0;
  263. Future.delayed(Duration(milliseconds: 300)).whenComplete(() {
  264. _hidePlayControl = true;
  265. });
  266. });
  267. });
  268. }
  269. void _toggleFullScreen() {
  270. setState(() {
  271. if (_isFullScreen) {
  272. // 如果是全屏就切换竖屏
  273. OrientationPlugin.forceOrientation(DeviceOrientation.portraitUp);
  274. } else {
  275. OrientationPlugin.forceOrientation(DeviceOrientation.landscapeRight);
  276. }
  277. _startPlayControlTimer(); // 操作完控件开始计时隐藏
  278. });
  279. }
  280. }