Flutter-->自定义容器Widget(类比Android自定义ViewGroup)

80 阅读2分钟

上一篇Flutter-->自定义Widget(类比Android自定义View)Flutter-->自定义Widget(类比A - 掘金 (juejin.cn)

介绍了如何自定义一个Widget, 这一篇文章介绍如果自定义容器Widget, 相当于Android中的ViewGroup

Android自定义ViewGroup

先来简单介绍一下Android中自定义的ViewGroup:

class CustomViewGroup(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //进行测量操作, 确定自身的大小, 已经测量child的大小
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //进行布局操作, 确定子view的位置
    }

    override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
        return LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
        )
    }

    class LayoutParams(width: Int, height: Int) : ViewGroup.LayoutParams(width, height) {
        //自定义的布局参数
    }
}
  • onMeasure方法: 进行测量操作, 确定自身的大小, 已经测量child的大小
  • onLayout方法: 进行布局操作, 确定子view的位置
  • LayoutParams child的布局参数, 在parent中读取生效

Flutter自定义容器Widget

那么在Flutter中, 怎么自定义容器Widget呢?

自定义只有一个child的容器如下:

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

  @override
  RenderObject createRenderObject(BuildContext context) =>
      CustomContainerRenderObject();
}

/// [RenderObjectWithChildMixin]
class CustomContainerRenderObject extends RenderBox
    with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox> {
    
  @override
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! CustomContainerParentData) {
      child.parentData = CustomContainerParentData();
    }
  }

  @override
  void performLayout() {
    if (child != null) {
      final parentData = child!.parentData as CustomContainerParentData;
      parentData.offset = const Offset(100, 100); // 确定子元素的位置
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final RenderBox? child = this.child;
    if (child == null) {
      return;
    }
    context.paintChild(child, offset);
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    return child?.hitTest(result, position: position) ?? false;
  }
}

Flutter中, 有可能代码都是通过mixin混入的方式实现的, 这一点非常方便.

  • setupParentData方法, 用来设置child的参数, 提供给容器使用的.
  • performLayout方法, 用来确定自身大小child大小, 还有child位置
  • paint方法, 用来自绘和绘制child
  • hitTestChildren方法, 用来实现child的事件支持

自定义一组child的容器如下:

class CustomListContainerWidget extends MultiChildRenderObjectWidget {
  const CustomListContainerWidget({super.key, super.children});

  @override
  RenderObject createRenderObject(BuildContext context) =>
      CustomListContainerRenderObject();
}

/// [ContainerRenderObjectMixin]
class CustomListContainerRenderObject extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, CustomContainerParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, CustomContainerParentData> {
  @override
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! CustomContainerParentData) {
      child.parentData = CustomContainerParentData();
    }
  }

  @override
  void performLayout() {
    visitChildren((child) {
      final parentData = child.parentData as CustomContainerParentData;
      parentData.offset = const Offset(100, 100); // 确定子元素的位置
    });
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    return defaultHitTestChildren(result, position: position);
  }
}

一组child的自定义容器, 核心方法和自定义一个child是一样的, 只不会每个方法都要处理一组child.

以上都是BoxConstraints约束模型的自定义, SliverConstraints约束模型, 比较复杂, 之后再谈...

/// [ParentDataWidget]
class CustomContainerParentData extends ContainerBoxParentData<RenderBox> {}

Flutter中有一个Widget专门用来配置ParentData 它就是ParentDataWidget.

所以一般都是自定义一个ParentDataWidget用来传递ParentData:

class CustomContainerParentDataWidget
    extends ParentDataWidget<CustomContainerParentData> {
  const CustomContainerParentDataWidget({super.key, required super.child});

  @override
  void applyParentData(RenderObject renderObject) {
    if (renderObject.parentData is! CustomContainerParentData) {
      renderObject.parentData = CustomContainerParentData();
    }
  }

  @override
  Type get debugTypicalAncestorWidgetClass => CustomContainerParentDataWidget;
}

总结

AndroidFlutter
测量入口View.onMeasureRenderObject.performLayout
child位置View.layoutBoxParentData.offset
child参数LayoutParamsParentData
触发重新绘制View.invalidateRenderObject.markNeedsPaint
触发重新布局View.requestLayoutRenderObject.markNeedsLayout

源码地址


群内有各(pian)种(ni)各(jin)样(qun)的大佬,等你来撩.

联系作者

点此QQ对话 该死的空格 点此快速加群 在这里插入图片描述

在这里插入图片描述