从 Android 转向 Flutter 后,受不了没有 ConstraintLayout 的日子,更反感嵌套阴间,网上的优化方法都治标不治本。于是继 2016 年开源 MagicIndicator(9100+ star) 今后,我只能再次发力了。

github.com/hackware199…

欢迎试用并给我反应。

Flutter ConstraintLayout

一个超级强壮的 Stack,运用束缚构建极为灵敏的布局,和 Android 下的 ConstraintLayout 和 iOS 下的 AutoLayout 相似。但代码完成却高效得多,它具有 O(n) 的布局时间杂乱度,无需线性方程求解。

它在功能、灵敏性、开发速度、可维护性方面全面超越传统嵌套写法。它几乎否定了固有特性丈量这种O(2n)的布局算法。

它是一个布局,也是一个更现代化的通用布局结构。

大幅进步 Flutter 的开发体验和效率,并提升运用功能

不管布局有多杂乱,束缚有多深,它一直有媲美单一 Flex 或 Stack 的功能,在面对杂乱的布局时,它能供给更好的功能,更大的灵敏性,更少的代码,以及十分扁平的代码层次结构,大大提升代码的可维护性。对”嵌套阴间“说不。

总归一句话,用了就回不去了。

改善”嵌套阴间“是我开发 Flutter ConstraintLayout 的初衷之一,但我不推重极致地寻求一层嵌套,这是不必要的。因而像链这种特性,Flex 本身现已很好的支撑了,因而 ConstraintLayout 不会积极支撑它。

检查 Flutter Web 在线示例

Flutter ConstraintLayout 有极高的布局功能。它不根据 Cassowary 算法,无需线性方程求解。 任何时候,每一个子元素都只会被 layout 一次,当本身的宽或高被设置为 wrapContent 时,部分子元素或许会核算两次 offset。束缚布局的布局过程包含以下三个步骤:

  1. 束缚核算
  2. 布局
  3. 制作

其中布局和制作的功能几乎与单一 Flex 或 Stack 相当,束缚核算的功能大致为 0.01 毫秒(一般杂乱度的布局,20 个子元素)。只要在束缚变化后才会从头核算束缚。

束缚布局本身能够被恣意嵌套而不带来功能问题,烘托树中的每个子元素都只会被 layout 一次,时间杂乱度为 O(n),而不是 O(2n) 或更糟糕的杂乱度。

更小的 Widget 树带来了更少的 build 耗时和更小的 Element 树。十分扁平的布局结构带来了更小的 RenderObject 树和更少的烘托耗时。大多数人容易忽略的工作是杂乱嵌套导致 build 耗时有时甚至超越烘托耗时。

推荐在顶层运用 ConstraintLayout。关于极点杂乱的布局(1000 个子元素,2000 个束缚),非首帧布局和制作的总耗时在 5 毫秒内(在 Windows 10 调试方式下,发布方式耗时更少),理论上首帧优势会更明显。关于常规杂乱布局(50 个子元素,100 个束缚),帧率可轻松到达 200 fps。

如非必要,尽量相关于 parent 布局,这样能够界说更少的 id,或许运用相对 id。

警告: 为了布局功能的考虑,束缚总是单向的,不允许存在两个子元素相互束缚对方(比方 A 的右边束缚在 B 的左面,而 B 的左面又反过来束缚在 A 的右边)。每一个束缚都应该切当的描述子元素是怎么定位的。虽然束缚只能单向,但你仍然能更好的处理以前双向束缚才干做到的工作,比方链(暂时还未支撑,请结合 Flex 运用)。

