由于flutter-fijkplayer播放器皮肤实在不能符合市场上的需求只能由自己重新根据官方源码来修改成自己需要的皮肤,但过程需要阅读源码和理解里面的api是干嘛的导致起码都得花个一两小时去处理,搜索出相关的案例也没几个很好用并且存在bug的情况,所以自己则封装了一个播放器具体源码可以到我的gitee查看

竖屏
横屏

依赖到的插件

  fijkplayer: ^0.9.0
  volume_controller: ^2.0.2
  screen_brightness: ^0.0.2
核心源码
  • 创建 customFijkPanel.dart
import 'dart:async';
import 'dart:math';

import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_controller/components/customFijkPanel/fijkPanelCenterController.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:volume_controller/volume_controller.dart';

class CustomFijkPanel extends StatefulWidget {
  final FijkPlayer player;
  final BuildContext buildContext;
  final Size viewSize;
  final Rect texturePos;
  final String videoTitle;
  final bool isNextNumber;
  final bool isPlayAd;
  final void Function()? onPlayAd;
  final void Function()? onBack;
  final void Function()? onError;
  final void Function()? onVideoEnd;
  final void Function()? onVideoPrepared;
  final void Function()? onVideoTimeChange;

  /// 播放器控制器具体到源代码目录查看参考fijkplayer\lib\ui\panel.dart
  /// ```
  /// @param {FijkPlayer} player -
  /// @param {BuildContext} buildContext -
  /// @param {Size} viewSize -
  /// @param {Rect} texturePos -
  /// @param {String} videoTitle -
  /// @param {bool} isNextNumber - 全屏后是否显示下一集按钮
  /// @param {bool} isPlayAd - 是否显示广告按钮
  /// @param {void Function()?} onPlayAd - 播放广告
  /// @param {void Function()?} onBack - 返回按钮
  /// @param {void Function()?} onError - 视频错误点击刷新
  /// @param {void Function()?} onVideoEnd - 视频结束
  /// @param {void Function()?} onVideoPrepared - 视频完成后台任务到稳定期
  /// @param {void Function()?} onVideoTimeChange - 视频时间更新
  /// ```
  const CustomFijkPanel({
    Key? key,
    required this.player,
    required this.buildContext,
    required this.viewSize,
    required this.texturePos,
    required this.videoTitle,
    this.isNextNumber = false,
    this.isPlayAd = false,
    this.onPlayAd,
    this.onBack,
    this.onError,
    this.onVideoEnd,
    this.onVideoPrepared,
    this.onVideoTimeChange,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _CustomFijkPanelState();
  }
}

class _CustomFijkPanelState extends State<CustomFijkPanel> {
  FijkPlayer get player => widget.player;
  bool get isFullScreen => player.value.fullScreen;

  /// 总时间
  Duration _duration = Duration();

  /// 动画时间
  Duration get _animatedTime => Duration(milliseconds: 400);

  /// 是否在播放
  bool _playing = false;

  /// 后台任务是否初步执行完成是用于正在加载中的状态
  bool _prepared = false;

  /// 视频状态是否执行完成成为稳定状态与_prepared不一致
  bool _playStatePrepared = false;

  bool _isPlayCompleted = false;

  /// 是否在加载中
  bool _buffering = false;
  int _bufferingPro = 0;
  late StreamSubscription _bufferingSubs;

  /// 拖动快进的时间 -1不显示
  double _seekPos = -1;

  /// 当前时间
  Duration _currentPos = Duration();
  late StreamSubscription _currentPosSubs;

  /// 预加载时间
  Duration _bufferPos = Duration();
  late StreamSubscription _bufferPosSubs;
  late StreamSubscription<int> _bufferPercunt;

  /// 控制器隐藏
  Timer? _hideTimer;
  bool _hideStuff = true;

  /// 视频错误
  bool _isPlayError = false;

  /// 声音 0-1范围
  double _currentVolume = 0;
  bool _showVolume = false;

  /// 屏幕亮度 0-1范围
  double _currentBrightness = 0;
  bool _showBrightness = false;

  int sendCount = 0;

