效果图
注:亮度调节和音量调节gif无法体现,功能是ok的,其次默认Icon锁的close和open实在难以分辨。
竖屏:

横屏:

gif详情:


关于Flutter视频的播放,大多数小伙伴们会有很多方案,Pub上也有很多优秀的插件。但是完全适合自己的项目总是很难,多数还是需要自己去封装。笔者也是的,拖沓了很久,还是决定自己封装一个适合自己项目的Flutter视频播放器,毫无疑问选择了官方的播放器插件
video_player
进行封装自定义。过程并不复杂,只要认真看完,相信每一位开发者都能定制属于自己的视频播放器!
环境:Flutter 2.8.1 channel stable ;Dart 2.15.1
需要音频播放器的看这里:Flutter音乐播放器
第三方插件
# 播放器
video_player: ^2.2.11
# 屏幕旋转
auto_orientation: ^2.2.1
# 亮度和音量调节
brightness_volume: ^1.0.3
项目结构

video_player_utils
重点说下这个工具类,因为视频播放,涉及到状态改变有很多,笔者刚开始选择使用InheritedWidget
来在众多的widget之间共享数据。但是总感觉这样有点繁琐,且不很优雅!
这里非广告,如果是使用GetX
就很简单了,笔者也使用了GetX
进行封装了,一泻千里的赶脚!,但是笔者还是那句话:刚开始接触Flutter的开发者不是很建议使用GetX
,可以先熟悉下Flutter状态管理的基础原理再行使用。而且为了尽量简洁,还是不引入其他的第三方了。
我们选择对第三方插件进行封装的目的不外乎这几个:
- 方便调用
- 适配业务需要
- 高内聚低耦合
- 后期迭代维护
于是笔者就写了一个工具类VideoPlayerUtils
,专门且只用来处理播放器的所有业务。包括暂停、播放、跳转、调节音量、调节亮度、切换视频等操作。在所有的widget中不会引用关于video_player
或其他第三方插件的任何信息,VideoPlayerUtils
负责widget与播放器之间的所有操作交互。后续优化迭代或更换播放器插件时,只需针对这个工具类进行修改,对所有widget不会有任何的影响,大大的解耦合了。
public 属性
static String get url => _instance._url; // 当前播放的url
static VideoPlayerState get state => _instance._state; // 当前播放状态
static bool get isInitialized => _instance._isInitialized; // 视频是否已经完成初始化
static Duration get duration => _instance._duration; // 视频总时长
static Duration get position => _instance._position; // 当前视频播放进度
static double get aspectRatio => _instance._aspectRatio; // 视频播放比例
其中VideoPlayerState
:
/// 播放状态
enum VideoPlayerState{
stopped, // 初始状态,已停止或发生错误
playing, // 正在播放
paused, // 暂停
completed // 播放结束
}
提供以上的公共属性,可以通过VideoPlayerUtils
来获取对应的值,使用get
只读,使外界不会误修改这些属性,以保证数值的安全性。开发者可根据自身需要自行添加属性。
public 方法
// 播放、暂停、切换视频等操作,内部自行判断是播放还是暂停,开发者不用关心
static void playerHandle(String url,{bool autoPlay = true,bool looping = false}) async{}
// 跳转播放
static void seekTo({required Duration position}) async{}
// 初始化结果监听,回调2个参数:1、初始化是否成功,2、播放的widget,方便setState()
static void initializedListener({required dynamic key,required Function(bool,Widget) listener}){}
// 移除初始化结果监听
static void removeInitializedListener(dynamic key){}
// 播放状态监听,stopped、playing、paused、completed等
static void statusListener({required dynamic key,required Function(VideoPlayerState) listener}){}
// 移除播放状态监听
static void removeStatusListener(dynamic key){}
// 播放进度监听
static void positionListener({required dynamic key,required Function(int) listener}){}
// 移除播放进度监听
static void removePositionListener(dynamic key){}
// 获取音量
static Future<double> getVolume() async{}
// 设置音量
static Future<void> setVolume(double volume) async{}
// 获取亮度
static Future<double> getBrightness() async{}
// 设置亮度
static Future<void> setBrightness(double brightness) async{}
// 设置播放速度
static Future<void> setSpeed(double speed) async{}
// 设置是否循环播放
static Future<void> setLooping(bool looping) async{}
// 设置横屏
static setLandscape(){}
// 设置竖屏
static setPortrait(){}
// 简单处理下时间格式化mm:ss (超过1小时可自行处理hh:mm:ss,严格来说不属于播放业务)
static String formatDuration(int second){}
// 释放资源
static dispose(){}
// 开发者可行添加比如:亮度、音量改变监听回调等。
提供以上方法来处理播放器的所有业务。同样的开发者可根据自身需要自行添加或修改。
playerHandle
static void playerHandle(String url,{bool autoPlay = true,bool looping = false}) async{}
重点说下这个方法,是整个业务的核心方法,控制视频的播放或暂停。开发者只要遇到播放或暂停是均可调用此方法,具体是播放或暂停,内部根据传入的url
自行判断,开发者不需要关心。
切换新视频也是使用此方法,传入的url
与上次不一致,自动切换新视频。笔者可根据statusListener
来监听播放状态的改变,以此处理自身逻辑。
initializedListener
// 初始化结果监听
static void initializedListener({required dynamic key,required Function(bool,Widget) listener}){}
这个也需要提下,视频播放器在播放新视频时会异步初始化,一般我们的操作是在initState()
初始化,成功后再setState()
。这里笔者遇到一个让人蛋疼的问题:
我们看video_player
的使用:
AspectRatio(
aspectRatio: controller.aspectRatio,
child: VideoPlayer(controller),
);
VideoPlayer(controller)
:widget中已经持有了controller。本来笔者封装的目的就是为了让widget与controller的之间解耦合。但此时的笔者。。。。

