chewie介紹
video_player外掛提供了對視訊播放的底層訪問。Chewie對video_player進行封裝,提供了一套友好的控制UI。
chewie真的很棒,使用非常簡潔。chewie提供了兩套完善的UI風格樣式,以及很大限度的自定義UI樣式。但某些場景下,不能滿足我們的需要,那就得進行功能擴充套件了。自定義UI樣式和功能擴充套件,就得對庫原始碼有一定了解。
chewie使用方式
先貼一個效果圖:左邊是預設效果,右邊是固定尺寸的效果。
新增依賴 pubspec.yaml
dependencies:
chewie: ^0.9.8+1
video_player: ^0.10.2+5
複製程式碼
封裝一個widget
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"
<application
android:name="io.flutter.app.FlutterApplication"
android:label="flutter_sample"
android:usesCleartextTraffic="true"
tools:targetApi="m"
android:icon="@mipmap/ic">
複製程式碼
到這裡我們就能愉快地使用Chewie我們的視訊了。
當然了,我們肯定不能就這麼滿足了。
chewie 原始碼解析
首先進入Chewie的構造方法,在檔案chewie_player.dart中,我們發現就一個controller引數,那構造的引數就放在了ChewieController中了。
Chewie({
Key key,
this.controller,
}) : assert(controller != null, 'You must provide a chewie controller'),
super(key: key);
複製程式碼
chewie_player.dart 程式碼繼續往下翻,找到了ChewieController類。類的開頭有一段註釋介紹,說了ChewieController的功能。
同時也提到播放狀態的變化不歸它管,播放狀態得找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);
}
....
}
複製程式碼
接下來,產生了疑問:
- 控制ControlWidget在哪?怎麼初始化的?
- 控制widget與ChewieController/VideoPlayerController如何互動的?
我們重新回到Chewie類中,找到它的build(BuildContext)方法。看一個Widget類的原始碼,首先是構造方法,其次就是build方法了。我們看到了兩個東西:
- _ChewieControllerProvider
- PlayerWithControls
@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