在介绍Row和Colum时,如果子widget超出屏幕范围,则会报溢出错误,如:

  1. Row(
  2. children: <Widget>[
  3. Text("xxx"*100)
  4. ],
  5. );

运行效果如图所示:
流式布局(Wrap、Flow) - 图1
可以看到,右边溢出部分报错。这是因为Row默认只有一行,字体太长没有折行导致超出屏幕。
我们把超出屏幕显示范围会自动折行的布局称为流式布局。Flutter中通过Wrap和Flow来支持流式布局,将上例中的Row换成Wrap后溢出部分则会自动折行,下面我们分别介绍Wrap和Flow.

Wrap

Wrap的定义:

  1. (new) Wrap Wrap({Key key,
  2. Axis direction = Axis.horizontal,
  3. WrapAlignment alignment = WrapAlignment.start,
  4. double spacing = 0.0,
  5. WrapAlignment runAlignment = WrapAlignment.start,
  6. double runSpacing = 0.0,
  7. WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start,
  8. TextDirection textDirection,
  9. VerticalDirection verticalDirection = VerticalDirection.down,
  10. List<Widget> children = const <Widget>[]})

我们可以看到Wrap的很多属性在Flex(包括Row和Column))中也有,如:

  • direction、
  • crossAxisAlignment、
  • textDirection、
  • verticalDirection等,这些参数意义是相同的

读者可以认为Wrap和Flex(包括Row和Column)除了超出显示范围后Wrap会折行外,其它行为基本相同。下面我们看一下Wrap特有的几个属性:

  • spacing:主轴方向子widget的间距
  • alignment:主轴上的对齐方式
  • runSpacing:纵轴方向的间距
  • runAlignment:纵轴方向的对齐方式

下面看一个示例子:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: false,
  8. title: 'Warp 流体布局',
  9. theme: ThemeData(
  10. primarySwatch: Colors.blue,
  11. ),
  12. home: Scaffold(
  13. appBar: AppBar(title: Text("Warp 流体布局")),
  14. body: FlexLayoutTestRoute()));
  15. }
  16. }
  17. class FlexLayoutTestRoute extends StatelessWidget {
  18. @override
  19. Widget build(BuildContext context) {
  20. return Wrap(
  21. spacing: 8.0, // 主轴(水平)方向间距
  22. runSpacing: 4.0, // 纵轴(垂直)方向间距
  23. alignment: WrapAlignment.center, //沿主轴方向居中
  24. children: <Widget>[
  25. new Chip(
  26. avatar:
  27. new CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
  28. label: new Text('Hamilton'),
  29. ),
  30. new Chip(
  31. avatar:
  32. new CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
  33. label: new Text('Lafayette'),
  34. ),
  35. new Chip(
  36. avatar:
  37. new CircleAvatar(backgroundColor: Colors.blue, child: Text('H')),
  38. label: new Text('Mulligan'),
  39. ),
  40. new Chip(
  41. avatar:
  42. new CircleAvatar(backgroundColor: Colors.blue, child: Text('J')),
  43. label: new Text('Laurens'),
  44. ),
  45. ],
  46. );
  47. }
  48. }

流式布局(Wrap、Flow) - 图2

Chip参数属性

(new) Chip Chip({Key key,
 Widget avatar,
 Widget label,
 TextStyle labelStyle,
 EdgeInsetsGeometry labelPadding,
 Widget deleteIcon,
 void Function() onDeleted,
 Color deleteIconColor,
 String deleteButtonTooltipMessage,
 ShapeBorder shape,
 Clip clipBehavior = Clip.none,
 FocusNode focusNode,
 bool autofocus = false,
 Color backgroundColor,
 EdgeInsetsGeometry padding,
 MaterialTapTargetSize materialTapTargetSize,
 double elevation,
 Color shadowColor})

CircleAvatar参数属性

CircleAvatar CircleAvatar({Key key, 
Widget child, 
Color backgroundColor, 
ImageProvider<dynamic> backgroundImage,
Color foregroundColor,
double radius,
double minRadius,
double maxRadius})

Flow

我们一般很少会使用Flow,因为其过于复杂,需要自己实现子widget的位置转换,在很多场景下首先要考虑的是Wrap是否满足需求。Flow主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景。Flow有如下优点:

Flow参数属性
(new) Flow Flow({Key key, 
FlowDelegate delegate, 
List<Widget> children = const <Widget>[]})
  • 性能好Flow是一个对子组件尺寸以及位置调整非常高效的控件,Flow用转换矩阵在对子组件进行位置调整的时候进行了优化:
    • 在Flow定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate中重写paintChildren()方法
    • 在paintChildren()方法中调用context.paintChild进行重绘,而context.paintChild在重绘时使用了转换矩阵,并没有实际调整组件位置。
  • 灵活由于我们需要自己实现delegate属性中FlowDelegate的paintChildren()方法,所以我们需要自己计算每一个组件的位置,因此,可以自定义布局策略。

缺点:

  • 使用复杂。
  • 不能自适应子组件大小,必须通过指定父容器大小或实现FlowDelegate的getSize返回固定大小。

我们对六个色块进行自定义流式布局:

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'Flow自定义流式布局',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Scaffold(
            appBar: AppBar(title: Text("Flow自定义流式布局")),
            body: FlexLayoutTestRoute()));
  }
}

class FlexLayoutTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Flow(
      delegate: TestFlowDelegate(margin: EdgeInsets.all(1.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,
        ),
      ],
    );
  }
}

//继承FlowDelegate实现TestFlowDelegate,重绘
class TestFlowDelegate extends FlowDelegate {
  // EdgeInsets margin = EdgeInsets.zero;
  EdgeInsets margin;
  TestFlowDelegate({this.margin = EdgeInsets.zero});

  @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;
  }
}

流式布局(Wrap、Flow) - 图3
可以看到我们主要的任务就是实现paintChildren,它的主要任务是确定每个子widget位置。由于Flow不能自适应子widget的大小,我们通过在getSize返回一个固定大小来指定Flow的大小。