likes
comments
collection
share

Flutter自定义View基础,重写SingleChildRenderObjectWidget

作者站长头像
站长
· 阅读数 47

Flutter自定义View基础,重写SingleChildRenderObjectWidget

Flutter有很多种类的Widget, 其中处理界面绘制, 布局, 事件相关的则是RenderObjectWidget.

基本上大部分涉及界面绘制和事件处理的Widget都由RenderObjectWidget扩展而来.

今天我们的主角是SingleChildRenderObjectWidget, 看名字都知道这是一个含有一个Child的Widget, 在Flutter中有很多这种Widget, 比如Padding, Center, Align, Container其实应该也算, 只不过它是多个这种容器的组合.

基础实现

这里我们主要尝试重写SingleChildRenderObjectWidget来实现一个简单容器.

第一步我们实现一个SimpleContainer继承于SingleChildRenderObjectWidget, 并创建一个继承于Renderbox的SimpleRenderBox.

class SimpleContainer extends SingleChildRenderObjectWidget {
  const SimpleContainer({super.key, required super.child});

  @override
  RenderObject createRenderObject(BuildContext context) {
    return SimpleRenderBox();
  }

  @override
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
    super.updateRenderObject(context, renderObject);
  }
}

class SimpleRenderBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {


  @override
  void performLayout() {
    size = const Size(200, 200);
    child!.layout(constraints, parentUsesSize: true);
  }

}

其实这样我们就实现了一个最简单的容器, 我们可以写个简单页面测试下效果

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Simple Container"),
      ),
      body: Container(
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          width: 300,
          height: 300,
          color: Colors.lightBlueAccent,
          child: SimpleContainer(
            child: Container(
              width: 100,
              height: 100,
              color: Colors.yellowAccent,
            ),
          ),
        ),
      ),
    );
  }

Flutter自定义View基础,重写SingleChildRenderObjectWidget

可以看到我们这里只显示了最外层的Container.

如果我们要将SimpleContainer和最内层的Container显示出来还需要在SimpleRenderBox中实现paint方法.

我们简单实现一个

  @override
  void paint(PaintingContext context, Offset offset) {
    //简单绘制自己的大小和位置
    if (size > Size.zero) {
      context.canvas.drawRect(offset & size, Paint()..color = Colors.greenAccent);
    }
    //绘制子控件
    context.paintChild(child!, offset);
  }

我们再运行看一下

Flutter自定义View基础,重写SingleChildRenderObjectWidget

这里自身的大小和位置信息还有子控件已经正常显示出来了.

大小和位置控制

以上代码很简单的设置了控件的大小和子控件的位置, 很明显是不符合大部分时候需求的.

所以这里说一下自身大小的控制和子控件的位置控制.

自身大小的控制

其实自身大小的控制是比较简单的, 只需要在performLayout中配置一下size属性就行.

不过我们需要根据constraints来配置自身的大小, constraints定义了我们的大小范围, 如果我们想跟父容器一样大可以使用

size = Size(constraints.maxWidth, constraints.maxHeight);

如果我们想大小和子控件一样大可以使用

child!.layout(constraints, parentUsesSize: true);
size = child!.size;

注意这里一定需要先调用child的layout方法后才能拿到child的size.

子控件位置的控制

子控件的位置由子控件的parentData属性来辅助控制.

我们先添加一个方法来获取这个属性

  BoxParentData _getChildParentData() {
    return child!.parentData as BoxParentData;
  }

然后给这个属性配置Offset, 如果我们想控件居中的话可以使用如下方法重写performLayout

  @override
  void performLayout() {
    size = const Size(200, 200);
    final childConstraints = BoxConstraints(maxWidth: size.width, maxHeight: size.height);
    child!.layout(childConstraints, parentUsesSize: true);
    
    final offsetDx = (size.width - child!.size.width) / 2;
    final offsetDy = (size.height - child!.size.height) / 2;
    _getChildParentData().offset = Offset(offsetDx, offsetDy);
  }

然后我们还需要重写paint方法

  @override
  void paint(PaintingContext context, Offset offset) {
    //简单绘制自己的大小和位置
    if (size > Size.zero) {
      context.canvas.drawRect(offset & size, Paint()..color = Colors.greenAccent);
    }
    //绘制子控件
    context.paintChild(child!, offset + _getChildParentData().offset);
  }

如此我们便能让子控件居中显示了.

Flutter自定义View基础,重写SingleChildRenderObjectWidget

如果有更复杂的需求我们还可以通过重写setupParentData来自定义ParentData数据, 这里不多做赘述, 如有需要自寻查找资料.

事件处理

还没完, 我们还可以通过Renderbox来实现点击, 滑动, 鼠标悬停等事件的处理.

这里先说一下Flutter的事件处理机制, 其实它的机制很简单, 事件分发根据Render树从上到下深度遍历访问每一个RenderBox的hitTest做命中测试, 如果在这个方法里调用

result.add(BoxHitTestEntry(this, position));

则这个Renderbox则可以收到事件, 事件会发送到handleEvent方法中.

然后只要在hitTest中返回了true, 则表示这个事件被消耗了, 被消耗后不会再继续测试其它Renderbox是否接收此事件.

以下是Renderbox的hitTest的默认实现

  bool hitTest(BoxHitTestResult result, { required Offset position }) {
		//......
    if (_size!.contains(position)) {
      if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));
        return true;
      }
    }
    return false;
  }

很明显是只要child或者自身命中测试成功的话就会监听事件并拦截.

知道原理的话我们就能做很多事情, 比如我们如果想让SimpleContainer含有自己的点击事件但是在子控件范围不生效, 那我们可以尝试这样重写hitTest.

@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
  if (size.contains(position)) {
    if (child!.hitTest(result, position: position - _getChildParentData().offset)) {
      return true;
    } else {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}

然后为了效果我们在SimpleRenderBox简单增加一下handleEvent的逻辑

  @override
  void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
    if (event is PointerDownEvent) {
      showSimpleToast("Simple Container Clicked");
    }
  }

作为对比我们在界面代码中给最小的Container添加一个点击事件

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Simple Container"),
      ),
      body: Container(
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          width: 300,
          height: 300,
          color: Colors.lightBlueAccent,
          child: SimpleContainer(
            child: InkWell(
              onTap: () {
                showSimpleToast("Child Clicked");
              },
              child: Container(
                width: 100,
                height: 100,
                color: Colors.yellowAccent,
              ),
            ),
          ),
        ),
      ),
    );
  }

然后我们看看效果

Flutter自定义View基础,重写SingleChildRenderObjectWidget

这里是符合我们一般点击事件逻辑的, 如果我们想做点不寻常的事呢, 比如我们想截断子控件的事件, 那么我们改写hitTest如下

@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
  if (size.contains(position)) {
    result.add(BoxHitTestEntry(this, position));
    return true;
  }
  return false;
}

如此子控件将不会再有事件响应, 效果如下

Flutter自定义View基础,重写SingleChildRenderObjectWidget

好了至此你已经掌握了最基本的自定义控件.

转载自:https://juejin.cn/post/7242249906257641529
评论
请登录