特性

  1. 根本束缚
    1. left
      1. toLeft
      2. toCenter(默许偏移量为 0.5,代表中心)
      3. toRight
    2. right
      1. toLeft
      2. toCenter(默许偏移量为 0.5,代表中心)
      3. toRight
    3. top
      1. toTop
      2. toCenter(默许偏移量为 0.5,代表中心)
      3. toBottom
    4. bottom
      1. toTop
      2. toCenter(默许偏移量为 0.5,代表中心)
      3. toBottom
    5. baseline
      1. toTop
      2. toCenter(默许偏移量为 0.5,代表中心)
      3. toBaseline
      4. toBottom
  2. margin and goneMargin(当依靠的元素的可见性为 gone 或许其某一边的实际巨细为 0 时,goneMargin 就会收效,不然 margin 会收效,即使其本身的可见性为 gone)
  3. clickPadding( 快速扩大子元素的点击区域而无需改动子元素的实际巨细。这意味着你能够完全遵照视觉稿来布局,而不必为了考虑点击区域而做额定的工作,这会提升必定的开发效率。这也意味着子元素之间能够在不增加嵌套的情况下共享点击区域,有时或许需求结合 e-index 运用)
  4. 可见性控制
    1. visible
    2. invisible
    3. gone(有时更好的做法是运用条件表达式来防止创立子元素,运用 gone 的好处是能够保存状态)
  5. 完善的束缚缺失、非法、冗余提示
  6. 偏移(当一起设置了上下或左右束缚时,能够运用 horizontalBias 和 verticalBias 来调整偏移。默许值是 0.5,代表居中)
  7. z-index(制作次序,默许是子元素的次序)
  8. 平移、旋转
  9. 百分比布局(当巨细被设置为 matchConstraint 时,就会启用百分比布局,默许的百分比是 1(100%)。相关的特点是 widthPercent,heightPercent,widthPercentageAnchor,heightPercentageAnchor)
  10. 引导线
  11. 束缚和 Widget 分离
  12. 栅门
  13. 份额布局
    1. widthHeightRatio: 1 / 3,
    2. ratioBaseOnWidth: true, (默许值是 null,代表主动推断,未确定边的巨细会根据确定边的巨细和 widthHeightRatio 核算出来。未确定边的巨细有必要设置为 matchConstraint,确定边的巨细能够为 matchParent,固定巨细(>=0),matchConstraint)
  14. 相对 id(这是为懒癌患者设计的,因为命名是个麻烦事。假如现已为子元素界说了 id,则不能再运用相对 id 来引证他们)
    1. rId(3) 代表第三个子元素,以此类推
    2. sId(-1) 代表上一个兄弟元素,以此类推
    3. sId(1) 代表下一个兄弟元素,以此类推
  15. 包装束缚,是对根本束缚的封装,便于运用,最终会转化成根本束缚
    1. topLeftTo
    2. topCenterTo
    3. topRightTo
    4. centerLeftTo
    5. centerTo
    6. centerRightTo
    7. bottomLeftTo
    8. bottomCenterTo
    9. bottomRightTo
    10. centerHorizontalTo
    11. centerVerticalTo
    12. outTopLeftTo
    13. outTopCenterTo
    14. outTopRightTo
    15. outCenterLeftTo
    16. outCenterRightTo
    17. outBottomLeftTo
    18. outBottomCenterTo
    19. outBottomRightTo
    20. centerTopLeftTo
    21. centerTopCenterTo
    22. centerTopRightTo
    23. centerCenterLeftTo
    24. centerCenterRightTo
    25. centerBottomLeftTo
    26. centerBottomCenterTo
    27. centerBottomRightTo
  16. 瀑布流、网格、列表(列表是一个特别的瀑布流,网格也是一个特别的瀑布流)
  17. 圆形定位
  18. 图钉定位
  19. 随意定位
  20. e-index(事情分发次序,默许是 z-index,一般用来处理点击区域)
  21. 子元素的巨细能够被设置为:
    1. 固定巨细(>=0)
    2. matchParent
    3. wrapContent(默许值,支撑最大、最小设置)
    4. matchConstraint
  22. 本身的巨细能够被设置为:
    1. 固定巨细(>=0)
    2. matchParent(default)
    3. wrapContent(暂不支撑最大、最小设置)
  23. 布局调试
    1. showHelperWidgets
    2. showClickArea
    3. showZIndex
    4. showChildDepth
    5. debugPrintConstraints
    6. showLayoutPerformanceOverlay

后续开发方案:

  1. 束缚可视化
  2. 供给可视化编辑器,经过拖拽创立布局
  3. 主动将设计稿转成代码
  4. 更多…

订阅我的微信大众号(FlutterFirst)及时获取束缚布局的最新动态。后续也会共享一些高质量的、独特的、有思维的 Flutter 技术文章。

当下,我正在根据 View 体系为 Android 开发一款全新的声明式 UI 结构 weiV。它具有以下优势:

  1. 声明式的 UI 写法让原生开发效率翻倍

  2. 到达或超越 View 体系的功能

    我将我的 Flutter ConstraintLayout 移植到 Android,依托它先进的布局算法,在不引进固有特性丈量的情况下,让 View 树中的子元素在任何情况都只会被 layout 一次,使得恣意嵌套不会引起功能问题。即使 View 树中的每个层级宽高都是 wrap_content 和 match_parent 混用。

    一起,xml 将被抛弃。

  3. 你一切的现有 View 体系的经历都将得到保存

  4. 一切的现有 UI 组件都将得以复用

目前已完成 DSL 的界说和 Widget 树的生成,欢迎订阅我的大众号关注后续发展。github.com/hackware199…

支撑的渠道:

  1. Android
  2. iOS
  3. Mac
  4. Windows
  5. Linux
  6. Web

导入

支撑空安全

dependencies:
  flutter_constraintlayout:
    git:
      url: 'https://github.com/hackware1993/Flutter-ConstraintLayout.git'
      ref: 'v1.5.1-stable'