  @override
  void initState() {
    super.initState();
    _duration = player.value.duration;
    _currentPos = player.currentPos;
    _bufferPos = player.bufferPos;
    _prepared = player.value.prepared;
    var playerState = player.state;
    _playing = playerState == FijkState.started;
    _isPlayError = playerState == FijkState.error;
    _isPlayCompleted = playerState == FijkState.completed;
    _playStatePrepared = playerState == FijkState.prepared;
    _buffering = player.isBuffering;
    initScreenBrightness();
    // FijkVolume.setUIMode(FijkVolume.hideUIWhenPlayable);
    VolumeController().getVolume().then((volume) {
      print("多媒体声音$volume");
      _currentVolume = volume;
    });

    /// 由于变化太小无法监听到基本监听物理按键调整的情况
    VolumeController().listener((volume) {
      print("多媒体声音变化$volume");
      _currentVolume = volume;
    });
    player.addListener(_playerValueChanged);

    /// 当前进度
    _currentPosSubs = player.onCurrentPosUpdate.listen((value) {
      setState(() {
        _currentPos = value;
        if (_buffering == true) {
          _buffering = false; // 避免有可能出现已经播放时还在显示缓冲中
        }
        if (_playing == false) {
          _playing = true; // 避免播放在false时导致bug
        }
      });
      // 每n次才进入一次不然太频繁发送处理业务太复杂则会增加消耗
      if (sendCount % 50 == 0) {
        widget.onVideoTimeChange?.call();
      }
      sendCount++;
    });

    /// 视频预加载进度
    _bufferPosSubs = player.onBufferPosUpdate.listen((value) {
      setState(() {
        _bufferPos = value;
      });
    });

    /// 视频卡顿回调
    _bufferingSubs = player.onBufferStateUpdate.listen((value) {
      print("视频加载中$value");
      if (value == false && _playing == false) {
        _playOrPause();
      }
      setState(() {
        _buffering = value;
      });
    });

    /// 视频卡顿当缓冲量回调
    _bufferPercunt = player.onBufferPercentUpdate.listen((value) {
      setState(() {
        _bufferingPro = value;
      });
    });
  }

  @override
  void dispose() {
    player.removeListener(_playerValueChanged);
    VolumeController().removeListener();
    _hideTimer?.cancel();
    _currentPosSubs.cancel();
    _bufferPosSubs.cancel();
    _bufferingSubs.cancel();
    _bufferPercunt.cancel();
    ScreenBrightness.resetScreenBrightness().catchError((error) {
      print("重置亮度-异常$error");
    });
    super.dispose();
  }

  Future<void> initScreenBrightness() async {
    double _brightness = 0.5;
    try {
      _brightness = await ScreenBrightness.initial;
      // print("获取屏幕亮度$_brightness");
    } catch (error) {
      print("获取屏幕亮度-异常$error");
    }
    setState(() {
      _currentBrightness = _brightness;
    });
  }

  void _playerValueChanged() {
    var value = player.value;
    if (value.duration != _duration) {
      setState(() {
        _duration = value.duration;
      });
    }

    var valueState = value.state;
    var playing = (valueState == FijkState.started);
    var prepared = value.prepared;
    var isPlayError = valueState == FijkState.error;
    var completed = valueState == FijkState.completed;
    if (isPlayError != _isPlayError ||
        playing != _playing ||
        prepared != _prepared ||
        completed != _isPlayCompleted) {
      setState(() {
        _isPlayError = isPlayError;
        _playing = playing;
        _prepared = prepared;
        _isPlayCompleted = completed;
      });
    }

    /// [value.prepared]不会等于[playStatePrepared]所以单独判断
    bool playStatePrepared = valueState == FijkState.prepared;
    if (_playStatePrepared != playStatePrepared) {
      if (playStatePrepared) {
        widget.onVideoPrepared?.call();
      }
      _playStatePrepared = playStatePrepared;
    }
    bool isPlayCompleted = valueState == FijkState.completed;
    if (isPlayCompleted) {
      print("视频状态结束是否还有下一集${widget.isNextNumber}");
      if (widget.isNextNumber) {
        widget.onVideoEnd?.call();
      } else {
        _isPlayCompleted = isPlayCompleted;
      }
    }
  }

  /// 播放开始
  void _playOrPause() {
    if (_playing == true) {
      player.pause();
    } else {
      player.start();
    }
  }

  void _startHideTimer() {
    _hideTimer?.cancel();
    _hideTimer = Timer(const Duration(seconds: 10), () {
      setState(() {
        _hideStuff = true;
      });
    });
  }

  /// 控制器显示隐藏
  void _cancelAndRestartTimer() {
    if (_hideStuff == true) {
      _startHideTimer();
    }
    setState(() {
      _hideStuff = !_hideStuff;
    });
  }

