又有一段時間沒有寫文章了,閒暇時間比較少。癡迷遊戲。哎~~~前端
最近在玩flutter
,對於vuejs感受沒啥可分享的了。每次看到羣友們問的問題,我都只能嘆口氣。vue
不用我說,應該都知道flutter
是基於dart
語言的,我目前的體驗來講,除了組件嵌套比較噁心以外,真的比js舒服,懂的天然懂~~~ios
多是因爲平時比較喜歡看視頻吧,上手flutter以後,還沒多久呢,就想搞一搞視頻播放。中間陸陸續續用了社區好幾個現成的視頻插件,都感受沒有達到本身想要的效果,固然也許這些插件能夠知足正在看這個文章的你,先列一下吧。api
ijkplayer
,增長了ui和橫屏(ui的橫屏,手機仍是豎屏),不過ijkplayer
在ios總是報錯,而且這個插件沒有封面圖,因此我。。ijkplayer
,有封面圖,可是沒有全屏,而且api有點難受。就說這幾個吧,很少說了,可能這些插件已經知足你了,卻知足不了我,誰叫我是處女座呢。緩存
首先準備一個空頁面,就叫media.dart
吧。app
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: '示例視頻',
// 這個vw是MediaQueryData.fromWindow(window).size.width屏幕寬度
width: _isFullScreen?vh:vw,
height: _isFullScreen?vw:vw/16*9, // 豎屏時容器爲16:9
),
),
)
}
}
複製代碼
OK,空頁面準備好了。框架
我這裏選用的是官方的video_play
,由於其餘插件都增長了東西缺讓我不滿意,因此我只能選擇最原始的插件,本身增長滿意的東西。async
video_player: ^0.10.2+5
複製代碼
好,開始編寫咱們想要的ui~~ide
// 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(),
),
),
),
_bottomControl,// 控件ui下半部 看下面
],
):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;
});
});
}
void _videoListener() async {
Duration res = await _controller.position;
if (res >= _controller.value.duration) {
_controller.pause();
_controller.seekTo(Duration(seconds: 0));
}
setState(() {
_position = res;
});
}
}
複製代碼
// 控件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(),
),
),
),
);
複製代碼
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;
});
});
});
}
複製代碼
void _toggleFullScreen() {
setState(() {
if (_isFullScreen) { // 若是是全屏就切換豎屏
OrientationPlugin.forceOrientation(DeviceOrientation.portraitUp);
} else {
OrientationPlugin.forceOrientation(DeviceOrientation.landscapeRight);
}
_startPlayControlTimer(); // 操做完控件開始計時隱藏
});
}
複製代碼
切換成橫屏後,須要用_isFullScreen
和Offstage
把你不想顯示的組件隱藏掉,例如appBar等等。
代碼隨意copy,重在學習,能夠隨時和我一塊兒研究flutter,歡迎關注,後續有時間會繼續分享flutter。