dependencies:
  flutter_constraintlayout: ^1.5.1-stable
import 'package:flutter_constraintlayout/flutter_constraintlayout.dart';

示例 Flutter Web Online Example

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class SummaryExampleState extends State<SummaryExample> {
  double x = 0;
  double y = 0;
  ConstraintId box0 = ConstraintId('box0');
  ConstraintId box1 = ConstraintId('box1');
  ConstraintId box2 = ConstraintId('box2');
  ConstraintId box3 = ConstraintId('box3');
  ConstraintId box4 = ConstraintId('box4');
  ConstraintId box5 = ConstraintId('box5');
  ConstraintId box6 = ConstraintId('box6');
  ConstraintId box7 = ConstraintId('box7');
  ConstraintId box8 = ConstraintId('box8');
  ConstraintId box9 = ConstraintId('box9');
  ConstraintId box10 = ConstraintId('box10');
  ConstraintId box11 = ConstraintId('box11');
  ConstraintId barrier = ConstraintId('barrier');
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const CustomAppBar(
        title: 'Summary',
        codePath: 'example/summary.dart',
      ),
      backgroundColor: Colors.black,
      body: ConstraintLayout(
        // Constraints can be separated from widgets
        childConstraints: [
          Constraint(
            id: box0,
            size: 200,
            bottomLeftTo: parent,
            zIndex: 20,
          )
        ],
        children: [
          Container(
            color: Colors.redAccent,
            alignment: Alignment.center,
            child: const Text('box0'),
          ).applyConstraintId(
            id: box0, // Constraints can be separated from widgets
          ),
          Container(
            color: Colors.redAccent,
            alignment: Alignment.center,
            child: const Text('box1'),
          ).apply(
            constraint: Constraint(
              // Constraints set with widgets
              id: box1,
              width: 200,
              height: 100,
              topRightTo: parent,
            ),
          ),
          Container(
            color: Colors.blue,
            alignment: Alignment.center,
            child: const Text('box2'),
          ).applyConstraint(
            // Constraints set with widgets easy way
            id: box2,
            size: matchConstraint,
            centerHorizontalTo: box3,
            top: box3.bottom,
            bottom: parent.bottom,
          ),
          Container(
            color: Colors.orange,
            width: 200,
            height: 150,
            alignment: Alignment.center,
            child: const Text('box3'),
          ).applyConstraint(
            id: box3,
            right: box1.left,
            top: box1.bottom,
          ),
          Container(
            color: Colors.redAccent,
            alignment: Alignment.center,
            child: const Text('box4'),
          ).applyConstraint(
            id: box4,
            size: 50,
            bottomRightTo: parent,
          ),
          GestureDetector(
            child: Container(
              color: Colors.pink,
              alignment: Alignment.center,
              child: const Text('box5 draggable'),
            ),
            onPanUpdate: (details) {
              setState(() {
                x += details.delta.dx;
                y += details.delta.dy;
              });
            },
          ).applyConstraint(
            id: box5,
            width: 120,
            height: 100,
            centerTo: parent,
            zIndex: 100,
            translate: Offset(x, y),
            translateConstraint: true,
          ),
          Container(
            color: Colors.lightGreen,
            alignment: Alignment.center,
            child: const Text('box6'),
          ).applyConstraint(
            id: box6,
            size: 120,
            centerVerticalTo: box2,
            verticalBias: 0.8,
            left: box3.right,
            right: parent.right,
          ),
          Container(
            color: Colors.lightGreen,
            alignment: Alignment.center,
            child: const Text('box7'),
          ).applyConstraint(
            id: box7,
            size: matchConstraint,
            left: parent.left,
            right: box3.left,
            centerVerticalTo: parent,
            margin: const EdgeInsets.all(50),
          ),
          Container(
            color: Colors.cyan,
            alignment: Alignment.center,
            child: const Text('child[7] pinned to the top right'),
          ).applyConstraint(
            width: 200,
            height: 100,
            left: box5.right,
            bottom: box5.top,
          ),
          const Text(
            'box9 baseline to box7',
            style: TextStyle(
              color: Colors.white,
            ),
          ).applyConstraint(
            id: box9,
            baseline: box7.baseline,
            left: box7.left,
          ),
          Container(
            color: Colors.yellow,
            alignment: Alignment.bottomCenter,
            child: const Text(
                'percentage layout\nwidth: 50% of parent\nheight: 30% of parent'),
          ).applyConstraint(
            size: matchConstraint,
            widthPercent: 0.5,
            heightPercent: 0.3,
            horizontalBias: 0,
            verticalBias: 0,
            centerTo: parent,
          ),
          Barrier(
            id: barrier,
            direction: BarrierDirection.left,
            referencedIds: [box6, box5],
          ),
          Container(
            color: const Color(0xFFFFD500),
            alignment: Alignment.center,
            child: const Text('align to barrier'),
          ).applyConstraint(
            width: 100,
            height: 200,
            top: box5.top,
            right: barrier.left,
          )
        ],
      ),
    );
  }
}

