Flutter:基於video_player實現視訊相關手勢控制、全屏播放

一位頭髮茂盛的程式猿發表於2020-01-06

簡介

最近公司需要開發視訊播放的功能,官方提供的video_player除了視訊播放功能就沒有提供其他的控制功能,包括最基本的全屏播放功能。同時也比較了一下第三方元件也不是很能滿足需求。那我們就只好自己動手在video_player基礎上進行改造。由於使用的純flutter進行開發android、ios上介面一致。廢話不多說直接上圖:

Flutter:基於video_player實現視訊相關手勢控制、全屏播放

1. 主要功能

  1. 輕觸螢幕彈出控制按鈕(進度條、全屏播放按鈕、暫停播放按鈕、標題)
  2. 右側滑動控制音量
  3. 左側滑動控制亮度
  4. 水平滑動快進快退
  5. 雙擊暫停播放
  6. 播放時螢幕常亮

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. 定義屬性

這裡定義了三種讀取視訊的方式networkassetfile,分別對應網路視訊工程視訊本地視訊檔案

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. 完整程式碼

VideoPlayerUI完整程式碼

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 完整程式碼

VideoPlayerControl完整程式碼

VideoPlayerSlider進度條完整程式碼

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 完整程式碼

這個控制元件就不多講了,直接上完整程式碼

完整程式碼VideoPlayerPan

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、或者給這篇文章點個贊,謝謝大家了,原始碼拷過去就能用!

原始碼地址

相關文章