Flutter入門三:自動布局(Row/Column/Stack)與兩種Widget

Flutter入門 學習大綱

  1. CenterAlignment
  2. 三種布局方式(Row、Column、Stack
  3. Expanded自動填充和AspectRatio寬高比
  4. StatefulWidget可變組件 與 StatelessWidget不可變組件

  • 本節(jié)代碼,都默認使用下面main.dart文件:
    設(shè)置APP入口組件,使用MaterialApp默認樣式,設(shè)置home根組件,使用Scaffold支持導航控制器,設(shè)置body內(nèi)容組件。
import 'package:flutter/material.dart';
import 'layout_demo.dart';

// 入口,展示MyWidget組件
void main() => runApp(App());

// 根組件
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Home(),
      theme: ThemeData(
        primaryColor: Colors.yellow
      ),
    );
  }
}

// Home 組件
class Home extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      appBar: AppBar(
        title: Text("Flutter Demo"),
      ),
      body: LayoutDemo(), // 展示LayoutDemo組件
    );
  }
}
  • 下面的代碼,都在LayoutDemo中實現(xiàn)。

1. Center和Alignment

  • Center: 居中
  • Alignment: 自定義水平垂直方向位置

1.1 Center

  • Center是常用的快捷布局方式,將子組件居中展示在父組件中:
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      // Center: 居中
      child: Center(
        child: Text( "Layout Demo" ),
      ),
    );
  }
}
  • 展示樣式:


    image.png

1.2 Alignment

  • Alignment是自定義水平垂直方向位置。 默認(0,0)居中對齊,與center一樣。
    【參數(shù)一】水平方向的百分比。 -1:對齊最左邊,0水平居中,1:對齊最右邊。 -0.50.5中間數(shù)值,是按比例展示位置。
    【參數(shù)二】垂直方向的百分比。-1:對齊最上邊,0垂直居中,1:對齊最下邊。 -0.50.5中間數(shù)值,是按比例展示位置。
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      //  Alignment: 按比例展示位置
      alignment: Alignment(-1,-1),
      child: Text( "Layout Demo" ),
    );
  }
}
  • 展示樣式:


    image.png

2. 三種布局方式(Row、Column、Stack)

  • Row、Column、Stack實際上是空間坐標系x、y、z三個方向布局。
    【Row】X軸(水平方向)布局
    【Column】Y軸(垂直方向)布局
    【Stack】Z軸(前后縱深方向)布局

2.1 Row

  • Row: X軸(水平方向)布局,默認水平撐滿父容器空間。
  • Alignment(0, 0)標注child子容器Row居中對齊
    由于Row水平撐滿父容器空間,所以會看到Row子元素沒水平居中對齊的假象。 實際上我們Alignment是影響了Row容器水平對齊了,Row子元素的對齊方式,是Row內(nèi)部處理的。

  • mainAxisAlignment:主軸方向?qū)R(RowColumn都有這個屬性)
    Row中:
    start靠左(默認)
    end: 靠右
    center: 居中
    spaceAround: 剩下空間平均分配周圍(每個部件周圍等間距)
    spaceBetween:剩下空間平均分配在小部件中間(兩邊無間距,中間等間距)
    spaceEvenly: 完全等間距

  • crossAxisAlignment:交叉軸方向?qū)R(RowColumn都有這個屬性)
    Row中:
    start居上
    center: 居中(默認)
    end: 居下
    baseline: 文字底部對齊(如果在Column中,必須配合textBaseline使用,后面具體分析)
    stretch: 垂直填充長條

  • Expanded填充式布局,完全等分主軸寬度,會自動換行。row寬度無效,column高度無效(后面會具體分析)