  /// 时间转换显示
  String _duration2String(Duration duration) {
    if (duration.inMilliseconds < 0) {
      return "00:00";
    }
    String twoDigits(int n) {
      if (n >= 10) {
        return "$n";
      } else {
        return "0$n";
      }
    }

    String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
    String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
    int inHours = duration.inHours;
    if (inHours > 0) {
      return "$inHours:$twoDigitMinutes:$twoDigitSeconds";
    } else {
      return "$twoDigitMinutes:$twoDigitSeconds";
    }
  }

  /// 快进视频时间
  void _onVideoTimeChangeUpdate(double value) {
    if (_duration.inMilliseconds < 0 ||
        value < 0 ||
        value > _duration.inMilliseconds) {
      return;
    }
    _startHideTimer();
    setState(() {
      _seekPos = value;
    });
  }

  /// 快进视频松手开始跳时间
  void _onVideoTimeChangeEnd(double value) {
    var time = _seekPos.toInt();
    _currentPos = Duration(milliseconds: time);
    print("跳转时间$time ${_duration.inMilliseconds}");
    player.seekTo(time).then((value) {
      if (!_playing) {
        player.start();
      }
    });
    setState(() {
      _seekPos = -1;
    });
  }

  /// 获取视频当前时间, 如拖动快进时间则显示快进的时间
  double getCurrentVideoValue() {
    double duration = _duration.inMilliseconds.toDouble();
    double currentValue;
    if (_seekPos > 0) {
      currentValue = _seekPos;
    } else {
      currentValue = _currentPos.inMilliseconds.toDouble();
    }
    currentValue = min(currentValue, duration);
    currentValue = max(currentValue, 0);
    return currentValue;
  }

  /// 顶部栏
  Widget _buildTopmBar() {
    return Stack(
      children: <Widget>[
        AnimatedOpacity(
          opacity: _hideStuff ? 0 : 1,
          duration: _animatedTime,
          child: Container(
            height: 50,
            decoration: BoxDecoration(
              gradient: LinearGradient(
                // 渐变位置
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                stops: [0.0, 1.0], // [渐变起始点, 渐变结束点]
                // 渐变颜色[始点颜色, 结束颜色]
                colors: [
                  Color.fromRGBO(0, 0, 0, 1),
                  Color.fromRGBO(0, 0, 0, 0),
                ],
              ),
            ),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                !isFullScreen ? Container(width: 40) : _backBtn(),
                Expanded(
                  child: Text(
                    widget.videoTitle,
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 14,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ],
            ),
          ),
        ),

        /// 返回按钮 小屏幕状态下显示 或者错误播放等情况
        Positioned(
          top: 0,
          left: 0,
          right: 0,
          child: Row(
            children: <Widget>[
              isFullScreen ? Container() : _backBtn(),
            ],
          ),
        ),
      ],
    );
  }

