微信交流群

老孟导读:此篇文章非常详细的讲解了 Flutter 布局系统的工作原理

翻译自:https://itnext.io/flutter-layout-system-overview-c70bbe9ba909?source=bookmarks---------17------------------

最近,我决定专注于Flutter基础知识。这次,我试图更好地理解“布局系统的工作原理”,并回答以下问题:

  • 我的小部件的尺寸看起来不合适,怎么回事?
  • 我只想将Widget放置在特定位置,但是没有任何属性可以控制它,为什么呢?
  • 我一直看到诸如BoxConstraints,RenderBox和Size之类的术语。它们之间有什么关系?
  • 对布局系统如何工作有一个大概的了解?

本文并不意味着对以上所有内容进行深入而详细的描述。但是,我们将对最重要的内容进行很好的概述,力图将一切可视化。

# “两个阶段” 布局系统和约束

首先,小部件是Flutter SDK的构建块,但它们不负责将其自身绘制到屏幕中。每个小部件都与负责此操作的RenderBox对象相关联。这些框是2D直角坐标系,其大小表示为距原点的偏移。每个RenderBox还将与一个BoxConstraints对象相关联,该对象包含四个值:最大|最小宽度和最大|最小高度。 RenderBox可以选择具有所需的任何大小,但它必须遵守这些值/约束。小部件的大小/位置完全取决于这些RenderBox的属性。

原文:The same way Widgets build a Widget three, RenderBoxes make a render three.

我觉得three可能写错了,应该是tree,译文:以同样的方式小部件生成 组件树,RenderBoxes生成渲染树。

我们可以将Flutter的布局系统视为两阶段系统。在第一个阶段中,framework 以递归地方式沿着渲染树 把BoxConstraints传递给子组件。它为父组件提供了一种方式来调节/增强子组件的尺寸,并根据需要更新这些限制。换句话说,这是负责传播约束信息的阶段,让每个人知道其最大/最小值。

完成后,第二阶段开始。这次,每个RenderBox都将其选择的大小传递回其父对象。父级收集所有子级的大小,然后使用此几何信息将每个子级正确定位在自己的笛卡尔系统中。这个阶段负责确定大小和位置,在此阶段,父组件知道每个子组件的大小以及他们的位置。

那么,这到底意味着什么?

这意味着父组件有责任定义/限制/约束子组件的尺寸,并相对于其坐标系进行定位。换句话说,小部件可以选择其大小,但是它必须始终遵守从其父级收到的约束。此外,小部件不知道其在屏幕上的位置,但其父级知道。

如果您对小部件的大小或位置有疑问,请尝试查看(更新)其父组件。

# Example

好的,让我们将所有内容可视化,尝试通过示例了解正在发生的事情。但是在此之前,以下是一些在调试约束时可能有用的术语,

下面的术语未翻译,因为这些术语本身比译文更好理解:

  • If *max(w|h) = min (w|h)*, that is *tightly* constrained.
  • If *min(w|h) = 0*, we have a *loose* constraint.
  • If *max(w|h) != infinite*, the constraint is *bounded.*
  • If *max(w|h) = infinite*, the constraint is *unbounded.*
  • If *min(w|h) = infinite*, is just said to be *infinite*

我们将使用的是初始应用模板的修改版本。通常,您可以通过两种简单的方法来检查窗口小部件RenderBox及其属性:

  1. 通过代码执行:我们可以使用LayoutBuilder在布局系统第一阶段拦截BoxConstraints传播,并检查约束。然后,在第二阶段完成后,我们使用键来获取小部件的RenderBox并能够检查Size,Position。

  2. 或使用DevTools窗口小部件检查器

import 'package:flutter/material.dart';

GlobalKey _keyMyApp = GlobalKey();
GlobalKey _keyMaterialApp = GlobalKey();
GlobalKey _keyHomePage = GlobalKey();
GlobalKey _keyScaffold = GlobalKey();
GlobalKey _keyAppbar = GlobalKey();
GlobalKey _keyCenter = GlobalKey();
GlobalKey _keyFAB = GlobalKey();
GlobalKey _keyText = GlobalKey();

void printConstraint(String name, BoxConstraints c) {
  print(
    'CONSTRAINT of $name: min(w=${c.minWidth.toInt()},h=${c.minHeight.toInt()}) max(w=${c.maxWidth.toInt()},h=${c.maxHeight.toInt()})',
  );
}

void printSizes() {
  printSize('MyApp', _keyMyApp);
  printSize('MaterialApp', _keyMaterialApp);
  printSize('HomePage', _keyHomePage);
  printSize('Scaffold', _keyScaffold);
  printSize('Appbar', _keyAppbar);
  printSize('Center', _keyCenter);
  printSize('Text', _keyText);
  printSize('FAB', _keyFAB);
}

void printSize(String name, GlobalKey key) {
  final RenderBox renderBox = key.currentContext.findRenderObject();
  final size = renderBox.size;
  print("SIZE of $name: w=${size.width.toInt()},h=${size.height.toInt()}");
}

void printPositions() {
  printPosition('MyApp', _keyMyApp);
  printPosition('MaterialApp', _keyMaterialApp);
  printPosition('HomePage', _keyHomePage);
  printPosition('Scaffold', _keyScaffold);
  printPosition('Appbar', _keyAppbar);
  printPosition('Center', _keyCenter);
  printPosition('Text', _keyText);
  printPosition('FAB', _keyFAB);
}

