[Flutter翻译]Flutter Anatomy - 布局内部的第1部分

1,022 阅读8分钟

原文地址:medium.com/snapp-mobil…

原文作者:medium.com/@jasperamor…

发布时间:2019年8月21日

照片:Mathew Schwartz on Unsplash。

Flutter Anatomy是一系列关于什么让Flutter...Flutter的文章。我们试图深入了解Flutter如何工作,以更好地了解框架的一些伟大功能。

在这篇文章中,我们开始看Flutter如何使用独特的方法来计算屏幕布局,这有助于Flutter的速度和流畅的UI渲染。这也是让Flutter能够使用非常简单的Widget组成模型,即使是复杂的屏幕也是如此。

对于图形用户界面框架来说,布局是决定用户界面元素的大小和位置的活动。尺寸和位置被称为几何。 对于我们经常在移动应用中看到的流体和相对布局,计算UI元素的几何形状变得很困难。布局经理通常必须在UI元素的层次结构中进行几次传递,以计算父元素及其子元素的尺寸和位置--这被称为多传递布局。

另一方面,Flutter使用线性(以及在可能的情况下使用子线性)布局。但这意味着什么?

简单地说,它意味着Flutter中的布局是通过UI Widgets树来计算每一个UI元素的几何形状的一个通道(向下和向上)。(这并不总是可能的,我们将在以后的文章中讨论)。

一个重要的含义是,Widget树的子集可以被更新,而不必计算整个屏幕的布局。这种优化就是子线型布局的意思。

如果你熟悉浏览器的重绘和回流,Flutter的方法应该已经看起来是一个大规模的优化了。

这是如何工作的呢?

核心概念是父Widget对允许子Widget的大小进行限制。基于这些约束,子Widgets将其计算出的大小传回给父Widget。最后,父Widget决定子Widget的位置。

当然也有例外,但我们会在后面的文章中讲到。

让我们用一个简单的图来可视化。

小组件树中的约束和尺寸

蓝色的父节点约束黄色的子节点,而黄色的子节点又约束其子节点。这决定了子节点的最大和最小尺寸。然后,小组件根据这些约束计算出的大小会传回树上。

约束是什么样子的?

约束是子节点允许的最大和最小高度和宽度的简单组合。我们将在另一篇文章中更详细地研究约束)。

限制条件

假设上面树中的蓝色widget宽为100.0,高为100.0,那么黄色的子widget的宽度和高度都可以最小为0.0,最大为100.0。

请注意,我们在Flutter中并没有设置X/Y位置,尽管对于某些widget来说,一些子部件的定位是可能的,例如,使用Positionied widget与Stack Widget 。


让我们通过创建一个受其父体约束的子部件,来看看这种基于约束的方法在行动。 我们创建一个宽度为100.0、高度为100.0的容器(父容器)。父容器的子容器将是另一个容器,但希望比其父容器更宽--我们设置宽度为200.0,高度为100.0。

子容器的首选宽度和高度将违反其父容器所规定的约束。父容器会告诉子容器,它的最大宽度和高度只能是100.0。基于Flutter布局算法,子代将其大小限制在w=100.0,h=100.0。

让我们根据这个简单的例子来深入了解一些代码。

class SimpleParentChildExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.blue,
        width: 100.0,
        height: 100.0,
        child: Container(
          color: Colors.yellow,
          height: 100.0,
          width: 200.0,
        ));
  }

这段代码很琐碎,运行应用后,可以看到我们期望看到的东西--黄色的子Widget与父Widget大小相同。

简单Flutter布局

要想了解更多的情况,我们来看看渲染树。


渲染树(快速绕行)

等等,这个 "渲染树 "是什么东西?这个'渲染树'是什么东西?我们先绕道来了解一下这是什么。

在Flutter中,渲染树是计算Widget树布局的结果。换句话说,渲染树包含了描述如何绘制一个Widget的低级UI对象。在本文中,我们感兴趣的是,每个UI对象(称为RenderObject)都有一个几何体--即将在屏幕上绘制的东西的大小和位置。


让我们再来看看我们简单UI的渲染树。

Container Widget实际上是由两个Widgets组成的,这就是为什么你会看到每个Container的RenderConstrainedBox和RenderDecoratedBox。为了更清楚,我将渲染对象分别用蓝色(父容器)和黄色(子容器)标记。

你可以去Container Widget的源码中看一看,看看它是如何工作的 bit.ly/2PqGvQq

与我们理解Flutter中的Layout有关的是三个绿色箭头。这些箭头告诉我们以下内容。

  • 黄色子Widget的BoxConstraints与蓝色父Widget的BoxConstraints相匹配(即w=100.0,h=100.0)。
  • 渲染对象的大小是w=100.0,h=100.0--这符合父Widget所规定的BoxContraints,而不是我们在黄色子Widget中设置的200.0的宽度。换句话说,没有隐藏的溢出)。
  • 'extraConstraints'属性保留了我们为子Widget设置的首选宽度和高度的记录。

