Flutter入门系列-VideoPlayer在列表使用

Flutter 开发过程中视频列表应该是比较复杂的功能,因为这里涉及到同一个视频在不同页面之间无缝的切换,控制其他在播的视频停止播放并播放当前点击的视频,还有视频秒开,视频弹幕等问题。在这里仅仅实现列表播放和切换到全屏播放功能,其他功能后面有时间再去进行研究。Flutter 并没有提供自带的视频播放控件,所以必须依靠第三方插件来实现,比如 VideoPlayer,fijkplayer 等等,但是 VideoPlayer 是使用相对比较简单的,所以先介绍该视频播放器。

一、依赖

dependencies:
  video_player: ^2.2.3

二、介绍

class VideoPlayer extends StatefulWidget {
  /// Uses the given [controller] for all video rendered in this widget.
  VideoPlayer(this.controller);

  /// The [VideoPlayerController] responsible for the video being rendered in
  /// this widget.
  final VideoPlayerController controller;

  @override
  _VideoPlayerState createState() => _VideoPlayerState();
}

VideoPlayer 是播放视频的控件,VideoPlayerController 是视频播放的控制器。

初始化 VideoPlayerController 有多种方法,最常见的就是通过 asset 或者 network 静态方法来创建。通过 play() 和 pause() 方法来实现播放和停止播放,dispose() 方法在 Widget 执行 onDispose 之前执行调用可以释放资源。同时 VideoPlayerController 有value属性来获取总时长 duration,当前播放进度 position, 和播放状态 isPlaying 等其他属性内容,value的类型结构如下:

class VideoPlayerValue {
  /// Constructs a video with the given values. Only [duration] is required. The
  /// rest will initialize with default values when unset.
  VideoPlayerValue({
    required this.duration,
    this.size = Size.zero,
    this.position = Duration.zero,
    this.caption = Caption.none,
    this.buffered = const [],
    this.isInitialized = false,
    this.isPlaying = false,
    this.isLooping = false,
    this.isBuffering = false,
    this.volume = 1.0,
    this.playbackSpeed = 1.0,
    this.errorDescription,
  });
....

}

如上可以看出,可以获取当前播放控制器的属性有很多。如上基本上已经介绍了 VideoPlayer 常见的所有功能,下面会实现一个视频列表页面,该列表可以在点击播放其他视频的时候,停止播放当前视频,并且可以进入到全面播放状态。

三、代码

import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ijkplayer/lifecycle/lifecycle_state.dart';
import 'package:flutter_ijkplayer/util/video_data.dart';
import 'package:flutter_ijkplayer/widget/video_item_new.dart';
import 'package:video_player/video_player.dart';

class ListVideoPage extends StatefulWidget {
  const ListVideoPage({Key? key}) : super(key: key);

  @override
  _ListVideoPageState createState() => _ListVideoPageState();
}

class _ListVideoPageState extends LifeCycleState {
  List _listVideo = [
    "assets/video/anranxiaohun.mp4",
    "assets/video/xiangxinai.mp4",
    "assets/video/ycxhs098.mp4",
    //"http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4",
    //"http://vjs.zencdn.net/v/oceans.mp4",
    //"https://media.w3.org/2010/05/sintel/trailer.mp4",
    //"http://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4"
  ];

  //创建一个多订阅流
  final StreamController _streamController = new StreamController.broadcast();

  //播放控制器
  late VideoPlayerController _videoPlayerController;

  //是否滚动
  bool isScroll = false;

