关于flutter:Flutter-布局系统概述

36次阅读

共计 6345 个字符,预计需要花费 16 分钟才能阅读完成。

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

正文完
 0