高级用法

  1. 引导线 Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class GuidelineExample extends StatelessWidget {
  const GuidelineExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    ConstraintId guideline = ConstraintId('guideline');
    return MaterialApp(
      home: Scaffold(
        body: ConstraintLayout(
          children: [
            Container(
              color: const Color(0xFF005BBB),
            ).applyConstraint(
              width: matchParent,
              height: matchConstraint,
              top: parent.top,
              bottom: guideline.top,
            ),
            Guideline(
              id: guideline,
              horizontal: true,
              guidelinePercent: 0.5,
            ),
            Container(
              color: const Color(0xFFFFD500),
            ).applyConstraint(
              width: matchParent,
              height: matchConstraint,
              top: guideline.bottom,
              bottom: parent.bottom,
            ),
            const Text(
              'Align to Guideline',
              style: TextStyle(
                fontSize: 40,
                color: Colors.white,
              ),
            ).applyConstraint(
              width: wrapContent,
              height: wrapContent,
              centerHorizontalTo: parent,
              bottom: guideline.bottom,
            )
          ],
        ),
      ),
    );
  }
}
  1. 栅门 Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class BarrierExample extends StatelessWidget {
  const BarrierExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    ConstraintId leftChild = ConstraintId('leftChild');
    ConstraintId rightChild = ConstraintId('rightChild');
    ConstraintId barrier = ConstraintId('barrier');
    return MaterialApp(
      home: Scaffold(
        body: ConstraintLayout(
          debugShowGuideline: true,
          children: [
            Container(
              color: const Color(0xFF005BBB),
            ).applyConstraint(
              id: leftChild,
              width: 200,
              height: 200,
              top: parent.top,
              left: parent.left,
            ),
            Container(
              color: const Color(0xFFFFD500),
            ).applyConstraint(
              id: rightChild,
              width: 200,
              height: matchConstraint,
              right: parent.right,
              top: parent.top,
              bottom: parent.bottom,
              heightPercent: 0.5,
              verticalBias: 0,
            ),
            Barrier(
              id: barrier,
              direction: BarrierDirection.bottom,
              referencedIds: [leftChild, rightChild],
            ),
            const Text(
              'Align to barrier',
              style: TextStyle(
                fontSize: 40,
                color: Colors.blue,
              ),
            ).applyConstraint(
              width: wrapContent,
              height: wrapContent,
              centerHorizontalTo: parent,
              top: barrier.bottom,
              goneMargin: const EdgeInsets.only(top: 20),
            )
          ],
        ),
      ),
    );
  }
}
  1. 角标 Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class BadgeExample extends StatelessWidget {
  const BadgeExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    ConstraintId anchor = ConstraintId('anchor');
    return Scaffold(
      body: ConstraintLayout(
        children: [
          Container(
            color: Colors.yellow,
          ).applyConstraint(
            width: 200,
            height: 200,
            centerTo: parent,
            id: anchor,
          ),
          Container(
            color: Colors.green,
            child: const Text(
              'Indeterminate badge size',
              style: TextStyle(
                color: Colors.black,
                fontSize: 20,
              ),
            ),
          ).applyConstraint(
            left: anchor.right,
            bottom: anchor.top,
            translate: const Offset(-0.5, 0.5),
            percentageTranslate: true,
          ),
          Container(
            color: Colors.green,
          ).applyConstraint(
            width: 100,
            height: 100,
            left: anchor.right,
            right: anchor.right,
            top: anchor.bottom,
            bottom: anchor.bottom,
          )
        ],
      ),
    );
  }
}
  1. 网格 Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class GridExample extends StatelessWidget {
  const GridExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    List<Color> colors = [
      Colors.redAccent,
      Colors.greenAccent,
      Colors.blueAccent,
      Colors.orangeAccent,
      Colors.yellow,
      Colors.pink,
      Colors.lightBlueAccent
    ];
    return Scaffold(
      body: ConstraintLayout(
        children: [
          ...constraintGrid(
              id: ConstraintId('grid'),
              left: parent.left,
              top: parent.top,
              itemCount: 50,
              columnCount: 8,
              itemWidth: 50,
              itemHeight: 50,
              itemBuilder: (index) {
                return Container(
                  color: colors[index % colors.length],
                );
              },
              itemMarginBuilder: (index) {
                return const EdgeInsets.only(
                  left: 10,
                  top: 10,
                );
              })
        ],
      ),
    );
  }
}
  1. 瀑布流 Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class StaggeredGridExample extends StatelessWidget {
  const StaggeredGridExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    List<Color> colors = [
      Colors.redAccent,
      Colors.greenAccent,
      Colors.blueAccent,
      Colors.orangeAccent,
      Colors.yellow,
      Colors.pink,
      Colors.lightBlueAccent
    ];
    const double smallestSize = 40;
    const int columnCount = 8;
    Random random = Random();
    return Scaffold(
      body: ConstraintLayout(
        children: [
          TextButton(
            onPressed: () {
              (context as Element).markNeedsBuild();
            },
            child: const Text(
              'Upset',
              style: TextStyle(
                fontSize: 32,
                height: 1.5,
              ),
            ),
          ).applyConstraint(
            left: ConstraintId('horizontalList').right,
            top: ConstraintId('horizontalList').top,
          ),
          ...constraintGrid(
              id: ConstraintId('horizontalList'),
              left: parent.left,
              top: parent.top,
              margin: const EdgeInsets.only(
                left: 100,
              ),
              itemCount: 50,
              columnCount: columnCount,
              itemBuilder: (index) {
                return Container(
                  color: colors[index % colors.length],
                  alignment: Alignment.center,
                  child: Text('$index'),
                );
              },
              itemSizeBuilder: (index) {
                if (index == 0) {
                  return const Size(
                      smallestSize * columnCount + 35, smallestSize);
                }
                if (index == 6) {
                  return const Size(smallestSize * 2 + 5, smallestSize);
                }
                if (index == 7) {
                  return const Size(smallestSize * 6 + 25, smallestSize);
                }
                if (index == 19) {
                  return const Size(smallestSize * 2 + 5, smallestSize);
                }
                if (index == 29) {
                  return const Size(smallestSize * 3 + 10, smallestSize);
                }
                return Size(
                    smallestSize, (2 + random.nextInt(4)) * smallestSize);
              },
              itemSpanBuilder: (index) {
                if (index == 0) {
                  return columnCount;
                }
                if (index == 6) {
                  return 2;
                }
                if (index == 7) {
                  return 6;
                }
                if (index == 19) {
                  return 2;
                }
                if (index == 29) {
                  return 3;
                }
                return 1;
              },
              itemMarginBuilder: (index) {
                return const EdgeInsets.only(
                  left: 5,
                  top: 5,
                );
              })
        ],
      ),
    );
  }
}
  1. 圆形定位 Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class CirclePositionExampleState extends State<CirclePositionExample> {
  late Timer timer;
  late int hour;
  late int minute;
  late int second;
  double centerTranslateX = 0;
  double centerTranslateY = 0;
  @override
  void initState() {
    super.initState();
    calculateClockAngle();
    timer = Timer.periodic(const Duration(seconds: 1), (_) {
      calculateClockAngle();
    });
  }
  void calculateClockAngle() {
    setState(() {
      DateTime now = DateTime.now();
      hour = now.hour;
      minute = now.minute;
      second = now.second;
    });
  }
  @override
  void dispose() {
    super.dispose();
    timer.cancel();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ConstraintLayout(
        children: [
          GestureDetector(
            child: Container(
              decoration: const BoxDecoration(
                color: Colors.red,
                borderRadius: BorderRadius.all(
                  Radius.circular(1000),
                ),
              ),
            ),
            onPanUpdate: (details) {
              setState(() {
                centerTranslateX += details.delta.dx;
                centerTranslateY += details.delta.dy;
              });
            },
          ).applyConstraint(
            width: 20,
            height: 20,
            centerTo: parent,
            zIndex: 100,
            translate: Offset(centerTranslateX, centerTranslateY),
            translateConstraint: true,
          ),
          for (int i = 0; i < 12; i++)
            Text(
              '${i + 1}',
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 25,
              ),
            ).applyConstraint(
              centerTo: rId(0),
              translate: circleTranslate(
                radius: 205,
                angle: (i + 1) * 30,
              ),
            ),
          for (int i = 0; i < 60; i++)
            if (i % 5 != 0)
              Transform.rotate(
                angle: pi + pi * (i * 6 / 180),
                child: Container(
                  color: Colors.grey,
                  margin: const EdgeInsets.only(
                    top: 405,
                  ),
                ),
              ).applyConstraint(
                width: 1,
                height: 415,
                centerTo: rId(0),
              ),
          Transform.rotate(
            angle: pi + pi * (hour * 30 / 180),
            alignment: Alignment.topCenter,
            child: Container(
              color: Colors.green,
            ),
          ).applyConstraint(
            width: 5,
            height: 80,
            centerTo: rId(0),
            translate: const Offset(0, 0.5),
            percentageTranslate: true,
          ),
          Transform.rotate(
            angle: pi + pi * (minute * 6 / 180),
            alignment: Alignment.topCenter,
            child: Container(
              color: Colors.pink,
            ),
          ).applyConstraint(
            width: 5,
            height: 120,
            centerTo: rId(0),
            translate: const Offset(0, 0.5),
            percentageTranslate: true,
          ),
          Transform.rotate(
            angle: pi + pi * (second * 6 / 180),
            alignment: Alignment.topCenter,
            child: Container(
              color: Colors.blue,
            ),
          ).applyConstraint(
            width: 5,
            height: 180,
            centerTo: rId(0),
            translate: const Offset(0, 0.5),
            percentageTranslate: true,
          ),
          Text(
            '$hour:$minute:$second',
            style: const TextStyle(
              fontSize: 40,
            ),
          ).applyConstraint(
            outTopCenterTo: rId(0),
            margin: const EdgeInsets.only(
              bottom: 250,
            ),
          )
        ],
      ),
    );
  }
}
  1. margin Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class MarginExample extends StatelessWidget {
  const MarginExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ConstraintLayout(
        children: [
          Container(
            color: const Color(0xFF005BBB),
          ).applyConstraint(
            size: 50,
            topLeftTo: parent,
            margin: const EdgeInsets.only(
              left: 20,
              top: 100,
            ),
          ),
          Container(
            color: const Color(0xFFFFD500),
          ).applyConstraint(
            size: 100,
            top: sId(-1).bottom,
            right: parent.right.margin(100),
          ),
          Container(
            color: Colors.pink,
          ).applyConstraint(
            size: 50,
            topRightTo: parent.rightMargin(20).topMargin(50),
          ),
        ],
      ),
    );
  }
}
  1. 图钉定位 Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class PinnedPositionExampleState extends State<PinnedPositionExample> {
  late Timer timer;
  double angle = 0;
  @override
  void initState() {
    super.initState();
    timer = Timer.periodic(const Duration(milliseconds: 16), (_) {
      setState(() {
        angle++;
      });
    });
  }
  @override
  void dispose() {
    super.dispose();
    timer.cancel();
  }
  @override
  Widget build(BuildContext context) {
    ConstraintId anchor = ConstraintId('anchor');
    return Scaffold(
      appBar: const CustomAppBar(
        title: 'Pinned Position',
        codePath: 'example/pinned_position.dart',
      ),
      body: ConstraintLayout(
        children: [
          Container(
            color: Colors.yellow,
          ).applyConstraint(
            id: anchor,
            size: 200,
            centerTo: parent,
          ),
          Container(
            color: Colors.cyan,
          ).applyConstraint(
            size: 100,
            pinnedInfo: PinnedInfo(
              anchor,
              Anchor(0.2, AnchorType.percent, 0.2, AnchorType.percent),
              Anchor(1, AnchorType.percent, 1, AnchorType.percent),
              angle: angle,
            ),
          ),
          Container(
            color: Colors.orange,
          ).applyConstraint(
            size: 60,
            pinnedInfo: PinnedInfo(
              anchor,
              Anchor(1, AnchorType.percent, 1, AnchorType.percent),
              Anchor(0, AnchorType.percent, 0, AnchorType.percent),
              angle: 360 - angle,
            ),
          ),
          Container(
            color: Colors.black,
          ).applyConstraint(
            size: 60,
            pinnedInfo: PinnedInfo(
              anchor,
              Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
              Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
              angle: angle,
            ),
          ),
          Container(
            decoration: const BoxDecoration(
              color: Colors.red,
              borderRadius: BorderRadius.all(Radius.circular(10)),
            ),
          ).applyConstraint(
            size: 10,
            centerBottomRightTo: anchor,
          ),
          Container(
            decoration: const BoxDecoration(
              color: Colors.red,
              borderRadius: BorderRadius.all(Radius.circular(10)),
            ),
          ).applyConstraint(
            size: 10,
            centerTopLeftTo: anchor,
          ),
          Container(
            decoration: const BoxDecoration(
              color: Colors.red,
              borderRadius: BorderRadius.all(Radius.circular(10)),
            ),
          ).applyConstraint(
            size: 10,
            centerTo: anchor,
          )
        ],
      ),
    );
  }
}
  1. 平移 Flutter Web 在线示例

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

