老孟导读:此篇文章十分具体的解说了 Flutter 布局零碎的工作原理 。
翻译自:https://itnext.io/flutter-lay…
最近,我决定专一于 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 及其属性:
- 通过代码执行:咱们能够应用 LayoutBuilder 在布局零碎第一阶段拦挡 BoxConstraints 流传,并查看束缚。而后,在第二阶段实现后,咱们应用键来获取小部件的 RenderBox 并可能查看 Size,Position。
- 或应用 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 {
@override
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;
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _counter = 0;
void _incrementCounter() {setState(() {_counter++;});
}
@override
void initState() {WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();}
void _afterLayout(_) {printSizes();
printPositions();}
@override
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),
);
},
),
);
});
}
}
让咱们一步一步来看看产生了什么(在这里咱们将疏忽 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
好的,这里产生了很多事件。让咱们尝试理解:
- Scaffold 告知 Center 其束缚,让其抉择在 0 < width < 392 和 0 < height < 697 中抉择。请留神,最大高度为 759(屏幕最大高度)减去 80(AppBar 抉择的高度)。
- Center 转到其子组件“Text”,转发雷同的束缚。
- Text 抉择一个足以显示其数据的大小(279:16),而后回复 Center。
- 借助手上的几何信息(大小),Center 能够在其笛卡尔零碎内正确定位文本。作为父母,Center 有权抉择其子组件地位,在这种状况下,它决定将其居中。
流程持续:
- 而后,Center 为本人抉择一个大小,而不是仅抉择一个“足够”的大小(如“Text”一样),而是决定尽可能大,因而受到了限度。
- Scaffold 收到 Center 所需的尺寸,并且流程持续向其最初一个孩子:FAB
- FAB 收到束缚,而后将其首选大小返回给 Scaffold(56:56)
- 最初,Scaffold 还具备将每个孩子都搁置在其笛卡尔零碎内所需的所有几何信息。
最初,对 Scaffold 以上的所有小部件反复该过程:
- Size 信息持续沿渲染树流传。
- 每个小部件都应用此信息将每个孩子搁置在笛卡尔零碎内。
- Scaffold 回复 HomePage,HomePage 回复 MaterialApp,MaterialApp 回复 MyApp。直到最初再次达到 Main。
- Main 获取此“最终”窗口小部件,并将其最终绑定到屏幕中。
RenderBox 树最终绑定在屏幕上。咱们有一个正在运行的应用程序。
乏味的事件要记住
- 小部件不晓得其在屏幕上的地位;它的父组件才晓得。
- 小部件能够抉择想要的大小,但必须依据其父级的限度。
- 束缚向下流传,而大小向上流传。
- 尝试理解约束条件,它们可能在当前有用。
我心愿所有这些都能够帮忙您更好地理解 Flutter 布局零碎的工作形式。
交换
老孟 Flutter 博客地址(330 个控件用法):http://laomengit.com
欢送退出 Flutter 交换群(微信:laomengit)、关注公众号【老孟 Flutter】: