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
複製代碼
封裝一個widgetapp
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"less
<application
android:name="io.flutter.app.FlutterApplication"
android:label="flutter_sample"
android:usesCleartextTraffic="true"
tools:targetApi="m"
android:icon="@mipmap/ic">
複製代碼
到這裏咱們就能愉快地使用Chewie咱們的視頻了。async
固然了,咱們確定不能就這麼知足了。ide
首先進入Chewie的構造方法,在文件chewie_player.dart中,咱們發現就一個controller參數,那構造的參數就放在了ChewieController中了。oop
Chewie({
Key key,
this.controller,
}) : assert(controller != null, 'You must provide a chewie controller'),
super(key: key);
複製代碼
chewie_player.dart 代碼繼續往下翻,找到了ChewieController類。類的開頭有一段註釋介紹,說了ChewieController的功能。ui
同時也提到播放狀態的變化不歸它管,播放狀態得找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