void printPosition(String name, GlobalKey key) {
  final RenderBox renderBox = key.currentContext.findRenderObject();
  final position = renderBox.localToGlobal(Offset.zero);
  print("POSITION of $name: $position ");
}

void main() {
  runApp(LayoutBuilder(
    builder: (context, constraints) {
      printConstraint('MyApp', constraints);
      return MyApp();
    },
  ));
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return LayoutBuilder(
      key: _keyMyApp,
      builder: (context, constraints) {
        printConstraint('MaterialApp', constraints);
        return MaterialApp(
          key: _keyMaterialApp,
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: LayoutBuilder(
            builder: (context, constraints) {
              printConstraint('HomePage', constraints);
              return HomePage(
                key: _keyHomePage,
                title: 'Flutter Demo Home Page',
              );
            },
          ),
        );
      },
    );
  }
}

class HomePage extends StatefulWidget {
  HomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
    super.initState();
  }

  void _afterLayout(_) {
    printSizes();
    printPositions();
  }

  
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      printConstraint('Scaffold', constraints);
      return Scaffold(
        backgroundColor: Colors.purple,
        key: _keyScaffold,
        appBar: AppBar(
          key: _keyAppbar,
          title: Text(widget.title),
        ),
        body: LayoutBuilder(
          builder: (context, constraints) {
            printConstraint('Center', constraints);
            return Center(
              key: _keyCenter,
              child: LayoutBuilder(builder: (context, constraints) {
                printConstraint('Text', constraints);
                return Text(
                  'You have pushed the button this many times:',
                  key: _keyText,
                  style: TextStyle(color: Colors.white),
                );
              }),
            );
          },
        ),
        floatingActionButton: LayoutBuilder(
          builder: (context, constraints) {
            printConstraint('FAB', constraints);
            return FloatingActionButton(
              key: _keyFAB,
              onPressed: printSizes,
              tooltip: 'Increment',
              child: Icon(Icons.add),
            );
          },
        ),
      );
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

让我们一步一步来看看发生了什么(在这里我们将忽略LayoutBuilders)。

在我们的示例中发生的第一件事是执行runApp(..)。此函数检查屏幕当前大小(在我们的示例中为392:759),然后创建一个BoxConstraints对象,其中包含将发送到我们的第一个小部件(MyApp)的约束。注意,max | min的宽度和高度都相等;因此,runApp使用了严格的约束-通过这样做,MyApp除了选择屏幕上的可用空间外,在选择其大小时将别无选择。

然后将约束向下传播到Widget树。 MyApp,MaterialApp,HomePage和Scaffold都被告知相同的严格约束。因此,所有人将被迫填满整个屏幕。每个小部件都有机会向其子项通知不同的BoxConstraints(仍然尊重已收到的子项)。但是,在这种情况下,他们选择不这样做。

现在事情开始变得越来越有趣。Scaffold告知AppBar有关必须使用的BoxConstraints的信息,但是,这一次,它使用了宽松的约束(min h = 0)。它使AppBar有机会选择所需的任何高度,但仍必须使用width = 390。

AppBar是一种特殊的小部件,称为PreferredSizeWidget。这种类型的小部件不会对其子级施加任何约束。如果尝试使用LayoutBuilder获取Title的约束,则会出现错误。而是,AppBar以首选/默认大小响应Scaffold:高度= 80,宽度= 392(受接收到的约束的约束)

获得AppBar的大小后,Scaffold继续下一个子项:Center

好的,这里发生了很多事情。让我们尝试了解:

  1. Scaffold告知Center其约束,让其选择在 0 < width < 392 和 0 < height < 697 中选择。请注意,最大高度为759(屏幕最大高度)减去80(AppBar选择的高度)。
  2. Center转到其子组件“Text”,转发相同的约束。
  3. Text选择一个足以显示其数据的大小(279:16),然后回复Center。
  4. 借助手上的几何信息(大小),Center可以在其笛卡尔系统内正确定位文本。作为父母,Center有权选择其子组件位置,在这种情况下,它决定将其居中。

流程继续:

  1. 然后,Center为自己选择一个大小,而不是仅选择一个“足够”的大小(如“Text”一样),而是决定尽可能大,因此受到了限制。
  2. Scaffold收到Center所需的尺寸,并且流程继续向其最后一个孩子:FAB
  3. FAB收到约束,然后将其首选大小返回给Scaffold(56:56)
  4. 最后,Scaffold还具有将每个孩子都放置在其笛卡尔系统内所需的所有几何信息。

最后,对Scaffold以上的所有小部件重复该过程:

  1. Size信息继续沿渲染树传播。
  2. 每个小部件都使用此信息将每个孩子放置在笛卡尔系统内。
  3. Scaffold回复HomePage,HomePage回复MaterialApp,MaterialApp回复MyApp。直到最后再次到达Main。
  4. Main获取此“最终”窗口小部件,并将其最终绑定到屏幕中。

RenderBox树最终绑定在屏幕上。我们有一个正在运行的应用程序。

# 有趣的事情要记住

  • 小部件不知道其在屏幕上的位置;它的父组件才知道。
  • 小部件可以选择想要的大小,但必须根据其父级的限制。
  • 约束向下传播,而大小向上传播。
  • 尝试了解约束条件,它们可能在以后有用。

我希望所有这些都可以帮助您更好地了解Flutter布局系统的工作方式。