import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      alignment: Alignment(0, 0), // 居中對齊
      // Row 水平(X軸水平方向)
      // Row 是在父控件`Container`的居中展示。內(nèi)部元素默認是從左到右
      child: Row(
        // 主軸方向(start:靠左(默認),end: 靠右, center: 居中
        //          spaceAround:  剩下空間平均分配在周圍(每個部件周圍等間距)
        //          spaceBetween: 剩下空間平均分配在小部件中間(兩邊無間距,中間等間距),
        //          spaceEvenly:  完全等間距)
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        // 交叉軸(y軸方向) (start: 居上,center: 居中(默認), end: 居下,
        //                 baseline: 文字底部對齊,stretch: 垂直填充長條)
        crossAxisAlignment: CrossAxisAlignment.baseline,
        children: <Widget>[
          // 使用Expanded填充式布局,完全等分主軸寬度,會自動換行。row寬度無效,column高度無效
          Expanded(
            child: Container(
                child: Icon(
                  Icons.add,
                  size: 120,
                ),
                color: Colors.red),
          ),
          Expanded(
            child: Container(
                child: Icon(
                  Icons.ac_unit,
                  size: 60,
                ),
                color: Colors.blue),
          ),
          Expanded(
            child: Container(
                child: Icon(
                  Icons.access_alarm,
                  size: 30,
                ),
                color: Colors.white),
          ),
        ],
      ),
    );
  }
}
  • 展示樣式:


    image.png

2.2 Column

  • Column:Y軸(垂直方向)布局,默認垂直撐滿父容器空間。
  • Alignment對齊方式和mainAxisAlignment主軸對齊、crossAxisAlignment交叉軸對齊與Row類似,只是一個是水平一個是垂直方向。
  • 需要注意crossAxisAlignmenttextBaseline類型,必須配合textBaseline使用(后面具體分析)
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      alignment: Alignment(0, 0),
      // Column 垂直(y軸垂直)
      // Column 是在父控件`Container`的居中展示。內(nèi)部元素是從上到下
      child: Column(
        // 主軸方向(start:靠上(默認),end: 靠下, center: 居中,
        //          spaceAround:  剩下空間平均分配在周圍(每個部件周圍等間距)
        //          spaceBetween: 剩下空間平均分配在小部件中間(兩邊無間距,中間等間距),
        //          spaceEvenly:  完全等間距)
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        // 交叉軸(y軸方向) (start: 居左,center: 居中(默認), end: 居右,
        //                 baseline: 文字底部對齊(針對文本,需要配合textBaseLine),
        //                 stretch: 水平填充長條)
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Container(
              child: Icon(
                Icons.add,
                size: 120,
              ),
              color: Colors.red),
          Container(
              child: Icon(
                Icons.ac_unit,
                size: 60,
              ),
              color: Colors.blue),
          Container(
              child: Icon(
                Icons.access_alarm,
                size: 30,
              ),
              color: Colors.white),
        ],
      ),
    );
  }
}
  • 展示樣式:


    image.png
2.2.1 演示baseline(文字底部對齊)
  • baseline: 基于文字底部對齊,我們用下面這個案例多個Text組件高度相同,字體不同)來體會

crossAxisAlignment設(shè)置為baseline時,需要添加textBaseline屬性(Row非必須,但Column必須),可以設(shè)置為:

  • alphabetic字母
  • ideographic中文 (好像沒區(qū)別)
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      // 演示baseline(文字底部對齊)
      alignment: Alignment(0,0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        // row不會報錯,但是Column會報錯,必須指定textBaseline
        crossAxisAlignment: CrossAxisAlignment.baseline,
        // 文本基線textBaseline: alphabetic:英文字符  ideographic: 中文字符
        textBaseline: TextBaseline.alphabetic,
        children: <Widget>[
          Container(
            child: Text("你好!", style: TextStyle(fontSize: 20)),
            color: Colors.red,
            height: 80
          ),
          Container(
              child: Text("我是", style: TextStyle(fontSize: 30)),
              color: Colors.green,
              height: 80
          ),
          Container(
              child: Text("哈哈哈", style: TextStyle(fontSize: 40)),
              color: Colors.blue,
              height: 80
          )],
      )
    );
  }
}
image.png

