又有一段時間沒有寫文章了,閒暇時間比較少。痴迷遊戲。哎~~~
最近在玩flutter
,對於vuejs感覺沒啥可分享的了。每次看到群友們問的問題,我都只能嘆口氣。
不用我說,應該都知道flutter
是基於dart
語言的,我目前的體驗來說,除了元件巢狀比較噁心之外,真的比js舒服,懂的自然懂~~~
場景分析
可能是由於平時比較喜歡看視訊吧,上手flutter之後,還沒多久呢,就想搞一搞視訊播放。中間陸陸續續用了社群好幾個現成的視訊外掛,都感覺沒有達到自己想要的效果,當然也許這些外掛可以滿足正在看這個文章的你,先列一下吧。
- 首推flutter官方的video_player,只有視訊播放,無ui(其實藏了一個帶手勢操作的進度條),無特殊功能。
- chewie,在官方的基礎上做了一些ui,不過這個外掛全屏(只是單純的豎屏全屏,類似於H5的全屏),並不是我要的效果,新增的ui不是我的style
- flutter_ijkplayer,基於
ijkplayer
,增加了ui和橫屏(ui的橫屏,手機還是豎屏),不過ijkplayer
在ios老是報錯,並且這個外掛沒有封面圖,所以我。。 - fijkplayer,和上面的差不多,也是基於
ijkplayer
,有封面圖,但是沒有全屏,並且api有點難受。
就說這幾個吧,不多說了,可能這些外掛已經滿足你了,卻滿足不了我,誰叫我是處女座呢。
進入正題
首先準備一個空頁面,就叫media.dart
吧。
class MediaPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return MediaPageState();
}
}
class MediaPageState extends State<MediaPage> {
// 記錄當前裝置是否橫屏,後續用到
bool get _isFullScreen => MediaQuery.of(context).orientation == Orientation.landscape;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Media'),
),
body: Container(
child: MyVideo( // 這個是等會兒要編寫的元件
url: 'http://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4',
title: '示例視訊',
width: vw, // 這個vw是MediaQueryData.fromWindow(window).size.width螢幕寬度
height: vw/16*9, // 設定容器為16:9
),
),
)
}
}
複製程式碼
OK,空頁面準備好了。
打造播放器
我這裡選用的是官方的video_play
,因為其他外掛都增加了東西缺讓我不滿意,所以我只能選擇最原始的外掛,自己增加滿意的東西。
video_player: ^0.10.2+5
複製程式碼
好,開始編寫我們想要的ui~~
// MyVideo.dart
import 'package:video_player/video_player.dart'; // 引入官方外掛
class MyVideo extends StatefulWidget {
MyVideo({
@required this.url, // 當前需要播放的地址
@required this.width, // 播放器尺寸(大於等於視訊播放區域)
@required this.height,
this.title = '', // 視訊需要顯示的標題
});
// 視訊地址
final String url;
// 視訊尺寸比例
final double width;
final double height;
// 視訊標題
final String title;
@override
State<MyVideo> createState() {
return _MyVideoState();
}
}
class _MyVideoState extends State<MyVideo> {
// 指示video資源是否載入完成,載入完成後會獲得總時長和視訊長寬比等資訊
bool _videoInit = false;
// video控制元件管理器
VideoPlayerController _controller;
// 記錄video播放進度
Duration _position = Duration(seconds: 0);
// 記錄播放控制元件ui是否顯示(進度條,播放按鈕,全屏按鈕等等)
Timer _timer; // 計時器,用於延遲隱藏控制元件ui
bool _hidePlayControl = true; // 控制是否隱藏控制元件ui
double _playControlOpacity = 0; // 通過透明度動畫顯示/隱藏控制元件ui
// 記錄是否全屏
bool get _isFullScreen => MediaQuery.of(context).orientation==Orientation.landscape;
@override
Widget build(BuildContext context) {
// 繼續往下看
}
}
複製程式碼
現在已經完成了視訊播放器元件的大框架了,開始編寫渲染播放器和控制元件ui。
想法:控制元件的ui我希望分為上半部分和下半部分,上半部顯示標題,下半部顯示播放按鈕,全屏按鈕,進度條,點選視訊區域控制控制元件ui顯示/隱藏。ok。開幹
- 先寫視訊播放區
class _MyVideoState extends State<MyVideo> {
// ......
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
color: Colors.black,
child: widget.url!=null?Stack( // 因為控制元件ui和視訊是重疊的,所以要用定位了
children: <Widget>[
GestureDetector( // 手勢元件
onTap: () { // 點選顯示/隱藏控制元件ui
_togglePlayControl();
},
child: _videoInit?
Center(
child: AspectRatio( // 載入url成功時,根據視訊比例渲染播放器
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
),
):
Center( // 沒載入完成時顯示轉圈圈loading
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(),
),
),
),
],
):Center( // 判斷是否傳入了url,沒有的話顯示"暫無視訊資訊"
child: Text(
'暫無視訊資訊',
style: TextStyle(
color: Colors.white
),
),
),
)
}
@override
void initState() {
_urlChange(); // 初始進行一次url載入
super.initState();
}
@override
void didUpdateWidget(MyVideo oldWidget) {
if (oldWidget.url != widget.url) {
_urlChange(); // url變化時重新執行一次url載入
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
if (_controller!=null) { // 慣例。元件銷燬時清理下
_controller.removeListener(_videoListener);
_controller.dispose();
}
super.dispose();
}
void _urlChange() {
if (widget.url==null || widget.url=='') return;
if (_controller!=null) { // 如果控制器存在,清理掉重新建立
_controller.removeListener(_videoListener);
_controller.dispose();
}
setState(() { // 重置元件引數
_hidePlayControl = true;
_videoInit = false;
_position = Duration(seconds: 0);
});
// 載入network的url,也支援本地檔案,自行閱覽官方api
_controller = VideoPlayerController.network(widget.url)
..initialize().then((_) {
// 載入資源完成時,監聽播放進度,並且標記_videoInit=true載入完成
_controller.addListener(_videoListener);
setState(() {
_videoInit = true;
});
});
}
}
複製程式碼
- 然後編寫控制元件ui
// 控制元件ui下半部
Widget _bottomControl = Positioned( // 需要定位
left: 0,
bottom: 0,
child: Offstage( // 控制是否隱藏
offstage: _hidePlayControl,
child: AnimatedOpacity( // 加入透明度動畫
opacity: _playControlOpacity,
duration: Duration(milliseconds: 300),
child: Container( // 底部控制元件的容器
width: widget.width,
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient( // 來點黑色到透明的漸變優雅一下
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Color.fromRGBO(0, 0, 0, .7), Color.fromRGBO(0, 0, 0, .1)],
),
),
child: _videoInit?Row( // 載入完成時才渲染,flex佈局
children: <Widget>[
IconButton( // 播放按鈕
padding: EdgeInsets.zero,
iconSize: 26,
icon: Icon( // 根據控制器動態變化播放圖示還是暫停
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
),
onPressed: (){
setState(() { // 同樣的,點選動態播放或者暫停
_controller.value.isPlaying
? _controller.pause()
: _controller.play();
_startPlayControlTimer(); // 操作控制元件後,重置延遲隱藏控制元件的timer
});
},
),
Flexible( // 相當於前端的flex: 1
child: VideoProgressIndicator( // 嘻嘻,這是video_player編寫好的進度條,直接用就是了~~
_controller,
allowScrubbing: true, // 允許手勢操作進度條
padding: EdgeInsets.all(0),
colors: VideoProgressColors( // 配置進度條顏色,也是video_player現成的,直接用
playedColor: Theme.of(context).primaryColor, // 已播放的顏色
bufferedColor: Color.fromRGBO(255, 255, 255, .5), // 快取中的顏色
backgroundColor: Color.fromRGBO(255, 255, 255, .2), // 為快取的顏色
),
),
),
Container( // 播放時間
margin: EdgeInsets.only(left: 10),
child: Text( // durationToTime是通過Duration轉成hh:mm:ss的格式,自己實現。
durationToTime(_position)+'/'+durationToTime(_controller.value.duration),
style: TextStyle(
color: Colors.white
),
),
),
IconButton( // 全屏/橫屏按鈕
padding: EdgeInsets.zero,
iconSize: 26,
icon: Icon( // 根據當前螢幕方向切換圖示
_isFullScreen?Icons.fullscreen_exit:Icons.fullscreen,
color: Colors.white,
),
onPressed: (){ // 點選切換是否全屏
_toggleFullScreen();
},
),
],
):Container(),
),
),
),
);
複製程式碼
- 先看下顯示/隱藏控制元件ui的方法
void _togglePlayControl() {
setState(() {
if (_hidePlayControl) { // 如果隱藏則顯示
_hidePlayControl = false;
_playControlOpacity = 1;
_startPlayControlTimer(); // 開始計時器,計時後隱藏
} else { // 如果顯示就隱藏
if (_timer!=null) _timer.cancel(); // 有計時器先移除計時器
_playControlOpacity = 0;
Future.delayed(Duration(milliseconds: 300)).whenComplete(() {
_hidePlayControl = true; // 延遲300ms(透明度動畫結束)後,隱藏
});
}
});
}
void _startPlayControlTimer() { // 計時器,用法和前端js的大同小異
if (_timer!=null) _timer.cancel();
_timer = Timer(Duration(seconds: 3), () { // 延遲3s後隱藏
setState(() {
_playControlOpacity = 0;
Future.delayed(Duration(milliseconds: 300)).whenComplete(() {
_hidePlayControl = true;
});
});
});
}
複製程式碼
- 再來看下全屏的方法,此處採用了切換橫屏豎屏的外掛orientation
void _toggleFullScreen() {
setState(() {
if (_isFullScreen) { // 如果是全屏就切換豎屏
OrientationPlugin.forceOrientation(DeviceOrientation.portraitUp);
} else {
OrientationPlugin.forceOrientation(DeviceOrientation.landscapeRight);
}
_startPlayControlTimer(); // 操作完控制元件開始計時隱藏
});
}
複製程式碼
切換成橫屏後,需要用_isFullScreen
和Offstage
把你不想顯示的元件隱藏掉,例如appBar等等。
至此,已經完成我想要的效果。下面看下效果吧。
咳咳。
程式碼隨意copy,重在學習,可以隨時和我一起研究flutter,歡迎關注,後續有時間會繼續分享flutter。