class TrackPainter extends CustomPainter {
  Queue<Offset> points = Queue();
  Paint painter = Paint();
  TrackPainter(this.points);
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawPoints(PointMode.polygon, points.toList(), painter);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class TranslateExampleState extends State<TranslateExample> {
  late Timer timer;
  double angle = 0;
  double earthRevolutionAngle = 0;
  Queue<Offset> points = Queue();
  @override
  void initState() {
    super.initState();
    timer = Timer.periodic(const Duration(milliseconds: 16), (_) {
      setState(() {
        angle += 1;
        earthRevolutionAngle += 0.1;
      });
    });
  }
  @override
  void dispose() {
    super.dispose();
    timer.cancel();
  }
  @override
  Widget build(BuildContext context) {
    ConstraintId anchor = ConstraintId('anchor');
    return Scaffold(
      appBar: const CustomAppBar(
        title: 'Translate',
        codePath: 'example/translate.dart',
      ),
      body: ConstraintLayout(
        children: [
          CustomPaint(
            painter: TrackPainter(points),
          ).applyConstraint(
            width: matchParent,
            height: matchParent,
          ),
          Container(
            decoration: const BoxDecoration(
              color: Colors.redAccent,
              borderRadius: BorderRadius.all(Radius.circular(1000)),
            ),
            child: const Text('----'),
            alignment: Alignment.center,
          ).applyConstraint(
            id: cId('sun'),
            size: 200,
            pinnedInfo: PinnedInfo(
              parent,
              Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
              Anchor(0.3, AnchorType.percent, 0.5, AnchorType.percent),
              angle: earthRevolutionAngle * 365 / 25.4,
            ),
          ),
          Container(
            decoration: const BoxDecoration(
              color: Colors.blue,
              borderRadius: BorderRadius.all(Radius.circular(1000)),
            ),
            child: const Text('----'),
            alignment: Alignment.center,
          ).applyConstraint(
            id: cId('earth'),
            size: 100,
            pinnedInfo: PinnedInfo(
              cId('sun'),
              Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
              Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
              angle: earthRevolutionAngle * 365,
            ),
            translate: circleTranslate(
              radius: 250,
              angle: earthRevolutionAngle,
            ),
            translateConstraint: true,
          ),
          Container(
            decoration: const BoxDecoration(
              color: Colors.grey,
              borderRadius: BorderRadius.all(Radius.circular(1000)),
            ),
            child: const Text('----'),
            alignment: Alignment.center,
          ).applyConstraint(
            id: cId('moon'),
            size: 50,
            pinnedInfo: PinnedInfo(
              cId('earth'),
              Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
              Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
              angle: earthRevolutionAngle * 365 / 27.32,
            ),
            translate: circleTranslate(
              radius: 100,
              angle: earthRevolutionAngle * 365 / 27.32,
            ),
            translateConstraint: true,
            paintCallback: (_, __, ____, offset, ______) {
              points.add(offset!);
              if (points.length > 2000) {
                points.removeFirst();
              }
            },
          ),
          Text('Sun rotates ${(earthRevolutionAngle * 365 / 25.4) ~/ 360} times')
              .applyConstraint(
            outTopCenterTo: cId('sun'),
          ),
          Text('Earth rotates ${earthRevolutionAngle * 365 ~/ 360} times')
              .applyConstraint(
            outTopCenterTo: cId('earth'),
          ),
          Text('Moon rotates ${(earthRevolutionAngle * 365 / 27.32) ~/ 360} times')
              .applyConstraint(
            outTopCenterTo: cId('moon'),
          ),
          Container(
            color: Colors.yellow,
          ).applyConstraint(
            id: anchor,
            size: 250,
            centerRightTo: parent.rightMargin(300),
          ),
          Container(
            color: Colors.red,
            child: const Text('pinned translate'),
          ).applyConstraint(
            centerTo: anchor,
            translate: PinnedTranslate(
              PinnedInfo(
                null,
                Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
                null,
                angle: angle,
              ),
            ),
          ),
          Container(
            color: Colors.blue,
            child: const Text('circle translate'),
          ).applyConstraint(
            size: wrapContent,
            centerTo: anchor,
            translate: circleTranslate(
              radius: 100,
              angle: angle,
            ),
          ),
          Container(
            color: Colors.cyan,
            child: const Text('pinned & circle translate'),
          ).applyConstraint(
            centerTo: anchor,
            translate: PinnedTranslate(
              PinnedInfo(
                null,
                Anchor(0.5, AnchorType.percent, 0.5, AnchorType.percent),
                null,
                angle: angle,
              ),
            ) +
                circleTranslate(
                  radius: 150,
                  angle: angle,
                ),
          ),
          Container(
            color: Colors.orange,
            child: const Text('normal translate'),
          ).applyConstraint(
            size: wrapContent,
            outBottomCenterTo: anchor,
            translate: Offset(0, angle / 5),
          )
        ],
      ),
    );
  }
}

功能优化