  @override
  void initState() {
    super.initState();
    _streamController.stream.listen((event) {
      if (!mounted) return;
      if (_videoPlayerController != event) {
        _videoPlayerController.pause(); // 原来的controller进行暂停
      }
      _videoPlayerController = event; //赋值新的videoController
    });
    _videoPlayerController = VideoPlayerController.asset(_listVideo[0])
      ..initialize().then((_) {
        setState(() {});
      });
  }

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('视频列表'),
      ),
      body: buildListView(),
    );
  }

  buildListView() {
    return NotificationListener(
        onNotification: (ScrollNotification notification) {
          if (notification.runtimeType == ScrollStartNotification) {
            //开始滚动
            isScroll = true;
            //setState(() {});
          } else if (notification.runtimeType == ScrollEndNotification) {
            //结束滚动
            //setState(() {});
            isScroll = false;
          }
          return false;
        },
        child: ListView.separated( //创建视频列表
          separatorBuilder: (context, index) {
            return Divider();
          },
          //不缓存
          cacheExtent: 2,
          //加载20条数据
          itemCount: 20,
          itemBuilder: (BuildContext context, int index) {
            return buildListViewItem(index);
          },
        ));
  }

  buildListViewItem(index) {
    return Container(
      height: 240,
      child: Column(
        children: [
          Container(
            padding: EdgeInsets.only(top: 10),
            child: Row(
              children: [
                Container(
                  child: Icon(
                    Icons.account_circle,
                    size: 20,
                  ),
                ),
                SizedBox(
                  width: 10,
                ),
                Text(
                  'Flutter开发-$index',
                  style: TextStyle(fontSize: 16),
                )
              ],
            ),
          ),
          Expanded(
            child: Container(
                child: VideoItemNew(
                    url: _listVideo[index % _listVideo.length],
                    streamController: _streamController,
                    //isScroll: isScroll
                    isScroll: false,
                    type: VideoType.asset,
                )),
          )
        ],
      ),
    );
  }

  @override
  void onCreate() {
    super.onCreate();
    log("视频列表 onCreate");
  }

  @override
  void onResume() {
    super.onResume();
    log("视频列表 onResume");
  }

  @override
  void onPause() {
    super.onPause();
    log("视频列表 onPause");
  }

  @override
  void onForeground() {
    super.onForeground();
    log("视频列表 onForeground");
  }

  @override
  void onBackground() {
    super.onBackground();
    log("视频列表 onBackground");
  }

  @override
  void onDestroy() {
    super.onDestroy();
    log("视频列表 onDestroy");
  }
}

视频列表项代码 

import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_ijkplayer/page/full_screen_advance_page.dart';
import 'package:flutter_ijkplayer/page/full_screen_page.dart';
import 'package:flutter_ijkplayer/util/date_time_util.dart';
import 'package:flutter_ijkplayer/util/video_data.dart';
import 'package:flutter_ijkplayer/widget/progress_colors.dart';
import 'package:video_player/video_player.dart';
import 'material_video_progressbar.dart';


class VideoItemNew extends StatefulWidget {
  //视频类型
  VideoType type;
  //ListView是否在滚动
  bool isScroll;
  String url;
  //全局流控制器
  StreamController streamController;

  VideoItemNew({Key? key,
    required this.url,
    required this.streamController,
    required this.isScroll,
    this.type=VideoType.asset}) : super(key: key);

  @override
  _VideoItemState createState() => _VideoItemState();
}

class _VideoItemState extends State with TickerProviderStateMixin{

  late VideoPlayerController _controller;

  bool isPlaying = false; // true 播放 false 不播放

  @override
  void initState() {
    super.initState();
    _controller = widget.type == VideoType.asset
        ? VideoPlayerController.asset(widget.url)
        : VideoPlayerController.network(widget.url)
    ..initialize().then((_){
        setState(() {});
    });
    _controller.addListener(() {
        //监听播放器的状态
        //如果播放器不播放了,那么就需要更新isPlaying的状态
        if (!mounted) return;
        if (isPlaying && !_controller.value.isPlaying){
            isPlaying = false;
            setState(() {});
        }
        setState(() {
          _sliderValue = _controller.value.position.inMilliseconds/
          _controller.value.duration.inMilliseconds;
        });
    });
  }

