一、前言相信做過移動(dòng)端視頻開發(fā)的同學(xué)應(yīng)該了解,想要實(shí)現(xiàn)視頻從普通播放到全屏播放的邏輯并不是很簡單,比如在 GSYVideoPlayer 中的動(dòng)態(tài)全屏切換效果,就使用了創(chuàng)建全新的 Surface 來替換實(shí)現(xiàn): 創(chuàng)建全新的 Surface ,并將對(duì)于的 View 添加到應(yīng)用頂層的 DecorView 中; 在全屏?xí)r將新創(chuàng)建的 Surface 并設(shè)置到 Player Core ; 同步兩個(gè) View 的播放狀態(tài)參數(shù)和旋轉(zhuǎn)系統(tǒng)界面; 退出全屏?xí)r移除 DecorView 中的 Surface ,切換 List Item 中的 Surface 給 Player Core ,同步狀態(tài)。

當(dāng)然,不同的播放內(nèi)核可能還需要做一些額外操作,但是這一切在 Flutter 中就變得極為簡單。 事實(shí)上 Flutter 中實(shí)現(xiàn)全屏切換效果很簡單,后面會(huì)一并介紹為什么在 Flutter 上實(shí)現(xiàn)會(huì)如此簡單。
二、實(shí)現(xiàn)效果如下圖所示是 Flutter 中實(shí)現(xiàn)后的全屏效果,而實(shí)現(xiàn)這個(gè)效果的關(guān)鍵就是跳堆棧就可以了!是的,F(xiàn)lutter 中簡單地跳頁面就能夠?qū)崿F(xiàn)無縫的全屏切換。 
如下代碼所示,首先在正常播放頁面下加入官方 video_player 插件的 VideoPlayer 控件,并且初始化 VideoPlayerController 用于加載需要播放的視頻并初始化,另外此處還用了 Hero 控件用于實(shí)現(xiàn)頁面跳轉(zhuǎn)過渡的動(dòng)畫效果。 @override
void initState() {
super.initState();
_controller = VideoPlayerController.network(
'https://res./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 控件實(shí)現(xiàn)過渡動(dòng)畫和視頻渲染。 這里的 VideoPlayerController 可以通過構(gòu)造方法傳遞進(jìn)來,也可以通過 InheritedWidget 實(shí)現(xiàn)共享傳遞,只要是和前面普通播放界面的 controller 是同一個(gè)即可。 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 方法就可以快速實(shí)現(xiàn)應(yīng)用的橫豎屏切換。 最后如下代碼所示,只需要通過 Navigator 調(diào)用頁面跳轉(zhuǎn)就可以實(shí)現(xiàn)全屏和非全屏的無縫切換了。 Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
return VideoFullPage(_controller);
}));
是不是很簡單,只需要 VideoPlayer 、Hero 和 Navigator 就可以快速實(shí)現(xiàn)全屏切換播放的效果,那為什么在 Flutter 上可以這樣簡單的實(shí)現(xiàn)呢? 三、實(shí)現(xiàn)邏輯之所以可以如此簡單地實(shí)現(xiàn)動(dòng)態(tài)化全屏效果,其實(shí)主要涉及到 video_player 插件在 Flutter 上的實(shí)現(xiàn):外接紋理 Texture 。 因?yàn)?Flutter 中的控件基本上是平臺(tái)無關(guān)的,而其控件主要是由 Flutter Engine 直接繪制,簡單地說就是:原生平臺(tái)僅僅提供了一個(gè) Activity / ViewController 容器, 之后由容器內(nèi)提供一個(gè) Surface 給 Flutter Engine 用戶繪制。 所以 Flutter 中控件的渲染堆棧是獨(dú)立的,沒辦法和原生平臺(tái)直接混合使用,這時(shí)候?yàn)榱四軌蛟?Flutter 中插入原生平臺(tái)的部分功能,Flutter 除了提供了 PlatformView 這樣的實(shí)現(xiàn)邏輯之外,還提供了 Texture 作為 外接紋理的支持。 
如上圖所示,在《Flutter 完整實(shí)戰(zhàn)詳解》 中介紹過,Flutter 的界面渲染是需要經(jīng)歷 Widget -> RenderObject -> Layer 的過程,而在 Layer 的渲染過程中,當(dāng)出現(xiàn)一個(gè) TextureLayer 節(jié)點(diǎn)時(shí),說明這個(gè)節(jié)點(diǎn)使用了 Flutter 中的 Texture 控件,那么這個(gè)控件的內(nèi)容就會(huì)由原生平臺(tái)提供,而管理 Texture 主要是通過 textureId 進(jìn)行識(shí)別的。 
舉個(gè)例子,在 Android 原生層中 video_player 使用的是 exoplayer 播放內(nèi)核,那么如上圖所示,VideoPlayerController 會(huì)在初始化的時(shí)候通過 MethodChannel 和原生端通信,之后準(zhǔn)備好播放內(nèi)核和 Surface ,最后將對(duì)應(yīng)的 textureId 返回到 Dart 中。 所以在前面的代碼中,需要在全屏和非全屏頁面使用同一個(gè) VideoPlayerController ,這樣它們就具備了同一個(gè) textureId 。 具備同一個(gè) textureId 后,那么只要原生層不停止播放, textureId 對(duì)應(yīng)的原生數(shù)據(jù)就一直處于更新狀態(tài),而這時(shí)候雖然跳轉(zhuǎn)路由頁面,但不同的 VideoPlayer 內(nèi)部的 Texture 控件用的是同一個(gè) VideoPlayerController ,也就是同一個(gè) textureId ,所以它們會(huì)呈現(xiàn)出通用的畫面。 如下圖所示,這個(gè)過程簡單總結(jié)就是: Flutter 和原生平臺(tái)通過 PixelBuffer 為介質(zhì)進(jìn)行交互,原生層將數(shù)據(jù)寫入 PixelBuffer ,F(xiàn)lutter 通過注冊好的 textureId 獲取到 PixelBuffer 之后由 Flutter Engine 繪制。 
最后需要注意的是,在 iOS 上在實(shí)現(xiàn)頁面旋轉(zhuǎn)時(shí), SystemChrome.setPreferredOrientations 方法可能會(huì)出現(xiàn)無效,這個(gè)問題在 issue #23913 和 #13238 中有提及,這里可能需要自己多實(shí)現(xiàn)一個(gè)原生接口進(jìn)行兼容,當(dāng)然在 auto_orientation 或者 orientation 等第三方庫也進(jìn)行了這方面的兼容。 另外 iOS 的頁面旋轉(zhuǎn)還確定是否打開了旋轉(zhuǎn)配置的開關(guān)。 
資源推薦本文 Demo : flutter_video_full_controller Github : https://github.com/CarGuo 開源 Flutter 完整項(xiàng)目:https://github.com/CarGuo/GSYGithubAppFlutter 開源 Flutter 多案例學(xué)習(xí)型項(xiàng)目: https://github.com/CarGuo/GSYFlutterDemo 開源 Fluttre 實(shí)戰(zhàn)電子書項(xiàng)目:https://github.com/CarGuo/GSYFlutterBook 開源 React Native 項(xiàng)目:https://github.com/CarGuo/GSYGithubApp
|