Flow
我们一般情况下很少使用 Flow,因为其比较复杂,需要手动对 widget 进行布局,相当于是 android 中的 onLayout 方法。
Flow 主要用于以下需要高度自定义布局或者性能要求较高(如动画中) 的场景,
Flow 有如下优点
性能好:Flow 是一个队子组件尺寸以及位置调整非常高效的控件。Flow 用转换矩阵对子组件进行位置调整的时候进行了优化:在 Flutter 定位过后,如果子组件尺寸发生了变化,在 FlowDelegate 中的 paintChildren() 方法中调用 context.paintChild 进行重绘,而 contextPaintChild 进行重绘的时候使用了转换矩阵,并没有实际调整组件的位置
灵活:由于需要自定实现 FlowDelegate 的 parintChildren() 方法,所以我们需要手动计算每一个组件的位置,因此,可以自定义布局策略
缺点
使用复杂
不能自适应子组件大小,必须通过指定父容器大小或者实现 TestFlowDelegate 的 getSize 返回固定大小
示例
class TestFlowDelegate extends FlowDelegate { EdgeInsets margin = EdgeInsets.zero; TestFlowDelegate({this.margin}); @override void paintChildren(FlowPaintingContext context) { var x = margin.left; var y = margin.top; //计算每一个自 widget 的位置 for (int i = 0; i < context.childCount; i++) { //获取宽度 var width = context .getChildSize(i) .width + x + margin.right; //是否需要换行 print('$width ---- ${context.size.width}'); if (width < context.size.width) { //绘制第一个 context.paintChild(i, transform: Matrix4.translationValues(x, y, 0)); x = width + margin.left; } else { //绘制后面的 x = margin.left; y += context .getChildSize(i) .height + margin.top + margin.bottom; //绘制子widget(有优化) context.paintChild(i, transform: Matrix4.translationValues(x, y, 0)); x += context .getChildSize(i) .width + margin.left + margin.right; } } } @override bool shouldRepaint(covariant FlowDelegate oldDelegate) { return oldDelegate != this; } @override Size getSize(BoxConstraints constraints) { return Size(double.infinity, 200); } }
class WrapAndFlowTest extends StatelessWidget { final List<String> _list = const [ "爱是你我", "一壶老酒", "最炫民族风", "怒放的生命", "再见青春", "北京,北京" ]; List<Widget> getMusicList() { return _list .map((e) => Container( width: 140, height: 40, child: RaisedButton( child: Text(e), onPressed: () => print(e), ), )) .toList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("WarpAndFlow"), ), body: Padding( padding: EdgeInsets.all(10), child: Flow( delegate: TestFlowDelegate(margin: EdgeInsets.all(10)), children: getMusicList(), ), )); } }
可以看到主要的任务就是实现 paintChildren,他的主要任务就是确定每个子 Widget 的位置,由于 Flow 不能自适应 Widget 的大小,所以在 getSize 中返回一个固定大小来指定 Flow 的大小
层叠布局 Stack,Positioned
层叠布局和 Android 中的 FrameLayout 布局是相似的,子组件可以通过父容器的四个角的位置来确定自身的位置。
绝对定位允许子组件堆叠起来(按照代码中声明的顺序)。Flutter 中使用 Stack 和 Positioned 这两个 组件来配合实现决定定位。
Stack 允许组件堆叠,而 Positioned 用于根据 Stack 的四个角来确定子组件的位置
Stack
Stack({ this.alignment = AlignmentDirectional.topStart, this.textDirection, this.fit = StackFit.loose, this.overflow = Overflow.clip, List<Widget> children = const <Widget>[], })
alignment:此参数决定如何去对齐没有定位**(没有使用 Positioned)** 或部分定位的子组件。
部分定位指的是没有在某一个轴上定位:left ,right 为横轴,top ,bottom 为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位
textDirection:和 Row,Column 中的 textDirection 功能一样,都用于确定 alignment 对齐的参考系,即 textDirection 值为 ltr,则 alignment 代表左,end 为右。如果 textDirecion为 rtl,start 则为 有,end 为左
fit:此参数用于确定没有定位的子组件如何使用 Stack 的大小。StackFit.loose 表示使用子组件的大小, expand 表示扩伸到 Stack 的大小
overflow:此属性决定如何显示超出 Stack 显示空间的子组件;值为 Overflow.clip 时,超出部分会被剪裁(隐藏),只为 Overflow.visible 则不会
Positioned
const Positioned({ Key key, this.left, this.top, this.right, this.bottom, this.width, this.height, @required Widget child, })
left,top,right,bottom 分别代表 tack 四个边的距离,widget 耦合 height 用于指定需要定位元素的宽度和高度。
注意,Positioned 的 widget,height 是用于配合 left,top ,right,bottom 来定位组件,举个例子,在 水平方向是,只能指定 left,right,width 三个属性的两个,如指定 left 和 widget 后,right 会自动推算出 (left+widget),如果同时指定三个属性则会报错,垂直方向同理
栗子:
class StackAndPositionedTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("StackAndPositioned"), ), ///通过 ConstrainedBox 来确保 Stack 占满屏幕 body: ConstrainedBox( constraints: BoxConstraints.expand(), child: Stack( alignment: Alignment.center, children: [ Container( child: Text( "hello world", style: TextStyle(color: Colors.white), ), color: Colors.red, ), Positioned( left: 18, child: Text("I am 345"), ), Positioned( top: 18, child: Text("your friend"), ) ], ), )); } }
代码中第一个组件 hellow world 没有使用 Positioned 组件,所以 会受到 Aligment.center 的约束,所以他在图中是居中显示的。
第二个子组件 I am 345 只指定了 水平方位 left,属于部分定位,即垂直没有定位,那么他在垂直方向上会按照 aligment 进行对齐,即为垂直居中
第三个 your friend 和 第二个一样,只不过是制定了 垂直 top,没有水平定位,则水平方向居中
修改代码如下:
Stack( alignment: Alignment.center, fit: StackFit.expand, children: [ Positioned( left: 18, child: Text("I am 345"), ), Container( child: Text( "hello world", style: TextStyle(color: Colors.white), ), color: Colors.red, ), Positioned( top: 18, child: Text("your friend"), ) ], )
上面使用了 fit 属性,并且是 expand,表示没有使用定位的子组件会扩伸到 Stack 的大小
由于第二个子组件的宽高和 Stack 一样大,所以就会导致第一个组件被覆盖
第三个组件在最上层,正常显示
对齐与相对定位 Align
通过 Stack 和 Positioned 可以指定一个或多个子组件相对于父元素的各个边进行精确偏移,并且可以重叠,
但是如果只想简单调整一个子组件在父元素中的位置的话,使用 Align 组件会更简单一些
Align
Align({ Key key, this.alignment = Alignment.center, this.widthFactor, this.heightFactor, Widget child, })
Align 组件可以调整子组件的位置,并根据子组件的宽高来确定自身的宽高
aligment:需要一个 AlignmentGeometry 类型的值,表示子组件在父组件中的起始位置,AlignmentGeometry 是一个抽象类,常用的有两个子类 Aligment 和 FractionalOffset
widthFactor 和 heightFactor 用于确定 Align 自身的宽高属性;他们是两个缩放因子,分别会乘以子组件的宽高,最终的结果就是 Align 的宽高,如果为 null,则组件的宽高会占用尽可能多的空间
栗子
class AlignTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Align"), ), body: Container( height: 120, width: 120, color: Colors.blue[50], child: Align( alignment: Alignment.topRight, child: FlutterLogo( size: 60, ), ), ), ); } }
FlutterLogo 是 Fluter sdk 的一个组件,内容就是 Flutter 的商品
在 Container 中 制定了 宽高为 120,如果不指定 Container 的宽高,同时指定 widthFactor 和 heightFactor 为 2也可以达到相同的效果
Alignment.topRight 表示子组件的位置为 顶部右上角,具体的值为 Aligment(1,-1)
Aligment
Aligment 继承自 AligmentGemetry,表示矩形内的一个点,他有两个属性 x,y,分别代表水平和垂直的偏移,定义如下:
Alignment(this.x, this.y)
Aligment 会以矩形的中心点作为坐标的原点(Aligemtn(0.0,0.0)), x,y 的值从 -1 到 1, 分别代表矩形从左到右的距离 和 顶部 到底边的距离。因此 2 个水平/垂直 单位则等于 矩形的宽/高。
如 Aligment(-1,-1) 代表左侧顶点,1,1代表 右侧底部终点;1,-1,则是右侧顶点,即为 Aligment.topRight。 为了使用方便,矩形的原点,四个顶点都已经在 Aligment 中定义了静态常量。
Aligment 可以通过其 坐标转换公式将其坐标转为子元素的具体偏移坐标:
偏移量 = (Aligment.x * childWidth/2 + childWidth /2 , Aligment.y * chilHeight/2 + childHeight /2)
其中 childWidth 为子元素的宽度,childHeight 为子元素的高度
回过头在看一下上面的栗子,我们将 Aligment(1,-1) 带入上面的公式,可知 FlutterLogo 的偏移坐标正是 (60,0)
修改上面的栗子如下:
class AlignTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Align"), ), body: Container( // height: 120, // width: 120, color: Colors.blue[50], // child: Align( // alignment: Alignment.topRight, // child: FlutterLogo( // size: 60, // ), // ), child: Align( // 2x60/2+60/2 ,0x60/2 + 60/2 //=90 =30 alignment: Alignment(2, 0), widthFactor: 2, heightFactor: 2, child: FlutterLogo( size: 60, ), ), ), ); } }
根据代码中注释的计算,可以得出 x偏移 90,y 偏移30
FractionalOffset
FractionalOffset 继承自 Aligment , 他和 Aligment 的唯一区别就是坐标点不同
FactionalOffset 的坐标原点为矩形左侧顶点,这和系统布局一直,所以理解比较容易一点。他的坐标转换公式为:
偏移=(FractionalOffset.x*childWidth , FractionalOffset * childHeight)
小栗子:
body: Container( height: 120, width: 120, color: Colors.blue[50], child: Align( alignment: FractionalOffset(0.2, 0.6), // 0.2 *60 , 0.6 * 60 child: FlutterLogo( size: 60, ), ), )
带入公式,偏移量为 (12,60),结果如下:
Align 和 Stack 对比
Align 和 Stack/Positioned 都可以用于指定子元素相对于父元素的偏移,他们主要区别如下
定位参考系统不同
Stack/Positioned 定位参考的是父容器的四个顶点
Align 则需要先通过参数 alignment 来确定具体的坐标,最终的偏移是通过 aligment 的公式计算出的
Stack 可以有多个子元素,并且可以堆叠,而 Align 只有一个元素,不存在堆叠
Center 组件
Center 组件用来居中子元素,在之前我们已经使用过他了,下面来介绍一下他,Center 定义如下
class Center extends Align { const Center({ Key key, double widthFactor, double heightFactor, Widget child }) : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child); }
Center 继承子 Align,它相比 Align 只少了一个 aligment 参数;
由于 Align 中 alignment 值为 center,所以,Center 组件的对齐方式为 Aglinment.center 了
widthFactory 或者 heightFactory 的长度为 null 时表示尽可能占用更多的空间,这点需要特别注意一下
总结
Row / Column
沿水平或者垂直方向排列子组件
Flex
弹性布局,个人感觉有点类似于 Android 线性布局中的 layout_weight 属性,子组件通过 flex 表示当前组件需要占总大小的多少。
流式布局 Wrap/Flow
Wrap 自动排列,可以指定 对齐属性等,超过宽度自动折行
Flow 高度自定义的 Widget,需要手动计算折行位置,排列等,比较适用于高度的自定义
层叠布局 Stack,Positioned
Stack 层叠布局,可以有多个子组件,子组件 Postioned 用于可根据 Stack 的四个角来确定当前组件的位置,没有使用 Positioned ,则会按照 aligment 进行排列
Align
只能有一个子组件,通过 Aligment / FractionalOffset 进行定位
Aligment / FractionalOffset
两者都代表这偏移量 ,FractionalOffset 继承自 Aligment
Aligment 的原点为 Widget 的中心,即中心点为 Aligment(0,0),具体的偏移可根据公式计算
FractionalOffset 的原点为Widget 的左上角顶点,即FractionalOffset(0,0),和系统布局一样。具体偏移需要公式计算
Center
继承自 Align,相比与 Align 少了 aligment 参数,该参数默认为居中