2.3 Stack

  • Stack:Z軸(前后縱深方向)布局(設(shè)置多個大小不同的組件,就可以看到效果:)
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      alignment: Alignment(0, 0),
      //  Stack 重疊 (Z軸縱深方向)
      // Stack 是在父控件`Container`的居中展示。內(nèi)部元素是從里到外
      child: Stack(
        children: <Widget>[
          Container(
              child: Icon(
                Icons.add,
                size: 120,
              ),
              color: Colors.red),
          Container(
              child: Icon(
                Icons.ac_unit,
                size: 60,
              ),
              color: Colors.blue),
          Container(
              child: Icon(
                Icons.access_alarm,
                size: 30,
              ),
              color: Colors.white),
        ],
      ),
    );
  }
}
  • 展示樣式:


    image.png
2.3.1 positioned
  • 在一個200 * 200 白色背景組件中,如果我們想自主控制每個組件的位置,可以通過positioned來設(shè)置:

  • 具體的參數(shù)設(shè)置,可參考positioned構(gòu)造方法和內(nèi)部描述。

第一個子控件左上角展示,第二個子控件右下角展示,第三個子控件靠右,離頂部60像素展示

import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.yellow,
      alignment: Alignment(0, 0),
      child: Container(
        width: 200,
        height: 200,
        color: Colors.white,
        child: getStack(),
      )
    );
  }

  // 獲取Stack組件
  Widget getStack() {
    return Stack(
      children: <Widget>[
        // 左上(默認)
        Positioned(
          child:
          Container(
              child: Icon(
                Icons.add,
                size: 120,
              ),
              color: Colors.red),
        ),
        // 右下
        Positioned(
          right: 0,
          bottom: 0,
          child:
          Container(
              child: Icon(
                Icons.ac_unit,
                size: 60,
              ),
              color: Colors.blue),
        ),
        // 右上(距離頂部60像素)
        Positioned(
          right: 0,
          top: 60,
          child:
          Container(
              child: Icon(
                Icons.access_alarm,
                size: 30,
              ),
              color: Colors.white),
        ),
      ],
    );
  }
}
  • 展示樣式:


    image.png

3. Expanded自動填充和AspectRatio寬高比

3.1 Expanded自動填充

上面提到Expanded填充式布局,完全等分主軸寬度,會自動換行。(row寬度無效,column高度無效)

  • 現(xiàn)在,我們以多個文本組件為例,體驗兩種情況:
  1. 組件內(nèi)所有子組件都是Expanded,布局情況如何?
  2. 組件內(nèi)Expanded其他組件共存,布局情況如何?

1. 全Expanded

  • Row組件中,child設(shè)置三個Text組件,字體內(nèi)容不一樣,但使用Expanded后,所有組件寬度相等自動換行:
// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.yellow, alignment: Alignment(0, 0), child: getRow());
  }

  // 獲取Row組件
  Widget getRow() {
    return Row(children: <Widget>[
      Expanded(
        child: Container(
            child: Text("你好!你好!你好!你好!你好!", style: TextStyle(fontSize: 20)),
            color: Colors.red),
      ),
      Expanded(
        child: Container(
            child: Text("我是我是我是我是我是", style: TextStyle(fontSize: 30)),
            color: Colors.green),
      ),
      Expanded(
        child: Container(
            child: Text("哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈", style: TextStyle(fontSize: 40)),
            color: Colors.blue),
      ),
    ]);
  }
}
  • 展示樣式:


    image.png

2. Expanded與其他組件共存

  • 我們將第二個子組件改為非Expanded組件,可以看到``非Expanded組件布局完成之后,剩余空間給Expanded組件平分寬度
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.yellow, alignment: Alignment(0, 0), child: getRow());
  }

  // 獲取Row組件
  Widget getRow() {
    return Row(children: <Widget>[
      Expanded(
        child: Container(
            child: Text("你好!你好!你好!你好!你好!", style: TextStyle(fontSize: 20)),
            color: Colors.red),
      ),
      // 第二個子組件不是`Expanded`
      Container(
          child: Text("我是我是我是我是我是我是", style: TextStyle(fontSize: 30)),
          color: Colors.green),
      Expanded(
        child: Container(
            child: Text("哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈", style: TextStyle(fontSize: 40)),
            color: Colors.blue),
      ),
    ]);
  }
}
  • 展示樣式:


    image.png