  /// 中间区域
  Widget _buildCentetContext() {
    double currentValue = getCurrentVideoValue();
    return FijkPanelCenterController(
      size: Size(double.infinity, double.infinity),
      onTap: _cancelAndRestartTimer,
      onDoubleTap: _playOrPause,
      currentTime: currentValue,
      onHorizontalStart: _onVideoTimeChangeUpdate,
      onHorizontalChange: _onVideoTimeChangeUpdate,
      onHorizontalEnd: _onVideoTimeChangeEnd,
      currentBrightness: _currentBrightness,
      onLeftVerticalStart: (value) {
        setState(() {
          _showBrightness = true;
        });
      },
      onLeftVerticalChange: (value) {
        ScreenBrightness.setScreenBrightness(value);
        setState(() {
          _currentBrightness = value;
        });
      },
      currentVolume: _currentVolume,
      onLeftVerticalEnd: (value) {
        setState(() {
          _showBrightness = false;
        });
      },
      onRightVerticalStart: (value) {
        setState(() {
          _showVolume = true;
        });
      },
      onRightVerticalChange: (value) {
        VolumeController().setVolume(value, showSystemUI: false);
        // FijkVolume.setVol(value);
        setState(() {
          _currentVolume = value;
        });
      },
      onRightVerticalEnd: (value) {
        setState(() {
          _showVolume = false;
        });
      },
      builderChild: (context) {
        Widget videoLoading = Container(); // 视频缓冲
        if (_buffering) {
          videoLoading = Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Container(
                width: 25,
                height: 25,
                margin: EdgeInsets.only(bottom: 10),
                child: CircularProgressIndicator(
                  backgroundColor: Color.fromRGBO(250, 250, 250, 0.5),
                  valueColor: AlwaysStoppedAnimation(Colors.white70),
                ),
              ),
              Text(
                "缓冲中 $_bufferingPro %",
                style: TextStyle(
                  color: Colors.white70,
                  fontSize: 14,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ],
          );
        }
        return Stack(
          children: <Widget>[
            /// 中间内容目前没有东西展示
            AnimatedOpacity(
              opacity: _hideStuff ? 0 : 1,
              duration: _animatedTime,
            ),
            Positioned(
              left: 0,
              right: 0,
              top: 0,
              bottom: 0,
              child: videoLoading,
            ),

            /// 快进时间
            Positioned(
              left: 0,
              right: 0,
              top: 0,
              bottom: 0,
              child: Offstage(
                offstage: _seekPos == -1,
                child: Center(
                  child: Container(
                    padding: EdgeInsets.all(5),
                    decoration: BoxDecoration(
                      color: Color.fromRGBO(0, 0, 0, 0.5),
                      borderRadius: BorderRadius.circular(5),
                    ),
                    child: Text(
                      "${_duration2String(
                        Duration(milliseconds: _seekPos.toInt()),
                      )} / ${_duration2String(_duration)}",
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
            ),

            /// 声音
            Positioned(
              left: 0,
              right: 0,
              top: 0,
              bottom: 0,
              child: AnimatedOpacity(
                opacity: _showVolume ? 1 : 0,
                duration: _animatedTime,
                child: _buildVolumeOrBrightnessProgress(
                  type: 1,
                  value: _currentVolume,
                  maxValue: 1,
                ),
              ),
            ),

            /// 亮度
            Positioned(
              left: 0,
              right: 0,
              top: 0,
              bottom: 0,
              child: AnimatedOpacity(
                opacity: _showBrightness ? 1 : 0,
                duration: _animatedTime,
                child: _buildVolumeOrBrightnessProgress(
                  type: 2,
                  value: _currentBrightness,
                  maxValue: 1,
                ),
              ),
            ),
          ],
        );
      },
    );
  }

  /// 声音或亮度进度
  Widget _buildVolumeOrBrightnessProgress({
    required int type,
    required double value,
    required double maxValue,
  }) {
    IconData? iconData;
    if (type == 1) {
      iconData = value <= 0 ? Icons.volume_off_sharp : Icons.volume_up;
    } else {
      iconData = Icons.brightness_4;
    }
    double maxProgressWidth = 90;
    return Center(
      child: Container(
        width: 130,
        padding: EdgeInsets.only(top: 5, bottom: 5, right: 10),
        decoration: BoxDecoration(
          color: Color.fromRGBO(0, 0, 0, 0.5),
          borderRadius: BorderRadius.circular(5),
        ),
        child: Row(
          children: <Widget>[
            Expanded(
              child: Container(
                margin: EdgeInsets.symmetric(horizontal: 5),
                child: Icon(
                  iconData,
                  size: 20,
                  color: Colors.white,
                ),
              ),
            ),
            Container(
              width: maxProgressWidth,
              height: 3,
              decoration: BoxDecoration(
                color: Color.fromRGBO(250, 250, 250, 0.5),
                borderRadius: BorderRadius.circular(5),
              ),
              child: Row(
                children: <Widget>[
                  Container(
                    width: value / maxValue * maxProgressWidth,
                    height: 3,
                    color: Colors.white,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  /// 视频时间进度条
  Widget _buildVideoTimeBar() {
    double currentValue = getCurrentVideoValue();
    return FijkSlider(
      min: 0,
      max: _duration.inMilliseconds.toDouble(),
      value: currentValue,
      cacheValue: _bufferPos.inMilliseconds.toDouble(),
      colors: FijkSliderColors(
        playedColor: Color(0xff4075d1),
        cursorColor: Colors.white,
        baselineColor: Color(0xff807e7c),
        bufferedColor: Color(0xff6494e6),
      ),
      // onChangeStart: _onVideoTimeChangeUpdate,
      onChanged: _onVideoTimeChangeUpdate,
      onChangeEnd: _onVideoTimeChangeEnd,
    );
  }

  /// 底部栏
  AnimatedOpacity _buildBottomBar() {
    return AnimatedOpacity(
      opacity: _hideStuff ? 0 : 1,
      duration: _animatedTime,
      child: Container(
        height: isFullScreen ? 80 : 50,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter, // 渐变位置
            end: Alignment.bottomCenter,
            stops: [0, 1], // [渐变起始点, 渐变结束点]
            colors: [
              Color.fromRGBO(0, 0, 0, 0),
              Color.fromRGBO(0, 0, 0, 1),
            ], // 渐变颜色[始点颜色, 结束颜色]
          ),
        ),
        child: Column(
          children: <Widget>[
            Container(
              height: isFullScreen ? 25 : 0,
              padding: EdgeInsets.symmetric(horizontal: 10),
              child: isFullScreen ? _buildVideoTimeBar() : null,
            ),
            Expanded(
              child: Row(
                children: <Widget>[
                  /// 播放按钮
                  GestureDetector(
                    onTap: _playOrPause,
                    child: Container(
                      padding: EdgeInsets.symmetric(horizontal: 15),
                      color: Colors.transparent,
                      height: double.infinity,
                      child: Icon(
                        _playing ? Icons.pause : Icons.play_arrow,
                        color: Colors.white,
                        size: 18,
                      ),
                    ),
                  ),

                  /// 下一集按钮(全屏下可以看到)
                  Offstage(
                    offstage: !widget.isNextNumber || !isFullScreen,
                    child: GestureDetector(
                      onTap: widget.onVideoEnd,
                      child: Container(
                        padding: EdgeInsets.only(right: 15),
                        color: Colors.transparent,
                        height: double.infinity,
                        child: Icon(
                          Icons.skip_next_sharp,
                          color: Colors.white,
                          size: 18,
                        ),
                      ),
                    ),
                  ),

                  /// 当前时长
                  Text(
                    _duration2String(_currentPos),
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.white,
                    ),
                  ),

                  Expanded(
                    child: Padding(
                      padding: EdgeInsets.symmetric(horizontal: 10),
                      child: !isFullScreen ? _buildVideoTimeBar() : null,
                    ),
                  ),

                  /// 总时长
                  Text(
                    _duration2String(_duration),
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.white,
                    ),
                  ),

                  /// 全屏按钮
                  isFullScreen
                      ? SizedBox(width: 30)
                      : GestureDetector(
                          onTap: () {
                            player.enterFullScreen();
                            Future.delayed(Duration(seconds: 1), () {
                              setViewStatusBar(true);
                            });
                          },
                          child: Container(
                            padding: EdgeInsets.symmetric(horizontal: 13),
                            color: Colors.transparent,
                            height: double.infinity,
                            child: Icon(
                              isFullScreen
                                  ? Icons.fullscreen_exit
                                  : Icons.fullscreen,
                              color: Colors.white,
                              size: 25,
                            ),
                          ),
                        ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  /// 返回按钮
  Widget _backBtn() {
    return GestureDetector(
      onTap: widget.onBack,
      child: Container(
        padding: EdgeInsets.all(8),
        child: Icon(
          Icons.chevron_left,
          size: 34,
          color: Colors.white,
        ),
      ),
    );
  }

  /// 视频异常状态
  Widget _renderVideoStatusView() {
    var bgImg = BoxDecoration(
      color: Colors.black,
      // image: DecorationImage(
      //   fit: BoxFit.cover,
      //   image: AssetImage(
      //     "xxx.jpg", // 可以设置一个背景图
      //   ),
      // ),
    );
    if (_isPlayError) {
      /// 错误
      return GestureDetector(
        onTap: widget.onError,
        child: Container(
          width: double.infinity,
          height: double.infinity,
          decoration: bgImg,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: EdgeInsets.only(bottom: 15),
                child: Icon(
                  Icons.error_rounded,
                  color: Colors.white70,
                  size: 70,
                ),
              ),
              RichText(
                text: TextSpan(
                  text: "播放异常!",
                  style: TextStyle(
                    color: Colors.white70,
                    fontSize: 14,
                    fontWeight: FontWeight.w600,
                  ),
                  children: <InlineSpan>[
                    TextSpan(
                      text: "刷新",
                      style: TextStyle(
                        color: Color(0xff79b0ff),
                      ),
                    )
                  ],
                ),
              ),
            ],
          ),
        ),
      );
    } else if (widget.isPlayAd) {
      return Container(
        width: double.infinity,
        height: double.infinity,
        decoration: bgImg,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              "要看广告",
              style: TextStyle(
                color: Colors.white,
                fontSize: 14,
              ),
            ),
            SizedBox(height: 10),
            Text(
              "播放一段视频广告",
              style: TextStyle(
                color: Colors.white70,
                fontSize: 12.5,
              ),
            ),
            SizedBox(height: 20),
            GestureDetector(
              onTap: widget.onPlayAd,
              child: Container(
                padding: EdgeInsets.symmetric(
                  vertical: 8,
                  horizontal: 20,
                ),
                decoration: BoxDecoration(
                  color: Color(0xff2d73ed),
                  borderRadius: BorderRadius.circular(5),
                ),
                child: Text(
                  "点击广告",
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 13,
                  ),
                ),
              ),
            ),
          ],
        ),
      );
    } else if (!_prepared) {
      /// 加载中
      return Container(
        width: double.infinity,
        height: double.infinity,
        decoration: bgImg,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              width: 50,
              height: 50,
              margin: EdgeInsets.only(bottom: 20),
              child: CircularProgressIndicator(
                backgroundColor: Colors.white70,
                valueColor: AlwaysStoppedAnimation(Color(0xff79b0ff)),
              ),
            ),
            Text(
              "努力加载中...",
              style: TextStyle(
                color: Colors.white70,
                fontSize: 14,
                fontWeight: FontWeight.w600,
              ),
            ),
          ],
        ),
      );
    } else if (_isPlayCompleted) {
      /// 是否显示播放完
      return GestureDetector(
        onTap: () {
          player.start();
          setState(() {
            _isPlayCompleted = false;
          });
        },
        child: Container(
          width: double.infinity,
          height: double.infinity,
          color: Color.fromRGBO(0, 0, 0, 0.5),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Icon(
                Icons.play_circle_outline_outlined,
                size: 30,
                color: Colors.white70,
              ),
              SizedBox(height: 10),
              Text(
                "重新播放",
                style: TextStyle(
                  color: Colors.white70,
                  fontSize: 12.5,
                ),
              ),
            ],
          ),
        ),
      );
    } else {
      return Container();
    }
  }

  /// 设置页面全屏化显示隐藏状态栏和虚拟按键
  setViewStatusBar(bool isHide) {
    if (isHide) {
      SystemChrome.setEnabledSystemUIOverlays([]);
    } else {
      SystemChrome.setEnabledSystemUIOverlays([
        SystemUiOverlay.top,
        SystemUiOverlay.bottom,
      ]);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isPlayError || !_prepared || _isPlayCompleted || widget.isPlayAd) {
      /// 错误播放 | 没加载好 | 播放完成没有下一集
      return Stack(
        children: <Widget>[
          _renderVideoStatusView(),
          Positioned(
            top: 0,
            left: 0,
            right: 0,
            child: Container(
              width: double.infinity,
              color: Colors.transparent,
              alignment: Alignment.centerLeft,
              child: _backBtn(),
            ),
          ),
        ],
      );
    } else {
      var viewSize = widget.viewSize;
      return Positioned.fromRect(
        rect: Rect.fromLTWH(0, 0, viewSize.width, viewSize.height),
        child: Column(
          children: <Widget>[
            _buildTopmBar(),
            Expanded(
              child: _buildCentetContext(),
            ),
            _buildBottomBar(),
          ],
        ),
      );
    }
  }
}

  • 然后创建一个 fijkPanelCenterController.dart 用于触摸监听手势使用的
import 'package:flutter/material.dart';

class FijkPanelCenterController extends StatefulWidget {
  final Size size;
  final void Function()? onTap;
  final void Function()? onDoubleTap;
  final double currentTime;
  final void Function(double)? onHorizontalStart;
  final void Function(double)? onHorizontalChange;
  final void Function(double)? onHorizontalEnd;
  final double currentBrightness;
  final void Function(double)? onLeftVerticalStart;
  final void Function(double)? onLeftVerticalChange;
  final void Function(double)? onLeftVerticalEnd;
  final double currentVolume;
  final void Function(double)? onRightVerticalStart;
  final void Function(double)? onRightVerticalChange;
  final void Function(double)? onRightVerticalEnd;
  final Widget Function(BuildContext context) builderChild;

  /// 自定义的触摸控制器
  /// ```
  /// @param {Size} size - 框大小
  /// @param {void Function()?} onTap -
  /// @param {void Function()?} onDoubleTap -
  /// @param {double} currentTime - 传入当前视频时间onHorizontal时计算用得到
  /// @param {void Function(double)?} onHorizontalStart -
  /// @param {void Function(double)?} onHorizontalChange -
  /// @param {void Function(double)?} onHorizontalEnd -
  /// @param {double} currentBrightness - 传入当前系统亮度onLeftVertical时计算用得到
  /// @param {void Function(double)?} onLeftVerticalStart - 左边上下拖动(亮度)
  /// @param {void Function(double)?} onLeftVerticalChange -
  /// @param {void Function(double)?} onLeftVerticalEnd -
  /// @param {double} currentVolume - 传入当前系统声音onRightVertical时计算用得到
  /// @param {void Function(double)?} onRightVerticalStart - 右边上下拖动(声音)
  /// @param {void Function(double)?} onRightVerticalChange -
  /// @param {void Function(double)?} onRightVerticalEnd -
  /// @param {Widget Function(BuildContext context)} builderChild - 子节点内容直接由外界传入
  /// ```
  const FijkPanelCenterController({
    Key? key,
    required this.size,
    this.onTap,
    this.onDoubleTap,
    required this.currentTime,
    this.onHorizontalStart,
    this.onHorizontalChange,
    this.onHorizontalEnd,
    required this.currentBrightness,
    this.onLeftVerticalStart,
    this.onLeftVerticalChange,
    this.onLeftVerticalEnd,
    required this.currentVolume,
    this.onRightVerticalStart,
    this.onRightVerticalChange,
    this.onRightVerticalEnd,
    required this.builderChild,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _FijkPanelCenterController();
  }
}

class _FijkPanelCenterController extends State<FijkPanelCenterController> {
  /// 上下滑动时在开始的时候记录 0-左边 1-右边
  int verticalType = 0;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: widget.onTap,
      onDoubleTap: widget.onDoubleTap,
      onHorizontalDragStart: (details) {
        widget.onHorizontalStart?.call(widget.currentTime);
      },
      onHorizontalDragUpdate: (details) {
        // 来自上次更新以来,指针在事件接收器的坐标空间中沿主轴移动的量。
        double deltaDx = details.delta.dx;
        if (deltaDx == 0) {
          return; // 避免某些手机会返回0.0
        }
        var dragValue = (deltaDx * 4000) + widget.currentTime;
        widget.onHorizontalChange?.call(dragValue);
      },
      onHorizontalDragEnd: (details) {
        widget.onHorizontalEnd?.call(widget.currentTime);
      },
      onVerticalDragStart: (details) {
        double dx = details.localPosition.dx;
        var winWidth = context.size?.width ?? 0;
        if (dx < winWidth / 2) {
          verticalType = 0;
          widget.onLeftVerticalStart?.call(widget.currentBrightness);
        } else {
          verticalType = 1;
          widget.onRightVerticalStart?.call(widget.currentVolume);
        }
      },
      onVerticalDragUpdate: (details) {
        double deltaDy = details.delta.dy;
        if (deltaDy == 0) {
          return; // 避免某些手机会返回0.0
        }
        double moveTo = 0;
        if (deltaDy > 0) {
          moveTo = -0.01;
        } else {
          moveTo = 0.01;
        }
        double dragValue = 0;
        switch (verticalType) {
          case 0:
            dragValue = moveTo + widget.currentBrightness;
            if (dragValue > 1) {
              dragValue = 1;
            } else if (dragValue < 0) {
              dragValue = 0;
            }
            print("设置亮度$dragValue");
            widget.onLeftVerticalChange?.call(dragValue);
            break;
          case 1:
            dragValue = moveTo + widget.currentVolume;
            if (dragValue > 1) {
              dragValue = 1;
            } else if (dragValue < 0) {
              dragValue = 0;
            }
            print("设置声音$dragValue");
            widget.onRightVerticalChange?.call(dragValue);
            break;
          default:
        }
      },
      onVerticalDragEnd: (details) {
        switch (verticalType) {
          case 0:
            widget.onLeftVerticalEnd?.call(widget.currentBrightness);
            break;
          case 1:
            widget.onRightVerticalEnd?.call(widget.currentVolume);
            break;
          default:
        }
      },
      child: Container(
        width: widget.size.width,
        height: widget.size.height,
        color: Colors.transparent,
        child: widget.builderChild(context),
      ),
    );
  }
}
  • 具体核心源码只有两个文件上下则是调用出来的业务逻辑
import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_controller/components/customFijkPanel/customFijkPanel.dart';

class VideoCountListParam {
  String url;
  String title;
  VideoCountListParam({
    required this.url,
    required this.title,
  });
}

class Video extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _VideoState();
  }
}

class _VideoState extends State<Video> {
  FijkPlayer _player = FijkPlayer();

