Flutter了解之入门篇7(布局类组件)

目录
  1. 线性布局(Row和Column)
  2. 弹性布局(Flex、Expanded)
  3. 流式布局(Wrap、Flow)
  4. 层叠布局 Stack、Positioned(绝对定位)、Align(相对定位)、Center
  5. LayoutBuilder

Flutter官方并没有对Widget进行官方分类,对其分类主要是为了对Widget进行功能区分。
布局类组件:指直接或间接继承MultiChildRenderObjectWidget(拥有一个children属性)且有布局功能的Widget。不同的布局类组件对子组件布局方式不同。

Widget分类(根据是否需要包含子节点)
继承关系:
  (Leaf/SingleChild/MultiChild)RenderObjectWidget 继承自 RenderObjectWidget 继承自 Widget

RenderObjectWidget中定义了创建及更新RenderObject(最终布局、渲染UI界面的对象)的方法(子类必须实现)。
布局类组件的布局算法是通过对应的RenderObject对象来实现的,比如Stack(层叠布局)对应的RenderObject对象就是RenderStack(实现了层叠布局)。
/*
StatelessWidget和StatefulWidget是两个用于组合Widget的基类,本身并不关联最终的RenderObject渲染对象。
Flutter中的很多Widget是直接继承自StatelessWidget或StatefulWidget,然后在build()方法中构建真正的RenderObjectWidget。比如Text,继承自StatelessWidget,然后在build()方法中通过RichText(继承自MultiChildRenderObjectWidget)来构建其子树。
*/

1. 线性布局(Row和Column,都继承自Flex,属于基础组件库)

沿水平/垂直方向排列子组件。
类似于Android中的LinearLayout控件。

主轴和纵轴:
  沿水平方向则水平方向是主轴,垂直方向是纵轴。
  沿垂直方向则垂直方向是主轴,水平方向是纵轴。

对齐方式(枚举类)
  MainAxisAlignment 主轴对齐
  CrossAxisAlignment 纵轴对齐
  1. Row

沿水平方向(横向)排列子widget

Row({
/*
  水平方向子组件的布局顺序(从左往右 或 从右往左)。
  默认:为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)。
  是mainAxisAlignment的参考系。
*/
  TextDirection textDirection,    
/*
  水平方向的占用空间。
  MainAxisSize.max(默认,尽可能多的占用空间)、MainAxisSize.min(尽可能少的占用水平空间)。
*/
  MainAxisSize mainAxisSize = MainAxisSize.max,    
/*
  水平方向的对齐方式。
  只有当mainAxisSize值为MainAxisSize.max时此属性才有意义,因为当mainAxisSize值为MainAxisSize.min时子组件的宽度等于Row的宽度。
  MainAxisAlignment枚举类型
    start:沿textDirection的初始方向对齐。默认。
    end:和start正好相反。  
    center:居中对齐。
    spaceEvenly:均匀分布。
    spaceBetween:2边没距离,元素间距相同。
    spaceAround:元素间距是两边间距的2倍。
    spaceEvenly:两边间距是元素间距的2倍。
*/
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
/*
  垂直方向子组件的顺序。 
  VerticalDirection.down从上到下(默认)、VerticalDirection.up从下到上。
*/
  VerticalDirection verticalDirection = VerticalDirection.down,  
/*
  纵轴方向的对齐方式。
  取值和MainAxisAlignment一样,参考系是verticalDirection。
  Row的高度等于子组件中最高的子元素高度。
*/
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  // 子组件数组
  List<Widget> children = const <Widget>[],  
})

特殊情况(嵌套):
  如果Row里面嵌套Row,或Column里面嵌套Column,那么只有最外面的Row或Column会占用尽可能大的空间,而里面Row或Column所占用的空间为实际大小。
  如果要让里面的Column占满外部Column,可以使用Expanded 组件。

示例(Column、Row)

Column(
  // 测试Row对齐方式,排除Column默认居中对齐的干扰
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    // mainAxisSize默认占满屏幕,所以居中会显示在屏幕中央
    Row( 
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    // mainAxisSize为min则Row宽为所有组件宽之合,mainAxisAlignment属性失效
    Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    // TextDirection.rtl表示右侧为开始处,MainAxisAlignment.end表示从结尾处开始布局
    Row(
      mainAxisAlignment: MainAxisAlignment.end,
      textDirection: TextDirection.rtl,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    // VerticalDirection.up表示底部为开始处,CrossAxisAlignment.start表示从开始处开始布局
    Row(
      crossAxisAlignment: CrossAxisAlignment.start,  
      verticalDirection: VerticalDirection.up,
      children: <Widget>[
        Text(" hello world ", style: TextStyle(fontSize: 30.0),),
        Text(" I am Jack "),
      ],
    ),
  ],
);

示例(特殊情况:嵌套)

Container(
  color: Colors.green,
  child: Padding(
    padding: const EdgeInsets.all(16.0),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.max, // 有效,外层Colum高度为整个屏幕
      children: <Widget>[
        Container(
          color: Colors.red,
          child: Column(
            mainAxisSize: MainAxisSize.max,// 无效,内层Colum高度为实际高度  
            children: <Widget>[
              Text("hello world "),
              Text("I am Jack "),
            ],
          ),
        )
      ],
    ),
  ),
);
将里面的Column使用Expanded包装后,高才会尽可能多的占用空间。
Expanded( 
  child: Container(
    color: Colors.red,
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center, // 垂直方向居中对齐
      children: <Widget>[
        Text("hello world "),
        Text("I am Jack "),
      ],
    ),
  ),
)
  1. Column

沿垂直方向排列子组件(参数和Row一样,但主纵轴相反)。

示例

import 'package:flutter/material.dart';
class CenterColumnRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // mainAxisAlignment默认从上到下。mainAxisSize默认占最多空间,此处为屏幕高。
    // crossAxisAlignment.center横向居中。Column宽度为最大子组件的宽。
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Text("hi"),
        Text("world"),
      ],
    );
  }
}
Row和Column都只会在主轴方向占用尽可能大的空间,而纵轴的长度则取决于他们最大子元素的长度。
如果想让本例中的两个文本控件在整个手机屏幕中间对齐:
  方法1. 使用Center组件
  方法2. 通过ConstrainedBox或SizedBox强制将Column宽指定为屏幕宽
    ConstrainedBox(
      // double.infinity占用尽可能多的空间
      constraints: BoxConstraints(minWidth: double.infinity), 
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Text("hi"),
          Text("world"),
        ],
      ),
    );

2. 弹性布局(Flex)

子组件按照一定比例来分配父容器空间,通过Flex和Expanded来配合实现。
类似H5中的弹性盒子布局,Android中的FlexboxLayout。

Flex(继承自MultiChildRenderObjectWidget)

沿水平/垂直方向排列子组件。
Flex和Expanded组件配合可以实现弹性布局。

Flex({
  ...
  @required this.direction, // 弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
  List<Widget> children = const <Widget>[],
})
Flex对应的RenderObject为RenderFlex(实现了其布局算法)。

如果知道主轴方向,使用Row或Column更方便,因为Row和Column都继承自Flex,参数基本相同。所以能使用Flex的地方基本上都可以使用Row或Column。

Expanded 自动扩展

按比例扩伸Row/Column/Flex子组件所占用的空间。
只能作为Flex(以及继承Flex的子组件) 的子组件,否则会报错。

Expanded({
  int flex = 1,  // 弹性系数,如果为0或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其flex的比例来分割主轴的全部空闲空间。默认为1。
  @required Widget child,
})
如果有SizeBox,会减去SizeBox的宽/高,再按比例。

示例

左侧hello文本固定宽度,右侧world文本(屏幕款 - hello文本宽)
Row(
  children:<Widget>[
    Text('hello'),
    Expanded(
      flex:1,
      child:Text('world'),
    ),
  ];
);

示例

class FlexLayoutTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        // Flex的两个子widget按1:2来占据水平空间  
        Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Expanded(
              flex: 1,
              child: Container(
                height: 30.0,
                color: Colors.red,
              ),
            ),
            Expanded(
              flex: 2,
              child: Container(
                height: 30.0,
                color: Colors.green,
              ),
            ),
          ],
        ),
        Padding(
          padding: const EdgeInsets.only(top: 20.0),
          child: SizedBox(
            height: 100.0,
            // Flex的三个子widget,在垂直方向按2:1:1来占用100像素的空间  
            child: Flex(
              direction: Axis.vertical,
              children: <Widget>[
                Expanded(
                  flex: 2,
                  child: Container(
                    height: 30.0,
                    color: Colors.red,
                  ),
                ),
                Spacer(
                  flex: 1,
                ),
                Expanded(
                  flex: 1,
                  child: Container(
                    height: 30.0,
                    color: Colors.green,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}
/*
示例中的Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装类。
看一下Spacer的定义:
class Spacer extends StatelessWidget {
  const Spacer({Key key, this.flex = 1})
    : assert(flex != null),
      assert(flex > 0),
      super(key: key);
  final int flex;
  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: const SizedBox.shrink(),
    );
  }
}
*/

3. 流式布局

流式布局:超出屏幕显示范围会自动折行的布局。

示例(溢出报错)

// Row只有一行,如果超出屏幕不会折行,会报溢出错误。
Row(
  children: <Widget>[
    Text("xxx"*100)
  ],
);
Row和Colum的子widget超出屏幕范围时会报溢出错误。
可以通过Wrap和Flow来支持流式布局,将上例中的Row换成Wrap后溢出部分则会自动折行。
溢出报错

Wrap

除了超出显示范围后Wrap会折行外,其它行为和Row基本相同。

Wrap({
  this.direction = Axis.horizontal,
  this.alignment = WrapAlignment.start,
  this.spacing = 0.0,    
  this.runAlignment = WrapAlignment.start,  
  this.runSpacing = 0.0,  
  this.crossAxisAlignment = WrapCrossAlignment.start,
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  List<Widget> children = const <Widget>[],
})

说明:
1. direction
  主轴方向。默认水平。
2. spacing
  主轴方向的间距
3. alignment
  主轴方向的对齐方式
4. runSpacing
  副轴方向的间距
5. runAlignment
  副轴方向的对齐方式

示例

Wrap(
  spacing: 8.0, // 主轴(水平)方向间距
  runSpacing: 4.0, // 纵轴(垂直)方向间距
  alignment: WrapAlignment.center, // 沿主轴方向居中
  children: <Widget>[
    new Chip(
      avatar: new CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
      label: new Text('Hamilton'),
    ),
    new Chip(
      avatar: new CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
      label: new Text('Lafayette'),
    ),
    new Chip(
      avatar: new CircleAvatar(backgroundColor: Colors.blue, child: Text('H')),
      label: new Text('Mulligan'),
    ),
    new Chip(
      avatar: new CircleAvatar(backgroundColor: Colors.blue, child: Text('J')),
      label: new Text('Laurens'),
    ),
  ],
)

Flow

用于一些需要自定义布局策略或性能要求较高(动画)的场景。

一般很少用Flow(过于复杂,需要自己实现子widget的位置转换),优先考虑Wrap是否满足需求。
自定义布局策略的首选方式是直接继承RenderObject重写performLayout。

优点:
    1. 性能好;Flow是一个对子组件尺寸以及位置调整非常高效的控件,Flow用转换矩阵在对子组件进行位置调整的时候进行了优化:在Flow定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate中的paintChildren()方法中调用context.paintChild进行重绘,而context.paintChild在重绘时使用了转换矩阵,并没有实际调整组件位置。
    2. 灵活;由于需要自己实现FlowDelegate的paintChildren()方法,所以要自己计算每一个组件的位置,因此可以自定义布局策略。

缺点:
    1. 使用复杂。
    2. 不能自适应子组件大小,必须通过指定父容器大小或实现TestFlowDelegate的getSize返回固定大小。

示例

Flow(
  delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
  children: <Widget>[
    new Container(width: 80.0, height:80.0, color: Colors.red,),
    new Container(width: 80.0, height:80.0, color: Colors.green,),
    new Container(width: 80.0, height:80.0, color: Colors.blue,),
    new Container(width: 80.0, height:80.0,  color: Colors.yellow,),
    new Container(width: 80.0, height:80.0, color: Colors.brown,),
    new Container(width: 80.0, height:80.0,  color: Colors.purple,),
  ],
)
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 w = context.getChildSize(i).width + x + margin.right;
      if (w < context.size.width) {
        context.paintChild(i,
            transform: new Matrix4.translationValues(
                x, y, 0.0));
        x = w + margin.left;
      } else {
        x = margin.left;
        y += context.getChildSize(i).height + margin.top + margin.bottom;
        // 绘制子widget(有优化)  
        context.paintChild(i,
            transform: new Matrix4.translationValues(
                x, y, 0.0));
         x += context.getChildSize(i).width + margin.left + margin.right;
      }
    }
  }
  @override
  getSize(BoxConstraints constraints){
    // 指定Flow的大小  
    return Size(double.infinity,200.0);
  }
  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return oldDelegate != this;
  }
}
主要的任务就是实现paintChildren,确定每个子widget位置。
由于Flow不能自适应子widget的大小,通过在getSize返回一个固定大小来指定Flow的大小。

4. 层叠布局 Stack、Positioned(绝对定位)、Align(相对定位)、Center

Stack(允许子组件堆叠)和Positioned(通过上下左右宽高来确定子组件位置)组合可实现绝对定位。
类似于:Web中的绝对定位、iOS和Android中的Frame布局。

  1. Stack
Stack({
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})
说明:
1. alignment:
  如何去对齐没有定位(没有使用Positioned)或部分定位的子组件。
  部分定位:指在某一个轴上定位:left、right为横轴,top、bottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。
  Alignment(1,1) 右下角 。Alignment(0,0)居中 
2. textDirection:
  和Row、Wrap的textDirection功能一样,用于确定alignment对齐的参考系
  TextDirection.ltr:从左往右。alignment的start代表左,end代表右。
  TextDirection.rtl:从右往左。alignment的start代表右,end代表左。
3. fit:
  用于确定完全没有定位的子组件如何去适应Stack的大小。
  StackFit.loose:使用子组件的大小。
  StackFit.expand:扩伸到Stack的大小。
4. overflow:
  决定如何显示超出Stack显示空间的子组件;
  Overflow.clip:超出部分会被剪裁(隐藏)。
  Overflow.visible: 不会剪裁。
  新版本为clipBehavior:Clip.hardEdge 表示直接剪裁,不应用抗锯齿。

示例

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    var stack = new Stack(
      alignment: const Alignment(0.6, 0.6),
      children: [
        new CircleAvatar(
          backgroundImage: new AssetImage('images/pic.jpg'),
          radius: 100.0,
        ),
        new Container(
          decoration: new BoxDecoration(
            color: Colors.black45,
          ),
          child: new Text(
            'Mia B',
            style: new TextStyle(
              fontSize: 20.0,
              fontWeight: FontWeight.bold,
              color: Colors.white,
            ),
          ),
        ),
      ],
    );
    // ...
  }
}
  1. Positioned