这里是一个简单的可视化的情况。

简单布局父约束

那又怎样?

这种方法的简单性掩盖了它的重要性。让我们来研究一些重要的意义。

1/ 复杂屏幕的高效布局

有很多嵌套的Widgets的屏幕可以有效地布局。即使您的屏幕变得复杂,Flutter可以(在大多数情况下)通过所有Widgets的一次传递来计算布局,并再次返回。

这就是为什么您可以在您的布局中使用大量的Widgets,并且仍然可以看到快速流畅的屏幕。

2/ 屏幕更新时的部分布局

屏幕被更新以反映新的状态(例如显示新的数据)或显示动画或响应输入。当这种情况发生时,Flutter不必每次都计算屏幕的整个布局--只计算已经改变的屏幕部分。

这种优化就是所谓的子线性布局。它是可能的,因为父Widget的约束不受子Widget中发生的动画或其他更新的影响。因此Flutter不需要再次重新计算父部件的布局。这就是所谓的中继边界。

与浏览器中的回流和重绘相比,DOM树深处的变化可能会导致一路到根部的变化。HTML开发者需要考虑到这一点,而Flutter开发者则不需要--嗯......这几乎是真的,请看下一点。

我们将在后面的文章中更详细地研究这个问题,也会研究使局部布局效率低下的异常情况。

3/ 混合无状态和有状态的Widgets

在Flutter的布局机制中,拥有Stateless和Stateful Widgets的方法是有意义的。

Stateless Widgets可以提供布局约束,对于一个屏幕来说是不会改变的。Stateful Widgets可以改变Widget的数据或者一些视觉约束,比如AnimatedWidget。

屏幕的部分布局意味着StatefulWidgets可以非常高效。然而要看到这个好处,我们必须避免将StatefulWidgets作为我们屏幕的大部分的父节点。

参见 bit.ly/2VBRRYm ,了解如何优化Stateful Widgets中子代布局的技巧。

其他UI框架有何不同?

快速看看其他UI框架如何处理布局是很有趣的。这有助于我们看到Flutter的方法与你之前可能看到的有些不同。

让我们在三个不同的框架中画出我们简单的2框布局。

HTML

在浏览器中,父代的大小自然是要适合子代的。在本例中,我们可以看到父DIV被推出来,宽度为200px。当然也可以将溢出样式属性设置为 "隐藏",这样子DIV就被剪掉了,视觉上是100px×100px。

<html>

<body>

  <div style="width: 100px; height: 100px; background-color: blue;">
    <div style="width: 200px; height: 100px; background-color: yellow"></div>
  </div>

</body>

</html>

这种布局看起来如下。

简单的HTML布局

iOS

在iOS中,UIViews可以 "包含 "其他UIVIew作为子视图,这并不完全是父子关系。关键的区别在于,这些视图居住在不同的层中,而子视图位于其父视图之上。这意味着子视图的宽度将达到200。另一个重要的区别是,UIViews有位置--在这种情况下,约束用于使用锚约束来定位一个View与另一个View的相对位置。

class ViewController: UIViewController {

    lazy var yellowSquare: UIView = {
        let square = UIView(frame: .zero)
        square.backgroundColor = .yellow
        square.translatesAutoresizingMaskIntoConstraints = false
        return square
    }()

    lazy var blueSquare: UIView = {
        let square = UIView(frame: .zero)
        square.backgroundColor = .blue
        square.translatesAutoresizingMaskIntoConstraints = false
        return square
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        blueSquare.addSubview(yellowSquare)

        blueSquare.addConstraints([
            yellowSquare.topAnchor.constraint(equalTo: blueSquare.topAnchor),
            yellowSquare.bottomAnchor.constraint(equalTo: blueSquare.bottomAnchor),
            yellowSquare.leadingAnchor.constraint(equalTo: blueSquare.leadingAnchor),
            yellowSquare.trailingAnchor.constraint(equalTo: blueSquare.trailingAnchor),
            yellowSquare.widthAnchor.constraint(equalToConstant: 200),
            yellowSquare.heightAnchor.constraint(equalToConstant: 100)
        ])

        view.addSubview(blueSquare)

        view.addConstraints([
            blueSquare.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
            blueSquare.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50)
        ])

    }

}

布局看起来如下。

iOS上的简单布局

安卓系统

马上就到了

TL;DR

Flutter中的屏幕布局方法很简单但很强大。父Widgets对子Widgets实施约束,所以通过Widget树的一次传递就足以计算布局和定位。

本文将通过一个非常简单的例子来展示Flutter中的关键布局概念,以及这与其他UI框架有何不同。

在接下来的文章中,我们将深入研究一些布局机制,以及如何使用这些知识来改进你的Flutter UIs。