video_player插件提供了对视频播放的底层访问。Chewie对video_player进行封装,提供了一套友好的控制UI。android
chewie真的很棒,使用很是简洁。chewie提供了两套完善的UI风格样式,以及很大限度的自定义UI样式。但某些场景下,不能知足咱们的须要,那就得进行功能扩展了。自定义UI样式和功能扩展,就得对库源码有必定了解。git
先贴一个效果图:左边是默认效果,右边是固定尺寸的效果。github
添加依赖 pubspec.yamlbash
dependencies:
chewie: ^0.9.8+1
video_player: ^0.10.2+5
复制代码
封装一个widgetmarkdown
class ChewieVideoWidget1 extends StatefulWidget { //https://nico-android-apk.oss-cn-beijing.aliyuncs.com/landscape.mp4 final String playUrl; ChewieVideoWidget1(this.playUrl); @override _ChewieVideoWidget1State createState() => _ChewieVideoWidget1State(); } class _ChewieVideoWidget1State extends State<ChewieVideoWidget1> { VideoPlayerController _videoPlayerController; ChewieController _chewieController; @override void initState() { super.initState(); _videoPlayerController = VideoPlayerController.network(widget.playUrl); _chewieController = ChewieController( videoPlayerController: _videoPlayerController, autoPlay: true, //aspectRatio: 3 / 2.0, //customControls: CustomControls(), ); } @override void dispose() { _videoPlayerController.dispose(); _chewieController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( child: Chewie(controller: _chewieController,), ); } } 复制代码
最后注意,Android9.0后没法播放http的视频资源,须要在AndroidManifest.xml中配置android:usesCleartextTraffic="true"app
<application android:name="io.flutter.app.FlutterApplication" android:label="flutter_sample" android:usesCleartextTraffic="true" tools:targetApi="m" android:icon="@mipmap/ic"> 复制代码
到这里咱们就能愉快地使用Chewie咱们的视频了。less
固然了,咱们确定不能就这么知足了。async
首先进入Chewie的构造方法,在文件chewie_player.dart中,咱们发现就一个controller参数,那构造的参数就放在了ChewieController中了。ide
Chewie({ Key key, this.controller, }) : assert(controller != null, 'You must provide a chewie controller'), super(key: key); 复制代码
chewie_player.dart 代码继续往下翻,找到了ChewieController类。类的开头有一段注释介绍,说了ChewieController的功能。oop
同时也提到播放状态的变化不归它管,播放状态得找VideoPlayerController要。实际上与VideoPlayerController的互动,做者放在了MaterialControls/CupertinoControls中。
咱们先来看看找到了ChewieController类,截取一部分代码。从构造方法发现,该有视频配置几乎都有,好比说自动播放,循环,全屏,seekTo,禁音等等。同时还能够配置进度条颜色,甚至自定义的控制widget。
///ChewieController用于配置和驱动Chewie播放组件。 ///它提供了控制播放的方法,例如[pause]和[play], ///以及控制播放器呈现的方法,例如[enterFullScreen]或[exitFullScreen]。 ///此外,你还能够监听ChewieController呈现形式变化,例如进入和退出全屏模式。 ///若是要监听播放状态的变化(好比播放器的进度变化),还得依靠VideoPlayerController。 class ChewieController extends ChangeNotifier { ChewieController({ //video_player库中须要的VideoPlayerController this.videoPlayerController, //容器宽高比。不设置的话,会根据屏幕计算默认的长宽比。 //思考要设置成视频长宽比怎么作? this.aspectRatio, this.autoInitialize = false, this.autoPlay = false, this.startAt, this.looping = false, this.materialProgressColors,//进度条颜色 this.customControls,//自定义控制层样式 this.allowFullScreen = true,//是否容许全屏 this.allowMuting = true,//... this.routePageBuilder = null, ... }) : assert(videoPlayerController != null, 'You must provide a controller to play a video') { _initialize(); } 复制代码
在ChewieController的构造方法中调用了_initialize(),省略了一部分代码。咱们能够看到,若是设置了autoPlay,那视频这个时候就能播放了。此时完成了对VideoPlayerController配置和操做。如今video_player用起来了!
Future _initialize() async { await videoPlayerController.setLooping(looping); if ((autoInitialize || autoPlay) && !videoPlayerController.value.initialized) { await videoPlayerController.initialize(); } if (autoPlay) { await videoPlayerController.play(); } if (startAt != null) { await videoPlayerController.seekTo(startAt); } .... } 复制代码
接下来,产生了疑问:
咱们从新回到Chewie类中,找到它的build(BuildContext)方法。看一个Widget类的源码,首先是构造方法,其次就是build方法了。咱们看到了两个东西:
@override Widget build(BuildContext context) { return _ChewieControllerProvider( controller: widget.controller, child: PlayerWithControls(), ); } 复制代码
_ChewieControllerProvider。看到Provider,那必然就是InheritedWidget。这个地方就很清晰了。ChewieController经过InheritedWidget进行了共享。child(PlayerWithControls),能够经过widget树,向上找到ChewieController,同时拿到ChewieController中的VideoPlayerController
static ChewieController of(BuildContext context) { final chewieControllerProvider = context.inheritFromWidgetOfExactType(_ChewieControllerProvider) as _ChewieControllerProvider; return chewieControllerProvider.controller; } class _ChewieControllerProvider extends InheritedWidget { const _ChewieControllerProvider({ Key key, @required this.controller, @required Widget child, }) : assert(controller != null), assert(child != null), super(key: key, child: child); final ChewieController controller; @override bool updateShouldNotify(_ChewieControllerProvider old) => controller != old.controller; } 复制代码
controller的访问解决了,那再看看控制widget:PlayerWithControls。选取了部分代码,而且加了注释解读。
class PlayerWithControls extends StatelessWidget { @override Widget build(BuildContext context) { //拿到_ChewieControllerProvider共享的ChewieController final ChewieController chewieController = ChewieController.of(context); return Center( child: Container( //设置宽度:使用屏幕的宽度 width: MediaQuery.of(context).size.width, //使用配置的长宽比aspectRatio,若是没有默认值,就经过屏幕尺寸计算。 //咱们能够看到,始终都是用了固定的长宽比 child: AspectRatio( aspectRatio: chewieController.aspectRatio ?? _calculateAspectRatio(context), child: _buildPlayerWithControls(chewieController, context), ), ), ); } Container _buildPlayerWithControls( ChewieController chewieController, BuildContext context) { return Container( child: Stack( children: <Widget>[ //构建播放器Widget, //VideoPlayer构造使用了chewieController中的videoPlayerController。 Center( child: AspectRatio( aspectRatio: chewieController.aspectRatio ?? _calculateAspectRatio(context), child: VideoPlayer(chewieController.videoPlayerController), ), ), //构建控制widget,customControls/meterialControls/cupertinoControls _buildControls(context, chewieController), ], ), ); } //计算长宽比。 double _calculateAspectRatio(BuildContext context) { final size = MediaQuery.of(context).size; final width = size.width; final height = size.height; return width > height ? width / height : height / width; } } 复制代码
咱们进入 _buildControls(BuildContext)。进入其中一个Controls:MaterialControls。你在界面看到的开始/暂停/全屏/进度条等等控件就都在这了。截取的部分源码,有省略。 UI与底层的交互其实就两部分:首先是读取底层数据渲染UI和设置对底层的控制事件。其次是获取到底层的数据变化从而从新绘制UI。
1.播放按钮widget使用VideoPlayerController.VideoPlayerValue数据构建,接着按钮点击操做VideoPlayerController的过程。
///在build(BuildContext)中,咱们找到了构建Widget的内容。 @override Widget build(BuildContext context) { child: Column( children: <Widget>[ _buildHitArea(), _buildBottomBar(context), } ///进入_buildBottomBar(BuildContext),。 AnimatedOpacity _buildBottomBar(BuildContext context,) { return AnimatedOpacity( child: Row( children: <Widget>[ _buildPlayPause(controller), _buildProgressBar(), } //进入 _buildPlayPause(controller) GestureDetector _buildPlayPause(VideoPlayerController controller) { return GestureDetector( onTap: _playPause, child: Container( child: Icon( controller.value.isPlaying ? Icons.pause : Icons.play_arrow, } //播放widget 触发的点击事件。 void _playPause() { setState(() { if (controller.value.isPlaying) { controller.pause(); } else { controller.play(); } }); } 复制代码
2.经过VideoPlayerController.addListener监听变化,当变化时,取出VideoPlayerController.VideoPlayerValue。 setState,更新_latestValue,从而达到刷新UI的操做。
@override void didChangeDependencies() { if (_oldController != chewieController) { _dispose(); _initialize(); } super.didChangeDependencies(); } Future<Null> _initialize() async { controller.addListener(_updateState); _updateState(); } void _updateState() { setState(() { _latestValue = controller.value; }); } 复制代码
VideoPlayerValue包含了实时的播放器数据。
VideoPlayerValue({ @required this.duration, this.size, this.position = const Duration(), this.buffered = const <DurationRange>[], this.isPlaying = false, this.isLooping = false, this.isBuffering = false, this.volume = 1.0, this.errorDescription, }); 复制代码
到这里咱们分析了部分主流程的代码。固然了,Chewie库远不止这些。
了解了这些,咱们就能够入门了。能够尝试解决下面的问题了。
这个问题就比较简单了,构建ChewieController时,咱们能够设置customControls。咱们能够参考MaterialControls,写写咱们本身的controls。在这个代码仓库里有参考代码custom_controls.dart。代码在这里
ChewieController( videoPlayerController: _videoPlayerController, autoPlay: true, customControls: CustomControls(), ); 复制代码
设想这样的场景:咱们但愿在一个300*300的容器内播放视频,且视频不该当出现变形。
翻看源码发现,Chewie只提供了改变长宽比aspectRatio的机会。容器的widget是写死的PlayerWithControls。而从上面的源码能够看到的宽度是固定屏幕的宽度。我猜测是做者不想外部调用破坏到内部自己的结构,尽量的保证原子性和高复用性。
那若是咱们要实现效果就只能把 Chewie的代码copy出来,而后进行改造了。改造部分以下。 0.把Chewie库中全部代码复制出来,同时去掉pubspec.yaml中的chewie依赖
video_player: ^0.10.2+5 screen: 0.0.5 #chewie: ^0.9.8+1 复制代码
1.chewie_player.dart
class Chewie extends StatefulWidget { ///增长一个课配置的child Widget child ; Chewie({ Key key, this.controller, this.child, }) : assert(controller != null, 'You must provide a chewie controller'), super(key: key); } class ChewieState extends State<Chewie> { @override Widget build(BuildContext context) { return _ChewieControllerProvider( controller: widget.controller, //不在写死PlayerWithControls child: widget.child??PlayerWithControls(), ); } } 复制代码
2.构造Chewie地方增长child属性
Chewie(
controller: _chewieController,
child: CustomPlayerWithControls(),
)
复制代码
3.自定义的控制Widget。custom_player_with_controls.dart 这里
class CustomPlayerWithControls extends StatelessWidget { final double width; final double height; ///入参增长容器宽和高 CustomPlayerWithControls({ Key key, this.width = 300, this.height = 300, }) : super(key: key); @override Widget build(BuildContext context) { final ChewieController chewieController = ChewieController.of(context); return _buildPlayerWithControls(chewieController, context); } Container _buildPlayerWithControls( ChewieController chewieController, BuildContext context) { return Container( width: width, height: height, child: Stack( children: <Widget>[ //自定义的部分主要在这个容器里面了。 VideoPlayerContainer(width, height), _buildControls(context, chewieController), ], ), ); } Widget _buildControls( BuildContext context, ChewieController chewieController, ) { return chewieController.showControls && chewieController.customControls != null ? chewieController.customControls : Container(); } } ///与源码中的PlayerWithControls相比,VideoPlayerContainer继承了StatefulWidget,监听videoPlayerController的变化,拿到视频宽高比。 class VideoPlayerContainer extends StatefulWidget { final double maxWidth; final double maxHeight; ///根据入参的宽高,计算获得容器宽高比 double _viewRatio; VideoPlayerContainer( this.maxWidth, this.maxHeight, { Key key, }) : _viewRatio = maxWidth / maxHeight, super(key: key); @override _VideoPlayerContainerState createState() => _VideoPlayerContainerState(); } class _VideoPlayerContainerState extends State<VideoPlayerContainer> { double _aspectRatio; ChewieController chewieController; @override void dispose() { _dispose(); super.dispose(); } void _dispose() { chewieController.videoPlayerController.removeListener(_updateState); } @override void didChangeDependencies() { final _oldController = chewieController; chewieController = ChewieController.of(context); if (_oldController != chewieController) { _dispose(); chewieController.videoPlayerController.addListener(_updateState); _updateState(); } super.didChangeDependencies(); } void _updateState() { VideoPlayerValue value = chewieController?.videoPlayerController?.value; if (value != null) { double newAspectRatio = value.size != null ? value.aspectRatio : null; if (newAspectRatio != null && newAspectRatio != _aspectRatio) { setState(() { _aspectRatio = newAspectRatio; }); } } } @override Widget build(BuildContext context) { if (_aspectRatio == null) { return Container(); } double width; double height; ///两个宽高比进行比较,保证VideoPlayer不超出容器,且不会产生变形 if (_aspectRatio > widget._viewRatio) { width = widget.maxWidth; height = width / _aspectRatio; } else { height = widget.maxHeight; width = height * _aspectRatio; } return Center( child: Container( width: width, height: height, child: VideoPlayer(chewieController.videoPlayerController), ), ); } } 复制代码
chewie的使用就介绍到这了。 全部代码都在这里。 入口文件: main_video.dart