个性化 UI

 

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

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

自定义 UI 接口

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

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

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

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

接口参数

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

  • player 播放器 FijkPlayer 对象,FijkView 所显示视频的数据来源,自定义UI所控制的播放器对象。
    监听此 player 的属性变化并在 UI 上作出相应的改变。
  • context build Widget 的上下文。
  • viewSize 对应 FijkView 的实际显示大小
  • texturePos FijkView 中实际视频显示的相对位置,这个相对位置可能超出 FijkView 的实际大小

结合 播放器填充与裁剪 中的内容,可以对 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, BuildContext context, Size viewSize, Rect texturePos) {
    return CustomFijkPanel(
      player: player,
      buildContext: context,
      viewSize: viewSize,
      texturePos: texturePos);
  },
)

期待你的杰作

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

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


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

每年国庆节前后是临潼石榴集中成熟上市销售的时期,我老丈人家就在秦岭山脉的骊山脚下,家中有十多亩石榴园,全部是绿色种植,施农家肥,人工套袋。 产出的石榴皮薄籽大,甜美多汁。10月份购买的订单均是现摘现发,保证您吃到新鲜美味的石榴。