1. 程式人生 > 實用技巧 >Flutter —佈局系統概述

Flutter —佈局系統概述

老孟導讀:此篇文章非常詳細的講解了 Flutter 佈局系統的工作原理

翻譯自:https://itnext.io/flutter-layout-system-overview-c70bbe9ba909?source=bookmarks---------17------------------

最近,我決定專注於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】: