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