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