Я давно не писал статей, и у меня меньше свободного времени. увлекся играми. эй~~~
недавно игралflutter
, я не чувствую, что мне есть чем поделиться с vuejs. Каждый раз, когда я вижу вопросы, заданные группой друзей, я могу только вздыхать.
Мне не нужно говорить вам, вы должны знатьflutter
основывается наdart
В плане языка, по моему нынешнему опыту, помимо отвратительной вложенности компонентов, он действительно удобнее, чем js, и я его естественно понимаю~~~
Анализ сценария
Может быть, это потому, что я обычно люблю смотреть видео.После того, как я начал работать с флаттером, я недавно хотел заняться воспроизведением видео. Я использовал несколько готовых видео-плагинов в сообществе один за другим, но чувствую, что они не добились того эффекта, которого я хочу.Конечно, возможно, эти плагины могут удовлетворить вас, кто читает эту статью, так что давайте перечислите их первыми.
- Первый, кто подтолкнул официальный флаттерvideo_player, только проигрывание видео, никакого ui (на самом деле скрыт прогресс-бар с работой жестов), никаких специальных функций.
- chewie, сделал некоторый пользовательский интерфейс на официальной основе, но этот плагин является полноэкранным (просто вертикальный полноэкранный режим, аналогичный полноэкранному режиму H5), а не тот эффект, который я хочу, новый пользовательский интерфейс не в моем стиле
-
flutter_ijkplayer, на основе
ijkplayer
, добавлен пользовательский интерфейс и горизонтальный экран (горизонтальный экран пользовательского интерфейса, мобильный телефон все еще вертикальный), ноijkplayer
Он всегда сообщает об ошибке в ios, и у этого плагина нет изображения обложки, поэтому я. . -
fijkplayer, аналогично предыдущему, также на основе
ijkplayer
, обложка есть, а на весь экран нет, да и апи немного неудобный.
Давайте просто поговорим об этих немногих, особо нечего сказать, возможно эти плагины удовлетворили вас, но они не могут удовлетворить меня, назвавшего меня Девой.
В заголовок
Сначала подготовьте пустую страницу, просто позвоните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: '示例视频',
// 这个vw是MediaQueryData.fromWindow(window).size.width屏幕宽度
width: _isFullScreen?vh:vw,
height: _isFullScreen?vw:vw/16*9, // 竖屏时容器为16:9
),
),
)
}
}
Хорошо, пустая страница готова.
Создайте игрока
Я использую официальный здесь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) {
// 继续往下看
}
}
Теперь, когда большой кадр компонента видеопроигрывателя завершен, начните писать проигрыватель рендеринга и управляющий пользовательский интерфейс.
Идея: пользовательский интерфейс элемента управления, который я хочу разделить на верхнюю и нижнюю половину, верхняя половина показывает заголовок, нижняя половина показывает кнопку воспроизведения, полноэкранную кнопку, индикатор выполнения, щелкните область видео, чтобы управлять элементом управления пользовательский интерфейс, чтобы показать/скрыть. 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;
});
});
});
}
- Рассмотрим полноэкранный метод, здесь используется плагин для переключения между горизонтальным и вертикальным экраном.orientation
void _toggleFullScreen() {
setState(() {
if (_isFullScreen) { // 如果是全屏就切换竖屏
OrientationPlugin.forceOrientation(DeviceOrientation.portraitUp);
} else {
OrientationPlugin.forceOrientation(DeviceOrientation.landscapeRight);
}
_startPlayControlTimer(); // 操作完控件开始计时隐藏
});
}
После переключения в ландшафт нужно использовать_isFullScreen
а такжеOffstage
Скройте компоненты, которые вы не хотите показывать, например панель приложений и т. д.
На данный момент желаемый эффект достигнут. Давайте посмотрим на эффект ниже.
кашель кашель.
Код копируется по желанию, и основное внимание уделяется обучению. Вы можете изучать флаттер со мной в любое время. Добро пожаловать на внимание. Я продолжу делиться флаттером, когда у меня будет время.