开发者

flutter布局约束原理深入解析

目录
  • 引言
    • 1、flutter的widget类型
    • 2、Container是个组合类
    • 3、flutter布局约束
    • 4、Container布局行为解惑
  • 总结

    引言

    刚开始接触flutter的时候,Container组件是用得最多的。它就像html中的div一样普遍,专门用来布局页面的。

    但是使用Container嵌套布局的时候,经常出现一些令人无法理解的问题。就如下面代码,在一个固定的容器中,子组件却铺满了全屏。

    /// 例一
    @override
    Widget build(BuildContext context) {
       return Container(
         width: 300,
         height: 300,
         color: Colors.amber,
         child: Container(width: 50, hei开发者_Go开发ght: 50, color: Colors.red,),
      );
    }
    

    flutter布局约束原理深入解析

    然后要加上alignment属性,子组件正常显示了,但容器还是铺满全屏。

    /// 例二
    @override
    Widget build(BuildContext context) {
       return Container(
         width: 300,
         height: 300,
         color: Colors.amber,
         alignment: Alignment.center,
         child: Container(width: 50, height: 50, color: Colors.red,),
      );
    }
    

    flutter布局约束原理深入解析

    而在容器外层添加一个Scaffold组件,它就正常显示了。

    /// 例三
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: Container(
          width: 300,
          height: 300,
          color: Colors.amber,
          alignment: Alignment.center,
          child: Container(width: 50, height: 50, color: Colors.red,),
        ),
      );
    }
    

    flutter布局约束原理深入解析

    这一切的怪异行为困扰了我很久,直到我深入了flutter布局的学习,才渐渐解开这些疑惑。

    1、flutter的widget类型

    flutter的widget可以分为三类,组合类ComponentWidget代理类ProxyWidget绘制类RenderObjectWidget

    flutter布局约束原理深入解析

    组合类:如ContainerScaffoldMaterialApp还有一系列通过继承StatelessWidgetStatefulWidget的类。组合类是我们开发过程中用得最多的组件。

    代理类Inheritedwidget,功能型组件,它可以高效快捷的实现共享数据的跨组件传递。如常见的ThemeMediaQuery就是InheritedWidget的应用。

    绘制类:屏幕上看到的UI几乎都会通过RenderObjectWidget实现。通过继承它,可以进行界面的布局和绘制。如AlignPaddingConstrainedBox等都是通过继承RenderObjectWidget,并通过重写createRenderObject方法来创建RenderObject对象,实现最终的布局(layout)和绘制(paint)。

    2、Container是个组合类

    显而易见Container继承StatelessWidget,它是一个组合类,同时也是一个由DecoratedBoxConstrainedBoxTransformPaddingAlign等组件组合的多功能容器。可以通过查看Container类,看出它实际就是通过不同的参数判断,再进行组件的层层嵌套来实现的。

    @override
    Widget build(BuildContext context) {
      Widget? current = child;
      if (childandroid == null && (constraints == null || !constraints!.isTight)) {
        current = LimitedBox(
          maxWidth: 0.0,
          maxHeight: 0.0,
          child: ConstrainedBox(constraints: const BoxConstraints.expand()),
        );
      } else if (alignment != null) {
        current = Align(alignment: alignment!, child: current);
      }
      final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
      if (effectivePadding != null) {
        current = Padding(padding: effectivePadding, child: current);
      }
      if (color != null) {
        current = ColoredBox(color: color!, child: current);
      }
      if (clipBehavior != Clip.none) {
        assert(decoration != null);
        current = ClipPath(
          clipper: _DecorationClipper(
            textDirection: Directionality.maybeOf(context),
            decoration: decoration!,
          ),
          clipBehavior: clipBehavior,
          child: current,
        );
      }
      if (decoration != null) {
        current = DecoratedBox(decoration: decoration!, child: current);
      }
      if (foregroundDecoration != null) {
        current = DecoratedBox(
          decoration: foregroundDecoration!,
          position: DecorationPosition.foreground,
          child: current,
        );
      }
      if (constraints != null) {
        current = ConstrainedBox(constraints: constraints!, child: current);
      }
      if (margin != null) {
        current =javascript Padding(padding: margin!, child: current);
      }
      if (transform != null) {
        current = Transform(transform: transform!, alignment: transformAlignment, child: current);
      }
      return current!;
    }
    

    组合类基本不参与ui的绘制,都是通过绘制类的组合来实现功能。

    3、flutter布局约束

    flutter中有两种布局约束BoxConstraints盒约束和SliverConstraints线性约束,如Align、Padding、ConstrainedBox使用的是盒约束。

    BoxConstraints盒约束是指flutter框架在运行时遍历整个组件树,在这过程中 「向下传递约束,向上传递尺寸」,以此来确定每个组件的尺寸和大小。

    flutter布局约束原理深入解析

    BoxConstraints类由4个属性组成,最小宽度minWidth、最大宽度maxWidth、最小高度minHeight、最大高度maxHeight

    BoxConstraints({
        this.minWidth,
        this.maxWidth,
        this.minHeight,
        this.maxHeight,
    });
    

    根据这4个属性的变化,可以分为“紧约束(tight)”、“松约束(loose)”、“无界约束”、“有界约束”。

    紧约束:最小宽(高)度和最大宽(高)度值相等,此AVvnNv时它是一个固定宽高的约束。

    BoxConstraints.tight(Size size)
        : minWidth = size.width,
          maxWidth = size.width,
          minHeight = size.height,
          maxHeight = size.height;
    

    松约束:最小宽(高)值为0,最大宽(高)大于0,此时它是一个约束范围。

    BoxConstraints.loose(Size size)
        : minWidth = 0.0,
          maxWidth = size.width,
          minHeight = 0.0,
          maxHeight = size.height;
    

    无界约束:最小宽(高)和最大宽(高)值存在double.infinity(无限)。

    BoxConstraints.expand({double? width, double? height}) 
    : minWidth = width ?? double.infinity,
         maxWidth = width ?? double.infinity,
         minHeight = height ?? double.infinity,
         maxHeight = height ?? double.infinity;
    

    有界约束:最小宽(高)和最大宽(高)值均为固定值。

    BoxConstraints(100, 300, 100, 300)
    

    4、Container布局行为解惑

    了解了BoxConstraints布局约束,回到本文最开始的问题。

    /// 例一
    @override
    Widget build(BuildContext context) {
       return Container(
         width: 300,
         height: 300,
         color: Colors.amber,
         child: Container(width: 50, height: 50, color: Colors.red,),
      );
    }
    

    例一中,两个固定宽高的Container,为什么子容器铺满了全屏?

    根据BoxConstraints布局约束,遍历整个组件树,最开始的root是树的起点,它向下传递的是一个紧约束。因为是移动设备,root即是屏幕的大小,假设屏幕宽414、高896。于是整个布局约束如下:

    flutter布局约束原理深入解析

    这里有个问题,就是Container分明已经设置了固定宽高,为什么无效?

    因为父级向下传递的约束,子组件必须严格遵守。这里Container容器设置的宽高超出了父级的约束范围,就会自动被忽略,采用符合约束的值。

    例一两上Container都被铺满屏幕,而最底下的红色Container叠到了最上层,所以最终显示红色。

    /// 例二
    @override
    Widget build(BuildContext context) {
       return Container(
         width: 300,
         height: 300,
         color: Colors.amber,
         alignment: Alignment.center,
         child: Container(width: 50, height: 50, color: Colors.red,),
      );
    }
    

    例二也同样可以根据布局约束求证,如下图:

    flutter布局约束原理深入解析

    这里Container为什么是ConstrainedBoxAlign组件?前面说过Container是一个组合组件,它是由多个原子组件组成的。根据例二,它是由ConstrainedBox和Align嵌套而成。

    Align提供给子组件的是一个松约束,所以容器自身设置50宽高值是在合理范围的,因此生效,屏幕上显示的就是50像素的红色方块。ConstrainedBox受到的是紧约束,所以自身的300宽高被忽略,显示的是铺满屏幕的黄色块。

    /// 例三
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: Container(
          width: 300,
          height: 300,
          color: Colors.amber,
          alignment: Alignment.center,
          child: Container(width: 50, height: 50, color: Colors.red,),
        ),
      );
    }
    

    例三中Scaffold向下传递的是一个松约束,所以黄色Container的宽高根据自身设置的300,在合理的范围内,有效。Container再向下传递的也是松约束,最终红色Container宽高为50。

    flutter布局约束原理深入解析

    这里还有个问题,怎么确定组件向下传递的是紧约束还是松约束?

    这就涉及到组件的内部实现了,这里通过Align举个例。

    Align是一个绘制组件,它能够进行界面的布局和绘制,这是因为Align的继承链为:

    Align -> SingleChildRenderObjectWidget -> RenderObjectWidget

    Align需要重写createRenderObject方法,返回RenderObject的实现,这里Align返回的是RenderPositionedBox,所以核心内容就在这个类中

    class Align extends SingleChildRenderObjectWidget {
        /// ...
        @override
        RenderPositionedBox createRenderObject(BuildContext context) {
          return RenderPositionedBox(
            alignment: alignment,
            widthFactor: widthFactor,
            heightFactor: heightFactor,
            textDirection: Directionality.maybeOf(context),
          );
        }
        /// ...
    }
    

    而RenderPositionedBox类中,重写performLayout方法,该方法用于根据自身约束条件,计算出子组件的布局,再根据子组件的尺寸设置自身的尺寸,形成一个至下而上,由上到下的闭环,最终实现界面的整个绘制。

    RenderPositionedBox -> RenderAligningShiftedBox -> RenderShiftedBox -> RenderBox

    class RenderPositionedBox extends RenderAligningShiftedBox {
        /// ...
        @override
        void performLayout() {
          final BoxConstraints constraints = this.constraints; // 自身的约束大小
          final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
          final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
          /// 存在子组件
          if (child != null) {
            /// 开始布局子组件
            child!.layout(constraints.loosen(), parentUsesSize: true);
            /// 根据子组件的尺寸设置自身尺寸
            size = constraints.constrain(Size(
              shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
              shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0python) : double.infinity,
            ));
            /// 计算子组件的位置
            alignChild();
          } else {
            /// 不存在子组件
            size = constraints.constrain(Size(
              shrinkWrapWidth ? 0.0 : double.infinity,
              shrinkWrapHeight ? 0.0 : double.infinity,
            ));
          }
        }
        /// ...
    }
    

    根据Align中performLayout方法的实现,可以确定该组件最终会给子组件传递一个怎么样的约束。

    /// constraints.loosen提供的是一个松约束
    child!.layout(constraints.loosen(), parentUsesSize: true);
    
    /// loosen方法
    BoxConstraints loosen() {
      assert(debugAssertIsValid());
      /// BoxConstraints({double minWidth = 0.0, double maxWidth = double.infiniAVvnNvty, double minHeight = 0.0, double maxHeight = double.infinity})
      return BoxConstraints(
        maxWidth: maxWidth,
        maxHeight: maxHeight,
      );
    }
    

    其它绘制类的组件基本跟Align大同小异,只要重点看performLayout方法的实现,即可判断出组件提供的约束条件。

    总结

    1、flutter的widget分为,组合类、代理类和绘制类。

    2、Container是一个组合类,由DecoratedBox、ConstrainedBox、Transform、Padding、Align等绘制组件组合而成。

    3、flutter中有两种布局约束BoxConstraints盒约束和SliverConstraints线性约束。

    4、BoxConstraints的约束原理是: 「向下传递约束,向上传递尺寸」

    5、BoxConstraints的约束类型为:紧约束、松约束、无界约束、有界约束。

    6、判断一个绘制组件的约束行为可以通过查看performLayout方法中layout传入的约束值。

    以上就是flutter布局约束原理深入解析的详细内容,更多关于flutter布局约束原理的资料请关注我们其它相关文章!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