  @override
  void dispose() {
    if(!mounted) return;
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    //widget.isScroll 是否在滚动状态,如果是的,则显示加载中,否则就加载播放器视图
    return widget.isScroll
        ? Center(child: Text('加载中'),)
        : buildVideo();
  }

  buildVideo() {
    return Stack(
      children: [
        //占满全屏并设置播放器
        Positioned.fill(
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: VideoPlayer(_controller),
            )
        ),
        //编辑透明文字
        buildCenterText(),
        //底部滑动条
        Positioned(
            bottom: 2,
            right: 10,
            left: 10,
            child: isPlaying? Container(): buildSlider()
        )
      ],
    );
  }

  buildCenterText() {
    return Positioned.fill( //占满全屏
        child: AnimatedOpacity(
            opacity: _controller.value.isPlaying? 0: 1,
            duration: Duration(milliseconds: 500),
            child: GestureDetector(
              onTap: buildOnTap,
              child: Container(
                color: Colors.grey.withOpacity(0.5),
                child: Center(
                  child: Icon(
                      _controller.value.isPlaying
                      ? Icons.pause
                      : Icons.play_arrow,
                    color: Colors.white,
                  ),
                ),
              ),
            ),
        ),
    );
  }

  //点击播放
  buildOnTap(){
    isPlaying = !isPlaying;
    if (isPlaying){
      widget.streamController.add(_controller);
       //视频播放总时长
      Duration duration = _controller.value.duration;
      //视频播放进度
      Duration currPosition = _controller.value.position;
      if (currPosition == duration){
        _controller.seekTo(Duration.zero);
      }
      isPlaying= true;
      _controller.play();
    } else {
      isPlaying= false;
      _controller.pause();
    }
    setState(() {

    });
  }

  //当前播放器播放进度
  double _sliderValue = 0.0;

  buildSlider() {
    return Container(
      height: 48,
      child: Row(
        children: [
          //进度
          Text(buildTextString(_controller.value.position),
            style: TextStyle(color: Colors.white, fontSize: 14),
          ),
          //进度条
          //initSlider(),
          _buildProgressBar(),
          //总进度
          Text(buildTextString(_controller.value.duration),
            style: TextStyle(color: Colors.white, fontSize: 14),
          ),
          initFullScreen()
        ],
      ),
    );
  }

   
  //这里弃用了,不使用Slider来作为视频播放器的进度条,我们通过自定义来实现
  initSlider() {
    return Expanded(
        child: Slider(
            activeColor: Colors.lightBlue,
            inactiveColor: Colors.grey,
            //默认为0 必须小于或者等于最大值
            //如果min 和 max 相同,则滑块禁用
            min: 0,
            //默认1, 必须大于或者等于min
            max: 1,
            value: _sliderValue,
            onChanged: (double value){
               setState(() {
                 _sliderValue = value;
                 //通过进度条修改视频播放进度
                 _controller.seekTo(_controller.value.duration*_sliderValue);
               });
            },
            //滑块开始滑动
            onChangeStart: (double value){
              //log("onChangeStart...");
            },
            //滑块结束
            onChangeEnd: (double value){
              //log("onChangeEnd....");
            },
        )
    );
  }

  initFullScreen(){
    return GestureDetector(
      onTap:(){
        Navigator.of(context).push(MaterialPageRoute(builder: (context){
            //return VideoFullScreenPage(url: widget.url, videoType: widget.type);
            return VideoFullScreenAdvancePage(url: widget.url, videoType: widget.type, controller: _controller,);
        }));
      },
      child: Icon(
        Icons.fullscreen,
        color: Colors.white,
      ),
    );
  }

  bool _dragging = false;

  //自定义MaterialVideoProgressBar来实现进度条
  Widget _buildProgressBar() {
    return Expanded(
      child: Padding(
        padding: EdgeInsets.only(right: 15,left: 15),
        child: MaterialVideoProgressBar(
          _controller,
          onDragStart: () {
            setState(() {
              _dragging = true;
            });
          },
          onDragEnd: () {
            setState(() {
              _dragging = false;
            });
          },
          colors: ProgressColors(
              playedColor: Theme.of(context).accentColor,
              handleColor: Theme.of(context).accentColor,
              bufferedColor: Theme.of(context).backgroundColor,
              backgroundColor: Theme.of(context).disabledColor),
          onDragUpdate: () {

          },
        ),
      ),
    );
  }

}

