个性化 UI

 

听说你嫌弃播放器默认 UI 太丑 😫,或者少了你想要的显示组件 😂?
没关系,你完全可以按照你的想法重新实现播放器控制 UI。

读完这一节的内容,你就能够掌握了如何自定义播放器控制 UI。

使用 fijkPanel2Builder

fijkplayer 中除了默认的 UI 之外,还提供了一个更高级的 panel builder 函数。

用法如下:

FijkView(
  player: player,
  panelBuilder: fijkPanel2Builder(),           
)

其中 fijkPanel2Builder 还提供了多个参数,可以控制最终的控制 UI 效果。

  • fill, UI 的实际区域,覆盖完整 FijkView 区域,或者仅仅覆盖视频区域
  • duration,UI 消失的延迟时间,单位是毫秒
  • doubleTap,是否使用双击 播放、暂停的控制功能
  • onBack, 返回回掉函数,如果不是 null,则panel 左上角始终显示一个返回按钮,点击会后调用此 callback 函数

自定义 UI 接口

之前在播放器填充和裁剪的文档中讲到 FijkView 构造函数中的参数,省去了和缩放裁剪无关的参数。 在这一节中,就要从这省去的参数中说起。

typedef FijkPanelWidgetBuilder = Widget Function(
    FijkPlayer player, FijkData data, BuildContext context, Size viewSize, Rect texturePos);

FijkView({
    @required FijkPlayer player,
    double width,
    double height,
    Color color = Colors.blueGrey,
    FijkFit fit = FijkFit.contain,
    ImageProvider cover,
    FijkPanelWidgetBuilder panelBuilder,
}) 

FijkPanelWidgetBuilder 是一个用于 build 播放器控制 UI 的函数签名。在 FijkView 的缺省构造中,panelBuilder 也有一个缺省的实现,效果就是你看到的那个丑丑的控制 UI。

接口参数

先说一下 FijkPanelWidgetBuilder 函数签名中的这几个参数:

  • player 播放器 FijkPlayer 对象,FijkView 所显示视频的数据来源,自定义UI所控制的播放器对象。
    监听此 player 的属性变化并在 UI 上作出相应的改变。
  • data 播放器在全屏模式和非全屏模式切换时,panel 会销毁并重新初始化,如果要在全屏模式和非全屏模式的 panel 中共享一些数据,可以把数据保存在 FijkData 中。
  • context build Widget 的上下文。
  • viewSize 对应 FijkView 的实际显示大小
  • texturePos FijkView 中实际视频显示的相对位置,这个相对位置可能超出 FijkView 的实际大小

如果在 panel 控制中修改了一些系统参数,可以记录到 FijkData 中,等 FijkView 销毁时,在 FijkView 的 onDispose 回掉函数中恢复系统默认值。

结合 播放器填充与裁剪 中的内容,可以对 viewSizetexturePos 两个参数有更好的理解。

  • FijkFit.contain 模式下,texturePos 是绝对不会超出 viewSize 的大小。
  • FijkFit.fill 模式下,texturePos 的宽高肯定是和 viewSize 的宽高相等,texturePos 的相对偏移是 0。
  • FijkFit.cover 模式,且 FijkView 宽高比例和实际视频宽高比例不等的情况下,texturePos 宽高肯定超出 viewSize 的大小。

返回值

FijkPanelWidgetBuilder 返回的 Widget 实际上会在组件树中作为一个 Stack 组件的子组件,texturePos 就是视频显示区域在 Stack 中的相对位置 所以返回一个 Positioned 也是可以的。

FijkPanelWidgetBuilder 返回的 Widget 覆盖在 Stack 子组件 Texture (实际渲染视频的组件) 的上方。

牛刀小试

利用上面描述的自定义播放器控制 UI 的接口,我们实际编码实现一个非常简单的 UI。

UI 描述: 在视频显示区域的左下角根据实际播放器状态显示一个播放、暂停按钮。

无状态 UI ?

Widget simplestUI(FijkPlayer player, BuildContext context, Size viewSize, Rect texturePos) {
  // texturePos 可能超出 viewSize 大小,所以先进行大小约束。
  Rect rect = Rect.fromLTRB(
      max(0.0, texturePos.left),
      max(0.0, texturePos.top),
      min(viewSize.width, texturePos.right),
      min(viewSize.height, texturePos.bottom));
  bool isPlaying = player.state == FijkState.started;
  return Positioned.fromRect(
    rect: rect,
    child: Container(
      alignment: Alignment.bottomLeft,
      child: IconButton(
        icon: Icon(
          isPlaying ? Icons.pause : Icons.play_arrow,
          color: Colors.white,
        ),
        onPressed: () {
          isPlaying ? player.pause() : player.start();
        },
      ),
    ),
  );
}

