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
组合类:如Container
、Scaffold
、MaterialApp
还有一系列通过继承StatelessWidget和StatefulWidget的类。组合类是我们开发过程中用得最多的组件。
代理类:InheritedWidget
,功能型组件,它可以高效快捷的实现共享数据的跨组件传递。如常见的Theme
、MediaQuery
就是InheritedWidget的应用。
绘制类:屏幕上看到的UI几乎都会通过RenderObjectWidget
实现。通过继承它,可以进行界面的布局和绘制。如Align
、Padding
、ConstrainedBox
等都是通过继承RenderObjectWidget,并通过重写createRenderObject方法来创建RenderObject对象,实现最终的布局(layout)和绘制(paint)。
2、Container是个组合类
显而易见Container继承StatelessWidget,它是一个组合类,同时也是一个由DecoratedBox
、ConstrainedBox
、Transform
、Padding
、Align
等组件组合的多功能容器。可以通过查看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为什么是ConstrainedBox
和Align
组件?前面说过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传入的约束值。
加载全部内容