Flutter - Widget 分类对比

Widget 分类

如果按照是否是有状态的分类方式,那么Widget就分为StatelessWidgetStatefulWidgetStatelessWidgetStatefulWidgetElement都是ComponentElement,并且都不具备RenderObject

他们UI的构建都是调用build方法。区别就是StatelessWidget只是简单的实现了ComponentElement,而StatefulWidget则复杂了许多,他的build是由_state去控制的,状态和数据都保存在这里面, 这个在之前的文章中有提及。

StatelessElement代码示例:

class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
}

可以看出在更新的时候也只是把_dirty脏标记设置为true,然后就重新构建。

State和Element的生命周期对比

image-20210313144623647.png

State数据的传递

数据传递

上面的代码,当点击FloatingActionButton之后,最终显示在屏幕上的文字是什么?为什么?

答案:

上面的代码当我们点击按钮之后,内容并不会发生改变,因为StatePagestate已经被创建过了,所以createState不会走两次,故而data并不会发生改变(但是StatePagedata是发生了改变的),如果我们想使用更新之后的值,我们可以使用widte.data来引用。

class StatePage extends StatefulWidget {
  StatePage({this.data});

  final String data;

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

class _StatePageState extends State {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(widget.data ?? ""),
    );
  }
}

setState是如何实现刷新的?

setState内部会调用_element.markNeedsBuild();方法markNeedsBuild方法会在内部把_dirty设置为true,然后加入到定时器当中,然后在下一帧的WidgetsBinding.drawFrame才会被绘制。此处也可以得知,setState并不会马上生效。

RenderObject分类

RenderBox

特性:会根据parentconstraints大小判断自己的布局方法,然后将constraints传递给child得到child的大小,最后根据child返回的Size决定自己的Size,如果没有child,就使用自己的Size

他用于那些不涉及的滚动的控件布局,他的两个关键参数就是BoxConstraintsSize

RenderSliver

特性:因为其主要用于RenderViewport之后,里面涉及的运算和属性对比RenderBox要复杂上许多。他的两个关键参数是SliverConstraints和SliverGeometry。

SliverConstraints和BoxConstraints对比,BoxContraints只包括了,最大/最小的高度/宽度。但是SliverConstraints则更多的是滑动方向、滑动偏移、滑动容器大小、容器缓存大小和位置等相关参数。

Size和SliverGeometry进行对比,Size只包括了宽和高。但是SliverGeometry包括了滑动方位、绘制范围、偏移等相关参数。

RenderBox和RenderSliver对比

RenderBox输入输出相较于RenderSliver更为简单,RenderSliver更为关注滑动、方向、缓存等关键点,这也是因为其需要和ViewPort配合展示。例如我们经常使用的ListView、GirdView、ScrollView等都是有Sliver和ViewPort组成的,可滑动的区域内不可以直接使用RenderBox,如果一定要使用必须用RenderSliver进行嵌套后进行布局。

对比

ViewPort

ViewPort根据自己的窗口的大小和偏移量,对child进行布局计算,通过对child输入SliverConstraints来得到childSliverGeometry,从而确定layoutpaint等相关信息。

RenderSliver对应的Sliver控件需要在ViewPort中使用。

image-20210316101636712.png

当外部的滑动事件产生时,就会触发到ViewPortmarkNeedsLayout方法,之后变化重新进行布局和绘制,并让SliverViewPort中进行偏移,达到看起来像是滑动了的效果。

RenderViewPort中为了避免性能消耗,对于滑动的时候内部就会尝试重新布局做了一个限制,最大的尝试次数不能超过10次

ListViewGridView内部都是一个SliverList构成,他们的children布局也是通过SliverList进行布局的。

RenderSliverList中,会通过传入的ramainingCacheExtentscrollOffset等参数去决定哪些child需要布局显示,哪些child不需要被布局绘制,从而保证了列表中内存优化和良好的绘制性能。