自定义播放器进度 

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_ijkplayer/widget/progress_colors.dart';
import 'package:video_player/video_player.dart';

class MaterialVideoProgressBar extends StatefulWidget {
  final VideoPlayerController controller;
  final ProgressColors colors;
  final Function() onDragStart;
  final Function() onDragEnd;
  final Function() onDragUpdate;

  MaterialVideoProgressBar(
    this.controller, {
    required ProgressColors colors,
    required this.onDragEnd,
    required this.onDragStart,
    required this.onDragUpdate,
    Key? key,}): colors = colors, super(key: key);

  @override
  _VideoProgressBarState createState() {
    return _VideoProgressBarState();
  }
}

class _VideoProgressBarState extends State {
  late VoidCallback listener;
  bool _controllerWasPlaying = false;

  VideoPlayerController get controller => widget.controller;

  _VideoProgressBarState() {
    listener = () {
      if (!mounted) return;
      setState(() {});
    };
  }

  @override
  void initState() {
    super.initState();
    controller.addListener(listener);
  }

  @override
  void deactivate() {
    controller.removeListener(listener);
    super.deactivate();
  }

  @override
  Widget build(BuildContext context) {

    void seekToRelativePosition(Offset globalPosition) {
      final box = context.findRenderObject() as RenderBox;
      final Offset tapPos = box.globalToLocal(globalPosition);
      final double relative = tapPos.dx / box.size.width;
      final Duration position = controller.value.duration * relative;
      controller.seekTo(position);
    }

    return GestureDetector(
      onHorizontalDragStart: (DragStartDetails details) {
        if (!controller.value.isInitialized) {
          return;
        }
        _controllerWasPlaying = controller.value.isPlaying;
        if (_controllerWasPlaying) {
          controller.pause();
        }

        if (widget.onDragStart != null) {
          widget.onDragStart();
        }
      },
      onHorizontalDragUpdate: (DragUpdateDetails details) {
        if (!controller.value.isInitialized) {
          return;
        }
        seekToRelativePosition(details.globalPosition);

        if (widget.onDragUpdate != null) {
          widget.onDragUpdate();
        }
      },
      onHorizontalDragEnd: (DragEndDetails details) {
        if (_controllerWasPlaying) {
          controller.play();
        }
        if (widget.onDragEnd != null) {
          widget.onDragEnd();
        }
      },
      onTapDown: (TapDownDetails details) {
        if (!controller.value.isInitialized) {
          return;
        }
        seekToRelativePosition(details.globalPosition);
      },
      child: Center(
        child: Container(
          height: MediaQuery.of(context).size.height / 2,
          width: MediaQuery.of(context).size.width,
          color: Colors.transparent,
          child: CustomPaint(
            painter: _ProgressBarPainter(
              controller.value,
              widget.colors,
            ),
          ),
        ),
      ),
    );
  }
}

class _ProgressBarPainter extends CustomPainter {
  _ProgressBarPainter(this.value, this.colors);

  VideoPlayerValue value;
  ProgressColors colors;