  1. 当布局杂乱时,假如子元素需求频深重绘,能够考虑运用 RepaintBoundary。当然合成 Layer 也有开销,所以需求合理运用。
class OffPaintExample extends StatelessWidget {
  const OffPaintExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: ConstraintLayout(
          children: [
            Container(
              color: Colors.orangeAccent,
            ).offPaint().applyConstraint(
              width: 200,
              height: 200,
              topRightTo: parent,
            )
          ],
        ),
      ),
    );
  }
}
  1. 尽量运用 const Widget。假如你无法将子元素声明为 const 而它本身又不会改动。能够运用内置的 OffBuildWidget 来防止子元素重复 build。
class OffBuildExample extends StatelessWidget {
  const OffBuildExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: ConstraintLayout(
          children: [
            /// subtrees that do not change
            Container(
              color: Colors.orangeAccent,
            ).offBuild(id: 'id').applyConstraint(
              width: 200,
              height: 200,
              topRightTo: parent,
            )
          ],
        ),
      ),
    );
  }
}
  1. 子元素会主动成为 RelayoutBoundary 除非它的宽或高是 wrapContent。能够酌情的减少 wrapContent 的运用,因为当 ConstraintLayout 本身的巨细发生变化时(通常是窗口巨细发生变化,移动端几乎不存在此类情况),一切宽或高为 wrapContent 的子元素都会被从头布局。而其他元素因为传递给它们的束缚未发生变化,不会触发真正的布局。

  2. 假如你在 children 列表中运用 Guideline 或 Barrier, Element 和 RenderObject 将不可防止的被创立,它们会被布局但不会制作。此时你能够运用 GuidelineDefine 或 BarrierDefine 来优化, Element 和 RenderObject 就不会再创立了。