单元素与多元素分类

根据Widgetchild是否支持单个/多个child又可以分为SingleChildRenderObjectWidgetMultiChildRenderObjectWidget

像我们经常使用的ClipOpacityPaddingAlignSizededBox等都属于SingleChildRenderObjectWidget;而StackRowColumnRichText等则属于MultiChildRenderObjectWidget。针对两个不同的RenderObjectWidgetFlutter提供了CustomSingleChildLayoutCustomMultiChildLayout的抽象封装。

SingleChildRenderObjectWidget

SingleChildRenderObjectWidget继承RenderObjectWidget,因为只有一个child,所以实现起来相对简单。绘制流程是通过RenderObject计算出自身的最大、最小宽高,并且通过performLayout综合得到child返回的Size、最后在进行绘制。

MultiChildRenderObjectWidget

MultiChildRenderObjectWidget

从上图可以看出相较于SingleChildRenderObjectWidgetMultiChildRenderObjectWidget实现起来要复杂许多,主要复杂的部分在于RenderBox,我们需要自定义一个类继承于RenderBox,同时还得混入ContainerRenderObjectMixinRenderBoxContainerDefaultsMixin,然后去重写他的两个方法:setupParentDataperformLayout,然后在重写paint方法,调用系统绘制方法,完成绘制操作。

下面用一个实际例子来演示:

01- 创建ContainerBoxparentData

这个就是对应上图中右下方的抽象类(ConstainerBoxParentData)的具体实现

class RenderCloudParentData extends ContainerBoxParentData {
  /// 定义宽高
  double width;
  double height;

  /// 通过offset和width、height得到一个矩形区域
  Rect get content => Rect.fromLTWH(
        offset.dx,
        offset.dy,
        width,
        height,
      );
}
02-创建RenderBox

这个就是对应上图的RenderBox的具体实现

