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

  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 {  @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

好的,这里产生了很多事件。让咱们尝试理解:

  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布局零碎的工作形式。

交换

老孟Flutter博客地址(330个控件用法):http://laomengit.com

欢送退出Flutter交换群(微信:laomengit)、关注公众号【老孟Flutter】: