老孟导读:此篇文章十分具体的解说了 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】: