阅读 547

Flutter实战之基本布局篇

布局

The core of Flutter’s layout mechanism is widgets. In Flutter, almost everything is a widget—even layout models are widgets.

1、Column & Row & Flex

将Column、Row、Flex放在一小节是因为不同之处只是对子Widget布局方式不同。Column垂直布局子组件,Row是水平布局子组件,Flex根据direction设置Axis.horizontal或是Axis.vertical。

Column(
  children: <Widget>[
    Text("text"),
    Text("text"),
    Text("text"),
  ],
);
Row(
  children: <Widget>[
    Text("text"),
    Text("text"),
    Text("text"),
  ],
);
Flex(
  direction: Axis.vertical,
  children: <Widget>[
    Text("text"),
    Text("text"),
    Text("text"),
  ],
);
Flex(
  direction: Axis.horizontal,
  children: <Widget>[
    Text("text"),
    Text("text"),
    Text("text"),
  ],
);
复制代码

另外组件都不支持滑动,一旦子组件较多超出可显示范围,在Debug模式下会出现warning提示错误。通常情况下一般是使用ListView而非Column去显示列表,当然可以在Column、Row、Flex外层嵌套SingleChildScrollView使其支持滑动。

Tips

  • Column和Row不支持对Padding,margin以及宽高的设置,一般外层嵌套Container或是Padding组件实现约束布局宽度和间距。
  • Expanded组件也是在Column和Row中经常使用,通过它对子组件进行权重大小划分。类似于Android的XML布局文件中weight属性,Expanded中有flex参数用于设置比重比例,数值越大比重越大。
  • MainAxisAlignment属性是与当前组件方向一致的轴,例如Row的主轴是水平方向,当设置MainAxisAlignment对于Row来说是在水平方向。支持的设置参数有:start、end、center、spaceBetween、spaceAround、spaceEvenly。详见文档
  • CrossAxisAlignment属性是与当前组件方向成垂直的轴,例如Row的交叉轴就是垂直方向,当设置MainAxisAlignment对于Row来说是在垂直方向。支持的设置参数有:start、end、center、stretch、baseline详见文档
  • MainAxisSize属性可设置:max、min,默认为max。类似于Android中math_content和wrap_content,当为min时大小根据子组件约束大小如果为max时大小根据自身约束大小。
  • Column和Row继承自Flex。个人理解在一些特定UI布局中可以使用Flex,例如需要切换垂直和水平布局的时候选用。

2、Expanded & Flexible

这节是弹性布局,上节已经有提到Column和Row使用子组件时通过嵌套弹性布局Expanded重来分配布局空间,同时也说过Flutter中弹性布局类似于Android中XML的weight属性。可知Expanded组件继承自Flexible,唯独不同的是Flexible的fit属性值默认FlexFit.loose并且可设置,Expanded的fit属性值是FlexFit.tight并且不能设置。

Flexible

class Flexible extends ParentDataWidget<Flex> {
  /// Creates a widget that controls how a child of a [Row], [Column], or [Flex]
  /// flexes.
  const Flexible({
    Key key,
    this.flex = 1,
    this.fit = FlexFit.loose,
    @required Widget child,
  }) : super(key: key, child: child);
复制代码

Expanded

const Expanded({
  Key key,
  int flex = 1,
  @required Widget child,
}) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
复制代码

FlexFit属性:tight、loose。tight会让子组件强制填充可用最大布局空间;loose会让子组件尽可能填充可用布局空间。字面上的意思tight比loose更强烈,如下实例设置tight比起loose会占用更多布局空间,而loose会根据嵌套的组件而定是否根据比重填充更多空间。

Column(
        children: <Widget>[
          Flexible(
            flex: 2,
            child: Container(
              color: Colors.blue,
              child: Center(
                child: Text(
                  "Flexible 2",
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
          Flexible(
            flex: 1,
            fit: FlexFit.tight,
            child: Container(
              child: Text(
                "Flexible 1 FlexFit.tight",
              ),
            ),
          ),
          Flexible(
            flex: 1,
            fit: FlexFit.tight,
            child: Container(
              child: Text(
                "Flexible 1 FlexFit.tight",
              ),
            ),
          ),
          Center(
            child: Text(
              "NO Flexible",
            ),
          ),
        ],
      );
复制代码

Tips

  • 例如Flexible内部嵌套了Center组件即便FlexFit是loose组件填充空间还是会按照比重获取对应比例的大小。而没有内部没有嵌套Center组件的Flexible则没有按照比重填充空间而是根据自身组件实际大小填充。详见文档
  • Spacer是一个空组件,主要用于填充空白区域,默认flex为1。
  • Flexible可以解决在Column和Row设置子组件Text的文本超出范围警告问题。原先以为使用Text属性overflow: TextOverflow.ellipsis可以解决但实际情况不可行。其实只需要在Text外层嵌套Flexible并能解决。

3、Stack

A widget that positions its children relative to the edges of its box.

Stack用于创建重叠样式的布局,子组件可以在Stack中叠加显示。同时可以使用Positioned组件对子组件做定位操作,使子组件根据Positioned定义的位置参数显示在Stack对应位置。或许会想到Stack和RelativeLayout是否有点相似,但注意的是Stack并没有RelativeLayout的blow和above的方法让组件之间有定位关系,它的定位关系只有子组件和Stack的关系。

Stack(
        fit: StackFit.expand,
        children: <Widget>[
            Text("1"),
            Text("2"),
            Text("3"),
            ],
     )
复制代码

添加Positioned让子组件在Stack中定位显示

Stack(
        fit: StackFit.expand,
        children: <Widget>[
            Positioned(
            top: 300,
            left: 40,
            child: Text("1"),
            ),
            Text("2"),
            Text("3"),
        ],
    )
复制代码

Tips

  • Stack的fit用于设置Stack空间大小,默认设置为loose,可选择expand、passthrough。可以认为Stack默认大小是根据它未定位的子组件大小决定。若选择expand则填充可用最大布局空间,passthrough则是根据父组件的约束而定。
  • Stack的子组件重叠若之后添加组件会覆盖之前组件,例如后添加组件有背景色等则之前添加的组件会不可见。
  • Stack中未做定位处理的子组件默认起始位置在top-left。
  • 若子组件定位位置超出Stack的大小,则子组件会被切割显示。

4、CustomSingleChildLayout

自定义单个组件布局,可通过自定义delegate控制父级大小和子组件约束。delegate需要我们自行实现,通过继承SingleChildLayoutDelegate实现我们需要的约束条件。

Container(
        color: Colors.blue,
        child: CustomSingleChildLayout(
            delegate: CustomSingleDelegate(),
            child: Center(
                child: Text("llll"),
            ),
        ),
    )
复制代码

这里实现了一个简单delegate。getPositionForChild方法可以获取父级和子组件的大小设置偏移量;getSize方法获取父级组件大小并重新设置约束;getConstraintsForChild获取子组件约束并重设约束;

class CustomSingleDelegate extends SingleChildLayoutDelegate {
  @override
  Offset getPositionForChild(Size size, Size childSize) {
    // return super.getPositionForChild(size, childSize);
    // 偏移量
    return Offset(0, 20);
  }
  @override
  Size getSize(BoxConstraints constraints) {
    //获取到父级组件约束
    //    return super.getSize(constraints);
    return Size(200,200);
  }

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    //获取子组件约束
    //    return super.getConstraintsForChild(constraints);
    return constraints.enforce(BoxConstraints(maxHeight: 100,maxWidth: 100));
  }

  @override
  bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) {
    return false;
  }

  CustomSingleDelegate();
}
复制代码

如下图所示为默认使用CustomSingleDelegate和修改CustomSingleDelegate各个方法之后的对比效果。可以看出通过CustomSingleDelegate可以设置子组件和父级组件各个约束参数达到预期想要的显示效果。

Tips

  • CustomSingleChildLayout为开发者提供了一种自定义开发组件约束的能力,通过父级自身去控制自身约束大小而不像Stack依赖于未定位子组件的约束做为约束条件。
  • 同时CustomSingleChildLayout中子组件若在外层嵌套Center后它的约束好像就不跟随父级,比如父级约束Size(200,200),设置子组件偏移量Offset(0, 300)即使超出父级约束还是能在布局居中显示。

5、CustomMultiChildLayout

CustomMultiChildLayout和Stack都继承自MultiChildRenderObjectWidget。但可以说CustomMultiChildLayout是Stack的高级使用或者说是CustomSingleChildLayout的进阶版,从控制一个子组件约束变为对多个子组件约束当然复杂度也就升级了。CustomMultiChildLayout使用MultiChildLayoutDelegate控制约束,children创建LayoutId的子组件并设置组件id。同样的MultiChildLayoutDelegate需要自行实现。

Container(
    color: Colors.blue,
    child: CustomMultiChildLayout(
      delegate: CustomMultiChildDelegate(),
      children: <Widget>[
        LayoutId(
          id: CustomMultiChildDelegate.leftChild,
          child: Container(color: Colors.green,child: Text("left"),),
        ),
        LayoutId(
          id: CustomMultiChildDelegate.rightChild,
          child: Container(color: Colors.red,child: Text("right"),),
        )
      ],
    ),
  )
复制代码

自定义一个CustomMultiChildDelegate继承MultiChildLayoutDelegate。在这里进行父级大小设置和子组件约束设置。performLayout方法是布局设置主入口,在这里去设置layoutChild约束子组件布局和positionChild定位子组件位置;getSize方法设置父级组件大小;layoutChild获取到子组件约束设置;positionChild获取子组件定位设置;

class CustomMultiChildDelegate extends MultiChildLayoutDelegate {
  static const String leftChild = "left";
  static const String rightChild = "right";

  @override
  Size layoutChild(Object childId, BoxConstraints constraints) {
    //获取子组件约束
    return super.layoutChild(childId, constraints);
  }

  @override
  void positionChild(Object childId, Offset offset) {
    // 设置子组件偏移量
    super.positionChild(childId, offset);
  }

  @override
  Size getSize(BoxConstraints constraints) {
    //父级大小
    //return super.getSize(constraints);
    constraints = BoxConstraints(maxWidth: 100,maxHeight: 100);
    return Size(100, 100);
  }

  @override
  void performLayout(Size size) {
     //布局设置入口,layoutChild和positionChild在此调用
    layoutChild(leftChild, BoxConstraints(minHeight: 200, minWidth: 200));
    layoutChild(rightChild, BoxConstraints(maxHeight: 60, maxWidth: 50));
    positionChild(leftChild, Offset(size.width + 30, 0));
    positionChild(leftChild, Offset(size.width - 30, 0));
    positionChild(rightChild, Offset(0, 0));
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {
    return false;
  }

  CustomMultiChildDelegate();
}
复制代码

*CustomMultiChildLayout中当子组件约束超出父级大小的时候如何显示。在之前CustomSingleChildLayout中有提到过子组件嵌套Center会导致子组件在父级约束大小外可正常显示。下面就看看但CustomMultiChildLayout的子组件超出父级约束后,外部组件是否也可以正常显示。

下面实验使用Row作为根布局,添加CustomMultiChildLayout让rightChild超出父级,然后在Row水平位置再添加Text组件,查看Text是否可以正常显示以及默认显示的位置。

//水平方向布局中增加自定义约束布局和另外的Text组件
Row(
    children: <Widget>[
      Container(
        color: Colors.blue,
        child: CustomMultiChildLayout(
          delegate: CustomMultiChildDelegate(),
          children: <Widget>[
            LayoutId(
              id: CustomMultiChildDelegate.leftChild,
              child: Container(color: Colors.green,child: Text("left"),),
            ),
            LayoutId(
              id: CustomMultiChildDelegate.rightChild,
              child: Container(color: Colors.red,child: Text("right" ),),
            )
          ],
        ),
      ),
      //
      Text("ooooo")
    ],
  )
复制代码

可以看到Text能够正常显示并且没有被CustomMultiChildLayout的rightChild覆盖,位置显示则定位在CustomMultiChildLayout设置的大小之后。

Tips

  • CustomMultiChildLayout的子组件必须使用LayoutId嵌套以设置Id。
  • layoutChild和positionChild并不会主动调用,务必在performLayout方法中先调用layoutChild约束子组件然后调用positionChild定位子组件。
  • 为子组件设置最大BoxConstraints布局约束时注意max和min的区别,当设置max未设置min时子组件大小是根据当前内容大小填充布局,若设置min后子组件默认填充min设置值大小。
  • 同样CustomMultiChildLayout的子组件约束不限制于父级,当子组件偏移量超出父级组件大小时也可以正常显示。这点在介绍CustomSingleChildLayout时也已经提到过。