/// 从类的定义就可以很好的看出,该类需要继承于RenderBox,
/// 同时还需要混入ContainerRenderObjectMixin、RenderBoxContainerDefaultsMixin
class RenderCloudWidget extends RenderBox
    with
        ContainerRenderObjectMixin,
        RenderBoxContainerDefaultsMixin {

  /// 构造方法
  /// * children
  /// * overflow 裁剪方式
  /// * ratio 比例
  RenderCloudWidget({
    List children,
    Clip overflow = Clip.none,
    double ratio,
  })  : _ratio = ratio,
        _overflow = overflow {
    /// 这个是ContainerRenderObjectMixin的内部方法,其内部是一个双线链表的结果,
    /// 主要是用于快速定位下一个、上一个renderObject
    addAll(children);
  }

  ///圆周
  double _mathPi = math.pi * 2;

  ///比例
  double _ratio;

  double get ratio => _ratio;

  set ratio(double value) {
    assert(value != null);
    if (_ratio != value) {
      _ratio = value;
      markNeedsPaint();
    }
  }

  /// 裁剪方式
  Clip get overflow => _overflow;

  set overflow(Clip value) {
    assert(value != null);
    if (_overflow != value) {
      _overflow = value;
      markNeedsPaint();
    }
  }

  Clip _overflow;

  /// 是否需要裁剪
  bool _needClip = false;

  /// 用于判断是否重复区域了
  bool overlaps(RenderCloudParentData data) {
    Rect rect = data.content;

    RenderBox child = data.previousSibling;

    if (child == null) {
      return false;
    }

    do {
      RenderCloudParentData childParentData = child.parentData;
      if (rect.overlaps(childParentData.content)) {
        return true;
      }
      child = childParentData.previousSibling;
    } while (child != null);
    return false;
  }

  /// 这个就是需要重写RenderBox其中的一个方法
  @override
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! RenderCloudParentData) {
      child.parentData = RenderCloudParentData();
    }
  }

  /// 内部布局方法,布局每一个child的位置大小
  @override
  void performLayout() {
    ///默认不需要裁剪
    _needClip = false;

    ///没有 childCount 不玩
    if (childCount == 0) {
      size = constraints.smallest;
      return;
    }

    ///初始化区域
    var recordRect = Rect.zero;
    var previousChildRect = Rect.zero;

    RenderBox child = firstChild;

    while (child != null) {
      var curIndex = -1;

      ///提出数据
      final RenderCloudParentData childParentData = child.parentData;

      child.layout(constraints, parentUsesSize: true);

      var childSize = child.size;

      ///记录大小
      childParentData.width = childSize.width;
      childParentData.height = childSize.height;

      do {
        ///设置 xy 轴的比例
        var rX = ratio >= 1 ? ratio : 1.0;
        var rY = ratio <= 1 ? ratio : 1.0;

        ///调整位置
        var step = 0.02 * _mathPi;
        var rotation = 0.0;
        var angle = curIndex * step;
        var angleRadius = 5 + 5 * angle;
        var x = rX * angleRadius * math.cos(angle + rotation);
        var y = rY * angleRadius * math.sin(angle + rotation);
        var position = Offset(x, y);

        ///计算得到绝对偏移
        var childOffset = position - Alignment.center.alongSize(childSize);

        ++curIndex;

        ///设置为遏制
        childParentData.offset = childOffset;

        ///判处是否交叠
      } while (overlaps(childParentData));

      ///记录区域
      previousChildRect = childParentData.content;
      recordRect = recordRect.expandToInclude(previousChildRect);

      ///下一个
      child = childParentData.nextSibling;
    }

    ///调整布局大小
    size = constraints
        .tighten(
      height: recordRect.height,
      width: recordRect.width,
    )
        .smallest;

    ///居中
    var contentCenter = size.center(Offset.zero);
    var recordRectCenter = recordRect.center;
    var transCenter = contentCenter - recordRectCenter;
    child = firstChild;
    while (child != null) {
      final RenderCloudParentData childParentData = child.parentData;
      childParentData.offset += transCenter;
      child = childParentData.nextSibling;
    }

    ///超过了嘛?
    _needClip =
        size.width < recordRect.width || size.height < recordRect.height;
  }

  /// 设置绘制默认
  @override
  void paint(PaintingContext context, Offset offset) {
    if (!_needClip || _overflow == Clip.none) {
      defaultPaint(context, offset);
    } else {
      context.pushClipRect(
        needsCompositing,
        offset,
        Offset.zero & size,
        defaultPaint,
      );
    }
  }
  

  /// 触摸测试,如果不想响应就返回false,反正则是true
  @override
  bool hitTestChildren(HitTestResult result, {Offset position}) {
    return defaultHitTestChildren(result, position: position);
  }
}
03-创建Widget

主要是把RenderObjectWidget进行关联起来

/// 创建Widget,继承与MultiChildRenderObjectWidget
/// 主要是和之前的RenderBox关联起来
class CloudWidget extends MultiChildRenderObjectWidget {
  /// 自定义的相关属性
  final Clip overflow;
  final double ratio;

  /// 构造方法
  CloudWidget({
    Key key,
    this.ratio = -1,
    this.overflow = Clip.none,
    List children = const [],
  }) : super(key: key, children: children);

  /// 重写创建RenderObject的方法,把之前创建的RenderCouldWidget返回
  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderCloudWidget(ratio: ratio, overflow: overflow);
  }

  /// 在这里更新RenderCloudWidget的两个关键参数
  @override
  void updateRenderObject(
      BuildContext context, covariant RenderCloudWidget renderObject) {
    /// ..表示级联操作符
    renderObject
      ..ratio = ratio
      ..overflow = overflow;
  }
}
04-demo
///云词图
class CloudDemoPage extends StatefulWidget {
  @override
  _CloudDemoPageState createState() => _CloudDemoPageState();
}

