最近公司須要開發視頻播放的功能,官方提供的video_player除了視頻播放功能就沒有提供其餘的控制功能,包括最基本的全屏播放功能。同時也比較了一下第三方組件也不是很能知足需求。那咱們就只好本身動手在video_player
基礎上進行改造。因爲使用的純flutter進行開發android、ios上界面一致。廢話很少說直接上圖:前端
video_player: ^0.10.5
auto_orientation: ^1.0.5 //控制橫豎屏控件
screen: ^0.0.5 //控制屏幕亮度以及屏幕常亮組件
common_utils: ^1.1.3 //格式化時間日期組件
複製代碼
爲了代碼可讀性,我將該組件拆分紅了3個控件,分別爲 控制按鈕控件
、手勢滑動控件
、視頻播放播放控件
。這三個控件依次嵌套默認填充滿父控件。因爲嵌套層數比較多,層層傳遞屬性有點麻煩,所以咱們這裏使用一個InheritedWidget
共享數據:android
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'video_player_control.dart';
class ControllerWidget extends InheritedWidget {
ControllerWidget({
this.controlKey,
this.child,
this.controller,
this.videoInit,
this.title
});
final String title;
final GlobalKey<VideoPlayerControlState> controlKey;
final Widget child;
final VideoPlayerController controller;
final bool videoInit;
//定義一個便捷方法,方便子樹中的widget獲取共享數據
static ControllerWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ControllerWidget>();
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
// TODO: implement updateShouldNotify
return false;
}
}
複製代碼
這裏面VideoPlayerController
這個controller咱們後面會常常使用,用於調用操做視頻相關api。ios
這裏定義了三種讀取視頻的方式network
、asset
、file
,分別對應網絡視頻
、工程視頻
、本地視頻文件
:git
class VideoPlayerUI extends StatefulWidget {
VideoPlayerUI.network({
Key key,
@required String url, // 當前須要播放的地址
this.width: double.infinity, // 播放器尺寸(大於等於視頻播放區域)
this.height: double.infinity,
this.title = '', // 視頻須要顯示的標題
}) : type = VideoPlayerType.network,
url = url,
super(key: key);
VideoPlayerUI.asset({
Key key,
@required String dataSource, // 當前須要播放的地址
this.width: double.infinity, // 播放器尺寸(大於等於視頻播放區域)
this.height: double.infinity,
this.title = '', // 視頻須要顯示的標題
}) : type = VideoPlayerType.asset,
url = dataSource,
super(key: key);
VideoPlayerUI.file({
Key key,
@required File file, // 當前須要播放的地址
this.width: double.infinity, // 播放器尺寸(大於等於視頻播放區域)
this.height: double.infinity,
this.title = '', // 視頻須要顯示的標題
}) : type = VideoPlayerType.file,
url = file,
super(key: key);
final url;
final VideoPlayerType type;
final double width;
final double height;
final String title;
@override
_VideoPlayerUIState createState() => _VideoPlayerUIState();
}
複製代碼
首先咱們須要在initState
生命週期中對視頻進行初始化,對視頻是否加載成功顯示不一樣的UI界面:加載中、加載成功、加載失敗。github
void _urlChange() async {
if (widget.url == null || widget.url == '') return;
if (_controller != null) {
/// 若是控制器存在,清理掉從新建立
_controller.removeListener(_videoListener);
_controller.dispose();
}
setState(() {
/// 重置組件參數
_videoInit = false;
_videoError = false;
});
if (widget.type == VideoPlayerType.file) {
_controller = VideoPlayerController.file(widget.url);
} else if (widget.type == VideoPlayerType.asset) {
_controller = VideoPlayerController.asset(widget.url);
} else {
_controller = VideoPlayerController.network(widget.url);
}
/// 加載資源完成時,監聽播放進度,而且標記_videoInit=true加載完成
_controller.addListener(_videoListener);
await _controller.initialize();
setState(() {
_videoInit = true;
_videoError = false;
_controller.play();
});
}
複製代碼
這裏有一個須要注意的點:_controller.addListener(_videoListener);
咱們添加監聽必定要在初始化以前添加,否則後續的加載狀態沒法響應。在監聽函數中咱們這裏使用了GlobalKey去調用組件方法,刷新子組件時間顯示的頁面顯示api
void _videoListener() async {
if (_controller.value.hasError) {
setState(() {
_videoError = true;
});
} else {
Duration res = await _controller.position;
if (res >= _controller.value.duration) {
await _controller.seekTo(Duration(seconds: 0));
await _controller.pause();
}
if (_controller.value.isPlaying && _key.currentState != null) {
/// 減小build次數
_key.currentState.setPosition(
position: res,
totalDuration: _controller.value.duration,
);
}
}
}
複製代碼
在傳入的url發生改變的時候,從新初始化視頻,這裏咱們就須要用到didUpdateWidget
這個生命週期:bash
@override
void didUpdateWidget(VideoPlayerUI oldWidget) {
if (oldWidget.url != widget.url) {
_urlChange(); // url變化時從新執行一次url加載
}
super.didUpdateWidget(oldWidget);
}
複製代碼
該組件主要的功能就是,輕觸屏幕會彈出操做按鈕,過兩秒後按鈕會消失,這裏咱們就須要一個Timer定時器,每次點擊屏幕就會取消以前的操做,從新開始計時:less
void _togglePlayControl() {
setState(() {
if (_hidePlayControl) {
/// 若是隱藏則顯示
_hidePlayControl = false;
_playControlOpacity = 1;
_startPlayControlTimer(); // 開始計時器,計時後隱藏
} else {
/// 若是顯示就隱藏
if (_timer != null) _timer.cancel(); // 有計時器先移除計時器
_playControlOpacity = 0;
Future.delayed(Duration(milliseconds: 500)).whenComplete(() {
_hidePlayControl = true; // 延遲500ms(透明度動畫結束)後,隱藏
});
}
});
}
void _startPlayControlTimer() {
/// 計時器,用法和前端js的大同小異
if (_timer != null) _timer.cancel();
_timer = Timer(Duration(seconds: 3), () {
/// 延遲3s後隱藏
setState(() {
_playControlOpacity = 0;
Future.delayed(Duration(milliseconds: 500)).whenComplete(() {
_hidePlayControl = true;
});
});
});
}
複製代碼
當咱們點擊全屏操做只須要將屏幕強制切換爲橫屏,同時將系統設置爲全屏模式async
void _toggleFullScreen() {
setState(() {
if (_isFullScreen) {
/// 若是是全屏就切換豎屏
AutoOrientation.portraitAutoMode();
///顯示狀態欄,與底部虛擬操做按鈕
SystemChrome.setEnabledSystemUIOverlays(
[SystemUiOverlay.top, SystemUiOverlay.bottom]);
} else {
AutoOrientation.landscapeAutoMode();
///關閉狀態欄,與底部虛擬操做按鈕
SystemChrome.setEnabledSystemUIOverlays([]);
}
_startPlayControlTimer(); // 操做完控件開始計時隱藏
});
}
複製代碼
該方法供視頻的監聽函數裏面進行調用,以讓進度條實時更新
// 供父組件調用刷新頁面,減小父組件的build
void setPosition({position, totalDuration}) {
setState(() {
_position = position;
_totalDuration = totalDuration;
});
}
複製代碼
對於手勢控制這裏其實沒什麼難度,無非就是經過滑動距離/屏幕寬(高)
獲取百分比加上當前的值,而後在設置亮度、音量、進度。這裏我須要注意必定要給VideoPlayerControl的container設置一個背景透明色,否則該控件沒法響應手勢(感受這裏寫的不夠優雅,有什麼好的解決辦法評論告訴我):
@override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: _playOrPause,
onTap: _togglePlayControl,
child: Container(
width: double.infinity,
height: double.infinity,
// 這裏須要價格透明色,否則沒法響應手勢,有沒有大佬知道更加優雅點的方式
color: Colors.transparent,
child: WillPopScope(
child: Offstage(
offstage: _hidePlayControl,
child: AnimatedOpacity(
// 加入透明度動畫
opacity: _playControlOpacity,
duration: Duration(milliseconds: 300),
child: Column(
children: <Widget>[_top(), _middle(), _bottom(context)],
),
),
),
onWillPop: _onWillPop,
),
),
);
}
複製代碼
這個控件就很少講了,直接上完整代碼
import 'package:flutter/material.dart';
import 'package:richway_flutter_cli/common/video/video_player_UI.dart';
class VideoPage extends StatelessWidget {
static final String routerName = '/VideoPage';
// Size get _window => MediaQueryData.fromWindow(window).size;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
// 該組件寬高默認填充父控件,你也能夠本身設置寬高
child: VideoPlayerUI.network(
url:
'https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo-transcode-crf/60609889_0b5d29ee8e09fad4cc4f40f314d737ca_0.mp4',
title: '示例視頻',
),
),
);
}
}
複製代碼
寫到這裏,視頻組件講解完了,若是恰好對你有用歡迎在我github上給個start、或者給這篇文章點個贊,謝謝你們了,源碼拷過去就能用!