const Positioned({
  Key key,
  // 距离Stack左、上、右、下四边的距离。
  this.left, 
  this.top,
  this.right,
  this.bottom,
  // 指定宽、度,用于配合left、top 、right、 bottom来定位组件
  // 在水平方向时,只能指定left、right、width三个属性中的两个,如指定left和width后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理。
  this.width,
  this.height,
  //
  @required Widget child,
})

示例

// 通过ConstrainedBox来确保Stack占满屏幕
ConstrainedBox(
  constraints: BoxConstraints.expand(),
  child: Stack(
    alignment:Alignment.center , // 指定未定位或部分定位widget的对齐方式
    children: <Widget>[
      // 居中显示
      Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
        color: Colors.red,
      ),
      // 水平方向距左18,垂直方向居中
      Positioned(
        left: 18.0,
        child: Text("I am Jack"),
      ),
      // 垂直方向距上18,水平方向居中
      Positioned(
        top: 18.0,
        child: Text("Your friend"),
      )        
    ],
  ),
);
给上例中的Stack指定一个fit属性,然后将三个子文本组件的顺序调整一下:

Stack(
  alignment:Alignment.center ,
  fit: StackFit.expand, // 未定位widget占满Stack整个空间
  children: <Widget>[
    Positioned(
      left: 18.0,
      child: Text("I am Jack"),
    ),
    // 完全没有定位,所以fit起作用
    Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
      color: Colors.red,
    ),
    Positioned(
      top: 18.0,
      child: Text("Your friend"),
    )
  ],
),
  1. Align

可以调整子组件在父元素中的位置,并且可以根据子组件的宽高来确定自身的的宽高。

Align({
  Key key,
  this.alignment = Alignment.center,
  this.widthFactor,
  this.heightFactor,
  Widget child,
})

说明:
1. alignment :
  子组件在父组件中的起始位置(AlignmentGeometry类型)。AlignmentGeometry抽象类有两个常用的子类:Alignment和 FractionalOffset(优先,因为它的坐标原点和布局系统相同,能更容易算出实际偏移)。
2. widthFactor和heightFactor
  用于确定Align组件本身的宽高,两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是Align 组件的宽高。如果值为null,则组件的宽高将会占用尽可能多的空间。
Align和Positioned(都可用于指定子元素相对于父元素的偏移)的区别:
  1. 定位参考系统不同;
    Stack/Positioned定位的的参考系可以是父容器矩形的四个顶点。
    Align是通过alignment(不同的alignment对应不同原点)来确定坐标原点,最终的偏移是需要通过alignment的转换公式来计算。
  2. Stack有多个子元素(可堆叠),而Align只能有一个子元素。

示例

Container(
  height: 120.0,
  width: 120.0,
  color: Colors.blue[50],
  child: Stack(
    children: <Widget>[
      Align(
        alignment: Alignment.topRight,
        child: Text('hello'),
      ),
      Align(
        alignment: Alignment(1,-0.5),
        child: Text('hello'),
      ),
    ],
  ),
)

示例2

Container(
  height: 120.0,
  width: 120.0,
  color: Colors.blue[50],
  child: Align(
    alignment: Alignment.topRight,
    child: FlutterLogo(  // Flutter SDK提供的一个组件
      size: 60,
    ),
  ),
)
在上例中,显式指定了Container的宽、高都为120。如果不显式指定宽高,而通过同时指定widthFactor和heightFactor 为2也是可以达到同样的效果:
Align(
  widthFactor: 2,
  heightFactor: 2,
  alignment: Alignment.topRight,
  child: FlutterLogo(
    size: 60,
  ),
),
因为FlutterLogo的宽高为60,则Align的最终宽高都为2*60=120。
运行结果
  1. Center组件(继承自Align)

比Align只少了一个alignment参数(固定为Alignment.center)。

示例

...// 省略无关代码
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Center(
    child: Text("xxx"),
  ),
),
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Center(
    widthFactor: 1,
    heightFactor: 1,
    child: Text("xxx"),
  ),
)
运行结果

Alignment(继承自AlignmentGeometry)