如果超過屏幕寬度,我們需要對寬高設(shè)置

3.2 AspectRatio 寬高比

  • AspectRatio寬高比部件,可以設(shè)置child子視圖的 寬高比:
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.yellow,
        alignment: Alignment(0, 0),
        child: Container(
          color: Colors.blue,
          width: 300,
          // AspectRatio 寬高比部件
          child: AspectRatio(
            aspectRatio: 2/1,   // 設(shè)置寬高比
            child: Icon(Icons.add),
          ),
        )
    );
  }
}
  • 展示樣式:


    image.png
  • 我們也可以通過屬性設(shè)定,來讀取寬高值。下面分析可變組件不可變組件本質(zhì)區(qū)別

4. StatefulWidget可變部件 與 StatelessWidget不可變部件

  • StatelessWidget: 不可變部件,所有變量都是final修飾,不可以變更狀態(tài)
    (實際每次變更狀態(tài),就是銷毀原部件,根據(jù)新初始值創(chuàng)建新不可變部件
  • StatefulWidget:可變部件,實際是狀態(tài)值(記錄UI狀態(tài))與不可變部件(描述UI)的組合。
    (每一次狀態(tài)值變更,都觸發(fā)不可變部件重新創(chuàng)建。而狀態(tài)值會跟隨StatefulWidget的銷毀而銷毀。)

所以實際上可變部件不可變部件的區(qū)別就是可變部件 多記錄狀態(tài)值,在UI上,本質(zhì)都是通過創(chuàng)建銷毀 不可變部件來進行展示。

4.1 重寫計數(shù)器Demo

  • 我們通過重寫Flutter默認的計數(shù)器Demo,來體驗StatelessWidget不可變部件和StatefulWidget可變部件的區(qū)別:
4.1.1 main.dart創(chuàng)建
  • 指定APP入口App(),使用MaterialApp構(gòu)建APP,將home根視圖設(shè)置為StateManagerDemo ()
import 'package:flutter/material.dart';
import 'package:hello_flutter/state_manager_demo.dart';

// 入口,展示MyWidget組件
void main() => runApp(App());

// 根組件
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 返回`MaterialApp`
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: StateManagerDemo(), // 指定根視圖為`StateManagerDemo`
      theme: ThemeData(
        primaryColor: Colors.blue
      ),
    );
  }
}
4.1.2 StateManagerDemo創(chuàng)建(StatelessWidget不可變部件)
  • 新建state_manager_demo.dart文件,StateManagerDemoStatelessWidget不可變部件,使用Scaffold導航控制器部件設(shè)置appBar導航欄、body內(nèi)容和floatingActionButton浮動按鈕。
    floatingActionButton新增onPressed點擊事件,觸發(fā)回調(diào)函數(shù)(每次點擊,count+1,打印count值)
import 'package:flutter/material.dart';

class StateManagerDemo extends StatelessWidget {

  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("StateManagerDemo"),
      ),
      body: Center(
        child: Chip(label: Text("$count"),), // 全圓角的組件
      ),
      // 浮動按鈕(默認右下角)
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        // 點擊觸發(fā): 給一個無參回調(diào)函數(shù),可以直接給`(){}`,也可以外部創(chuàng)建一個函數(shù),傳入函數(shù)名
        // 每次點擊,count+1,打印count值
        onPressed: (){
          count += 1;
          print("count = $count");
        },
      )
    );
  }
}
  • 當我們在StatelessWidget不可變部件中創(chuàng)建變量未使用final修飾,而使用var修飾時,會有提示:
    This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: StateManagerDemo.count
    image.png

    我們忽略不管(暫時不管,后面做比較),繼續(xù)運行,可以看到界面運行正常:
    image.png
  • 點擊浮動按鈕 ?號,打印臺可以看到count數(shù)遞增了,但是頁面中心的UI未同步更新。