  /// 集数列表
  List<VideoCountListParam> _videoCountList = [];

  int _videoPlayIndex = 0;

  int seekTime = 0;

  bool isPlayAd = false;

  @override
  void initState() {
    for (var i = 0; i < 20; i++) {
      var url = "xxx/xxx.mp4";
      if (i % 2 == 0) {
        url = 'xxx.mp4';
      }
      _videoCountList.add(VideoCountListParam(
        url: url,
        title: "标题${i + 1}",
      ));
    }
    if (!isPlayAd) {
      setVideoUrl(_videoCountList[_videoPlayIndex].url);
    }
    super.initState();
  }

  @override
  void dispose() {
    _player.release();
    super.dispose();
  }

  Future<void> setVideoUrl(String url) async {
    try {
      await _player.setDataSource(url, autoPlay: true, showCover: true);
    } catch (error) {
      print("播放-异常: $error");
      return;
    }
  }

  @override
  Widget build(BuildContext context) {
    MediaQueryData mediaQueryData = MediaQuery.of(context);
    double statusHeight = mediaQueryData.padding.top;
    Size size = mediaQueryData.size;
    double _videoHeight = size.width * 9 / 16;
    return AnnotatedRegion(
      value: SystemUiOverlayStyle.light,
      child: Scaffold(
        body: Column(
          children: <Widget>[
            Container(
              padding: EdgeInsets.only(top: statusHeight),
              color: Colors.black,
              child: FijkView(
                width: double.infinity,
                height: _videoHeight,
                player: _player,
                color: Colors.black,
                fsFit: FijkFit.contain, // 全屏模式下的填充
                fit: FijkFit.contain, // 正常模式下的填充
                panelBuilder:
                    (player, data, buildContext, viewSize, texturePos) {
                  return CustomFijkPanel(
                    player: player,
                    buildContext: context,
                    viewSize: viewSize,
                    texturePos: texturePos,
                    videoTitle: _videoCountList[_videoPlayIndex].title,
                    isPlayAd: isPlayAd,
                    isNextNumber: _videoPlayIndex + 1 < _videoCountList.length,
                    onPlayAd: () {
                      /// 播放广告 isPlayAd true 才会显示点击后处理播放后再开始播放视频
                    },
                    onError: () async {
                      await _player.reset();
                      setVideoUrl(_videoCountList[_videoPlayIndex].url);
                    },
                    onBack: () {
                      Navigator.pop(context); // 如果需要做拦截返回则在此判断
                    },
                    onVideoEnd: () async {
                      // 视频结束最后一集的时候会有个UI层显示出来可以触发重新开始
                      var index = _videoPlayIndex + 1;
                      if (index < _videoCountList.length) {
                        await _player.reset();
                        setState(() {
                          _videoPlayIndex = index;
                        });
                        setVideoUrl(_videoCountList[index].url);
                      }
                    },
                    onVideoTimeChange: () {
                      // 视频时间变动则触发一次,可以保存视频历史如不想频繁触发则在里修改 sendCount % 50 == 0
                    },
                    onVideoPrepared: () async {
                      // 视频初始化完毕,如有历史记录时间段则可以触发快进
                      try {
                        if (seekTime >= 1) {
                          /// seekTo必须在FijkState.prepared
                          await _player.seekTo(seekTime);
                          // print("视频快进-$seekTime");
                          seekTime = 0;
                        }
                      } catch (error) {
                        print("视频初始化完快进-异常: $error");
                      }
                    },
                  );
                },
              ),
            ),
            Container(
              width: 500,
              height: 50,
              margin: EdgeInsets.only(top: 15, bottom: 20, right: 10, left: 10),
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                // controller: _videoNumberController,
                padding: EdgeInsets.all(0),
                itemCount: _videoCountList.length,
                itemBuilder: (context, index) {
                  bool isCurrent = _videoPlayIndex == index;
                  Color textColor = Colors.black;
                  Color bgColor = Colors.white70;
                  Color borderColor = Colors.blueGrey;
                  if (isCurrent) {
                    borderColor = Colors.blue;
                    textColor = Colors.blue;
                    bgColor = Colors.transparent;
                  }
                  return GestureDetector(
                    onTap: () async {
                      await _player.reset();
                      setState(() {
                        _videoPlayIndex = index;
                      });
                      setVideoUrl(_videoCountList[index].url);
                    },
                    child: Container(
                      margin: EdgeInsets.only(left: index == 0 ? 0 : 10),
                      width: 50,
                      height: 50,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(5),
                        color: bgColor,
                        border: Border.all(
                          width: 1.5,
                          color: borderColor,
                        ),
                      ),
                      alignment: Alignment.center,
                      child: Text(
                        _videoCountList[index].title,
                        style: TextStyle(
                          fontSize: 15,
                          color: textColor,
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