簡介
最近公司需要開發視訊播放的功能,官方提供的video_player除了視訊播放功能就沒有提供其他的控制功能,包括最基本的全屏播放功能。同時也比較了一下第三方元件也不是很能滿足需求。那我們就只好自己動手在video_player
基礎上進行改造。由於使用的純flutter進行開發android、ios上介面一致。廢話不多說直接上圖:
1. 主要功能
- 輕觸螢幕彈出控制按鈕(進度條、全屏播放按鈕、暫停播放按鈕、標題)
- 右側滑動控制音量
- 左側滑動控制亮度
- 水平滑動快進快退
- 雙擊暫停播放
- 播放時螢幕常亮
2. 安裝元件
video_player: ^0.10.5
auto_orientation: ^1.0.5 //控制橫豎屏控制元件
screen: ^0.0.5 //控制螢幕亮度以及螢幕常亮元件
common_utils: ^1.1.3 //格式化時間日期元件
複製程式碼
3. 元件結構
為了程式碼可讀性,我將該元件拆分成了3個控制元件,分別為 控制按鈕控制元件
、手勢滑動控制元件
、視訊播放播放控制元件
。這三個控制元件依次巢狀預設填充滿父控制元件。由於巢狀層數比較多,層層傳遞屬性有點麻煩,因此我們這裡使用一個InheritedWidget
共享資料:
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。
4. 入口控制元件VideoPlayerUI
4.1. 定義屬性
這裡定義了三種讀取視訊的方式network
、asset
、file
,分別對應網路視訊
、工程視訊
、本地視訊檔案
:
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();
}
複製程式碼
4.2. 初始化視訊
4.2.1. 初始化
首先我們需要在initState
生命週期中對視訊進行初始化,對視訊是否載入成功顯示不同的UI介面:載入中、載入成功、載入失敗。
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去呼叫元件方法,重新整理子元件時間顯示的頁面顯示
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,
);
}
}
}
複製程式碼
4.2.2. 改變視訊源
在傳入的url發生改變的時候,重新初始化視訊,這裡我們就需要用到didUpdateWidget
這個生命週期:
@override
void didUpdateWidget(VideoPlayerUI oldWidget) {
if (oldWidget.url != widget.url) {
_urlChange(); // url變化時重新執行一次url載入
}
super.didUpdateWidget(oldWidget);
}
複製程式碼
4.3. 完整程式碼
5. 視訊控制按鍵VideoPlayerControl
5.1 輕觸顯示介面
該元件主要的功能就是,輕觸螢幕會彈出操作按鈕,過兩秒後按鈕會消失,這裡我們就需要一個Timer定時器,每次點選螢幕就會取消之前的操作,重新開始計時:
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;
});
});
});
}
複製程式碼
5.2 全屏播放
當我們點選全屏操作只需要將螢幕強制切換為橫屏,同事將系統設定為全屏模式
void _toggleFullScreen() {
setState(() {
if (_isFullScreen) {
/// 如果是全屏就切換豎屏
AutoOrientation.portraitAutoMode();
///顯示狀態列,與底部虛擬操作按鈕
SystemChrome.setEnabledSystemUIOverlays(
[SystemUiOverlay.top, SystemUiOverlay.bottom]);
} else {
AutoOrientation.landscapeAutoMode();
///關閉狀態列,與底部虛擬操作按鈕
SystemChrome.setEnabledSystemUIOverlays([]);
}
_startPlayControlTimer(); // 操作完控制元件開始計時隱藏
});
}
複製程式碼
5.3 重新整理進度條
該方法供視訊的監聽函式裡面進行呼叫,以讓進度條實時更新
// 供父元件呼叫重新整理頁面,減少父元件的build
void setPosition({position, totalDuration}) {
setState(() {
_position = position;
_totalDuration = totalDuration;
});
}
複製程式碼
5.4 完整程式碼
6. 手勢控制控制元件VideoPlayerPan
6.1 手勢控制方法
對於手勢控制這裡其實沒什麼難度,無非就是通過滑動距離/螢幕寬(高)
獲取百分比加上當前的值,然後在設定亮度、音量、進度。這裡我需要注意一定要給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,
),
),
);
}
複製程式碼
6.2 完整程式碼
這個控制元件就不多講了,直接上完整程式碼
7. 使用方式
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、或者給這篇文章點個贊,謝謝大家了,原始碼拷過去就能用!