这是一个几乎最简单的播放器控制 UI 了,只有一个根据当前状态显示的播放或者暂停按钮。
simplestUI 作为 panelBuilder 参数值,它能够正常工作吗?

我来告诉你答案吧。
首先 simplestUI 成功显示出来了,并且正是在我们想要的位置上。播放器加载过程显示了播放箭头,开始播放后显示了暂停图标。
哪里不正常呢? 播放器播放完成后,图标还是暂停图标,没有更新。 播放过程中点击按钮,播放器确实暂停了,但是图标没有变化。
这一切都是因为 simplestUI 是无状态的,不能通过 setState 进行 UI 刷新。

但是为什么开始播放前后,simplestUI 的图标会变化一次呢?
因为 FijkView 本身监听了播放器 prepared 状态,获取了视频像素宽高并且进行了 UI 重绘,simplestUI 返回的无状态 Widget 作为 FijkView 组件树中的一个子节点,也被刷新了。

有状态 UI

class CustomFijkPanel extends StatefulWidget {
  final FijkPlayer player;
  final BuildContext buildContext;
  final Size viewSize;
  final Rect texturePos;

  const CustomFijkPanel({
    @required this.player,
    this.buildContext,
    this.viewSize,
    this.texturePos,
  });

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

class _CustomFijkPanelState extends State<CustomFijkPanel> {

  FijkPlayer get player => widget.player;
  bool _playing = false;

  @override
  void initState() {
    super.initState();
    widget.player.addListener(_playerValueChanged);
  }

  void _playerValueChanged() {
    FijkValue value = player.value;

    bool playing = (value.state == FijkState.started);
    if (playing != _playing) {
      setState(() {
        _playing = playing;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    Rect rect = Rect.fromLTRB(
        max(0.0, widget.texturePos.left),
        max(0.0, widget.texturePos.top),
        min(widget.viewSize.width, widget.texturePos.right),
        min(widget.viewSize.height, widget.texturePos.bottom));

    return Positioned.fromRect(
      rect: rect,
      child: Container(
        alignment: Alignment.bottomLeft,
        child: IconButton(
          icon: Icon(
            _playing ? Icons.pause : Icons.play_arrow,
            color: Colors.white,
          ),
          onPressed: () {
            _playing ? widget.player.pause() : widget.player.start();
          },
        ),
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();
    player.removeListener(_playerValueChanged);
  }
}

CustomFijkPanel 是上一个 simplestUI 的有状态版本,并且通过 widget.player.addListener(_playerValueChanged) 监听播放器状态变化,主动刷新 UI。

作为 panelBuilder 参数传递给 FijkView 的构造函数。并且达到了我们在前面所描述的预期效果。

FijkView(
  player: player,
  panelBuilder: (FijkPlayer player, FijkData data, BuildContext context, Size viewSize, Rect texturePos) {
    return CustomFijkPanel(
      player: player,
      buildContext: context,
      viewSize: viewSize,
      texturePos: texturePos);
  },
)

设置封面图

FijkView 中的参数 cover 可用于设置封面图。 coverImageProvider 类型。

视频加载过程中,封面图显示在 FijkView 中,填充了 FijkView 中计算出的实际视频区域。

视频开始播放后封面图不再显示。

期待你的杰作

好了,自定义 UI 的接口说明和实际案例,都差不多讲完了。阅读到这里你应该可以实现自己漂亮的播放器控制 UI 了。
期待你能够分享漂亮的播放器控制 UI,如果可以贡献 pull request 更是感激不尽。

当然,如果还有什么不明白的,尽管在 issues 中提出来。

来自社区的自定义 UI

abcd498936590/fijkplayer_skin

功能支持:

  • 手势滑动,快进快退
  • 上下滑动(左:屏幕亮度 右:系统音量)
  • 视频内剧集切换 (全屏模式下,视频内部切换播放剧集)
  • 倍数切换,(全屏模式下,切换倍数)
  • 锁定,(锁定UI,防误触)
  • 设置视频顶部返回,标题
  • 支持部分UI配置显示隐藏

截图

fijkplayer_skin demo screenshots


您的支持是我的动力。你可以通过以下方式支持我:

  • 给该项目点赞   给该项目点赞
  • 关注我的 Github   关注我的 Github
微信赞赏码 支付宝
微信赞赏码 支付宝二维码