image.png

我們發(fā)現(xiàn),不用final修飾變量count,也可以正常運行,并且count完成了計數(shù)任務(wù),但是界面UI未更新

  • 如果我們使用final聲明變量,就等同于swift中的let聲明,是不可變的,不能進行count+=1的操作。

實際上,這是Flutter底層渲染原理決定的,StatelessWidget不可變部件,本身不支持記錄狀態(tài),每次都是銷毀重新渲染UI,所以編譯器提示我們不可變部件內(nèi)全部使用final修飾。這樣從編碼層減少錯誤產(chǎn)生。

  • 如果一定需要記錄狀態(tài),請使用StatefulWidget可變部件。
4.1.3 StateManagerDemo創(chuàng)建(StatefulWidget可變部件)
  • 上面說到,如果要記錄狀態(tài),請使用StatefulWidget可變部件。
    StatefulWidget本身并不是直接改變UI,而是在StatelessWidget不可變部件的基礎(chǔ)上,多了一個管理狀態(tài)的State組件。這個組件記錄狀態(tài),并決定了返回的UI樣式。
  • 所以,記錄狀態(tài)的變量(count)和返回組件構(gòu)造方法(build),都應(yīng)該放在State中。 而StatefulWidget就是直接返回State根據(jù)當前數(shù)據(jù)build返回的UI部件。
  • 需要補充說明的是: 我們需要部件及時更新,需要在數(shù)據(jù)變動處,調(diào)用setState(){}更新狀態(tài)。
import 'package:flutter/material.dart';

class StateManagerDemo extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // 直接返回State根據(jù)最新數(shù)據(jù)build的組件
    return _StateManagerState();
  }
}

class _StateManagerState extends State<StateManagerDemo> {
  // 記錄計數(shù)
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("StateManagerDemo"),
        ),
        body: Center(
          child: Chip(label: Text("$count"),), // 全圓角的組件
        ),
        // 浮動按鈕(默認右下角)
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          // 點擊觸發(fā): 給一個無參回調(diào)函數(shù),可以直接給`(){}`,也可以外部創(chuàng)建一個函數(shù),傳入函數(shù)名
          // 每次點擊,count+1,打印count值
          onPressed: (){
            count += 1; // 數(shù)據(jù)變動
            setState(() {}); // 更新狀態(tài)(自動觸發(fā)UI的重新渲染)
            print("count = $count");
          },
        )
    );
  }
}
  • 頁面樣式:


    image.png

總結(jié):

  • StatelessWidget不可變部件:

    直接build返回UI部件即可,變量都使用final修飾,不可更改。每次通過構(gòu)造方法傳入新數(shù)據(jù),都會進行舊部件銷毀新部件創(chuàng)建。

  • StatefulWidget 可變部件:

  1. State組件管理狀態(tài),并build返回最新狀態(tài)UI部件。每次狀態(tài)變更,需要setState(() {})更新UI部件
    (渲染機制會自動銷毀舊UI部件創(chuàng)建新UI部件,State的生命周期與StatefulWidget一致)

  2. StatefulWidget可變部件只需要返回State對象即可
    (實際是返回Statebuild最新UI部件

【思考】

實際上,在Flutter開發(fā)中我們需要注意一些優(yōu)化點。

  • 我們整個頁面,其實只有+號按鈕點擊,觸發(fā)中心的計數(shù)UI更新。并不需要每次數(shù)據(jù)變化,都更新整個外部部件。
  • 我們可以將Chip更新為一個StatefulWidget可變部件,點擊+號時,通過回調(diào)Chip部件重新渲染即可。

本節(jié),我們主要分析自動布局幾種方式,以及StatefulWidgetStatefulWidget的區(qū)別。
下一節(jié),Flutter入門四:搭建項目、資源調(diào)用、簡單開發(fā)。從項目入手,一步步理解熟悉Flutter。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容