放弃不是不可能放弃的,这辈子都不会放弃的!
于是笔者取了巧,写了一个初始化监听器initializedListener
,包换2个参数:bool,Widget
,初始化是否成功;其中widget为初始化成功返回需要展示的播放器UI,失败默认返回const SizedBox()
。
使用
到这里就可以简单使用了:
class _VideoPlayerUIState extends State<VideoPlayerUI> {
Widget? _playerUI;
@override
void initState() {
// TODO: implement initState
super.initState();
// 播放视频
VideoPlayerUtils.playerHandle("http://flv3.bn.netease.com/tvmrepo/2018/6/9/R/EDJTRAD9R/SD/EDJTRAD9R-mobile.mp4");
// 播放新视频,初始化监听
VideoPlayerUtils.initializedListener(key: this, listener: (initialize,widget){
if(initialize){ // 初始化成功后,更新UI
_playerUI = widget;
if(!mounted) return;
setState(() {});
}
});
}
@override
void dispose() {
// TODO: implement dispose
// 移除监听
VideoPlayerUtils.removeInitializedListener(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
width: 414,
height: 414*9/16,
color: Colors.black26,
child: _playerUI ?? const CircularProgressIndicator(
strokeWidth: 3,
)
);
}
}
没看错,视频播放就是这么简单。
Widget
如果有更多的业务功能,笔者也按照自己的需求写了一套,同样的开发者可根据自身需要自行添加或修改。
video_player_gestures
VideoPlayerGestures
主要是处理手势的,比如快进、快退等跳转播放;左侧上下滑动调节亮度;右侧上下滑动调节音量;单击是否开启沉浸式播放,所有widget的隐藏与显示;双击播放、暂停等。
GestureDetector(
onTap: _onTap, // 单击上下widget隐藏与显示
onDoubleTap: _onDoubleTap, // 双击暂停、播放
onVerticalDragStart:_onVerticalDragStart, // 根据起始位置。确定是调整亮度还是调整声音
onVerticalDragUpdate: _onVerticalDragUpdate,// 一般在更新的时候,同步调整亮度或声音
onVerticalDragEnd: _onVerticalDragEnd, // 结束后,隐藏百分比提示信息widget
onHorizontalDragStart: _onHorizontalDragStart, // 手势跳转播放起始位置
onHorizontalDragUpdate: _onHorizontalDragUpdate, // 根据手势更新快进或快退
onHorizontalDragEnd: _onHorizontalDragEnd, // 手势结束seekTo
child: Stack(
children: _children,
),
);
哦,还有PercentageWidget
也放到这个文件下了,就是这玩意:

因为显示的百分比与手势相关,随着手势移动而更新。开发者可自行处理。
video_player_top
笔者处出于简单考虑,就按照整个UI的位置命名了。瞅一眼就知道是啥玩意。

同样的开发者可根据自身需要自行添加或修改。
video_player_center
就是这玩意:

同样的开发者可根据自身需要自行添加或修改。话说这个锁的Icon
的open和close是真的难分辨!
video_player_bottom
就是这玩意:

同样的开发者可根据自身需要自行添加或修改。
video_player_slider
这玩意是自定义的,别问,问就是跟产品干一架落了下风

主要就是自定义这玩意:
SliderThemeData(
trackHeight: 8,
inactiveTrackColor: Colors.grey,
activeTrackColor: Colors.greenAccent,
thumbShape: SliderThumbImage(image: _customImage),// 自定义
trackShape: const CustomTrackShape(), // 自定义
),
同样的开发者可根据自身需要自定义。
注:这里没有添加缓冲的进度,开发可查看video_player
中的源码VideoProgressIndicator
,按业务自行定义。
video_player_page
这玩意就是整合以上的widget,再考虑下全屏的安全区域,没啥东西。开发者可自行处理!
SafeArea(
top: !_isFullScreen,
bottom: !_isFullScreen,
left: !_isFullScreen,
right: !_isFullScreen,
child: SizedBox(
height: _height,
width: _width,
child: _playerUI != null ? VideoPlayerGestures(
appearCallback: (appear){
_top!.opacityCallback(appear);
_lockIcon!.opacityCallback(appear);
_bottom!.opacityCallback(appear);
},
children: [
Center(
child: _playerUI,
),
_top!,
_lockIcon!,
_bottom!
],
) : Container(
alignment: Alignment.center,
color: Colors.black26,
child: const CircularProgressIndicator(
strokeWidth: 3,
),
)
),
);
RCFlutterVideoPlayer
具体的实现监听器的思路,看这里。
自此一个漂亮的Flutter视频播放器就已经结束了。如果您觉得对您有些许帮助的话,欢迎 Star !
网友评论