class _CloudDemoPageState extends State {

  ///Item数据
  List dataList = const [
    CloudItemData('CloudGSY11111', Colors.amberAccent, 10, false),
    CloudItemData('CloudGSY3333333T', Colors.limeAccent, 16, false),
    CloudItemData('CloudGSYXXXXXXX', Colors.black, 14, true),
    CloudItemData('CloudGSY55', Colors.black87, 33, false),
    CloudItemData('CloudGSYAA', Colors.blueAccent, 15, false),
    CloudItemData('CloudGSY44', Colors.indigoAccent, 16, false),
    CloudItemData('CloudGSYBWWWWWW', Colors.deepOrange, 12, true),
    CloudItemData('CloudGSY<<<', Colors.blue, 20, true),
    CloudItemData('FFFFFFFFFFFFFF', Colors.blue, 12, false),
    CloudItemData('BBBBBBBBBBB', Colors.deepPurpleAccent, 14, false),
    CloudItemData('CloudGSY%%%%', Colors.orange, 20, true),
    CloudItemData('CloudGSY%%%%%%%', Colors.blue, 12, false),
    CloudItemData('CloudGSY&&&&', Colors.indigoAccent, 10, false),
    CloudItemData('CloudGSYCCCC', Colors.yellow, 14, true),
    CloudItemData('CloudGSY****', Colors.blueAccent, 13, false),
    CloudItemData('CloudGSYRRRR', Colors.redAccent, 12, true),
    CloudItemData('CloudGSYFFFFF', Colors.blue, 12, false),
    CloudItemData('CloudGSYBBBBBBB', Colors.cyanAccent, 15, false),
    CloudItemData('CloudGSY222222', Colors.blue, 16, false),
    CloudItemData('CloudGSY1111111111111111', Colors.tealAccent, 19, false),
    CloudItemData('CloudGSY####', Colors.black54, 12, false),
    CloudItemData('CloudGSYFDWE', Colors.purpleAccent, 14, true),
    CloudItemData('CloudGSY22222', Colors.indigoAccent, 19, false),
    CloudItemData('CloudGSY44444', Colors.yellowAccent, 18, true),
    CloudItemData('CloudGSY33333', Colors.lightBlueAccent, 17, false),
    CloudItemData('CloudGSYXXXXXXXX', Colors.blue, 16, true),
    CloudItemData('CloudGSYFFFFFFFF', Colors.black26, 14, false),
    CloudItemData('CloudGSYZUuzzuuu', Colors.blue, 16, true),
    CloudItemData('CloudGSYVVVVVVVVV', Colors.orange, 12, false),
    CloudItemData('CloudGSY222223', Colors.black26, 13, true),
    CloudItemData('CloudGSYGFD', Colors.yellow, 14, true),
    CloudItemData('GGGGGGGGGG', Colors.deepPurpleAccent, 14, false),
    CloudItemData('CloudGSYFFFFFF', Colors.blueAccent, 10, true),
    CloudItemData('CloudGSY222', Colors.limeAccent, 12, false),
    CloudItemData('CloudGSY6666', Colors.blue, 20, true),
    CloudItemData('CloudGSY33333', Colors.teal, 14, false),
    CloudItemData('YYYYYYYYYYYYYY', Colors.deepPurpleAccent, 14, false),
    CloudItemData('CloudGSY  3  ', Colors.blue, 10, false),
    CloudItemData('CloudGSYYYYYY', Colors.black54, 17, true),
    CloudItemData('CloudGSYCC', Colors.lightBlueAccent, 11, false),
    CloudItemData('CloudGSYGGGGG', Colors.deepPurpleAccent, 10, false)
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: new Text("CloudDemoPage"),
      ),
      body: new Center(
        child: Container(
          width: MediaQuery.of(context).size.width,
          height: MediaQuery.of(context).size.width,

          ///利用 FittedBox 约束 child
          child: new FittedBox(
            /// Cloud 布局
            child: Container(
              padding: EdgeInsets.symmetric(vertical: 10, horizontal: 6),
              color: Colors.brown,

              ///布局
              child: CloudWidget(
                ///容器宽高比例
                ratio: 1,
                children: [
                  for (var item in dataList)

                  ///判断是否旋转
                    RotatedBox(
                      quarterTurns: item.rotate ? 1 : 0,
                      child: Text(
                        item.text,
                        style: new TextStyle(
                          fontSize: item.size,
                          color: item.color,
                        ),
                      ),
                    ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class CloudItemData {
  ///文本
  final String text;

  ///颜色
  final Color color;

  ///旋转
  final bool rotate;

  ///大小
  final double size;

  const CloudItemData(
      this.text,
      this.color,
      this.size,
      this.rotate,
      );
}

CustomMultiChildLayout

官方为了简化我们实现自定义布局的方式,还提供了CustomMultiChildLayout这样的类,这个类也是继承了MultiChildRenderObjectWidget,并通过一个代理(MultiChildLayoutDelegate)来完成自定义UI相关的功能,通过这个代理,我们可以直接去重写内部的performLayout方法,从而达到我们自定布局的效果。

01-创建Delegate
class CircleLayoutDelegate extends MultiChildLayoutDelegate {
  final List customLayoutId;
  final Offset center;

  Size childSize;

  CircleLayoutDelegate(
    this.customLayoutId, {
    this.center = Offset.zero,
    this.childSize,
  });

  @override
  void performLayout(Size size) {
    for (var item in customLayoutId) {
      if (hasChild(item)) {
        double r = 100;

        /// 下标
        int index = int.parse(item);

        /// 均分
        double step = 360 / customLayoutId.length;

        /// 角度
        double hd = (2 * math.pi / 360) * step * index;

        var x = center.dx + math.sin(hd) * r;
        var y = center.dy + math.cos(hd) * r;

        /// 使用??= 避免多次赋值
        childSize ??= Size(size.width / customLayoutId.length,
            size.height / customLayoutId.length);

        layoutChild(item, BoxConstraints.loose(childSize));

        final double centerX = childSize.width * 0.5;
        final double centerY = childSize.height * 0.5;

        var result = Offset(x - centerX, y - centerY);

        /// 设置child位置
        positionChild(item, result);
      }
    }
  }

  @override
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) {
    return true;
  }
}
02-使用

class CustomMultiLayoutPage extends StatefulWidget {
  @override
  _CustomMultiLayoutPageState createState() => _CustomMultiLayoutPageState();
}

class _CustomMultiLayoutPageState extends State {
  ///用于 LayoutId 指定
  ///CircleLayoutDelegate 操作具体 Child 的 ChildId 是通过 LayoutId 指定的
  List customLayoutId = ["0", "1", "2", "3", "4"].toList();

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final childSize = 66.0;
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Container(
          color: Colors.yellowAccent,
          width: size.width,
          height: size.width,
          child: CustomMultiChildLayout(
            delegate: CircleLayoutDelegate(
                customLayoutId,
                childSize: Size(childSize, childSize),
              center: Offset(size.width * 0.5, size.width * 0.5),
            ),
            children: [
              ///使用 LayoutId 指定 childId
              for (var item in customLayoutId)
                new LayoutId(id: item, child: ContentItem(item, childSize)),
            ],
          ),
        ),
      ),
      persistentFooterButtons: [
        TextButton(onPressed: () {
            setState(() {
              customLayoutId.add("${customLayoutId.length}");
            });
          },
          child: Icon(Icons.add),
        ),
        TextButton(onPressed: () {
            setState(() {
              if (customLayoutId.length > 1) {
                customLayoutId.removeLast();
              }
            });
          },
          child: Icon(Icons.remove),
        ),
      ],
    );
  }
}

class ContentItem extends StatelessWidget {
  final String text;

  final double childSize;

  ContentItem(this.text, this.childSize);

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Theme.of(context).primaryColor,
      borderRadius: BorderRadius.circular(childSize / 2.0),
      child: InkWell(
        radius: childSize / 2.0,
        customBorder: CircleBorder(),
        onTap: () {},
        child: Container(
          width: childSize,
          height: childSize,
          child: Center(
            child: Text(
              text,
              style: Theme.of(context)
                  .textTheme
                  .headline6
                  .copyWith(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}
效果图
111.gif

InheritedWidget共享状态

InheritedWidgeFlutter Widget中非常重要的一个构成部分,因为InheritedWidget常被用于数据共享。比如使用频率很高的:Theme/ThemeDataText/DefaultTextStyleSlider/SliderThemeIcon/IconTheme等内部都是通过InheritedWidget实现数据共享的。并且Flutter中部分的状态管理框架,内部的状态共享方法也是基于InheritedWidget去实现的。

InheritedWidget继承自ProxyWidget,本身并不具备绘制的能力,但共享这个Widget等与共享Widget内保存的数据,获取Widget就可以获取到其内部保存的数据,如下图:


image-20210317145532940.png

每一个Element当中都有一个成员变量:Map _inheritedWidgets,改成员变量默认是空,之后当父控件是InheritedWidget或者本身是InheritedWidget的时候才会初始化,当父控件是InheritedWidget的时候,这个Map会逐级向下传递于合并。

那么context.inheritedFromWidgetOfExactType内部做了啥呢?

通过查看Element的源码截图部分片段

  @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    /// 首先判断是否有inheritedElement类型的数据
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    
    /// 找到了
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      
      /// 添加到依赖集合中,并且通过updateDependencies将当前的Element添加到_dependencies Map中,并且返回InheritedWidget
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }


/// 下面两个方法就是添加过程的实现
  @override
  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    return dependOnInheritedElement(ancestor, aspect: aspect);
  }

  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    /// 创建_dependencies
    _dependencies ??= HashSet();
    /// 添加InheritedElement到集合中
    _dependencies.add(ancestor);
    /// 跟新依赖
    ancestor.updateDependencies(this, aspect);
    /// 返回InheritedWidget
    return ancestor.widget;
  }
InheritedWidget是如何通知StatefulWidget进行更新的?

例如:当我们在外界调用Theme.of(context)的时候,BuildContext的实现就是Element,所以当内部调用到context.inheritedFromWidgetOfExactType时,就会将context所代表的Element添加到InheritedElement_dependents中,当InheritedElement被更新的时候,就会触发到齐内部的notifyClients方法,该方法就会挨个遍历被加入到_dependents,从而触发到didChangeDependcies,然后就会更新UI

ErrorWidget 异常处理

在以往的开发中,当我们程序抛出一些未处理的异常或者错误的时候,就会引发程序的crash,但是在Flutter中则不会,这是因为Flutter中有一个全局处理的地方;

当我们的代码发生一些问题之后,在debug模式下可能会有某些或者整个页面变成红色,并显示一些错误信息;在release模式下,则会显示灰色的并没有错误提示。

为了能让我们的产品体验更好,我们可以在main方法中做一些处理,让错误看起来更加优雅

void main() {
  runZoned((){
    ErrorWidget.builder = (FlutterErrorDetails details) {
      Zone.current.handleUncaughtError(details.exception, details.stack);
      return Container(color: Colors.orange,);
    };

    FlutterError.onError = (FlutterErrorDetails details) async {
      FlutterError.dumpErrorToConsole(details);
      Zone.current.handleUncaughtError(details.exception, details.stack);
    };

    runApp(MyApp());
  }, onError: (Object obj, StackTrace stack) {
    print(obj);
    print(stack);
  });
}

你可能感兴趣的:(Flutter - Widget 分类对比)