  @override
  bool shouldRepaint(CustomPainter painter) {
    return true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    const height = 2.0;
    canvas.drawRRect(
      RRect.fromRectAndRadius(
        Rect.fromPoints(
          Offset(0.0, size.height / 2),
          Offset(size.width, size.height / 2 + height),
        ),
        const Radius.circular(4.0),
      ),
      colors.backgroundPaint,
    );
    if (!value.isInitialized) {
      return;
    }
    final double playedPartPercent =
        value.position.inMilliseconds / value.duration.inMilliseconds;
    final double playedPart =
        playedPartPercent > 1 ? size.width : playedPartPercent * size.width;
    for (final DurationRange range in value.buffered) {
      final double start = range.startFraction(value.duration) * size.width;
      final double end = range.endFraction(value.duration) * size.width;
      canvas.drawRRect(
        RRect.fromRectAndRadius(
          Rect.fromPoints(
            Offset(start, size.height / 2),
            Offset(end, size.height / 2 + height),
          ),
          const Radius.circular(4.0),
        ),
        colors.bufferedPaint,
      );
    }
    canvas.drawRRect(
      RRect.fromRectAndRadius(
        Rect.fromPoints(
          Offset(0.0, size.height / 2),
          Offset(playedPart, size.height / 2 + height),
        ),
        const Radius.circular(4.0),
      ),
      colors.playedPaint,
    );
    canvas.drawCircle(
      Offset(playedPart, size.height / 2 + height / 2),
      height * 3,
      colors.handlePaint,
    );
  }
}

全屏播放代码 

import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ijkplayer/util/screen_util.dart';
import 'package:flutter_ijkplayer/util/video_data.dart';
import 'package:video_player/video_player.dart';

//可以继续接着外部的进度来进行播放
class VideoFullScreenAdvancePage extends StatefulWidget {
  String url;
  VideoType videoType;
  VideoPlayerController controller;

  VideoFullScreenAdvancePage({Key? key,
    required this.url,
    required this.videoType,
    required this.controller
  }) : super(key: key);

  @override
  _VideoFullScreenPageState createState() => _VideoFullScreenPageState();

}

class _VideoFullScreenPageState extends State {

  late VideoPlayerController _controller;
  //是否是横屏 默认横屏
  bool isHorizontal = true;

  @override
  void initState() {
    super.initState();
    //如果不进行初始化,而是从外部传过来的
    // _controller = widget.videoType == VideoType.asset
    // ? VideoPlayerController.asset(widget.url)
    // : VideoPlayerController.network(widget.url)
    // ..initialize().then((value){
    //      setState(() {});
    // });
    _controller = widget.controller;
    _controller.addListener(() {
       //当前进度 == 总进度
       if (_controller.value.position ==  _controller.value.duration){
         //播放结束之后,自动退出
         Future.delayed(Duration.zero, (){
            //Navigator.of(context).pop();
          });
       }
    });
    //将状态栏的颜色改变为透明
    //ScreenUtil.setStatusBarColor(Colors.black);
    //设置横屏
    ScreenUtil.setHorizontal();
  }