class BarrierExample extends StatelessWidget {
  const BarrierExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    ConstraintId leftChild = ConstraintId('leftChild');
    ConstraintId rightChild = ConstraintId('rightChild');
    ConstraintId barrier = ConstraintId('barrier');
    return Scaffold(
      body: ConstraintLayout(
        childConstraints: [
          BarrierDefine(
            id: barrier,
            direction: BarrierDirection.bottom,
            referencedIds: [leftChild, rightChild],
          ),
        ],
        children: [
          Container(
            color: const Color(0xFF005BBB),
          ).applyConstraint(
            id: leftChild,
            width: 200,
            height: 200,
            topLeftTo: parent,
          ),
          Container(
            color: const Color(0xFFFFD500),
          ).applyConstraint(
            id: rightChild,
            width: 200,
            height: matchConstraint,
            centerRightTo: parent,
            heightPercent: 0.5,
            verticalBias: 0,
          ),
          const Text(
            'Align to barrier',
            style: TextStyle(
              fontSize: 40,
              color: Colors.blue,
            ),
          ).applyConstraint(
            centerHorizontalTo: parent,
            top: barrier.bottom,
          )
        ],
      ),
    );
  }
}   
  1. 每一帧,ConstraintLayout 会比对参数并决议以下工作:
    1. 是否需求从头核算束缚?
    2. 是否需求从头布局?
    3. 是否需求从头制作?
    4. 是否需求重排制作次序?
    5. 是否需求重排事情分发次序?

这些比对不会成为功能瓶颈,但会进步 CPU 占用率。假如你对 ConstraintLayout 内部原理满足了解,你能够运用 ConstraintLayoutController 来手动触发这些操作,中止参数比对。

class ConstraintControllerExampleState extends State<ConstraintControllerExample> {
  double x = 0;
  double y = 0;
  ConstraintLayoutController controller = ConstraintLayoutController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const CustomAppBar(
        title: 'Constraint Controller',
        codePath: 'example/constraint_controller.dart',
      ),
      body: ConstraintLayout(
        controller: controller,
        children: [
          GestureDetector(
            child: Container(
              color: Colors.pink,
              alignment: Alignment.center,
              child: const Text('box draggable'),
            ),
            onPanUpdate: (details) {
              setState(() {
                x += details.delta.dx;
                y += details.delta.dy;
                controller.markNeedsPaint();
              });
            },
          ).applyConstraint(
            size: 200,
            centerTo: parent,
            translate: Offset(x, y),
          )
        ],
      ),
    );
  }
}

扩展

ConstraintLayout 根据束缚的布局算法极其强壮和灵敏,似乎能够成为了一个通用的布局结构。你只需求生成束缚,将布局的任务交给 ConstraintLayout 即可。部分内置功能比方圆形定位、瀑布流、网格、列表以扩展的方式供给。

在线示例中的图表便是一个典型的扩展:

Flutter 嵌套地狱?不存在的,ConstraintLayout 来解救!

欢迎为 ConstraintLayout 开发扩展。

支撑我

假如它对你帮助很大,能够考虑赞助我一杯奶茶,或许给个 star。你的支撑是我持续维护的动力。

联系方式

hackware1993@gmail.com

协议

MIT License
Copyright (c) 2022 hackware1993
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.