亲宝软件园·资讯

展开

flutter布局约束原理深入解析

半点橘色 人气:0

引言

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

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

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

然后要加上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,),
  );
}

而在容器外层添加一个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布局的学习,才渐渐解开这些疑惑。

1、flutter的widget类型

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

组合类:如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 (child == 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 = 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框架在运行时遍历整个组件树,在这过程中 「向下传递约束,向上传递尺寸」,以此来确定每个组件的尺寸和大小。

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

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

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

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

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。于是整个布局约束如下:

这里有个问题,就是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,),
  );
}

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

这里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。

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

这就涉及到组件的内部实现了,这里通过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.0) : 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.infinity, 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传入的约束值。

加载全部内容

相关教程
猜你喜欢
用户评论