Flutter 實現影片全螢幕播放邏輯及解析
- 2020 年 2 月 24 日
- 筆記
一、前言
相信做過移動端影片開發的同學應該了解,想要實現影片從普通播放到全螢幕播放的邏輯並不是很簡單,比如在 GSYVideoPlayer 中的動態全螢幕切換效果,就使用了創建全新的 Surface
來替換實現:
- 創建全新的
Surface
,並將對於的View
添加到應用頂層的DecorView
中; - 在全螢幕時將新創建的
Surface
並設置到 Player Core ; - 同步兩個
View
的播放狀態參數和旋轉系統介面; - 退出全螢幕時移除
DecorView
中的Surface
,切換 List Item 中的Surface
給 Player Core ,同步狀態。

image
當然,不同的播放內核可能還需要做一些額外操作,但是這一切在 Flutter 中就變得極為簡單。
事實上 Flutter 中實現全螢幕切換效果很簡單,後面會一併介紹為什麼在 Flutter 上實現會如此簡單。
二、實現效果
如下圖所示是 Flutter 中實現後的全螢幕效果,而實現這個效果的關鍵就是跳堆棧就可以了!是的,Flutter 中簡單地跳頁面就能夠實現無縫的全螢幕切換。

image
如下程式碼所示,首先在正常播放頁面下加入官方 video_player
插件的 VideoPlayer
控制項,並且初始化 VideoPlayerController
用於載入需要播放的影片並初始化,另外此處還用了 Hero
控制項用於實現頁面跳轉過渡的動畫效果。
@override void initState() { super.initState(); _controller = VideoPlayerController.network( 'https://res.exexm.com/cw_145225549855002') ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. setState(() {}); }); } Container( height: 200, margin: EdgeInsets.only( top: MediaQueryData.fromWindow( WidgetsBinding.instance.window) .padding .top), color: Colors.black, child: _controller.value.initialized ? Hero( tag: "player", child: AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), ), ) : Container( alignment: Alignment.center, child: CircularProgressIndicator(), ), ))
如下程式碼所示,之後在全螢幕的頁面中同樣使用 Hero
控制項和 VideoPlayer
控制項實現過渡動畫和影片渲染。
這裡的 VideoPlayerController
可以通過構造方法傳遞進來,也可以通過 InheritedWidget
實現共享傳遞,只要是和前面普通播放介面的 controller
是同一個即可。
Container( color: Colors.black, child: Stack( children: <Widget>[ Center( child: Hero( tag: "player", child: AspectRatio( aspectRatio: widget.controller.value.aspectRatio, child: VideoPlayer(widget.controller), ), ), ), Padding( padding: EdgeInsets.only(top: 25, right: 20), child: IconButton( icon: const BackButtonIcon(), color: Colors.white, onPressed: () { Navigator.pop(context); }, ), ) ], ), )
另外在 Flutter 中,只需要通過 SystemChrome.setPreferredOrientations
方法就可以快速實現應用的橫豎屏切換。
最後如下程式碼所示,只需要通過 Navigator
調用頁面跳轉就可以實現全螢幕和非全螢幕的無縫切換了。
Navigator.of(context) .push(MaterialPageRoute(builder: (context) { return VideoFullPage(_controller); }));
是不是很簡單,只需要 VideoPlayer
、Hero
和 Navigator
就可以快速實現全螢幕切換播放的效果,那為什麼在 Flutter 上可以這樣簡單的實現呢?
三、實現邏輯
之所以可以如此簡單地實現動態化全螢幕效果,其實主要涉及到 video_player
插件在 Flutter 上的實現:外接紋理 Texture
。
因為 Flutter 中的控制項基本上是平台無關的,而其控制項主要是由 Flutter Engine 直接繪製,簡單地說就是:原生平台僅僅提供了一個 Activity
/ ViewController
容器, 之後由容器內提供一個 Surface
給 Flutter Engine 用戶繪製。
所以 Flutter 中控制項的渲染堆棧是獨立的,沒辦法和原生平台直接混合使用,這時候為了能夠在 Flutter 中插入原生平台的部分功能,Flutter 除了提供了 PlatformView
這樣的實現邏輯之外,還提供了 Texture
作為 外接紋理的支援。

image
如上圖所示,在《Flutter 完整實戰詳解》 中介紹過,Flutter 的介面渲染是需要經歷 Widget
-> RenderObject
-> Layer
的過程,而在 Layer
的渲染過程中,當出現一個 TextureLayer
節點時,說明這個節點使用了 Flutter 中的 Texture
控制項,那麼這個控制項的內容就會由原生平台提供,而管理 Texture
主要是通過 textureId
進行識別的。

image
舉個例子,在 Android 原生層中 video_player
使用的是 exoplayer
播放內核,那麼如上圖所示,VideoPlayerController
會在初始化的時候通過 MethodChannel
和原生端通訊,之後準備好播放內核和 Surface
,最後將對應的 textureId
返回到 Dart 中。
所以在前面的程式碼中,需要在全螢幕和非全螢幕頁面使用同一個 VideoPlayerController
,這樣它們就具備了同一個 textureId
。
具備同一個 textureId
後,那麼只要原生層不停止播放, textureId
對應的原生數據就一直處於更新狀態,而這時候雖然跳轉路由頁面,但不同的 VideoPlayer
內部的 Texture
控制項用的是同一個 VideoPlayerController
,也就是同一個 textureId
,所以它們會呈現出通用的畫面。
如下圖所示,這個過程簡單總結就是: Flutter 和原生平台通過 PixelBuffer
為介質進行交互,原生層將數據寫入 PixelBuffer
,Flutter 通過註冊好的 textureId
獲取到 PixelBuffer
之後由 Flutter Engine 繪製。

image
最後需要注意的是,在 iOS 上在實現頁面旋轉時, SystemChrome.setPreferredOrientations
方法可能會出現無效,這個問題在 issue #23913 和 #13238 中有提及,這裡可能需要自己多實現一個原生介面進行兼容,當然在 auto_orientation 或者 orientation 等第三方庫也進行了這方面的兼容。
另外 iOS 的頁面旋轉還確定是否打開了旋轉配置的開關。

image
資源推薦
- 本文 Demo : flutter_video_full_controller
- Github : https://github.com/CarGuo
- 開源 Flutter 完整項目:https://github.com/CarGuo/GSYGithubAppFlutter
- 開源 Flutter 多案例學習型項目: https://github.com/CarGuo/GSYFlutterDemo
- 開源 Fluttre 實戰電子書項目:https://github.com/CarGuo/GSYFlutterBook
- 開源 React Native 項目:https://github.com/CarGuo/GSYGithubApp

image