  @override
  void dispose() {
    //设置竖屏
    ScreenUtil.setVertical();
    SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top, SystemUiOverlay.bottom]);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    //显示顶部栏并隐藏底部栏
    //SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top]);
    //显示底部栏并隐藏顶部栏
    //SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
    //隐藏顶部栏和底部栏
    SystemChrome.setEnabledSystemUIOverlays([]);
    return Scaffold(
      resizeToAvoidBottomInset: false,
      body: Container(
        alignment: Alignment.center,
        color: Colors.black,
        child: Stack(
          children: [
            //初始化视频
            initVideo(),
            //初始化开始按钮
            initStartButton(),
            //返回按钮
            initPopButton()
          ],
        ),
      ),
    );
  }

  initVideo(){
    return Center(
      child: _controller.value.isInitialized
          ? AspectRatio(
            child: VideoPlayer(_controller),
            aspectRatio: _controller.value.aspectRatio)
          : Container(),
    );
  }

  initStartButton(){
    return Positioned.fill(
        child: Stack(
          children: [
            //初始化渐变开始按钮
            initOpacityStartButton(),
            //竖屏按钮
            initPosition()
          ],
        )
    );
  }

  initOpacityStartButton() {
    return AnimatedOpacity(
        opacity: _controller.value.isPlaying? 0: 1,
        duration: Duration(seconds: 1),
        child: GestureDetector(
          onTap: (){
            _controller.value.isPlaying? _controller.pause(): _controller.play();
            setState(() {});
          },
          child: Container(
            color: Colors.grey.withOpacity(0.3),
            child: Center(
              child: Icon(
                _controller.value.isPlaying? Icons.pause: Icons.play_arrow,
                color: Colors.white,
              ),
            ),
          ),
       ),
    );
  }

  initPosition() {
    return Positioned(
        right: 40,
        bottom: 40,
        child: GestureDetector(
          onTap: (){
            isHorizontal = !isHorizontal;
            isHorizontal? ScreenUtil.setHorizontal(): ScreenUtil.setVertical();
            setState(() {});
          },
          child: Container(
            width: 100,
            height: 45,
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.grey.withOpacity(0.8),
              borderRadius: BorderRadius.circular(10)
            ),
            child: Text(
              isHorizontal? "竖屏" : "横屏",
              style: TextStyle(color: Colors.white),
            ),
          ),
        ));
  }

  initPopButton(){
    return Positioned(
        top: 20,
        left: 20,
        child: GestureDetector(
          onTap: (){
            //如果和列表项是共享的,那么就不能将controller给dispose
            //_controller.dispose();
            if (_controller.value.isPlaying){
              _controller.pause();
            }
            Navigator.of(context).pop();
          },
          child: Container(
            width: 100,
            height: 45,
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.grey.withOpacity(0.8),
              borderRadius: BorderRadius.circular(10)
            ),
            child: Icon(
              Icons.keyboard_return,
              color: Colors.white,),
          ),
        )
    );
  }

}

ScreenUtil 类负责设置屏幕横屏和竖屏,这里用到了 SystemChrome, 该类可以通过静态方法 setPreferredOrientations() 来进行横竖屏设置,静态方法 setSystemUIOverlayStyle 来设置状态栏的颜色。 

import 'package:flutter/services.dart';

class ScreenUtil{

  //横屏
  static void setHorizontal(){
    SystemChrome.setPreferredOrientations(
        [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]
    );
  }

  //竖屏
  static void setVertical(){
    SystemChrome.setPreferredOrientations(
        [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]
    );
  }

  //修改顶部状态栏颜色
  static void setStatusBarColor(Color color){
    SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: color);
    SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
  }

}

其他代码

//video_data.dart
enum VideoType{
  asset, //本地视频
  network //网络视频
}
//date_time_util.dart
String buildTextString(Duration duration){
   int inMinutes = duration.inMinutes;//
   int inSeconds = duration.inSeconds%60; //时间跨越整分钟数
   String inMinutesStr = inMinutes <10? "0$inMinutes": "$inMinutes";
   String inSecondsStr = inSeconds <10? "0$inSeconds": "$inSeconds";
   return "$inMinutesStr:$inSecondsStr";
}

四、效果

如下就是视频播放列表的三种样式:

Flutter入门系列-VideoPlayer在列表使用_第1张图片

Flutter入门系列-VideoPlayer在列表使用_第2张图片

Flutter入门系列-VideoPlayer在列表使用_第3张图片

五、总结

通过上面的代码,虽然可以实现简单的视频播放功能,但是仍然有很多缺陷,比如滑动时比较卡顿,每一个列表项都对应一个 VideoPlayer,是否合理,能否重复利用,减少对原生系统的资源的利用,这样就可以提供内存使用效率,降低内存消耗。同时也有体验上的优化,比如在视频全屏播放的时候,是否可以加上音量和亮度调节,倍速播放,连续播放,小窗播放等功能。

你可能感兴趣的:(Flutter开发,flutter)