Flutter 组件 | CustomSingleChildLayout 通用单子布局

一、认识 CustomSingleChildLayout 组件

1. CustomSingleChildLayout 组件介绍

它可容纳一个子组件,并指定代理类对子组件进行 排布 。可以说它是最灵活的单子布局,我们通过代理类可以获取 父区域的信息限制子组件的区域 。一些单子布局的 疑难杂症 都可以通过 CustomSingleChildLayout 药到病除。

2. 测试初始代码

如下,这里父容器使用 300*200的灰色盒子 ,我们将在这个区域对 子组件进行排布

1import 'package:flutter/material.dart';
2
3void main() => runApp(MyApp());
4
5class MyApp extends StatelessWidget {
6  @override
7  Widget build(BuildContext context) {
8    return MaterialApp(
9        title: 'Flutter Demo',
10        theme: ThemeData(
11          primarySwatch: Colors.blue,
12        ),
13        home: Scaffold(
14          appBar: AppBar(),
15          body: Center(child: CustomSingleChildLayoutDemo()),
16        ));
17  }
18}
19
20class CustomSingleChildLayoutDemo extends StatelessWidget {
21  @override
22  Widget build(BuildContext context) {
23    return Container(
24      width: 300,
25      height: 200,
26      alignment: Alignment.center,
27      color: Colors.grey.withAlpha(11),
28    );
29  }
30}

3.  CustomSingleChildLayout 简单使用

CustomSingleChildLayout 组件可以说是 上有老,下有小 ,一方面可以获取父区域的限制,另一方面需要排布子组件的位置。它可以设置一个 child,还需要设置一个抽象代理类 SingleChildLayoutDelegate

Flutter 框架中并 没有提供 可用的实现类,所以只能自定义 _TolySingleChildLayoutDelegate

1class CustomSingleChildLayoutDemo extends StatelessWidget {
2  @override
3  Widget build(BuildContext context) {
4    return Container(
5      width: 300,
6      height: 200,
7      alignment: Alignment.center,
8      color: Colors.grey.withAlpha(11),
9      child: CustomSingleChildLayout(
10        delegate: _TolySingleChildLayoutDelegate(),
11        child: Icon(Icons.android_rounded),
12      ),
13    );
14  }
15}

SingleChildLayoutDelegate 抽象类中需要实现 shouldRelayout 决定是否需要重新布局。这和画板中的 shouldRePaint 方法是类似的作用。效果如下,子组件默认摆放在区域左上角。

1class _TolySingleChildLayoutDelegate extends SingleChildLayoutDelegate {
2  @override
3  bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) {
4    return false;
5  }
6}

二、SingleChildLayoutDelegate 获取布局信息

除了抽象方法外,还有三个可以复写的方法,通过这些方法我们可以获取一些重要的布局信息,来帮助我们对子组件进行排布。

1.获取父级约束区域

可以重写 getConstraintsForChild 方法,获取父区域的约束情况。如果需要对子组件区域进行重新约束,也可以返回新的约束。

1---->[_TolySingleChildLayoutDelegate]----
2@override
3BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
4  print('----getConstraintsForChild:----constraints:$constraints----');
5  return constraints;
6}

2.设置 CustomSingleChildLayout 尺寸

可以重写 getSize 方法设置 CustomSingleChildLayout 的尺寸,从源码中可以看出,默认 super.getSize(constraints) 是父区域约束的最大区域。如下,将 Size 设置为 Size(150,100) ,可以看出 CustomSingleChildLayout 本身的区域尺寸发生了变化。

1---->[_TolySingleChildLayoutDelegate]----
2@override
3Size getSize(BoxConstraints constraints) {
4  Size size = super.getSize(constraints);
5  print('----getSize:----constraints:$constraints---$size-');
6  return Size(150,100);
7}

3.设置子组件的偏移

getPositionForChild 是使用时最为重要的一个方法,我们可以在这个方法中对子组件的位置进行偏移。这个方法中,可以获取 CustomSingleChildLayout 的尺寸和 子组件的尺寸,默认处理是零偏移。

1---->[_TolySingleChildLayoutDelegate]----
2@override
3Offset getPositionForChild(Size size, Size childSize) {
4  print('----size:$size----childSize:$childSize----');
5  return super.getPositionForChild(size, childSize);
6}

如下,可以很简单地通过尺寸计算,让子组件的中心位于 CustomSingleChildLayout 区域的左上角。

1---->[_TolySingleChildLayoutDelegate]----
2@override
3Offset getPositionForChild(Size size, Size childSize) {
4  return Offset(-childSize.width/2, -childSize.height/2);
5}

