今天分享的内容主要是 flutter 中很基础的一个概念:尺寸与限制。
当然,这里并不是说尺寸、限制的实现,在 flutter 中就是采用 Container
,这里只是以 Container
为例。下面将跳出与 web 的比较,直接讲述 flutter 中的尺寸限制。
基础铺垫
flutter 中的布局与浏览器中的布局行为是类似的(flutter 中负责布局、渲染这一块的负责人也是 chrome 浏览器中负责页面布局、渲染的主要开发者),因此理解尺寸、限制对于布局是尤为重要的。
尺寸
这里的尺寸就是普通意义上的宽度、高度。常用的组件有:
Container
: 可设置 width
/height
;
SizedBox
: 可设置 width
/height
。
它看似表面上是以 width
/height
定死了容器的宽高,但在 flutter 内部,它会转换为 限制!这句话怎么理解呢?比如如下代码:
1 2 3 4 5 6 7 8 9 10
| class MyComponent1 extends StatelessWidget { @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: Colors.blue, ); } }
|
这里定义了一个 MyComponent1
组件,它只是组合了其他组件(widget 的分类详见本专题其他文章),内部使用了 Container
作为容器,宽高都为 100 物理像素(这个先埋个伏笔:这个蓝框框的实际大小会是 100*100 吗?)。我们看一下 Container
的构造函数(出于简化目的,之后都会省略非关键代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Container extends StatelessWidget { Container({ Key key, this.alignment, this.padding, Color color, Decoration decoration, this.foregroundDecoration, double width, double height, BoxConstraints constraints, this.margin, this.transform, this.child, }) : decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null), constraints = (width != null || height != null) ? constraints?.tighten(width: width, height: height) ?? BoxConstraints.tightFor(width: width, height: height) : constraints, super(key: key); }
|
可以很明显地发现 width
和 height
只是参数,不是类实例属性,在初始化列表中利用 width
和 height
对类实例属性 constraints
赋值。这里暂不讨论方法 #tighten 和 #tightFor 。
限制
限制就是对宽高的限制,比如最大宽度,最小高度等。常用的限制类组件有:
Container
: 可设置 constraints
;
LimitedBox
: 可设置 maxWidth
/maxHeight
;
ConstrainedBox
: 可设置 constraints
。
其实限制类的组件并没有很多。像刚才列举的 Container
可以作为限制容器,其实也是因为内部使用了 ConstrainedBox
作为包裹,才有了限制的功能。
Container
的注释
其实从 Container
的注释上,我们能发现一些端倪。这里我把关于布局相关的注释翻译一下,同时优先级也是按照从上到下的顺序:
- 如果没有设置 child, width, height, constraints, 并且父组件提供无限制的限制(有点拗口,可理解为无限大的限制),则该 Container 会尽可能小;
- 如果没有 child 和 alignment,但是至少有 width/height/constraints 的其中之一,当前限制会与父组件传递的限制组合为新的限制,该 Container 会在满足新的限制条件下尽可能小(Note: 这里不确定是翻译问题,还是理解问题,我得出的结论与官方相反。我的理解:应该是满足新的限制条件下尽可能大);
- 如果没有 child、height、width、contraints、alignment,但是父组件提供了有限的限制,那么该 Container 就会扩展去满足父组件的(最大)限制;
- 如果有 alignment,并且父组件提供无限制的限制,那么该 Container 会围绕 child 来调整它的大小;
- 如果有 alignment,父组件提供有限的限制,那么该 Container 会先扩展扩大去满足父组件的限制,然后按照对齐方式定位子元素。
- 否则,只有 child,没有 height、width、constraints、alignment,那么该 Container 会将父组件的限制传递给 child,然后调整该 Container 的大小去匹配 child 的大小。
看到这里,建议各位读者先仔细看几遍,然后再继续。后文将主要通过一些例子来验证上述限制的逻辑,同时解答一些常见的 overflow 报错。
验证 1
如果没有设置 child, width, height, constraints, 并且父组件提供无限制的限制(有点拗口,可理解为无限大的限制),则该 Container 会尽可能小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| void main() => runApp(MyComponent1());
class MyComponent1 extends StatelessWidget { @override Widget build(BuildContext context) { RenderBox rb = context.findRenderObject(); rb?.constraints; return UnconstrainedBox( child: Test(), ); } }
class Test extends StatelessWidget { @override Widget build(BuildContext context) { return Builder( builder: (BuildContext childContext) { Timer.run(() { RenderBox rb = context.findRenderObject(); print('Test constraints: ' + rb?.constraints.toString());
RenderBox rbc = childContext.findRenderObject(); print('Child size: ' + rbc?.size.toString());
}); return Container( color: Colors.blue, ); },); } }
|
输出结果:
1 2
| I/flutter (20090): Test constraints: BoxConstraints(unconstrained) I/flutter (20090): Child size: Size(0.0, 0.0)
|
Test 接收到的限制为 unconstrained(最大宽度、高度都是无穷大),内部蓝框框不设置 child, width, height, constraints,发现打印出的蓝框框大小就是 0,符合 1.
我们将限制只放在单一方向上,比如只让垂直方向无限制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void main() => runApp(MyComponent1());
class MyComponent1 extends StatelessWidget { @override Widget build(BuildContext context) { RenderBox rb = context.findRenderObject(); rb?.constraints; return UnconstrainedBox( child: Test(), constrainedAxis: Axis.horizontal, ); } }
class Test extends StatelessWidget {}
|
输出:
1 2
| I/flutter (20090): Test constraints: BoxConstraints(w=392.7, 0.0<=h<=Infinity) I/flutter (20090): Child size: Size(392.7, 0.0)
|
发现水平方向其实是有大小的,为 Test 组件的宽度大小。但垂直方向变为最小的。同样符合 1.
验证 2
如果没有 child 和 alignment,但是至少有 width/height/constraints 的其中之一,当前限制会与父组件传递的限制组合为新的限制,该 Container 会在满足新的限制条件下尽可能小(Note: 这里不确定是翻译问题,还是理解问题,我得出的结论与官方相反。我的理解:应该是满足新的限制条件下尽可能大):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import 'dart:async';
import 'package:flutter/material.dart';
class MyComponent2 extends StatelessWidget { @override Widget build(BuildContext context) { RenderBox rb = context.findRenderObject(); rb?.constraints; return UnconstrainedBox( child: ConstrainedBox( child: Test(), constraints: BoxConstraints(maxWidth: 300, minWidth: 100, maxHeight: 300, minHeight: 100), ), ); } }
class Test extends StatelessWidget { @override Widget build(BuildContext context) { return Builder( builder: (BuildContext childContext) { Timer.run(() { RenderBox rb = context.findRenderObject(); print('Test constraints: ' + rb?.constraints.toString());
RenderBox rbc = childContext.findRenderObject(); print('Child size: ' + rbc?.size.toString());
}); return Container( width: 150, height: 150, color: Colors.blue, ); },); } }
|
Test 组件的外层限制应该是最大宽度、高度为 300,最小宽度、高度为 100。我们看看输出:
1 2
| I/flutter (22330): Test constraints: BoxConstraints(100.0<=w<=300.0, 100.0<=h<=300.0) I/flutter (22330): Child size: Size(150.0, 150.0)
|
如果将蓝框框的宽高都设为 80 呢?看输出:
1 2
| I/flutter (22330): Test constraints: BoxConstraints(100.0<=w<=300.0, 100.0<=h<=300.0) I/flutter (22330): Child size: Size(100.0, 100.0)
|
不是设置的 80 了,而是满足当前限制与父组件的组合。父组件都说了,最小 100,最大 300,你自己设置的 80 我可不认,最小 100!就是这么霸道!
我们不用固定值,我们来使用限制呢?
1 2 3 4
| Container( constraints: BoxConstraints(minHeight: 100, maxHeight: 110, minWidth: 120, maxWidth: 220), color: Colors.blue, );
|
仅去掉蓝框框的 width/height,增加修改蓝框框的约束,看看结果呢?
1 2
| I/flutter (22330): Test constraints: BoxConstraints(100.0<=w<=300.0, 100.0<=h<=300.0) I/flutter (22330): Child size: Size(220.0, 110.0)
|
发现,大小并不是组合限制(本例中为 120<=w<=220, 100<=h<=110)中的最小值 (120, 100),而是最大值 (220, 110)。正如 2 中括号注释的一样(官方文档没有,笔者自己领悟的),当为紧约束时,为紧约束的大小;当为松约束时,为松约束的最大值。什么是紧送约束?
松紧约束
直接看代码:
1 2 3 4 5
| bool get hasTightWidth => minWidth >= maxWidth; bool get hasTightHeight => minHeight >= maxHeight;
bool get isTight => hasTightWidth && hasTightHeight;
|
很简单:紧(tight)约束就是最小宽度等于最大宽度 && 最小高度等于最大高度。也就是说,当我们设置:
1 2
| width: 100, height: 100,
|
时,会被转换成:
1 2
| constraints = BoxContraints.tightFor(width: 100, height: 100) // constraints == BoxContraints(minHeight: 100, maxHeight: 100, minWidth: 100, maxWidth: 100)
|
此时就说该限制是紧的。相反,当最小宽度、高度分别小于最大宽度、高度,此时就说是松约束。
那么松、紧约束在此条件下,为什么表现不一致呢?看一下 Container
的 build 方法:
1 2 3 4 5 6 7 8 9 10 11 12
| 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()), ); } }
|
当 child 为 null 并且(限制为 null 或者是松约束),则包一层 LimitedBox
。这个 LimitedBox
有啥作用?因为它不是本文主要内容,这里就简单讲解一下:
1 2 3 4 5 6
| const LimitedBox({ Key key, this.maxWidth = double.infinity, this.maxHeight = double.infinity, Widget child, })
|
LimitedBox
作用:当父组件给的限制是无限制的时候,则把对 child 的限制改为 maxWidth
、maxHeight
。当 child 为 null 时,在 Container
的最内层使用了 LimitedBox(width: 0, height: 0, child: ConstrainedBox(constraints: const BoxConstraints.expand()), )
,意思是:当父组件给的限制为无限制时,大小就为 0(这个是 LimitedBox 的作用),这个符合验证 1;当父组件给的限制为有限限制时,大小就为 BoxConstraints.expand (),也即满足限制的最大尺寸。这就解释了为什么验证 2 中我添加的注释。如果大家有疑问,可以留言。
验证 3
如果没有 child、height、width、contraints、alignment,但是父组件提供了有限的限制,那么该 Container 就会扩展去满足父组件的(最大)限制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import 'dart:async';
import 'package:flutter/material.dart';
class MyComponent3 extends StatelessWidget { @override Widget build(BuildContext context) { RenderBox rb = context.findRenderObject(); rb?.constraints; return UnconstrainedBox( child: ConstrainedBox( child: Test(), constraints: BoxConstraints(maxWidth: 300, minWidth: 100, maxHeight: 300, minHeight: 100), ), ); } }
class Test extends StatelessWidget { @override Widget build(BuildContext context) { return Builder( builder: (BuildContext childContext) { Timer.run(() { RenderBox rb = context.findRenderObject(); print('Test constraints: ' + rb?.constraints.toString());
RenderBox rbc = childContext.findRenderObject(); print('Child size: ' + rbc?.size.toString());
}); return Container( color: Colors.blue, ); },); }
}
|
输出:
1 2
| I/flutter (26984): Test constraints: BoxConstraints(100.0<=w<=300.0, 100.0<=h<=300.0) I/flutter (26984): Child size: Size(300.0, 300.0)
|
原因也很明显了,验证 2 中最后就拿根本原因 LimitedBox
解释过。因为父组件给的是有限限制,所以 LimitedBox 不生效,但 LimitedBox 中的 child 的限制却生效了:BoxConstraints.expand (),即满足限制条件的最大。
验证 4
如果有 alignment,并且父组件提供无限制的限制,那么该 Container 会围绕 child 来调整它的大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import 'dart:async';
import 'package:flutter/material.dart';
class MyComponent4 extends StatelessWidget { @override Widget build(BuildContext context) { RenderBox rb = context.findRenderObject(); rb?.constraints; return UnconstrainedBox( child: Test(), ); } }
class Test extends StatelessWidget { @override Widget build(BuildContext context) { return Builder( builder: (BuildContext childContext) { Timer.run(() { RenderBox rb = context.findRenderObject(); print('Test constraints: ' + rb?.constraints.toString());
RenderBox rbc = childContext.findRenderObject(); print('Child size: ' + rbc?.size.toString());
}); return Container( color: Colors.blue, child: SizedBox( width: 100, height: 120, child: DecoratedBox( decoration: BoxDecoration(color: Colors.yellow), ), ) ); },); } }
|
输出:
1 2
| I/flutter (26984): Test constraints: BoxConstraints(unconstrained) I/flutter (26984): Child size: Size(100.0, 120.0)
|
Container
的大小是 SizedBox
的大小(一定会是这样吗?),虽然没有设置 alignment
(也没必要,毕竟该 Container
的大小就为 child 的大小,定位是无意义的),但也符合 4。此时看一下页面显示:
下面看一下,分几种情况,我们应用 alignment
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Container( color: Colors.blue, width: 200, child: SizedBox( width: 100, height: 120, child: DecoratedBox( decoration: BoxDecoration(color: Colors.yellow), ), ), )
|
不设置 alignment
,此时讲道理,蓝框框会比黄的大。但此时页面:
输出:
1 2
| I/flutter (26984): Test constraints: BoxConstraints(unconstrained) I/flutter (26984): Child size: Size(200.0, 120.0)
|
黄框框完全遮挡住了蓝框框。为什么呢?因为父组件给的限制是紧宽度,松高度,即使 SizedBox
限制了宽度,由于父组件的绝对限制,导致宽度变成了父组件的宽度。
当设置对齐方式时,比如 center (水平垂直都居中):
1 2 3 4 5 6 7 8 9 10 11 12 13
| Container( color: Colors.blue, width: 200, height: 200, alignment: Alignment.center, child: SizedBox( width: 100, height: 100, child: DecoratedBox( decoration: BoxDecoration(color: Colors.yellow), ), ) );
|
此时,页面展示:
输出:
1 2
| I/flutter (26984): Test constraints: BoxConstraints(unconstrained) I/flutter (26984): Child size: Size(200.0, 200.0)
|
符合预期。外层 Container 宽高为 200,内层 SizedBox
宽高为 100,设置的对齐方式为水平垂直居中。
验证 5
如果有 alignment,父组件提供有限的限制,那么该 Container 会先扩展扩大去满足父组件的限制,然后按照对齐方式定位子元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| class MyComponent5 extends StatelessWidget { @override Widget build(BuildContext context) { RenderBox rb = context.findRenderObject(); rb?.constraints; return UnconstrainedBox( child: ConstrainedBox( constraints: BoxConstraints.loose(Size(300, 300)), child: Test(), ), ); } }
class Test extends StatelessWidget { @override Widget build(BuildContext context) { return Builder( builder: (BuildContext childContext) { Timer.run(() { RenderBox rb = context.findRenderObject(); print('Test constraints: ' + rb?.constraints.toString());
RenderBox rbc = childContext.findRenderObject(); print('Child size: ' + rbc?.size.toString());
}); return Container( color: Colors.blue, alignment: Alignment.center, child: SizedBox( width: 100, height: 120, child: DecoratedBox( decoration: BoxDecoration(color: Colors.yellow), ), ) ); },); } }
|
输出:
1 2
| I/flutter (26984): Test constraints: BoxConstraints(0.0<=w<=300.0, 0.0<=h<=300.0) I/flutter (26984): Child size: Size(300.0, 300.0)
|
页面:
Container 的大小即是扩展到父组件的最大,即 300*300,然后根据 alignment
去定位 child。
验证 6
否则,只有 child,没有 height、width、constraints、alignment,那么该 Container 会将父组件的限制传递给 child,然后调整该 Container 的大小去匹配 child 的大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import 'dart:async';
import 'package:flutter/material.dart';
class MyComponent6 extends StatelessWidget { @override Widget build(BuildContext context) { RenderBox rb = context.findRenderObject(); rb?.constraints; return UnconstrainedBox( child: ConstrainedBox( constraints: BoxConstraints.loose(Size(300, 300)), child: Test(), ), ); } }
class Test extends StatelessWidget { @override Widget build(BuildContext context) { return Builder( builder: (BuildContext childContext) { Timer.run(() { RenderBox rb = context.findRenderObject(); print('Test constraints: ' + rb?.constraints.toString());
RenderBox rbc = childContext.findRenderObject(); print('Child size: ' + rbc?.size.toString());
}); return Container( color: Colors.blue, child: SizedBox( width: 100, height: 120, child: DecoratedBox( decoration: BoxDecoration(color: Colors.yellow), ), ) ); },); }
}
|
输出:
1 2
| I/flutter (26984): Test constraints: BoxConstraints(0.0<=w<=300.0, 0.0<=h<=300.0) I/flutter (26984): Child size: Size(100.0, 120.0)
|
看到 Container 的大小就是 child 的大小。
当我们修改父组件的限制时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| class MyComponent6 extends StatelessWidget { @override Widget build(BuildContext context) { RenderBox rb = context.findRenderObject(); rb?.constraints; return UnconstrainedBox( child: ConstrainedBox( constraints: BoxConstraints(minWidth: 200, minHeight: 200), child: Test(), ), ); } }
class Test extends StatelessWidget { @override Widget build(BuildContext context) { return Builder( builder: (BuildContext childContext) { Timer.run(() { RenderBox rb = context.findRenderObject(); print('Test constraints: ' + rb?.constraints.toString());
RenderBox rbc = childContext.findRenderObject(); print('Child size: ' + rbc?.size.toString());
}); return Container( color: Colors.blue, child: SizedBox( width: 100, height: 120, child: DecoratedBox( decoration: BoxDecoration(color: Colors.yellow), ), ) ); },); } }
|
输出:
1 2
| I/flutter (26984): Test constraints: BoxConstraints(200.0<=w<=Infinity, 200.0<=h<=Infinity) I/flutter (26984): Child size: Size(200.0, 200.0)
|
看到 Container 的大小是 200*200 了(当然 SizedBox 也是 200*200,而不会管 100*120,原因上面也说过,就是父组件的限制强于自己的限制)。也就是说这种情况下将父组件的限制透传给了 child。作用于 SizedBox
的限制是 BoxConstraints(200.0<=w<=Infinity, 200.0<=h<=Infinity)
。
总结 1
- flutter 中的限制是从顶层(比如 MaterialApp 这样的功能组件)传入底层(任意分支),上一层的限制会限制这一层的限制。举个例:
COMPONENT2
会接收其父组件 COMPONENT1
传递的约束 1,同时自身也有约束 2,那么新的约束条件就是 3。图中的约束 3 所表示的宽高不一定是实际情况(根据组件功能的不同,情况会有变化),主要看对应 RenderObject
中 performLayout
中怎么生成新的组合限制。
2. flutter 中的 size 是从底层回传到顶层。