持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
前言
今天用Flutter来实现类似wx朋友圈的视频播放效果,包括拖拽关闭,路由动画,视频播放等相关的处理。
最终效果图
(示例视频来自weibo)
视频播放使用的是video_player插件。
上篇讲了拖拽效果的实现:Flutter 实现朋友圈视频播放效果(上)-- 拖拽效果
接下来我们实现他的主体,视频播放效果
视频资源的初始化
使用video_player会更自由一点,可以自己掌控视频播放器的UI。
首先是对资源的初始化,可以是network也可以是file、asset。
Future<void> initializePlayer() async {
_videoPlayerController = VideoPlayerController.network(videoUrl);
await _videoPlayerController.initialize();
// 初始化完成,刷新,初始化完成才能显示VideoPlayer
if (mounted) {
setState(() {});
}
// 设置循环
_videoPlayerController.setLooping(true);
// 播放
await _videoPlayerController.play();
}
初始化完成加载VideoPlayer
SizedBox(
width: 200,
child: _videoPlayerController.value.isInitialized ? AspectRatio(
aspectRatio:
_videoPlayerController.value.aspectRatio,
child: GestureDetector(
onTap: onTap,
child: VideoPlayer(_videoPlayerController),
),
)
: const SizedBox(),
)
_videoPlayerController.value.aspectRatio可以获取得到视频的比例。记得再套一个Hero。
因为组件和视频页的视频都是同步的,同一个视频,所以考虑使用同一个VideoPlayerController,打开视频页时传入。
Navigator.push(
context,
DragBottomDismissDialog(
builder: (context) {
return VideoPage(
videoPlayerController: _videoPlayerController,
heroTag: "video_page_player",
);
},
),
).then((value) {
_videoPlayerController.play();
});
DragBottomDismissDialog即是视频页的拖拽退出效果。
视频UI
然后进入到视频的全屏播放页,pyq的视频播放布局是上边视频窗口,宽屏居中,竖屏底部居中,下边再带有一个进度条,可拖拽进度。
进入视频页,保险起见检查一下是否初始化
_videoPlayerController = widget.videoPlayerController;
if (!_videoPlayerController.value.isInitialized) {
await _videoPlayerController.initialize();
}
绘制下视频播放窗口,根据比例判断,居中占满宽度,或者底部居中
Container(
width: double.maxFinite,
height: double.maxFinite,
alignment: _videoPlayerController.value.aspectRatio >= 1 ? Alignment.center : Alignment.bottomCenter,
child: DragBottomPopGesture(
child: Hero(
tag: widget.heroTag ?? "video_page_player",
transitionOnUserGestures: true,
child: _videoPlayerController.value.isInitialized
? AspectRatio(
aspectRatio:
_videoPlayerController.value.aspectRatio,
child: GestureDetector(
onTap: onTap,
child: VideoPlayer(_videoPlayerController),
),
)
: const SizedBox(),
),
),
)
DragBottomPopGesture规定只能拖拽视频这一区域
加个loading,初始化完成前可以显示加载图
!_videoPlayerController.value.isPlaying &&
((!_videoPlayerController.value.isInitialized) ||
_videoPlayerController.value.isBuffering)
? const Center(
child: SizedBox(
width: 53,
height: 53,
child: CircularProgressIndicator(
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
strokeWidth: 1.5,
),
),
)
: const SizedBox(),
进度条
进度条我们可以使用Slider来实现
value控制当前进度,拖拽进度时有三个监听:onChanged、onChangeStart、onChangeEnd
SliderTheme可以定义它的样式,因为它在拖拽时会变的更清晰,圆形更大,所以定义一个isDragging控制它的状态。
它的样式:
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 2, //trackHeight:滑轨的高度
activeTrackColor: Colors.grey[400], //已滑过轨道的颜色
inactiveTrackColor: Colors.grey[700], //未滑过轨道的颜色
thumbColor: Colors.white, //滑块中心的颜色(小圆头的颜色)/滑块边缘的颜色
thumbShape: RoundSliderThumbShape(
//可继承SliderComponentShape自定义形状
disabledThumbRadius: 5, //禁用时滑块大小
enabledThumbRadius: _isDragging ? 5 : 3, //滑块大小
),
overlayColor: Colors.transparent,
trackShape: const RoundSliderTrackShape(radius: 8),
)
默认的两头的轨道,不是圆形的,所以需要自定义下他的TrackShape,我们自己来画一个轨道。继承SliderTrackShape然后paint中画一下两头的圆角
//进度条两头圆角
final RRect leftTrackSegment = RRect.fromLTRBR(trackRect.left, trackRect.top,
thumbCenter.dx - horizontalAdjustment, trackRect.bottom, Radius.circular(radius));
context.canvas.drawRRect(leftTrackSegment, leftTrackPaint);
final RRect rightTrackSegment = RRect.fromLTRBR(thumbCenter.dx + horizontalAdjustment, trackRect.top,
trackRect.right, trackRect.bottom, Radius.circular(radius));
context.canvas.drawRRect(rightTrackSegment, rightTrackPaint);
拖拽时变的清晰,简单,加个Opacity
Opacity(
opacity:_isDragging
? 1
: 0.5,
拖拽时清晰显示,静默时透明度50%。松手后过几秒要恢复,所以我们在onChangedEnd中加个计时器,到点就半透明。
void readyToHide() {
_hideTimer = Timer(const Duration(seconds: 5), () {
setState(() {
_isDragging = false;
});
});
}
onChangeEnd: (double value) async {
await _videoPlayerController.seekTo(Duration(
milliseconds: (value * _total).toInt()));
if (_hasPlay) {
play();
}
readyToHide();
}
结束同时也要设置视频的进度,保持同步
为了避免拖拽进度时视频也在播放,没有同步,所以onChangeStart中先暂停播放。
onChangeStart: (double value) {
_hideTimer?.cancel();
_isDragging = true;
if (_videoPlayerController.value.isPlaying) {
_hasPlay = true;
pause();
} else {
_hasPlay = false;
}
if (mounted) {
setState(() {});
}
}
onChanged中保持同步即可
onChanged: (double value) {
_videoPlayerController.seekTo(Duration(
milliseconds: (value * _total).toInt()));
_processNotifier.value = value;
}
使用ValueListenable来控制视频播放进度与进度条的同步
ValueListenableBuilder<double>(
valueListenable: _processNotifier,
builder: (context, process, child) {
return Slider(
value: process > 1
? 1
: process < 0
? 0
: process,
为了避免一些奇奇怪怪的进度值,做了一下处理。
用个计时器来拿到视频的进度并同步
if (_timer?.isActive ?? false) {
return;
}
_timer = Timer.periodic(const Duration(milliseconds: 10), (timer) async {
if (!_videoPlayerController.value.isPlaying) {
return;
}
// debug差距较大会有明显跳动,profile和release基本没有差别
_timeProcess = _videoPlayerController.value.position.inMilliseconds;
_processNotifier.value = _timeProcess / _total;
});
结合拖拽处理
基本上视频的功能就实现完成了。然后再处理一些小细节。
比如拖拽视频时,视频应该暂停,我加了个拖拽的回调,使视频页可以拿到
@override
void onDragStart() {
if (_videoPlayerController.value.isPlaying) {
_hasPlay = true;
pause();
} else {
_hasPlay = false;
}
if (mounted) {
setState(() {
_pageDragging = true;
});
}
}
Opacity(
opacity: _pageDragging // 隐藏进度条
? 0
: _isDragging
? 1
: 0.5
拖拽的时候就暂停,并且隐藏进度条
拖拽结束时恢复
@override
void onDragEnd(Offset endStatus) {
if (_hasPlay) {
play();
}
if (mounted) {
setState(() {
_pageDragging = false;
});
}
}
_hasPlay是用来判断是否被暂停了,假如点击关闭页面的话不用也可以。
这时候体验一下,会发现进入视频页或者拖拽取消重置,若是在加载的话,动画还没结束就loading了,或者拖拽的时候也可能在loading,体验不好,所以loading再加一点限制,等待一下路由动画,或者拖拽时也不显示
_animationFinish &&
!_isDragging &&
!_videoPlayerController.value.isPlaying &&
((!_videoPlayerController.value.isInitialized) ||
_videoPlayerController.value.isBuffering)
最后加个点击关闭(或点击播放暂停)。
void onTap() {
_videoPlayerController.pause();
Navigator.pop(context);
// if (_videoPlayerController.value.isInitialized) {
// if (_videoPlayerController.value.isPlaying) {
// pause();
// _isPlaying = false;
// } else {
// play();
// _isPlaying = true;
// }
// setState(() {});
// }
}
若是Hero时想看到原组件区域保留,可以在组件的Hero加个占位图
Hero(
tag: 'video_page_player',
placeholderBuilder: (
BuildContext context,
Size heroSize,
Widget child,
) {
return child;
}
The End
最后这个pyq视频播放效果就实现了。
还可以加个网络监听是否是wifi来决定视频是否自动播放等功能,若是有什么问题可以评论区讨论下