三、CustomSingleChildLayout能干嘛?

从上面可以看出,使用 CustomSingleChildLayout 可以获取布局信息,并可以对子组件进行布局。一句话来说 用于排布一个组件。 这样我们可以简单封装一个用于偏移的组件。

1. 控制偏移的代理类

传入一个 Offset对象 控制子组件的偏移。

1class _OffSetDelegate extends SingleChildLayoutDelegate {
2  final Offset offset;
3
4  _OffSetDelegate({this.offset = Offset.zero});
5
6  @override
7  bool shouldRelayout(_OffSetDelegate oldDelegate) =>
8      offset != oldDelegate.offset;
9
10  @override
11  Offset getPositionForChild(Size size, Size childSize) {
12    return offset;
13  }
14}

2. 封装类 OffSetWidget

为了方便使用,封装 OffSetWidget组件 来实现子组件相对于父组件的偏移。

1class OffSetWidget extends StatelessWidget {
2  final Offset offset;
3  final Widget child;
4
5  OffSetWidget({this.offset = Offset.zero, this.child});
6
7  @override
8  Widget build(BuildContext context) {
9    return CustomSingleChildLayout(
10      delegate: _OffSetDelegate(offset: offset),
11      child: child,
12    );
13  }
14}

3. OffSetWidget 使用

这样就可以让子组件在父组件中发生相对偏移。

简约派代表: "等等...老子看了半天,你给我个简易版的Padding?还花里胡哨的。"

兄台莫急,且往下看。

1class OffSetWidgetDemo extends StatelessWidget {
2  @override
3  Widget build(BuildContext context) {
4    return Container(
5        width: 300,
6        height: 100,
7        alignment: Alignment.topRight,
8        color: Colors.grey.withAlpha(11),
9        child: OffSetWidget(
10          offset: Offset(2020),
11          child: Icon(Icons.android, size: 30,color: Colors.green,),
12        ));
13  }
14}

这里最大的优势是: Offset支持负偏移

1OffSetWidget(
2   offsetOffset(-20, 20),
3   childIcon(Icons.androidsize: 30,colorColors.green,),
4));

4. 方向版

通过动态计算,可以设置方向进行偏移,如下:

这就像 Position 的能力,但 Position 有必须和 Stack 连用。而 OffSetWidget 随意使用

1class OffSetWidget extends StatelessWidget {
2  final Offset offset;
3  final Widget child;
4  final Direction direction;
5
6  OffSetWidget({this.offset = Offset.zero,
7      this.child,
8      this.direction = Direction.topLeft});
9
10  @override
11  Widget build(BuildContext context) {
12    return CustomSingleChildLayout(
13      delegate: _OffSetDelegate(offset: offset, direction: direction),
14      child: child,
15    );
16  }
17}
18
19enum Direction { topLeft, topRight, bottomLeft, bottomRight }
20
21class _OffSetDelegate extends SingleChildLayoutDelegate {
22  final Offset offset;
23  final Direction direction;
24
25  _OffSetDelegate(
26      {this.offset = Offset.zero, this.direction = Direction.topLeft});
27
28  @override
29  bool shouldRelayout(_OffSetDelegate oldDelegate) =>
30      offset != oldDelegate.offset|| direction!=oldDelegate.direction;
31
32  @override
33  Offset getPositionForChild(Size size, Size childSize) {
34    var w = size.width;
35    var h = size.height;
36    var wc = childSize.width;
37    var hc = childSize.height;
38
39    switch (direction) {
40      case Direction.topLeft:
41        return offset;
42      case Direction.topRight:
43        return offset.translate(w - wc - offset.dx * 20);
44      case Direction.bottomLeft:
45        return offset.translate(0, h - hc - offset.dy * 2);
46      case Direction.bottomRight:
47        return offset.translate(w - wc - offset.dx * 2, h - hc - offset.dy * 2);
48    }
49    return offset;
50  }
51}

CustomSingleChildLayout 组件的用法还是比较简单的。上面代码只是简单演示一下使用方式,也许并不是太实用。 Positioned组件 可以实现定位, SizedOverflowBox组件 可以实现溢出。如下,想在色块的右侧引出一条线,可以使用 CustomSingleChildLayout 让子组件线进行偏移,虽然通过 Row 也可以实现,但是通过 CustomSingleChildLayout 可以得到更多尺寸信息,处理起来更加灵活。

当你遇到对某一个组件约束或定位困难时,或想要获取相关尺寸信息才能进行布局,那么 CustomSingleChildLayout 也许可以帮到你。本篇就这样,谢谢观看 ~