注册

深入flutter布局约束原理

刚开始接触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,),
);
}

image.png

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

image.png

而在容器外层添加一个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,),
),
);
}

image.png

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


1、flutter的widget类型


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


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


image.png

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


image.png

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

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


image.png


这里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。
image.png


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


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


作者:半点橘色
链接:https://juejin.cn/post/7183549888406224955
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册