---
title: 个性化 UI
permalink: /docs/zh/custom-ui.html
key: docs-custom-ui
---


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

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


## 自定义 UI 接口

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

```dart
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` 的实际大小

结合 [播放器填充与裁剪](docs/zh/fijkfit.html) 中的内容，可以对 `viewSize` 和 `texturePos` 两个参数有更好的理解。  
* 在 `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 ？
```dart
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
```dart
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 的构造函数。并且达到了我们在前面所描述的预期效果。

```dart
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](https://github.com/befovy/fijkplayer/issues) 中提出来。

