Flutter 开发过程中视频列表应该是比较复杂的功能,因为这里涉及到同一个视频在不同页面之间无缝的切换,控制其他在播的视频停止播放并播放当前点击的视频,还有视频秒开,视频弹幕等问题。在这里仅仅实现列表播放和切换到全屏播放功能,其他功能后面有时间再去进行研究。Flutter 并没有提供自带的视频播放控件,所以必须依靠第三方插件来实现,比如 VideoPlayer,fijkplayer 等等,但是 VideoPlayer 是使用相对比较简单的,所以先介绍该视频播放器。
dependencies:
video_player: ^2.2.3
class VideoPlayer extends StatefulWidget {
/// Uses the given [controller] for all video rendered in this widget.
VideoPlayer(this.controller);
/// The [VideoPlayerController] responsible for the video being rendered in
/// this widget.
final VideoPlayerController controller;
@override
_VideoPlayerState createState() => _VideoPlayerState();
}
VideoPlayer 是播放视频的控件,VideoPlayerController 是视频播放的控制器。
初始化 VideoPlayerController 有多种方法,最常见的就是通过 asset 或者 network 静态方法来创建。通过 play() 和 pause() 方法来实现播放和停止播放,dispose() 方法在 Widget 执行 onDispose 之前执行调用可以释放资源。同时 VideoPlayerController 有value属性来获取总时长 duration,当前播放进度 position, 和播放状态 isPlaying 等其他属性内容,value的类型结构如下:
class VideoPlayerValue {
/// Constructs a video with the given values. Only [duration] is required. The
/// rest will initialize with default values when unset.
VideoPlayerValue({
required this.duration,
this.size = Size.zero,
this.position = Duration.zero,
this.caption = Caption.none,
this.buffered = const [],
this.isInitialized = false,
this.isPlaying = false,
this.isLooping = false,
this.isBuffering = false,
this.volume = 1.0,
this.playbackSpeed = 1.0,
this.errorDescription,
});
....
}
如上可以看出,可以获取当前播放控制器的属性有很多。如上基本上已经介绍了 VideoPlayer 常见的所有功能,下面会实现一个视频列表页面,该列表可以在点击播放其他视频的时候,停止播放当前视频,并且可以进入到全面播放状态。
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ijkplayer/lifecycle/lifecycle_state.dart';
import 'package:flutter_ijkplayer/util/video_data.dart';
import 'package:flutter_ijkplayer/widget/video_item_new.dart';
import 'package:video_player/video_player.dart';
class ListVideoPage extends StatefulWidget {
const ListVideoPage({Key? key}) : super(key: key);
@override
_ListVideoPageState createState() => _ListVideoPageState();
}
class _ListVideoPageState extends LifeCycleState {
List _listVideo = [
"assets/video/anranxiaohun.mp4",
"assets/video/xiangxinai.mp4",
"assets/video/ycxhs098.mp4",
//"http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4",
//"http://vjs.zencdn.net/v/oceans.mp4",
//"https://media.w3.org/2010/05/sintel/trailer.mp4",
//"http://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4"
];
//创建一个多订阅流
final StreamController _streamController = new StreamController.broadcast();
//播放控制器
late VideoPlayerController _videoPlayerController;
//是否滚动
bool isScroll = false;
@override
void initState() {
super.initState();
_streamController.stream.listen((event) {
if (!mounted) return;
if (_videoPlayerController != event) {
_videoPlayerController.pause(); // 原来的controller进行暂停
}
_videoPlayerController = event; //赋值新的videoController
});
_videoPlayerController = VideoPlayerController.asset(_listVideo[0])
..initialize().then((_) {
setState(() {});
});
}
@override
void dispose() {
_streamController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('视频列表'),
),
body: buildListView(),
);
}
buildListView() {
return NotificationListener(
onNotification: (ScrollNotification notification) {
if (notification.runtimeType == ScrollStartNotification) {
//开始滚动
isScroll = true;
//setState(() {});
} else if (notification.runtimeType == ScrollEndNotification) {
//结束滚动
//setState(() {});
isScroll = false;
}
return false;
},
child: ListView.separated( //创建视频列表
separatorBuilder: (context, index) {
return Divider();
},
//不缓存
cacheExtent: 2,
//加载20条数据
itemCount: 20,
itemBuilder: (BuildContext context, int index) {
return buildListViewItem(index);
},
));
}
buildListViewItem(index) {
return Container(
height: 240,
child: Column(
children: [
Container(
padding: EdgeInsets.only(top: 10),
child: Row(
children: [
Container(
child: Icon(
Icons.account_circle,
size: 20,
),
),
SizedBox(
width: 10,
),
Text(
'Flutter开发-$index',
style: TextStyle(fontSize: 16),
)
],
),
),
Expanded(
child: Container(
child: VideoItemNew(
url: _listVideo[index % _listVideo.length],
streamController: _streamController,
//isScroll: isScroll
isScroll: false,
type: VideoType.asset,
)),
)
],
),
);
}
@override
void onCreate() {
super.onCreate();
log("视频列表 onCreate");
}
@override
void onResume() {
super.onResume();
log("视频列表 onResume");
}
@override
void onPause() {
super.onPause();
log("视频列表 onPause");
}
@override
void onForeground() {
super.onForeground();
log("视频列表 onForeground");
}
@override
void onBackground() {
super.onBackground();
log("视频列表 onBackground");
}
@override
void onDestroy() {
super.onDestroy();
log("视频列表 onDestroy");
}
}
视频列表项代码
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_ijkplayer/page/full_screen_advance_page.dart';
import 'package:flutter_ijkplayer/page/full_screen_page.dart';
import 'package:flutter_ijkplayer/util/date_time_util.dart';
import 'package:flutter_ijkplayer/util/video_data.dart';
import 'package:flutter_ijkplayer/widget/progress_colors.dart';
import 'package:video_player/video_player.dart';
import 'material_video_progressbar.dart';
class VideoItemNew extends StatefulWidget {
//视频类型
VideoType type;
//ListView是否在滚动
bool isScroll;
String url;
//全局流控制器
StreamController streamController;
VideoItemNew({Key? key,
required this.url,
required this.streamController,
required this.isScroll,
this.type=VideoType.asset}) : super(key: key);
@override
_VideoItemState createState() => _VideoItemState();
}
class _VideoItemState extends State with TickerProviderStateMixin{
late VideoPlayerController _controller;
bool isPlaying = false; // true 播放 false 不播放
@override
void initState() {
super.initState();
_controller = widget.type == VideoType.asset
? VideoPlayerController.asset(widget.url)
: VideoPlayerController.network(widget.url)
..initialize().then((_){
setState(() {});
});
_controller.addListener(() {
//监听播放器的状态
//如果播放器不播放了,那么就需要更新isPlaying的状态
if (!mounted) return;
if (isPlaying && !_controller.value.isPlaying){
isPlaying = false;
setState(() {});
}
setState(() {
_sliderValue = _controller.value.position.inMilliseconds/
_controller.value.duration.inMilliseconds;
});
});
}
@override
void dispose() {
if(!mounted) return;
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
//widget.isScroll 是否在滚动状态,如果是的,则显示加载中,否则就加载播放器视图
return widget.isScroll
? Center(child: Text('加载中'),)
: buildVideo();
}
buildVideo() {
return Stack(
children: [
//占满全屏并设置播放器
Positioned.fill(
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
),
//编辑透明文字
buildCenterText(),
//底部滑动条
Positioned(
bottom: 2,
right: 10,
left: 10,
child: isPlaying? Container(): buildSlider()
)
],
);
}
buildCenterText() {
return Positioned.fill( //占满全屏
child: AnimatedOpacity(
opacity: _controller.value.isPlaying? 0: 1,
duration: Duration(milliseconds: 500),
child: GestureDetector(
onTap: buildOnTap,
child: Container(
color: Colors.grey.withOpacity(0.5),
child: Center(
child: Icon(
_controller.value.isPlaying
? Icons.pause
: Icons.play_arrow,
color: Colors.white,
),
),
),
),
),
);
}
//点击播放
buildOnTap(){
isPlaying = !isPlaying;
if (isPlaying){
widget.streamController.add(_controller);
//视频播放总时长
Duration duration = _controller.value.duration;
//视频播放进度
Duration currPosition = _controller.value.position;
if (currPosition == duration){
_controller.seekTo(Duration.zero);
}
isPlaying= true;
_controller.play();
} else {
isPlaying= false;
_controller.pause();
}
setState(() {
});
}
//当前播放器播放进度
double _sliderValue = 0.0;
buildSlider() {
return Container(
height: 48,
child: Row(
children: [
//进度
Text(buildTextString(_controller.value.position),
style: TextStyle(color: Colors.white, fontSize: 14),
),
//进度条
//initSlider(),
_buildProgressBar(),
//总进度
Text(buildTextString(_controller.value.duration),
style: TextStyle(color: Colors.white, fontSize: 14),
),
initFullScreen()
],
),
);
}
//这里弃用了,不使用Slider来作为视频播放器的进度条,我们通过自定义来实现
initSlider() {
return Expanded(
child: Slider(
activeColor: Colors.lightBlue,
inactiveColor: Colors.grey,
//默认为0 必须小于或者等于最大值
//如果min 和 max 相同,则滑块禁用
min: 0,
//默认1, 必须大于或者等于min
max: 1,
value: _sliderValue,
onChanged: (double value){
setState(() {
_sliderValue = value;
//通过进度条修改视频播放进度
_controller.seekTo(_controller.value.duration*_sliderValue);
});
},
//滑块开始滑动
onChangeStart: (double value){
//log("onChangeStart...");
},
//滑块结束
onChangeEnd: (double value){
//log("onChangeEnd....");
},
)
);
}
initFullScreen(){
return GestureDetector(
onTap:(){
Navigator.of(context).push(MaterialPageRoute(builder: (context){
//return VideoFullScreenPage(url: widget.url, videoType: widget.type);
return VideoFullScreenAdvancePage(url: widget.url, videoType: widget.type, controller: _controller,);
}));
},
child: Icon(
Icons.fullscreen,
color: Colors.white,
),
);
}
bool _dragging = false;
//自定义MaterialVideoProgressBar来实现进度条
Widget _buildProgressBar() {
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: 15,left: 15),
child: MaterialVideoProgressBar(
_controller,
onDragStart: () {
setState(() {
_dragging = true;
});
},
onDragEnd: () {
setState(() {
_dragging = false;
});
},
colors: ProgressColors(
playedColor: Theme.of(context).accentColor,
handleColor: Theme.of(context).accentColor,
bufferedColor: Theme.of(context).backgroundColor,
backgroundColor: Theme.of(context).disabledColor),
onDragUpdate: () {
},
),
),
);
}
}
自定义播放器进度
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_ijkplayer/widget/progress_colors.dart';
import 'package:video_player/video_player.dart';
class MaterialVideoProgressBar extends StatefulWidget {
final VideoPlayerController controller;
final ProgressColors colors;
final Function() onDragStart;
final Function() onDragEnd;
final Function() onDragUpdate;
MaterialVideoProgressBar(
this.controller, {
required ProgressColors colors,
required this.onDragEnd,
required this.onDragStart,
required this.onDragUpdate,
Key? key,}): colors = colors, super(key: key);
@override
_VideoProgressBarState createState() {
return _VideoProgressBarState();
}
}
class _VideoProgressBarState extends State {
late VoidCallback listener;
bool _controllerWasPlaying = false;
VideoPlayerController get controller => widget.controller;
_VideoProgressBarState() {
listener = () {
if (!mounted) return;
setState(() {});
};
}
@override
void initState() {
super.initState();
controller.addListener(listener);
}
@override
void deactivate() {
controller.removeListener(listener);
super.deactivate();
}
@override
Widget build(BuildContext context) {
void seekToRelativePosition(Offset globalPosition) {
final box = context.findRenderObject() as RenderBox;
final Offset tapPos = box.globalToLocal(globalPosition);
final double relative = tapPos.dx / box.size.width;
final Duration position = controller.value.duration * relative;
controller.seekTo(position);
}
return GestureDetector(
onHorizontalDragStart: (DragStartDetails details) {
if (!controller.value.isInitialized) {
return;
}
_controllerWasPlaying = controller.value.isPlaying;
if (_controllerWasPlaying) {
controller.pause();
}
if (widget.onDragStart != null) {
widget.onDragStart();
}
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
if (!controller.value.isInitialized) {
return;
}
seekToRelativePosition(details.globalPosition);
if (widget.onDragUpdate != null) {
widget.onDragUpdate();
}
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_controllerWasPlaying) {
controller.play();
}
if (widget.onDragEnd != null) {
widget.onDragEnd();
}
},
onTapDown: (TapDownDetails details) {
if (!controller.value.isInitialized) {
return;
}
seekToRelativePosition(details.globalPosition);
},
child: Center(
child: Container(
height: MediaQuery.of(context).size.height / 2,
width: MediaQuery.of(context).size.width,
color: Colors.transparent,
child: CustomPaint(
painter: _ProgressBarPainter(
controller.value,
widget.colors,
),
),
),
),
);
}
}
class _ProgressBarPainter extends CustomPainter {
_ProgressBarPainter(this.value, this.colors);
VideoPlayerValue value;
ProgressColors colors;
@override
bool shouldRepaint(CustomPainter painter) {
return true;
}
@override
void paint(Canvas canvas, Size size) {
const height = 2.0;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(0.0, size.height / 2),
Offset(size.width, size.height / 2 + height),
),
const Radius.circular(4.0),
),
colors.backgroundPaint,
);
if (!value.isInitialized) {
return;
}
final double playedPartPercent =
value.position.inMilliseconds / value.duration.inMilliseconds;
final double playedPart =
playedPartPercent > 1 ? size.width : playedPartPercent * size.width;
for (final DurationRange range in value.buffered) {
final double start = range.startFraction(value.duration) * size.width;
final double end = range.endFraction(value.duration) * size.width;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(start, size.height / 2),
Offset(end, size.height / 2 + height),
),
const Radius.circular(4.0),
),
colors.bufferedPaint,
);
}
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(0.0, size.height / 2),
Offset(playedPart, size.height / 2 + height),
),
const Radius.circular(4.0),
),
colors.playedPaint,
);
canvas.drawCircle(
Offset(playedPart, size.height / 2 + height / 2),
height * 3,
colors.handlePaint,
);
}
}
全屏播放代码
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ijkplayer/util/screen_util.dart';
import 'package:flutter_ijkplayer/util/video_data.dart';
import 'package:video_player/video_player.dart';
//可以继续接着外部的进度来进行播放
class VideoFullScreenAdvancePage extends StatefulWidget {
String url;
VideoType videoType;
VideoPlayerController controller;
VideoFullScreenAdvancePage({Key? key,
required this.url,
required this.videoType,
required this.controller
}) : super(key: key);
@override
_VideoFullScreenPageState createState() => _VideoFullScreenPageState();
}
class _VideoFullScreenPageState extends State {
late VideoPlayerController _controller;
//是否是横屏 默认横屏
bool isHorizontal = true;
@override
void initState() {
super.initState();
//如果不进行初始化,而是从外部传过来的
// _controller = widget.videoType == VideoType.asset
// ? VideoPlayerController.asset(widget.url)
// : VideoPlayerController.network(widget.url)
// ..initialize().then((value){
// setState(() {});
// });
_controller = widget.controller;
_controller.addListener(() {
//当前进度 == 总进度
if (_controller.value.position == _controller.value.duration){
//播放结束之后,自动退出
Future.delayed(Duration.zero, (){
//Navigator.of(context).pop();
});
}
});
//将状态栏的颜色改变为透明
//ScreenUtil.setStatusBarColor(Colors.black);
//设置横屏
ScreenUtil.setHorizontal();
}
@override
void dispose() {
//设置竖屏
ScreenUtil.setVertical();
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top, SystemUiOverlay.bottom]);
super.dispose();
}
@override
Widget build(BuildContext context) {
//显示顶部栏并隐藏底部栏
//SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top]);
//显示底部栏并隐藏顶部栏
//SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
//隐藏顶部栏和底部栏
SystemChrome.setEnabledSystemUIOverlays([]);
return Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
alignment: Alignment.center,
color: Colors.black,
child: Stack(
children: [
//初始化视频
initVideo(),
//初始化开始按钮
initStartButton(),
//返回按钮
initPopButton()
],
),
),
);
}
initVideo(){
return Center(
child: _controller.value.isInitialized
? AspectRatio(
child: VideoPlayer(_controller),
aspectRatio: _controller.value.aspectRatio)
: Container(),
);
}
initStartButton(){
return Positioned.fill(
child: Stack(
children: [
//初始化渐变开始按钮
initOpacityStartButton(),
//竖屏按钮
initPosition()
],
)
);
}
initOpacityStartButton() {
return AnimatedOpacity(
opacity: _controller.value.isPlaying? 0: 1,
duration: Duration(seconds: 1),
child: GestureDetector(
onTap: (){
_controller.value.isPlaying? _controller.pause(): _controller.play();
setState(() {});
},
child: Container(
color: Colors.grey.withOpacity(0.3),
child: Center(
child: Icon(
_controller.value.isPlaying? Icons.pause: Icons.play_arrow,
color: Colors.white,
),
),
),
),
);
}
initPosition() {
return Positioned(
right: 40,
bottom: 40,
child: GestureDetector(
onTap: (){
isHorizontal = !isHorizontal;
isHorizontal? ScreenUtil.setHorizontal(): ScreenUtil.setVertical();
setState(() {});
},
child: Container(
width: 100,
height: 45,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.8),
borderRadius: BorderRadius.circular(10)
),
child: Text(
isHorizontal? "竖屏" : "横屏",
style: TextStyle(color: Colors.white),
),
),
));
}
initPopButton(){
return Positioned(
top: 20,
left: 20,
child: GestureDetector(
onTap: (){
//如果和列表项是共享的,那么就不能将controller给dispose
//_controller.dispose();
if (_controller.value.isPlaying){
_controller.pause();
}
Navigator.of(context).pop();
},
child: Container(
width: 100,
height: 45,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.8),
borderRadius: BorderRadius.circular(10)
),
child: Icon(
Icons.keyboard_return,
color: Colors.white,),
),
)
);
}
}
ScreenUtil 类负责设置屏幕横屏和竖屏,这里用到了 SystemChrome, 该类可以通过静态方法 setPreferredOrientations() 来进行横竖屏设置,静态方法 setSystemUIOverlayStyle 来设置状态栏的颜色。
import 'package:flutter/services.dart';
class ScreenUtil{
//横屏
static void setHorizontal(){
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]
);
}
//竖屏
static void setVertical(){
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]
);
}
//修改顶部状态栏颜色
static void setStatusBarColor(Color color){
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: color);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
}
}
其他代码
//video_data.dart
enum VideoType{
asset, //本地视频
network //网络视频
}
//date_time_util.dart
String buildTextString(Duration duration){
int inMinutes = duration.inMinutes;//
int inSeconds = duration.inSeconds%60; //时间跨越整分钟数
String inMinutesStr = inMinutes <10? "0$inMinutes": "$inMinutes";
String inSecondsStr = inSeconds <10? "0$inSeconds": "$inSeconds";
return "$inMinutesStr:$inSecondsStr";
}
如下就是视频播放列表的三种样式:
通过上面的代码,虽然可以实现简单的视频播放功能,但是仍然有很多缺陷,比如滑动时比较卡顿,每一个列表项都对应一个 VideoPlayer,是否合理,能否重复利用,减少对原生系统的资源的利用,这样就可以提供内存使用效率,降低内存消耗。同时也有体验上的优化,比如在视频全屏播放的时候,是否可以加上音量和亮度调节,倍速播放,连续播放,小窗播放等功能。