iOS- Flutter 布局组件-LayoutBuilder&AfterLayout

146 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情

通过LayoutBuilder,可以在布局过程中拿到父组件的约束信息,然后我们可以根据约束信息动态的构建不同的布局,类似于iOS中的UICollectionViewLayout中自定义约束一样。

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),
        LayoutLogPrint(child:Text("xx")) // 下面介绍
      ],
    );
  }
}

LayoutBuilder使用很简单,用起来非常的灵活,很多场景可能用到这个。

  • 可以使用LayoutBuilder来根据设备的尺寸来实现响应布局。
  • LayoutBuilder可以帮助我们高效排查问题,比如在遇到布局问题或者想要调试组件树中的某个节点布局约束时。

举例:封装一个能打印父组件传递给子组件约束的组件。

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

使用LayoutLogPrint 组件树中任意位置的约束信息:

LayoutLogPrint(child:Text("xx"))
//控制台输出
flutter: Text("xx"): BoxConstraints(0.0<=w<=428.0, 0.0<=h<=823.0)

LayoutBuilder还有很多的使用场景,在大多需要灵活布局的场景都可以使用。

AfterLayout

这是一个在布局完成后立刻回调的封装组件,用于在日常中组件布局完成后需要获取组件相对于屏幕(或父组件)的位置和大小。

获取组件大小和相对于屏幕的坐标

通常我们想获取组件大小的时候是这样的:

Builder(
  builder: (context) {
    return GestureDetector(
      child: Text('flutter@wendux'),
      onTap: () => print(context.size), //通过手势点击事件打印 text 的大小,此时布局已完成。
    );
  },
),

这样有两个问题:

  • 1、需要用户手动点击触发
  • 2、获取的时机较晚,大多时候是希望组件布局绘制完成后立刻能获取到组件的大小和位置。

为此,参考wendux封装的AfterLayout,它可以在子组件布局完成执行一个回调,并将RenderObject对象作为参数传递。

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)

获取组件相对于父组件的坐标

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(),
        );
        print('A 在 Container 中占用的空间范围为:${offset & ral.size}');
      },
      child: Text('A'),
    ),
  );
}),

AfterLayout 封装实例

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'),
        ),
        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('追加字符串'),
        ),
      ],
    );
  }
}

总结

通过观察LayoutBuilder的实例,可以发现Flutter的build和layout是可以交错执行,并不是严格按照顺序执行的,在一些场景上是可以先build在layout的。