Alignment(this.x, this.y) 表示矩形内的一个点,x、y属性分别表示在水平和垂直方向的偏移。

以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0) ,x、y的值从-1到1分别代表矩形左边到右边的距离和顶部到底边的距离。
为了方便使用,矩形的原点、四个顶点在Alignment类中都已经定义为了静态常量。

子元素的具体偏移坐标为:(Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2) 

示例

 Align(
  widthFactor: 2,
  heightFactor: 2,
  alignment: Alignment(2,0.0), // 将Alignment(2,0.0)带入上述坐标转换公式,可以得到FlutterLogo的实际偏移坐标为(90,30)
  child: FlutterLogo(
    size: 60,
  ),
)
运行结果

FractionalOffset(继承自 Alignment)

FractionalOffset(和Alignment唯一的区别就是坐标原点不同)以左上角为原点,和布局系统是一致的。

子元素的具体偏移坐标为: (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)

Container(
  height: 120.0,
  width: 120.0,
  color: Colors.blue[50],
  child: Align(
    alignment: FractionalOffset(0.2, 0.6),
    child: FlutterLogo(
      size: 60,
    ),
  ),
)
运行结果

5. LayoutBuilder、AfterLayout

  1. LayoutBuilder

在布局过程中拿到父组件传递的约束信息动态构建布局。

使用场景:
  1.  实现响应式布局(根据设备尺寸)。
  2.  在遇到布局问题时可以高效排查问题。

如果是Sliver布局,可以使用SliverLayoutBuiler

示例(响应式的Column组件)

当前可用的宽度小于200时,将子组件显示为一列,否则显示为两列。
class ResponsiveColumn extends StatelessWidget {
  const ResponsiveColumn({Key? key, required this.children}) : super(key: key);
  final List<Widget> children;
  @override
  Widget build(BuildContext context) {
    // 通过 LayoutBuilder 拿到父组件传递的约束,然后判断 maxWidth 是否小于200
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        if (constraints.maxWidth < 200) {
          // 最大宽度小于200,显示单列
          return Column(children: children, mainAxisSize: MainAxisSize.min);
        } else {
          // 大于200,显示双列
          var _children = <Widget>[];
          for (var i = 0; i < children.length; i += 2) {
            if (i + 1 < children.length) {
              _children.add(Row(
                children: [children[i], children[i + 1]],
                mainAxisSize: MainAxisSize.min,
              ));
            } else {
              _children.add(children[i]);
            }
          }
          return Column(children: _children, mainAxisSize: MainAxisSize.min);
        }
      },
    );
  }
}
class LayoutBuilderRoute extends StatelessWidget {
  const LayoutBuilderRoute({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    var _children = List.filled(6, Text("A"));
    // Column在本示例中在水平方向的最大宽度为屏幕的宽度
    return Column(
      children: [
        // 限制宽度为190,小于 200
        SizedBox(width: 190, child: ResponsiveColumn(children: _children)),
        ResponsiveColumn(children: _children),
        // 打印flutter: Text("xx"): BoxConstraints(0.0<=w<=428.0, 0.0<=h<=823.0)
        LayoutLogPrint(child:Text("xx"))  // 打印约束信息
      ],
    );
  }
}
// 封装一个能打印父组件传递给子组件约束的组件
class LayoutLogPrint<T> extends StatelessWidget {
  const LayoutLogPrint({
    Key? key,
    this.tag,
    required this.child,
  }) : super(key: key);
  final Widget child;
  final T? tag; //指定日志tag
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (_, constraints) {
      // assert在编译release版本时会被去除
      assert(() {
        print('${tag ?? key ?? child}: $constraints');
        return true;
      }());
      return child;
    });
  }
}
  1. AfterLayout(网上开发者自定义的开源组件)

在子组件布局完成后执行一个回调(将RenderObject对象作为参数传递)。

  1. 获取组件大小和相对于屏幕的坐标
