Флаттер подбросить и запомнить один (управление видео, поддерживающее горизонтальный экран)

Flutter
Флаттер подбросить и запомнить один (управление видео, поддерживающее горизонтальный экран)

Я давно не писал статей, и у меня меньше свободного времени. увлекся играми. эй~~~

недавно игралflutter, я не чувствую, что мне есть чем поделиться с vuejs. Каждый раз, когда я вижу вопросы, заданные группой друзей, я могу только вздыхать.

Мне не нужно говорить вам, вы должны знатьflutterосновывается наdartВ плане языка, по моему нынешнему опыту, помимо отвратительной вложенности компонентов, он действительно удобнее, чем js, и я его естественно понимаю~~~

Анализ сценария

Может быть, это потому, что я обычно люблю смотреть видео.После того, как я начал работать с флаттером, я недавно хотел заняться воспроизведением видео. Я использовал несколько готовых видео-плагинов в сообществе один за другим, но чувствую, что они не добились того эффекта, которого я хочу.Конечно, возможно, эти плагины могут удовлетворить вас, кто читает эту статью, так что давайте перечислите их первыми.

  1. Первый, кто подтолкнул официальный флаттерvideo_player, только проигрывание видео, никакого ui (на самом деле скрыт прогресс-бар с работой жестов), никаких специальных функций.
  2. chewie, сделал некоторый пользовательский интерфейс на официальной основе, но этот плагин является полноэкранным (просто вертикальный полноэкранный режим, аналогичный полноэкранному режиму H5), а не тот эффект, который я хочу, новый пользовательский интерфейс не в моем стиле
  3. flutter_ijkplayer, на основеijkplayer, добавлен пользовательский интерфейс и горизонтальный экран (горизонтальный экран пользовательского интерфейса, мобильный телефон все еще вертикальный), ноijkplayerОн всегда сообщает об ошибке в ios, и у этого плагина нет изображения обложки, поэтому я. .
  4. 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. открытый сухой

  1. Сначала напишите область воспроизведения видео
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;
    });
  }
}
  1. Затем напишите интерфейс управления
// 控件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(),
      ),
    ),
  ),
);
  1. Сначала посмотрите на метод отображения/скрытия пользовательского интерфейса управления.
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;
        });
      });
    });
}
  1. Рассмотрим полноэкранный метод, здесь используется плагин для переключения между горизонтальным и вертикальным экраном.orientation
void _toggleFullScreen() {
    setState(() {
      if (_isFullScreen) { // 如果是全屏就切换竖屏
        OrientationPlugin.forceOrientation(DeviceOrientation.portraitUp);
      } else {
        OrientationPlugin.forceOrientation(DeviceOrientation.landscapeRight);
      }
      _startPlayControlTimer(); // 操作完控件开始计时隐藏
    });
}

После переключения в ландшафт нужно использовать_isFullScreenа такжеOffstageСкройте компоненты, которые вы не хотите показывать, например панель приложений и т. д.

На данный момент желаемый эффект достигнут. Давайте посмотрим на эффект ниже.

效果图: https://cdn.chavesgu.com/flutter/SampleVideo.gif

кашель кашель.

Код копируется по желанию, и основное внимание уделяется обучению. Вы можете изучать флаттер со мной в любое время. Добро пожаловать на внимание. Я продолжу делиться флаттером, когда у меня будет время.