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

簡介

最近公司須要開發視頻播放的功能,官方提供的video_player除了視頻播放功能就沒有提供其餘的控制功能,包括最基本的全屏播放功能。同時也比較了一下第三方組件也不是很能知足需求。那咱們就只好本身動手在video_player基礎上進行改造。因爲使用的純flutter進行開發android、ios上界面一致。廢話很少說直接上圖:前端

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共享數據:android

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。ios

4. 入口控件VideoPlayerUI

4.1. 定義屬性

這裏定義了三種讀取視頻的方式networkassetfile,分別對應網絡視頻工程視頻本地視頻文件git

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界面:加載中、加載成功、加載失敗。github

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去調用組件方法,刷新子組件時間顯示的頁面顯示api

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這個生命週期:bash

@override
  void didUpdateWidget(VideoPlayerUI oldWidget) {
    if (oldWidget.url != widget.url) {
      _urlChange(); // url變化時從新執行一次url加載
    }
    super.didUpdateWidget(oldWidget);
  }
複製代碼

4.3. 完整代碼

VideoPlayerUI完整代碼網絡

5. 視頻控制按鍵VideoPlayerControl

5.1 輕觸顯示界面

該組件主要的功能就是,輕觸屏幕會彈出操做按鈕,過兩秒後按鈕會消失,這裏咱們就須要一個Timer定時器,每次點擊屏幕就會取消以前的操做,從新開始計時:less

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 全屏播放

當咱們點擊全屏操做只須要將屏幕強制切換爲橫屏,同時將系統設置爲全屏模式async

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

源碼地址

相關文章
相關標籤/搜索