Flutter是响应式UI框架(Widget被定义为不可变,没有提供任何操作组件的API),获取某个组件的大小和位置会很困难(命令式UI框架中不存在这个问题)。

只有当布局完成时,每个组件的大小和位置才能确定。
事件分发是在布局完成之后的:
Builder(
  builder: (context) {
    return GestureDetector(
      child: Text('flutter@wendux'),
      onTap: () => print(context.size), // 打印 text 的大小
    );
  },
),
context.size:可以获取当前上下文RenderObject的大小,对于Builder、StatelessWidget、StatefulWidget这样没有对应RenderObject的组件,获取的是子代中第一个拥有RenderObject组件的RenderObject对象。

想要在布局完成后就回调,使用AfterLayout:
AfterLayout(
  callback: (RenderAfterLayout ral) {
    print(ral.size); // 子组件的大小
    print(ral.offset);// 子组件在屏幕中坐标
  },
  child: Text('flutter@wendux'),
),
/*
输出:
flutter: Size(105.0, 17.0)
flutter: Offset(42.5, 290.0)
*/
  1. 获取组件相对于某个父组件的坐标
RenderAfterLayout类继承自RenderBox,RenderBox有一个localToGlobal方法,它可以将坐标转化为相对与指定的祖先节点的坐标。

示例

Builder(builder: (context) {
  return Container(
    color: Colors.grey.shade200,
    alignment: Alignment.center,
    width: 100,
    height: 100,
    child: AfterLayout(
      callback: (RenderAfterLayout ral) {
        Offset offset = ral.localToGlobal(
          Offset.zero,
          // 传一个父级元素
          ancestor: context.findRenderObject(),
        );
        // Text('A') 在 父 Container 中的坐标
        print('A 在 Container 中占用的空间范围为:${offset & ral.size}');
      },
      child: Text('A'),
    ),
  );
}),

示例2

class AfterLayoutRoute extends StatefulWidget {
  const AfterLayoutRoute({Key? key}) : super(key: key);
  @override
  _AfterLayoutRouteState createState() => _AfterLayoutRouteState();
}
class _AfterLayoutRouteState extends State<AfterLayoutRoute> {
  String _text = 'flutter 实战 ';
  Size _size = Size.zero;
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Builder(
            builder: (context) {
              return GestureDetector(
                child: Text(
                  'Text1: 点我获取我的大小',
                  textAlign: TextAlign.center,
                  style: TextStyle(color: Colors.blue),
                ),
                onTap: () => print('Text1: ${context.size}'),
              );
            },
          ),
        ),
        AfterLayout(
          callback: (RenderAfterLayout ral) {
            print('Text2: ${ral.size}, ${ral.offset}');
          },
          child: Text('Text2:flutter@wendux'),
        ),
        Builder(builder: (context) {
          return Container(
            color: Colors.grey.shade200,
            alignment: Alignment.center,
            width: 100,
            height: 100,
            child: AfterLayout(
              callback: (RenderAfterLayout ral) {
                Offset offset = ral.localToGlobal(
                  Offset.zero,
                  ancestor: context.findRenderObject(),
                );
                print('A 在 Container 中占用的空间范围为:${offset & ral.size}');
              },
              child: Text('A'),
            ),
          );
        }),
        Divider(),
        AfterLayout(
          child: Text(_text), 
          callback: (RenderAfterLayout value) {
            setState(() {
              // 更新尺寸信息
              _size = value.size;
            });
          },
        ),
        // 显示上面 Text 的尺寸
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 8.0),
          child: Text(
            'Text size: $_size ',
            style: TextStyle(color: Colors.blue),
          ),
        ),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _text += 'flutter 实战 ';
            });
          },
          child: Text('追加字符串'),
        ),
      ],
    );
  }
}

点击 Text1 就可以在日志面板看到它的大小。
点击 “追加字符串” 按钮,字符串大小变化后,屏幕上上也会显示。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345