Flutter-詳解布局

上一章我們詳細的學習了 Flutter 中的Widget,這一章我們將要學習 Flutter 的布局, 在上一章我們了解到了:Everything is a widget,在 Flutter 中幾乎所有的對象都是一個 Widget ,當然也包括布局,Flutter 的布局系統基于 Widget 樹,通過組合不同的布局 Widget 實現復雜的 UI,你在 Flutter 應用程序中能直接看到的圖像,圖標和文本都是 Widget。此外不能直接看到的也是 Widget,如用來排列、限制和對齊可見 Widget 的行、列和網格。

DEMO

布局規則

1.約束由父組件向子組件傳導,大小由子組件向父組件傳導,位置由父組件決定。

2.布局Widget是Flutter UI的基礎,理解它們的規則和適用場景非常重要

核心布局控件 (多子組件)

1. Row & Column

  • 說明: Row水平排列子Widget,Column垂直排列子Widget

  • 規則: 子Widget可以是非彈性(不擴展)或彈性(Expanded)。主軸-mainAxisAlignment(Row 為 X 軸,Column 為 Y 軸)和交叉軸-crossAxisAlignment(Row 為 Y 軸,Column 為 X 軸)

  • 注意:

  • 1.當子Widget的總長度超過主軸長度時,會溢出(常見錯誤:黃色黑色條紋警告)。可以使用Expanded或Flexible來避免,或者使用ListView代替

  • 2.如果沒有指定主軸對齊方式(mainAxisAlignment)和交叉軸對齊方式(crossAxisAlignment),默認是start和stretch

  • 推薦: 用于線性排列多個子Widget,如:導航欄(Row)、表單項列表(Column)、圖文混排(Row + Column 嵌套),如果子Widget較多且可能超出屏幕,考慮使用ListView。

Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主軸均勻分布crossAxisAlignment: CrossAxisAlignment.center, // 交叉軸居中children: [Container(width: 50, height: 50, color: Colors.red),Expanded(child: Container(height: 30, color: Colors.green)), // 彈性填充剩余空間Container(width: 50, height: 50, color: Colors.blue),],
)

ROW

Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主軸均勻分布crossAxisAlignment: CrossAxisAlignment.center, // 交叉軸居中children: [Container(width: 50, height: 50, color: Colors.red),Expanded(child: Container(height: 30, color: Colors.green)), // 彈性填充剩余空間Container(width: 50, height: 50, color: Colors.blue),],
)

Column

2. Stack

  • 說明: 將子Widget重疊在一起。第一個子Widget在底部,后面的依次在上層

  • 規則: 子Widget可以使用Positioned來定位,否則放置在左上角。如果沒有定位,則根據alignment屬性對齊(默認左上角),子 Widget 順序決定繪制順序(后繪制的在上層)

  • 注意: 如果不使用Positioned,且Stack沒有指定大小,那么Stack會調整到包裹所有未定位的子Widget(但如果有定位的子Widget,則定位的子Widget不影響Stack大小), Positioned 的子 Widget 可能超出 Stack 邊界(需 Clip 處理)

  • 推薦: 用于需要重疊的布局,如圖片上的標簽、浮動按鈕、自定義進度條

Stack(alignment: Alignment.center, // 所有子Widget居中children: [Container(width: 200, height: 200, color: Colors.blue), // 底層Positioned(bottom: 10,right: 10,child: Container(width: 50, height: 50, color: Colors.red), // 定位到右下),const Text("Stack Example"), // 文字居中],
)

Stack

3. Wrap

  • 說明: 當一行(或一列)放不下子Widget時,自動換行(或換列)

  • 規則: 可以設置方向(水平或垂直)、間距(spacing-主軸方向間距)和行間距(runSpacing-交叉軸方向行間距)

  • 注意: 與Row不同,Wrap不會溢出,而是換行。但要注意如果子Widget很大且沒有足夠空間,可能會超出屏幕(在換行方向),按需換行,避免溢出,子 Widget 數量極大時應用 Wrap.builder,計算間距不要忽略 spacing 和 runSpacing 不然容易導致布局重疊

  • 推薦: 用于流式布局,如標簽列表、篩選條件欄、自適應按鈕組

Wrap(spacing: 8.0, // 水平間距runSpacing: 4.0, // 垂直間距(行間距)children: [Chip(label: Text('標簽1')),Chip(label: Text('標簽2')),Chip(label: Text('標簽3')),Chip(label: Text('標簽4')),Chip(label: Text('標簽5')),Chip(label: Text('標簽6')),Chip(label: Text('標簽7')),Chip(label: Text('標簽8')),Chip(label: Text('標簽9')),Chip(label: Text('標簽10')),Chip(label: Text('標簽11')),Chip(label: Text('標簽12')),],
)

Wrap

4. Flow

  • 說明:

  • 1.Flow 需要自定義布局邏輯時使用,通過delegate控制每個子Widget的位置和大小。性能較好,因為子Widget可以獨立定位而不影響父Widget

  • 2.動態增減子元素:在delegate中監聽數據變化,調用context.invalidateLayout()重布局

  • 規則: Flow:高性能自定義布局(需實現 FlowDelegate)

  • 注意: 通過委托計算(delegate-calculated)實現動態自適應,勝任復雜場景,當布局復雜度低時,優先用Wrap簡化開發

  • 推薦: 復雜動畫布局,避免Wrap的多次測量,適合動態加載/高頻更新場景

class MyFlowDelegate extends FlowDelegate {@overridevoid paintChildren(FlowPaintingContext context) {var x = 0.0, y = 0.0;for (var i = 0; i < context.childCount; i++) {// 動態計算每個子組件位置final childSize = context.getChildSize(i)!;context.paintChild(i, transform: Matrix4.translationValues(x, y, 0));x += childSize.width * 0.8; // 重疊效果y += childSize.height * 0.2;}}@overridebool shouldRepaint(covariant FlowDelegate oldDelegate) => true;
}Flow(delegate: MyFlowDelegate(),children: List.generate(5,(index) => Container(width: 80,height: 80,color: Colors.primaries[index],)),
)

Flow

5. ListView

  • 說明: 可滾動的線性布局,支持水平和垂直方向

  • 規則: 如果子Widget數量固定,使用ListView(children: […]),如果數量多,使用ListView.builder按需構建

  • 注意: 直接使用children方式構建大量子Widget會導致性能問題,因為會一次性構建所有子Widget。對于長列表,務必使用builder,嵌套 ListView 時需明確滾動方向

  • 推薦: 用于需要滾動的列表,例如消息列表、設置菜單、長數據展示

ListView.builder(itemCount: 100,itemBuilder: (context, index) {return ListTile(title: Text('Item $index'),);},)

ListView

6. GridView

  • 說明: 網格布局

  • 規則: 有多種構造方式:GridView.count(固定列數)、GridView.extent(固定最大交叉軸長度)、GridView.builder(按需構建)

  • 注意: 同樣要注意性能,長列表使用builder,要正確設置 childAspectRatio 不然容易導致單元格變形

  • 推薦: 用于展示網格狀內容,如圖片墻、產品網格、儀表盤卡片

var array = [Colors.blue, Colors.red, Colors.yellow];
GridView.count(crossAxisCount: 3, // 每行3列childAspectRatio: 1.0, // 寬高比children: List.generate(9,(index) => Container(decoration: BoxDecoration(color: array[index % 3],borderRadius: BorderRadius.circular(8.0),),child: Center(child: Text('Item $index')),)),
)
)

GridView

7. Table

  • 說明: 是一種基于表格模型的布局方式

  • 規則: 每一行的高度由內容決定,列寬可通過 columnWidths 屬性自定義。
    支持多種列寬類型:FixedColumnWidth(固定寬度)、FlexColumnWidth(彈性比例)、FractionColumnWidth(百分比)等,通過 rowHeight 屬性設置固定行高,或通過 TableRow 的 height 屬性單獨調整某一行,在 TableRow 中嵌套 Row、Column 或其他布局組件,實現復雜內容排版,通過 MediaQuery 動態調整列寬比例,適配不同屏幕尺寸

  • 注意: 避免在大量數據中直接使用 Table,推薦結合 ListView 或 CustomScrollView 分頁加載

  • 推薦: 適用于需要行列對齊的復雜場景,Stack + Table:在表格中疊加其他組件(如懸浮按鈕),Wrap + Table:結合彈性布局實現自適應表格

Table(columnWidths: const {0: FixedColumnWidth(80), // 第一列固定寬度1: FlexColumnWidth(2), // 第二列彈性寬度(占比更大)2: IntrinsicColumnWidth(), // 第三列自適應內容寬度},border: TableBorder.all(color: Colors.black, width: 1.0),defaultVerticalAlignment: TableCellVerticalAlignment.middle, // 垂直居中children: const [TableRow(// 表頭行decoration: BoxDecoration(color: Colors.grey),children: [Text('姓名', textAlign: TextAlign.center),Text('性別', textAlign: TextAlign.center),Text('年齡', textAlign: TextAlign.center),],),TableRow(// 數據行children: [Text('張三'),Text('男'),Text('25'),],),],
)

Table

8. CustomScrollView

  • 說明: 是 Flutter 處理復雜滾動場景的核心工具,通過靈活組合 Sliver 組件,可實現頭部折疊、多類型布局、吸頂等高級交互,

  • 規則: 所有子組件共享同一個滾動控制器,確保聯動滑動,僅渲染可見區域的 Sliver,其子組件必須是 Sliver 家族成員(如 SliverList、SliverGrid、SliverToBoxAdapter 等),普通組件需通過 SliverToBoxAdapter 包裹才能嵌入

  • 注意: Sliver 的使用規范及性能優化,必要時結合 NestedScrollView 解決嵌套滾動問題,避免嵌套滾動組件

  • 推薦: 實現統一且復雜的滾動效果。它像“粘合劑”一樣將不同布局粘合為整體滾動區域,解決嵌套滾動沖突問題(例如 ListView 嵌套 GridView 需手動指定高度)

Table(columnWidths: const {0: FixedColumnWidth(80), // 第一列固定寬度1: FlexColumnWidth(2), // 第二列彈性寬度(占比更大)2: IntrinsicColumnWidth(), // 第三列自適應內容寬度},border: TableBorder.all(color: Colors.black, width: 1.0),defaultVerticalAlignment: TableCellVerticalAlignment.middle, // 垂直居中children: const [TableRow(// 表頭行decoration: BoxDecoration(color: Colors.grey),children: [Text('姓名', textAlign: TextAlign.center),Text('性別', textAlign: TextAlign.center),Text('年齡', textAlign: TextAlign.center),],),TableRow(// 數據行children: [Text('張三'),Text('男'),Text('25'),],),],
)

CustomScrollView

9. IndexedStack

  • 說明: IndexedStack 是 Stack 的子類(extends Stack),是一種層疊布局組件,專門用于在同一位置切換顯示不同子組件

  • 規則: IndexedStack 繼承自 Stack 組件,通過 index 屬性控制當前顯示的子組件。與 Stack 的疊加顯示不同,IndexedStack 僅渲染指定索引的子項,其他子項處于隱藏狀態,尺寸由最大的子組件決定,與當前顯示的 index 無關。支持 alignment 對齊屬性,可結合 Positioned 實現精確定位,所有子組件會預先加載到內存中,適合需要保持面狀態的場景(如 Tab 切換)。通過 Offstage 隱藏非活躍子項,而非銷毀

  • 注意: 子組件過多或復雜時可能引發內存壓力,通過 setState 修改 index 實現無動畫切換,適合高頻操作場景,可通過 RepaintBoundary 優化繪制性能,需要精確定位時,推薦組合使用 Positioned 而非 Align

  • 推薦: 優先用于少量子項的場景,復雜頁面建議結合 PageView 使用

class DynamicIndexedStack extends StatefulWidget {@override _DynamicIndexedStackState createState() => _DynamicIndexedStackState();
}class _DynamicIndexedStackState extends State<DynamicIndexedStack> {int _currentIndex = 0;final List<Widget> _pages = [PageWidget(title: "頁面1", color: Colors.red), PageWidget(title: "頁面2", color: Colors.green), PageWidget(title: "頁面3", color: Colors.blue), ];@override Widget build(BuildContext context) {return Column(children: [// 切換按鈕 Row(mainAxisAlignment: MainAxisAlignment.center, children: [_buildButton(0, "頁面1"),_buildButton(1, "頁面2"),_buildButton(2, "頁面3"),],),// IndexedStack區域 Expanded(child: IndexedStack(index: _currentIndex,children: _pages,),)],);}Widget _buildButton(int index, String text) {return ElevatedButton(onPressed: () => setState(() => _currentIndex = index),style: ElevatedButton.styleFrom( backgroundColor: _currentIndex == index ? Colors.amber  : null,),child: Text(text),);}
}// 帶狀態的頁面組件(驗證狀態保留)
class PageWidget extends StatefulWidget {final String title;final Color color;PageWidget({required this.title,  required this.color}); @override _PageWidgetState createState() => _PageWidgetState();
}class _PageWidgetState extends State<PageWidget> {int _counter = 0;@override Widget build(BuildContext context) {return Container(color: widget.color.withOpacity(0.3), child: Center(child: Column(mainAxisSize: MainAxisSize.min, children: [Text(widget.title,  style: TextStyle(fontSize: 24)),ElevatedButton(onPressed: () => setState(() => _counter++),child: Text("計數: $_counter"),),],),),);}
}

IndexedStack

10. Flex

  • 說明: 動態增減子元素:使用Key觸發子樹重建

  • 規則: Flex:更底層的 Row/Column(需配合 Flexible)

  • 注意: 通過約束驅動(constraint-driven)實現空間比例分配,適合結構化界面

  • 推薦: 自定義排版

Flex(// 等價于Columndirection: Axis.vertical,children: [Expanded(flex: 1, // 占1/3高度child: Container(color: Colors.red),),Expanded(flex: 2, // 占2/3高度child: Container(color: Colors.blue),),],
)

Flex

11. ListBody

  • 說明: 是一個沿指定軸方向順序排列子組件的布局控件,其核心特性是不限制主軸空間,需配合父容器約束使用

  • 規則: 必須嵌套在ListView、Column等有界容器內,子組件在交叉軸(如水平方向)自動拉伸,無需額外設置,ListBody僅負責線性排列,不包含滾動機制,ListView = ListBody + 滾動功能,適合長列表

  • 注意: 子項數量動態或可能超出屏幕時,必須用ListView替代,避免布局溢出,子組件尺寸變化會觸發整個ListBody重新布局,影響性能,對動態子項優先使用ListView.builder ,僅構建可見區域組件

  • 推薦: 固定數量子項的線性排列(如靜態菜單欄、卡片組),需父容器明確尺寸約束(如SizedBox、ConstrainedBox)

SingleChildScrollView(// 提供滾動支持child: ListBody(mainAxis: Axis.vertical,reverse: false,children: <Widget>[Container(height: 100, color: Colors.blue[50]),Container(height: 100, color: Colors.blue[100]),Container(height: 100, color: Colors.blue[200]),],),
)

ListBody

單子組件布局控件

1. Container

  • 說明: 最常用的布局 Widget,可以包含一個子Widget,并可以設置 padding、margin、邊框、背景等

  • 規則: 如果沒有子 Widget,Container 會盡可能大;如果有子 Widget,則根據子 Widget 和自身約束調整大小

  • 注意: 在沒有約束的情況下,Container會盡可能大(比如占滿整個屏幕);在有約束的情況下(如父Widget是Column),Container如果沒有子Widget,則會收縮到沒有大小

  • 推薦: 作為其他Widget的容器,用于裝飾或設置間距

Container(padding: const EdgeInsets.all(8.0),margin: const EdgeInsets.all(16.0),decoration: BoxDecoration(color: Colors.blue,borderRadius: BorderRadius.circular(8.0),),child: const Text('Hello World'),
)

Container頁面

2. Padding

  • 說明: 給子Widget設置內邊距

  • 規則: 只有一個子Widget

  • 注意: 與Container的padding不同,Padding是一個獨立的Widget

  • 推薦: 當需要內邊距但不需要Container的其他裝飾時使用

Container(decoration: BoxDecoration(color: Colors.blue,borderRadius: BorderRadius.circular(12),),child: const Padding(padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),child: Text("Flutter Padding 背景顏色示例",style: TextStyle(color: Colors.white),),),
)

Padding

3. Align

  • 說明: 控制子Widget在父Widget中的對齊方式

  • 規則: 通過alignment屬性設置對齊方式(如Alignment.center)

  • 注意: 如果父Widget沒有約束,Align會盡可能大;如果有約束,則根據約束調整大小,并將子Widget對齊

  • 推薦: 用于精確控制子Widget的位置

Container(height: 200,width: 200,color: Colors.grey,child: Align(alignment: Alignment.bottomRight,child: Container(width: 50, height: 50, color: Colors.red),),
)

Align

4. Center

  • 說明: 用于將子組件在父組件中水平和垂直居中顯示(是Align(alignment: Alignment.center)的簡寫),Center 組件繼承自 Align,因此它本質上是一個簡化版的 Align,專門用于居中布局

  • 規則: 如果 Center 沒有子組件且父容器提供無界約束(unbounded),則 Center 會盡可能縮小自身尺寸,如果父容器提供有界約束(bounded),則 Center 會擴展到父容器的大小。有子組件時,Center 的尺寸會根據子組件的大小和父容器的約束動態調整。
    如果設置了 widthFactor 或 heightFactor,子組件的尺寸會按比例縮放(例如 widthFactor: 2 會使子組件寬度翻倍)

  • 注意: widthFactor 和 heightFactor用于調整子組件的寬度和高度比例,避免嵌套過多層級,如果父容器限制了尺寸(如 SizedBox),需確保 Center 的子組件尺寸在約束范圍內

  • 推薦: 快速居中單個組件,與復雜布局結合,響應式布局

Center(child: Container(width: 200,height: 100,color: Colors.blue,child: const Text("居中內容",style: TextStyle(color: Colors.white),),),
)

Center

5. AspectRatio

  • 說明: 強制子Widget保持指定的寬高比

  • 規則: 需要設置aspectRatio屬性(寬/高)

  • 注意: 父Widget必須提供約束,否則無法計算

  • 推薦: 用于需要固定寬高比的場景,如播放器

AspectRatio(aspectRatio: 16 / 9,child: Container(color: Colors.blue),
)

AspectRatio

6. ConstrainedBox

  • 說明: 對子Widget施加額外的約束(BoxConstraints)

  • 規則: 可以設置最小、最大寬高

  • 注意: 如果子Widget本身有約束,可能會沖突,此時以更緊的約束為準

  • 推薦: 用于需要限制子Widget大小的情況

ConstrainedBox(constraints: const BoxConstraints(minWidth: 100,maxWidth: 200,minHeight: 50,maxHeight: 100,),child: Container(color: Colors.red),
)

ConstrainedBox

7. UnconstrainedBox

  • 說明: 解除父容器的約束,允許子控件按照自身大小繪制,然后根據實際大小再約束

  • 規則: 子組件,其尺寸不會受到UnconstrainedBox的約束,若子組件尺寸超過父容器區域,Debug模式下會顯示黃色溢出警告(Release模式會裁剪),若父級有多個約束組件(如嵌套的ConstrainedBox),UnconstrainedBox只能突破直接父級的約束,無法突破更高層約束

  • 注意: constrainedAxis:null(默認):完全解除約束 / Axis.horizontal :僅水平方向解除約束,垂直方向仍受父級限制 / Axis.vertical :僅垂直方向解除約束,水平方向受限制,過度使用可能導致布局計算復雜化,建議僅在必要時使用

  • 推薦: 適用于需要突破父容器約束的場景,例如在ListView或AppBar中保持子組件原始尺寸

ListView(children: [// 原始Container被拉伸至屏幕寬度 Container(width: 200, height: 100, color: Colors.red), // 使用UnconstrainedBox保持原始尺寸UnconstrainedBox(child: Container(width: 200, height: 100, color: Colors.blue), ),],
)

UnconstrainedBox

8. SizedBox

  • 說明: 可以指定固定大小的盒子,也可以用于設置間隔

  • 規則: 如果指定了width和height,則強制子Widget使用該大小(如果子Widget有約束,則可能會強制調整)

  1. SizedBox.shrink() 零尺寸占位符
  2. SizedBox.expand() 擴展填充占位符
  3. SizedBox.fromSize() 指定尺寸占位符
  • 注意: 當沒有子Widget時,SizedBox會占據指定大小的空間;有子Widget時,則強制子Widget大小為指定大小

  • 推薦: 用于固定尺寸的盒子或間隔(如SizedBox(width: 10))

SizedBox(width: 100,height: 100,child: Container(color: Colors.green),
)

SizedBox

9. FractionallySizedBox

  • 說明: 子Widget的大小相對于父Widget的百分比

  • 規則: 需要設置widthFactor和heightFactor(0.0到1.0)

  • 注意: 父Widget必須提供約束,否則無法計算百分比

  • 推薦: 用于需要相對父容器百分比大小的場景

Container(width: 200,height: 200,color: Colors.blue,child: FractionallySizedBox(widthFactor: 0.5,heightFactor: 0.5,child: Container(color: Colors.red),),
)

FractionallySizedBox

10. Transform

  • 說明: 對子Widget進行矩陣變換(平移、旋轉、縮放等)

  • 規則: 變換不影響布局,變換是在布局之后進行的,所以可能會超出父Widget區域

  • 注意: 變換后可能會超出父Widget范圍,導致被裁剪(可以使用Clip.none避免)

  • 推薦: 用于需要變換的場景,如旋轉一個圖標

Transform.rotate(angle: 3.14 / 4, // 45度child: Container(width: 100, height: 100, color: Colors.blue),
)

Transform

11. Baseline

  • 說明: 根據子Widget的基線對齊(常用于文本), Baseline 的對齊方式有兩種類型:

    alphabetic: 對齊字母底部基線(適用于英文等拉丁文字)

    ideographic: 對齊表意文字基線(適用于中文、日文等)

  • 規則: 需要設置baseline和baselineType(如TextBaseline.alphabetic)

  • 注意: 需要子Widget有基線(如Text),否則無效

  • 推薦: 尤其適用于文字排版場景,處理混合文字(如中英文)時,通過baselineType區分基線類型,避免排版錯亂

const Row(children: [Baseline(baseline: 50.0,baselineType: TextBaseline.alphabetic,child: Text('Hello', style: TextStyle(fontSize: 20)),),Baseline(baseline: 50.0,baselineType: TextBaseline.alphabetic,child: Text('World', style: TextStyle(fontSize: 30)),),],
)

Baseline

12. FittedBox

  • 說明: 對子Widget進行縮放和位置調整,以使其適應可用空間

  • 規則: 通過fit屬性設置適應方式(如BoxFit.contain)

  • 注意: 如果子Widget沒有約束,可能會出現問題

  • 推薦: 用于需要縮放的場景,如保持圖片比例并適應容器

Container(width: 200,height: 100,color: Colors.amber,child: FittedBox(fit: BoxFit.contain,child: Image.network('https://upload-images.jianshu.io/upload_images/1976231-cb638ee25dbc7368.png'),),
)

FittedBox

13. OverflowBox

  • 說明: 允許子Widget超出父Widget的約束,從而在父容器之外顯示

  • 規則: 設置自己的約束,子Widget可以突破父Widget的約束

  • 注意: 可能導致布局溢出(無警告),使用時需注意

  • 推薦: 在需要突破約束時使用,但需謹慎

Container(color: Colors.green,width: 200.0,height: 200.0,padding: const EdgeInsets.all(50.0),child: OverflowBox(alignment: Alignment.topLeft,maxWidth: 400.0,maxHeight: 400.0,child: Container(color: Colors.blueGrey,width: 300.0,height: 300.0,),),
)

OverflowBox

14. LimitedBox

  • 說明: 一個用于限制子組件最大尺寸的布局組件,當父Widget沒有約束時,限制子Widget的最大寬高

  • 規則: 在無約束時生效(如在ListView中,ListView沿著主軸方向有約束,但交叉軸方向無約束,此時在交叉軸方向使用LimitedBox可以限制最大寬高)

  • 注意: 在父Widget有約束時無效

  • 推薦: 在ListView中限制交叉軸方向的大小

Row(children: [Container(color: Colors.grey,width: 100.0,),LimitedBox(maxWidth: 150.0,maxHeight: 150.0,child: Container(color: Colors.lightGreen,width: 250.0,height: 250.0,),),],
)

LimitedBox

15. IntrinsicWidth/IntrinsicHeight

  • 說明: 調整子Widget到其內部內容的高度或寬度(有性能問題,慎用)

  • 規則: 迫使子Widget計算其內部內容的最大高度或寬度,并調整其他子Widget到相同高度或寬度

  • 注意: 性能差,因為需要遍歷子Widget兩次(一次測量,一次布局)

  • 推薦: 盡量避免使用,尋找其他布局方式替代。如果必須使用,注意性能影響

Column(mainAxisAlignment: MainAxisAlignment.start,children: [IntrinsicWidth(stepWidth: 100,child: Container(color: Colors.blue,child: Center(child: Container(color: Colors.red,width: 50,height: 50,),),),),],
)

IntrinsicWidth

16. CustomSingleChildLayout/CustomMultiChildLayout

  • 說明: 使用自定義的布局代理進行布局,可以實現復雜的布局效果,CustomSingleChildLayout 是一個用于控制單個子組件布局的組件,CustomMultiChildLayout 是一個用于控制多個子組件布局的組件

  • 規則: 需要自定義LayoutDelegate,SingleChildLayoutDelegate 來實現布局邏輯。該組件適用于需要對單個子組件進行復雜或非常規布局的場景,例如需要精確控制子組件的位置或尺寸時,MultiChildLayoutDelegate 來實現布局邏輯,適用于需要對多個子組件進行復雜布局的場景

  • 注意: 相對復雜,需要自己實現布局邏輯

  • 推薦: 當其他布局Widget無法滿足需求時使用,自定義的網格布局、層疊布局

CustomSingleChildLayout(delegate: _MyDelegate(),child: Container(color: Colors.blue), 
)class _MyDelegate extends SingleChildLayoutDelegate {@overrideBoxConstraints getConstraintsForChild(BoxConstraints constraints) {return constraints.loosen();  // 解除子組件約束 }@overrideOffset getPositionForChild(Size size, Size childSize) {return Offset(size.width/2,  0); // 頂部居中}@overridebool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {// TODO: implement shouldRelayoutthrow UnimplementedError();}
}

CustomSingleChildLayout

Container(width: 200.0,height: 100.0,color: Colors.yellow,child: CustomMultiChildLayout(delegate: TestLayoutDelegate(),children: <Widget>[LayoutId(id: TestLayoutDelegate.title,child: Container(color: Colors.red,child: const Text('Title'),),),LayoutId(id: TestLayoutDelegate.description,child: Container(color: Colors.green,child: const Text('Description'),),),],),
)class TestLayoutDelegate extends MultiChildLayoutDelegate {static const String title = 'title';static const String description = 'description';@overridevoid performLayout(Size size) {final BoxConstraints constraints = BoxConstraints(maxWidth: size.width);final Size titleSize = layoutChild(title, constraints);positionChild(title, const Offset(0.0, 0.0));final double descriptionY = titleSize.height;layoutChild(description, constraints);positionChild(description, Offset(0.0, descriptionY));}@overridebool shouldRelayout(TestLayoutDelegate oldDelegate) => false;
}

CustomMultiChildLayout

17. Placeholder

  • 說明: 主要用于在開發過程中快速構建頁面骨架,加速頁面流程的運行。它可以在布局中占位,幫助開發者快速預覽頁面結構。Placeholder 的大小默認適合其容器,若位于無界空間(unbounded space),則根據 fallbackWidth 和 fallbackHeight 調整大小。Placeholder 支持自定義顏色、線條寬度、fallbackHeight 和 fallbackWidth

  • 規則: 可以在 Row 或 Column 中使用 Placeholder,但需要通過 fallbackHeight 和 fallbackWidth 限定大小,在無界空間中,可以使用 fallbackWidth 和 fallbackHeight 來限定占位符的大小

  • 注意: 雖然 Placeholder 是一個簡單的占位符,但在復雜的布局中過多使用可能會影響性能,因此應合理使用

  • 推薦: 在頁面開發初期,可以使用 Placeholder 來快速構建頁面骨架,幫助開發者快速預覽頁面結構

const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[SizedBox(height: 100,child: Row(children: [Placeholder(fallbackWidth: 100,color: Colors.green,strokeWidth: 10,),],),),SizedBox(width: 100,child: Column(children: [Placeholder(fallbackHeight: 100,color: Colors.green,strokeWidth: 10,),],),),],),
);

Placeholder

彈性布局輔助控件

1. Expanded

  • 說明: 作為Row、Column或Flex的子Widget,可以擴展以填充主軸上的可用空間

  • 規則: 必須位于Row、Column或Flex內。flex參數用于分配剩余空間的比例(默認為1)

  • 注意: 在Row或Column中,如果子Widget的總主軸長度超過可用空間,使用Expanded可以避免溢出,因為它會壓縮子Widget(強制子Widget適應分配的空間)

  • 推薦: 在需要子Widget按比例分配空間時使用,比例分割布局(如 70%-30% 面板)

Row(children: [Expanded(flex: 2, child: Container(color: Colors.red)), // 占2/3空間Expanded(flex: 1, child: Container(color: Colors.green)), // 占1/3空間],
)

Expanded

2. Flexible

  • 說明: 與Expanded類似,但更靈活。Expanded是Flexible(fit: FlexFit.tight)的簡寫。Flexible的默認fit是FlexFit.loose,即子Widget不需要填滿分配的空間

  • 規則: 同樣必須位于Row、Column或Flex內

  • 注意: Flexible與Expanded的區別:Expanded會強制子Widget填滿空間,而Flexible則允許子Widget使用更小的空間

  • 推薦: 當需要子Widget有彈性但不需要強制填滿時使用,自適應表格列

Row(children: [Flexible(child: Container(height: 50, color: Colors.red),),Flexible(child: Container(height: 100,color: Colors.green), // 這個容器高度100,但Row高度由最高子Widget決定(100),紅色容器高度50,但會被拉伸到100(因為Row交叉軸是垂直方向,默認是stretch)),],
)

注意:Row的交叉軸對齊默認是stretch,所以子Widget在垂直方向會被拉伸。如果不想被拉伸,可以設置crossAxisAlignment: CrossAxisAlignment.start。

Flexible

3. Spacer

  • 說明: 在彈性布局中占據剩余空間(相當于Expanded包裹一個空的SizedBox),是一個用于在 Row、Column 或 Flex 布局中填充剩余空間的空白組件

  • 規則: 它會根據 flex 參數按比例占據主軸(水平或垂直方向)的可用空間,類似彈性布局中的“彈簧”,flex(默認值為1):指定空間分配比例。例如,兩個 Spacer(flex: 2) 和 Spacer(flex: 1) 會按 2:1 分配剩余空間

  • 注意: 必須作為 Row、Column 或 Flex 的直接子組件,空間不足時可能引發溢出錯誤(需確保父容器有足夠空間)

  • 推薦: 等間距按鈕組,左右對齊元素,多比例空間分配

Column(children: [const Text("頂部標題",style: TextStyle(color: Colors.blue,),),const Spacer(flex: 1),const Row(children: [Text("左側文本",style: TextStyle(color: Colors.black,),),Spacer(flex: 2),Text("中間文本",style: TextStyle(color: Colors.red,)),Spacer(flex: 1),Icon(Icons.star),],),const Spacer(flex: 3), // 下方大面積留白ElevatedButton(onPressed: () {},child: const Text("確認",style: TextStyle(color: Colors.blue,),),),],
)

Spacer

層疊布局輔助控件

1. Positioned

  • 說明: 僅作為 Stack 的直接子組件使用,用于在層疊布局中精確定位子組件,類似 CSS 的絕對定位

  • 規則: left、right、top、bottom(控制子組件相對于父 Stack 邊緣的距離),width、height(可選,用于固定子組件尺寸),支持 Positioned(topLeft/bottomRight) 等快捷方式

  • 注意: 若設置 left + right,則不能同時設置 width;同理適用于 top + bottom 與 height 的沖突,未設置尺寸時,需至少指定兩個相對定位屬性(如 left 和 right)以確定布局范圍

  • 推薦: 結合 MediaQuery 動態計算定位值,適配不同屏幕尺寸,使用 Align 簡化對齊邏輯

Stack(fit: StackFit.expand, // 填充父容器children: [Positioned(left: 10,right: 10,height: 80,child: Container(color: Colors.orange), // 水平居中且寬度自適應),Positioned(top: 10,bottom: 10,width: 80,child: Container(color: Colors.purple), // 垂直居中且高度自適應),],
)

Positioned

2. PositionedDirectional

  • 說明: 它用于在 Stack 中根據 textDirection 的方向來定位子部件。與 Positioned 不同,PositionedDirectional 接受的是一個偏移量和一個 textDirection 參數,從而決定子部件相對 Stack 的位置。這使得在布局中使用 PositionedDirectional 更加靈活和方便

  • 規則: start:在 TextDirection.ltr (從左到右)時對應 left,在 TextDirection.rtl (從右到左)時對應 right。end:在 TextDirection.ltr 時對應 right,在 TextDirection.rtl 時對應 left。top 和 bottom 保持固定方向,不受文本方向影響

  • 注意: 必須作為 Stack 的直接子組件,否則會拋出錯誤,支持 width 和 height 屬性,但需注意與 start/end 和 top/bottom 的組合規則

  • 推薦: 可通過 AnimatedPositionedDirectional 實現位置變化的動畫效果,適配不同語言方向的布局(如阿拉伯語、希伯來語)

Directionality(// 設置文本方向(ltr 或 rtl)textDirection: TextDirection.rtl,child: Stack(children: [PositionedDirectional(start: 20.0, // 在 rtl 環境下對應 right=20top: 50.0,child: Container(width: 100,height: 100,color: Colors.blue,child: const Center(child: Text('動態定位')),),),// 其他子組件...],),
)

PositionedDirectional

滾動布局相關控件

1. SingleChildScrollView

  • 說明: 為單個子控件添加滾動功能,支持垂直或水平滾動,類似于 Android 中的 ScrollView 或 iOS 中的 UIScrollView

  • 規則:

  1. scrollDirection: 滾動方向,默認為垂直方向(Axis.vertical)。
  2. reverse: 是否反轉滾動方向,默認為 false。
  3. padding: 子組件的內邊距。
  4. primary: 是否為主滾動視圖,用于判斷是否自動生成控制器。
  5. physics: 控制滾動行為,如滑動速度和邊緣效應。
  6. controller: 用于控制滾動位置的對象。
  7. child: 要包含在 SingleChildScrollView 中的子組件。
  8. dragStartBehavior: 拖動開始行為,默認為 DragStartBehavior.start。
  9. clipBehavior: 裁剪行為,默認為 Clip.hardEdge。
  10. restorationId: 恢復標識。
  11. keyboardDismissBehavior: 鍵盤關閉行為,默認為
    ScrollViewKeyboardDismissBehavior.manual。
  • 注意: 不支持 Sliver 延遲加載,內容過多時性能較差,建議改用 ListView,適用于內容不會超過屏幕尺寸太多的情況,因為當內容超出屏幕尺寸較多時,性能會受到影響

  • 推薦: 單個子組件的滾動(如表單、文章詳情頁),內容量較小,無需動態加載的場景

Scrollbar(child: SingleChildScrollView(scrollDirection: Axis.vertical,child: Center(child: Column(children: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((c) => Container(margin: const EdgeInsets.all(8.0),padding: const EdgeInsets.all(16.0),decoration: BoxDecoration(color: Colors.red,borderRadius: BorderRadius.circular(8.0),boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5),blurRadius: 4.0,offset: const Offset(2.0, 2.0),),],),child: Text(c,style: const TextStyle(color: Colors.white, fontSize: 24.0),),)).toList(),),),),
)

SingleChildScrollView

2. PageView

  • 說明: 是一個分頁滾動視圖,用于實現可滑動的頁面切換(水平/垂直)
  1. PageView():靜態頁面列表
  2. PageView.builder() :動態懶加載(大數據量優化)
  3. PageView.custom() :高度自定義子組件
  • 規則:
  1. scrollDirection: 視圖滾動方向,默認為水平方向(Axis.horizontal)。
  2. reverse: 是否反轉方向,默認為 false。
  3. controller: 用于監聽視圖滾動情況的控制器。
  4. onPageChanged: 索引改變時的回調函數。
  5. physics: 滾動邏輯,可以設置為不滾動、滾動或滾動到邊緣是否反彈等。
  6. pageSnapping: 是否啟用頁面對齊,默認為 true。
  7. allowImplicitScrolling: 是否允許隱式滾動,默認為 false。
  • 注意: 使用 PageView.builder 避免一次性構建所有頁面,搭配 AutomaticKeepAliveClientMixin 緩存頁面狀態,嵌套 ListView 時設置 physics: NeverScrollableScrollPhysics() 禁用父級滑動,避免垂直滑動沖突

  • 推薦: 適用于引導頁、輪播圖、全屏滑動視圖等場景

PageView(children: <Widget>[Container(alignment: Alignment.center,decoration: const BoxDecoration(color: Colors.green),child: const Text("頁面 0",style: TextStyle(fontSize: 20, color: Colors.black)),),Container(alignment: Alignment.center,decoration: const BoxDecoration(color: Colors.red),child: const Text("頁面 1",style: TextStyle(fontSize: 20, color: Colors.white)),),Container(alignment: Alignment.center,decoration: const BoxDecoration(color: Colors.blue),child: const Text("頁面 2",style: TextStyle(fontSize: 20, color: Colors.white)),),],
)

PageView

3. Scrollbar

  • 說明: 一個 Material 風格的滾動指示器,Flutter 的滾動視圖(如 ListView、GridView 等)沒有滾動條,但可以通過將滾動視圖包裹在 Scrollbar 組件中來添加滾動條

  • 規則:

  1. child: 必需屬性,用于指定需要滾動的內容。
  2. controller: 可選屬性,用于控制滾動行為。
  3. isAlwaysShown: 布爾值,表示是否始終顯示滾動條。默認為 false,需要設置 controller 才能顯示。
  4. thickness: 滾動條的寬度。
  5. radius: 滾動條的圓角半徑。
  6. showTrackOnHover: 布爾值,表示當鼠標懸停在滾動條上時是否顯示軌道。
  7. hoverThickness: 鼠標懸停時滾動條的寬度。
  8. notificationPredicate: 用于過濾滾動通知的謂詞。
  9. interactive: 布爾值,表示滾動條是否可以交互。
  10. scrollbarOrientation: 滾動條的方向。
  • 注意: Scrollbar 需要包裹可滾動組件(如 ListView),但不要求直接作為父級,只要在組件樹中存在即可

  • 推薦: 自定義滾動條樣式 、顯示滾動條

Scrollbar(child: ListView.builder(itemCount: 40,itemBuilder: (context, index) {return Card(child: Container(height: 45,alignment: Alignment.center,child: Text('$index'),),);},),
)

Scrollbar

4. RefreshIndicator

  • 說明: 實現 Material 風格下拉刷新功能的組件,通常包裹在可滾動的子組件(如 ListView、CustomScrollView)外層。當用戶下拉超過一定距離時,會觸發回調函數執行刷新操作,并顯示加載動畫

  • 規則:

  1. child: 必需參數,通常為 ListView 或 CustomScrollView。
  2. displacement: 可選參數,從子組件的頂部或底部邊緣到刷新指示符所在位置的距離。
  3. onRefresh: 必需參數,當用戶將刷新指示器拖到足夠遠以表明他們希望應用刷新時調用的函數。
  4. color: 可選參數,進度指示器前景色。
  5. backgroundColor: 可選參數,進度指示器背景色。
  6. notificationPredicate: 可選參數,用于過濾滾動通知。
  7. semanticsLabel: 可選參數,語義標簽。
  8. semanticsValue: 可選參數,語義值。
  • 注意: 上拉加載:需結合 ScrollController 監聽滾動位置,實現 LoadMore 功能,onRefresh 必須返回 Future,且需在數據加載完成后完成該 Future,否則刷新動畫不會停止

  • 推薦: 列表數據刷新

List<String> _items = List.generate(20, (index) => "Item $index");Future<void> _refresh() async {await Future.delayed(Duration(seconds: 2));setState(() {_items = List.generate(20, (index) => "Refreshed Item $index");});
}RefreshIndicator(onRefresh: _refresh,child: ListView.builder(itemCount: _items.length,itemBuilder: (context, index) {return ListTile(title: Text(_items[index]),);},),
) 

RefreshIndicator

5. DraggableScrollableSheet

  • 說明: 允許用戶通過手勢操作來拖動一個可滾動的區域。它常用于創建底部彈出式面板或可拖動的控件,類似于高德地圖首頁從底部滑動向上的效果。該組件繼承自 StatefulWidget,通過調整滾動區域的大小來響應拖動手勢,直到達到限制,然后開始滾動

  • 規則:

    1. initialChildSize: 設置初始高度占屏幕的比例,范圍為 0 到 1,默認為 0.5。
    2. minChildSize: 指定最小高度占屏幕的比例,默認為 0.25。
    3. maxChildSize: 指定最大高度占屏幕的比例,默認為 1.0。
    4. expand: 控制是否允許在內容小于屏幕高度時擴展以填充屏幕,默認為 true。
    5. snap: 布爾值,用于控制滾動停止時是否自動捕捉到接近的最小或最大值,默認為 false。
    6. builder: 構建函數,用于構建 DraggableScrollableSheet 的內容,接受兩個參數:
    BuildContext 和 ScrollController,返回一個 Widget,通常是 SingleChildScrollView 或 ListView。

  • 注意: 確保 maxChildSize >= initialChildSize >= minChildSize,否則會拋出異常

  • 推薦: 底部彈出式面板評論區、詳細信息展示

Stack(children: [Positioned(top: 0,bottom: 0,left: 0,right: 0,child: Container(color: Colors.grey[300],child: Center(child: Text('背景內容'),),),),DraggableScrollableSheet(initialChildSize: 0.5,minChildSize: 0.3,maxChildSize: 1.0,builder: (_, controller) {return Container(color: Theme.of(context).cardColor,child: Scrollbar(child: ListView.builder(controller: controller,itemCount: 20,itemBuilder: (context, index) {return ListTile(title: Text('Item $index'),);},),),);},),],
)

DraggableScrollableSheet

6. NestedScrollView

  • 說明: 用于協調多個獨立滾動區域的容器組件。它通過 NestedScrollViewCoordinator 協調兩個滾動控制器:外層滾動(Header)和內層滾動(Body)。外層滾動由 headerSliverBuilder 定義的 Sliver 組件組成(如 SliverAppBar),受 PrimaryScrollController 控制;內層滾動由 body 定義的普通滾動組件(如 ListView),擁有獨立的 ScrollController

  • 規則:
    1.外層滾動優先: 當用戶向下滑動時,外層滾動先消耗滾動事件,直到外層完全折疊后,內層滾動接管。
    2.內層反向優先: 當內層滾動到達頂部且用戶繼續上滑時,外層滾動會展開

  • 注意: 確保內部可滾動組件(body)的 physics 屬性設置正確,以避免滾動沖突

  • 推薦: 折疊頭部圖片效果

NestedScrollView(headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {return <Widget>[SliverAppBar(title: const Text('NestedScrollView Example'),pinned: true,floating: true,forceElevated: innerBoxIsScrolled,expandedHeight: 160.0,flexibleSpace: FlexibleSpaceBar(background: Image.network("https://miaobi-lite.bj.bcebos.com/miaobi/5mao/b%275aS05YOPYXBwXzE3MzM3NjcxODMuODQyNDgz%27/0.png",fit: BoxFit.cover,),),),];},body: ListView.builder(itemCount: 50,itemBuilder: (BuildContext context, int index) {return ListTile(title: Text('List Item $index'),);},),
)

NestedScrollView

7. ScrollConfiguration

  • 說明: 用于統一控制子組件樹的滾動行為(如滾動條樣式、拖拽反饋等),尤其適用于定制不同平臺的滾動體驗

  • 規則: ScrollConfiguration 應該直接放置在列表組件的父組件下,以避免與其他列表組件發生沖突,若需要全局設置整個應用程序的所有列表默認樣式,則可將 ScrollConfiguration 組件插入接近組件樹根部的位置

  • 注意: 由于 ScrollConfiguration 的子組件可能為 null,因此在使用時需要進行 null 檢查,以避免潛在的錯誤

  • 推薦: 自定義滾動條的樣式,如顏色、厚度

ScrollConfiguration(behavior: MyCustomBehavior(),child: ListView.separated(itemCount: 20,separatorBuilder: (_, __) => Divider(),itemBuilder: (_, index) {return Container(height: 56,alignment: Alignment.center,child: Text("這是第 ${index + 1} 項"),);},),
)class MyCustomBehavior extends ScrollBehavior {@overrideWidget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {// 自定義滾動條樣式return GlowingOverscrollIndicator(axisDirection: axisDirection,color: Colors.grey, // 設置滾動條顏色為灰色child: child,);}@overrideScrollPhysics getScrollPhysics(BuildContext context) {// 使用 ClampingScrollPhysics 防止過度滾動return ClampingScrollPhysics();}
}

ScrollConfiguration

Sliver 系列控件 (用于 CustomScrollView)

1. SliverList

  • 說明: 創建線性可滾動列表的組件,它屬于 Sliver 家族的一部分,SliverList 需要與 CustomScrollView 結合使用,以實現復雜的滾動效果

  • 規則: 使用 SliverChildDelegate 來構建子組件。常用的實現是 SliverChildBuilderDelegate 和 SliverChildListDelegate。SliverChildListDelegate: (靜態列表)適用于已知數量的子組件,一次性全部渲染。
    SliverChildBuilderDelegate: (動態構建列表項)適用于未知數量的子組件,按需加載和銷毀列表項,提升性能。

  • 注意: 必須通過代理生成子項,其核心特點是支持統一滾動、避免滑動沖突

  • 推薦: 滾動時兩個列表無縫銜接,無滑動沖突,實現吸頂 AppBar 和列表聯動

CustomScrollView(slivers: <Widget>[const SliverAppBar(expandedHeight: 250,flexibleSpace: FlexibleSpaceBar(title: Text('SliverList 示例'),),),SliverList(delegate: SliverChildBuilderDelegate((BuildContext context, int index) {return ListTile(title: Text('Item #$index'),);},childCount: 20, // 列表項數目),),SliverFixedExtentList(itemExtent: 50.0,delegate: SliverChildBuilderDelegate((BuildContext context, int index) {return Container(alignment: Alignment.center,color: Colors.blue[100 * (index % 9)],child: Text("list item $index"),);},childCount: 50,),),],
)

SliverList

2. SliverGrid

  • 說明: 創建網格布局的 Sliver 組件,它非常適合與其他 Sliver 組件結合使用,以構建復雜的可滾動布局,

  • 規則: SliverGrid 需要指定 gridDelegate 和 delegate 屬性。gridDelegate 控制網格布局,而 delegate 用于構建每個網格單元的內容,SliverGrid 提供了三種主要的構造方法:
    1.SliverGrid: 適用于動態顯示內容(從數據庫或 API 獲取)。
    2.SliverGrid.extent: 允許指定子項的最大跨軸范圍。
    3.SliverGrid.count: 用于指定跨軸上固定數量的子項

  1. delegate: 控制子項生成,常用:
  • SliverChildListDelegate: 靜態子項列表
  • SliverChildBuilderDelegate: 動態懶加載子項
  1. gridDelegate: 控制網格布局,兩種實現:
  • SliverGridDelegateWithFixedCrossAxisCount: 固定列數
  • SliverGridDelegateWithMaxCrossAxisExtent: 限制子項最大寬度
  • 注意: 按需加載子項,性能優于一次性構建所有子項的 GridView,與普通的網格布局組件(如 GridView)不同,SliverGrid 不會提前渲染所有網格項,而是在滾動時動態渲染當前可見的部分,從而節省內存和渲染時間

  • 推薦: 下拉刷新、嵌套滾動

CustomScrollView(slivers: [SliverAppBar(// 可折疊標題欄expandedHeight: 200,flexibleSpace: FlexibleSpaceBar(background: Image.network('https://miaobi-lite.bj.bcebos.com/miaobi/5mao/b%275aS05YOPYXBwXzE3MzM3NjcxODMuODQyNDgz%27/0.png',fit: BoxFit.cover)),),SliverGrid(// 網格區域delegate: SliverChildBuilderDelegate((ctx, i) => Container(color: [Colors.red,Colors.blue,Colors.black,Colors.yellow,Colors.green][i % 5],)),gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 150, // 自適應寬度mainAxisSpacing: 10,crossAxisSpacing: 10,),),],
)

SliverGrid

3. SliverAppBar

  • 說明: 創建可滾動應用欄的組件,它可以在滾動過程中動態變化,如展開、收縮或固定在頂部。SliverAppBar 通常與 CustomScrollView 搭配使用,可以實現復雜的滾動效果,類似于 Android 中的 CollapsingToolbarLayout

  • 規則:

  1. expandedHeight: 指定 SliverAppBar 展開時的高度。
  2. pinned: 設置為 true 時,AppBar 將在滾動到頂部時固定在屏幕頂部。
  3. floating: 設置為 true 時,當向下滾動時,AppBar 會重新出現。
  4. snap: 配合 floating 屬性使用,當設置為 true 時,SliverAppBar 會在滑動過程中自動“吸附”到頂部或消失。
  5. flexibleSpace: 用于定義可伸縮的空間,通常使用 FlexibleSpaceBar。
  6. backgroundColor: 設置 AppBar 的背景色。
  7. title: 設置 AppBar 的標題。
  8. leading: 設置 AppBar 前面顯示的一個控件。
  9. actions: 設置 AppBar 的操作按鈕。
  10. bottom: 設置 AppBar 的底部區域,通常用于添加 TabBar。
  • 注意: pinned 和 floating 不可同時為 true,否則引發布局沖突,必須嵌套在 Sliver 容器,避免在 flexibleSpace 中使用復雜動畫,可能導致滾動卡頓,如果需要實現滾動折疊效果,flexibleSpace 必須包含 FlexibleSpaceBar

  • 推薦: 折疊式圖片標題、快速展開的搜索欄、帶懸浮 Tab 的分頁布局

DefaultTabController(length: 2,child: Scaffold(body: NestedScrollView(headerSliverBuilder:(BuildContext context, bool innerBoxIsScrolled) {return <Widget>[SliverAppBar(expandedHeight: 200.0,floating: false,pinned: true,flexibleSpace: FlexibleSpaceBar(title: const Text('SliverAppBar Example'),background: Image.network('https://img.win3000.com/m00/76/6a/3fb7a5729f51fedf4261cb02addbd133.jpg',fit: BoxFit.cover,),),bottom: const TabBar(tabs: [Tab(icon: Icon(Icons.home), text: 'Home'),Tab(icon: Icon(Icons.settings), text: 'Settings'),],),),];},body: TabBarView(children: [CustomScrollView(slivers: [SliverList(delegate: SliverChildBuilderDelegate((BuildContext context, int index) {return Container(height: 85,alignment: Alignment.center,color: Colors.primaries[index % Colors.primaries.length],child: Text('$index',style: const TextStyle(color: Colors.white, fontSize: 20.0),),);},childCount: 25,),),],),CustomScrollView(slivers: [SliverList(delegate: SliverChildBuilderDelegate((BuildContext context, int index) {return Container(height: 85,alignment: Alignment.center,color: Colors.primaries[index % Colors.primaries.length],child: Text('$index',style: const TextStyle(color: Colors.white, fontSize: 20.0),),);},childCount: 25,),),],),],),),),
)

SliverAppBar

4. SliverPadding

  • 說明: 專門用于在 滾動布局(如 CustomScrollView)中為子 Sliver 組件添加內邊距的組件。它屬于 Sliver 家族,適用于需要精細控制滾動區域間距的場景,例如在 SliverList、SliverGrid 等組件外部包裹邊距

  • 規則: 僅能作為 CustomScrollView 的 slivers 數組的直接子組件使用,不可單獨用于普通布局。SliverPadding 的 sliver 屬性必須是一個 Sliver 類型的組件,否則會導致運行時錯誤

核心參數:
padding(必填): 通過 EdgeInsets 設置邊距(上、下、左、右或對稱邊距)。
sliver(必填): 接受任意 Sliver 組件(如 SliverList、SliverGrid)

  • 注意: 普通 Padding 用于靜態布局,而 SliverPadding 專為滾動視圖優化,能正確計算滾動區域和視口邊界,避免在 SliverPadding 內嵌套多層 Sliver 組件,推薦直接包裹目標組件,如需避開劉海屏或底部導航欄,優先使用 SliverSafeArea 而非 SliverPadding

  • 推薦: 列表/網格的全局邊距、復雜滾動布局的間隔控制

CustomScrollView(slivers: [const SliverAppBar(title: Text('SliverPadding 示例')),SliverPadding(padding: const EdgeInsets.all(20), // 全局邊距sliver: SliverList(delegate: SliverChildBuilderDelegate((context, index) => ListTile(title: Text("列表項 $index"),),childCount: 20,),),),SliverPadding(padding: const EdgeInsets.only(top: 30, bottom: 50), // 僅上下邊距sliver: SliverGrid(gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2,crossAxisSpacing: 10,mainAxisSpacing: 10,),delegate: SliverChildBuilderDelegate((context, index) => Container(color: Colors.blue,child: Center(child: Text("網格 $index")),),childCount: 6,),),),],
)

SliverPadding

5. SliverToBoxAdapter

  • 說明: 將普通 盒模型布局組件(如 Container、Text 等)嵌入到 Sliver 滾動布局(如 CustomScrollView)中的適配器。它允許非 Sliver 組件(即非滾動專用組件)作為 Sliver 列表的一部分參與滾動,在 CustomScrollView 的 slivers 列表中混合使用常規 Widget 與 Sliver 組件(如 SliverList、SliverGrid)

  • 規則: 只能用于 CustomScrollView.slivers 或嵌套的 Sliver 容器(如 SliverPadding),非 Sliver 組件(如 Container)必須指定高度或寬度,否則會因無限空間約束導致布局錯誤,若子組件包含 ListView 等自身可滾動的 Widget,需禁用其滾動行為(如 physics: NeverScrollableScrollPhysics())

  • 注意: 盡量避免在 SliverToBoxAdapter 中嵌套過多的復雜布局,這可能會導致布局計算變慢,雖然 SliverToBoxAdapter 提供了便利性,但在大量使用時需要注意性能問題,盡量減少不必要的包裹,如果 child 的高度沒有明確設置,可能會導致布局問題或渲染錯誤

  • 推薦: 列表頭部/尾部添加獨立 Widget(如 Banner、標題、按鈕)

CustomScrollView(slivers: [// 1. 頂部 BannerSliverToBoxAdapter(child: Container(height: 200,decoration: const BoxDecoration(gradient: LinearGradient(colors: [Colors.blue, Colors.green]),),child: const Center(child: Text('歡迎頁', style: TextStyle(fontSize: 24))),),),// 2. 分隔標題SliverToBoxAdapter(child: Padding(padding: const EdgeInsets.all(16),child: Text('產品列表',style: Theme.of(context).textTheme.headlineSmall),),),// 3. 網格布局SliverGrid(gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2,childAspectRatio: 1.5,),delegate: SliverChildBuilderDelegate((context, index) =>Card(child: Center(child: Text('產品 $index'))),childCount: 6,),),// 4. 底部按鈕SliverToBoxAdapter(child: Padding(padding: const EdgeInsets.symmetric(vertical: 20),child: ElevatedButton(onPressed: () {},child: const Text('加載更多'),),),),],
)

SliverToBoxAdapter

6. SliverFillRemaining

  • 說明: 用于 填充滾動視圖剩余空間 的 Sliver 布局組件。它會根據父容器的剩余高度或寬度自動調整自身大小,確保內容區域占滿屏幕可視區域。適用于需要動態適配剩余空間的場景

  • 規則: 作為 CustomScrollView 的 slivers 列表中的最后一個組件,用于填充剩余空間,允許組件自適應地填充剩余空間,無論剩余空間如何變化

關鍵參數

  1. child:必填,用于填充剩余空間的子組件。
  2. hasScrollBody:
  • 默認 true:子組件可滾動(如 ListView),適用于內容超出剩余空間的情況。
  • false:子組件不可滾動(如 Column),內容高度不超過剩余空間
  1. fillOverscroll:
  • true:在過度滾動時繼續填充(如 iOS 回彈效果)。
  • false:僅填充未滾動區域(默認值)
  • 注意: 當 hasScrollBody: true 時,子組件必須是可滾動的(如 ListView),否則會拋出布局錯誤,SliverFillRemaining 在滾動視圖中動態填充剩余空間,而 SingleChildScrollView 會嘗試包裹整個內容,可能導致布局溢出,在長列表底部使用 SliverFillRemaining 時,避免在 child 中嵌套復雜布局,防止重復構建

  • 推薦: 表單底部固定按鈕、分頁加載占位、全屏內容適配

CustomScrollView(slivers: <Widget>[const SliverAppBar(expandedHeight: 200.0,flexibleSpace: FlexibleSpaceBar(title: Text('Flexible Space'),),),SliverList(delegate: SliverChildListDelegate([Container(height: 500,color: Colors.amber,alignment: Alignment.center,child: const Text('List Item'),),],),),SliverFillRemaining(hasScrollBody: false,child: Container(color: Colors.blue,alignment: Alignment.center,child: const Text('Fill Remaining Space',style: TextStyle(color: Colors.white)),),),],
)

SliverFillRemaining

7. SliverPersistentHeader

  • 說明: 創建可折疊/懸浮的頭部的Sliver組件,在滾動視圖中創建一個持久化的頭部組件,該頭部組件可以根據滾動位置自動調整大小、透明度或其他屬性。可以固定在頁面頂部或底部,也可以隨著滾動而動態變化,從而實現更加靈活的布局效果

  • 規則:

  1. delegate:必需屬性,用于定義頭部組件的構建邏輯。
  2. pinned:布爾值,默認為 false,當設置為 true 時,頭部組件會在滾動到頂部時固定在視圖中。
  3. floating:布爾值,默認為 false,當設置為 true 時,頭部組件會在滾動到頂部時短暫顯示,然后隱藏。
  • 注意: pinned 和 floating 屬性不能同時為 true,避免在build()方法中執行耗時操作,推薦使用const組件或StatelessWidget。若內容復雜,用AutoDispose混合Riverpod管理狀態,不可直接嵌套ListView等滾動組件,需通過SliverToBoxAdapter,確保minExtent和maxExtent精確計算,避免小數精度誤差。必須顯式指定頭部組件的高度范圍(minExtent 和 maxExtent)。如果依賴 overlapsContent 參數構建子組件,則必須保證之前至少還有一個 SliverPersistentHeader 或 SliverAppBar

  • 推薦: 電商分類導航、漸變收縮的Banner圖

CustomScrollView(slivers: <Widget>[// 創建一個固定在頁面頂部的 SliverPersistentHeaderSliverPersistentHeader(pinned: true, // 固定在頁面頂部delegate: _MyHeaderDelegate(),),// 添加一個普通的 SliverListSliverList(delegate: SliverChildBuilderDelegate((BuildContext context, int index) {return ListTile(title: Text('Item $index'),leading: CircleAvatar(child: Text('$index')),);},childCount: 50,),),],
)class _MyHeaderDelegate extends SliverPersistentHeaderDelegate {@overrideWidget build(BuildContext context, double shrinkOffset, bool overlapsContent) {// 獲取狀態欄高度final double statusBarHeight = MediaQuery.of(context).padding.top;// 構建頭部組件return Container(padding: EdgeInsets.only(top: statusBarHeight),color: Colors.blue,alignment: Alignment.center,child: const Text('Header'),);}@overridedouble get maxExtent => 120; // 最大高度@overridedouble get minExtent => 40; // 最小高度@overridebool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>false;
}

SliverPersistentHeader

還有其他的一些Sliver布局控件

SliverFillViewport:填充視口

SliverAnimatedList:動畫列表

SliverAnimatedOpacity:動畫透明度

SliverIgnorePointer:忽略指針事件

SliverOpacity:透明度效果

SliverSafeArea:安全區域

SliverOverlapInjector:處理重疊

SliverOverlapAbsorber:吸收重疊

響應式布局控件

1. LayoutBuilder

  • 說明: 用于根據父容器的約束對其子組件進行布局。它在布局過程中獲取父組件傳遞的約束信息,從而動態構建布局。這使得開發者可以根據父容器的尺寸來調整子組件的布局,非常適合實現響應式布局

  • 規則: 根據設備尺寸動態調整布局,在開發過程中獲取和調試約束信息,便于排查問題,如果是 Sliver 布局,可以使用 SliverLayoutBuilder

  1. constraints.maxWidth/constraints.maxHeight :父容器要求的最大尺寸
  2. constraints.minWidth/minHeight :父容器要求的最小尺寸
  • 注意: 如果子組件超出顯示大小,可能會導致頻繁的 rebuild。解決方案是將 constraints 保存為成員變量,避免每次 build 都通過 LayoutBuilder 獲取 constraints,當父容器的約束變化(如屏幕旋轉、父容器尺寸調整)時,builder 會重新調用,考慮布局的復雜度和動畫效果,避免過重的計算和不必要的重繪

  • 推薦: 要根據屏幕尺寸調整布局、獲取約束信息,幫助調試布局問題

Center(child: Container(width: 300, // 父容器寬度height: 200, // 父容器高度color: Colors.blue,child: LayoutBuilder(builder: (context, constraints) {// 根據父容器寬度動態切換布局if (constraints.maxWidth > 200) {return _buildWideLayout(constraints);} else {return _buildNarrowLayout(constraints);}},),),
)// 寬布局:水平排列
Widget _buildWideLayout(BoxConstraints constraints) {
return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [Container(width: constraints.maxWidth * 0.3, // 寬度占父級30%height: 50,color: Colors.red,),Container(width: constraints.maxWidth * 0.5, // 寬度占父級50%height: 80,color: Colors.green,),],
);
}// 窄布局:垂直排列
Widget _buildNarrowLayout(BoxConstraints constraints) {
return Column(mainAxisAlignment: MainAxisAlignment.center,children: [Container(width: constraints.maxWidth * 0.8, // 寬度占父級80%height: 40,color: Colors.orange,),const SizedBox(height: 20),Container(width: constraints.maxWidth * 0.6, // 寬度占父級60%height: 60,color: Colors.purple,),],
);
}     

LayoutBuilder

2. OrientationBuilder

  • 說明: 根據設備的方向(縱向或橫向)動態構建布局。它提供了一個回調函數,當設備方向發生變化時,會調用該回調函數來重建布局。這使得開發者可以根據不同的屏幕方向提供不同的用戶界面布局

  • 規則: 依賴于父組件的尺寸來判斷當前的方向,而不是直接依賴于設備的方向。因此,在某些情況下,可能需要結合 MediaQuery 來獲取更準確的方向信息

  • 注意: 由于 OrientationBuilder 依賴于父組件的尺寸,因此在某些情況下可能無法準確檢測到設備的方向。建議在需要高精度方向檢測時,結合 MediaQuery.of(context).orientation 使用

  • 推薦: 橫豎屏顯示不同排列的網格、列表,在多設備適配方案中作為基礎工具

OrientationBuilder(builder: (context, orientation) {return orientation == Orientation.portrait? _buildVerticalLayout(): _buildHorizontalLayout();},
)Widget _buildVerticalLayout() {return ListView(children: const <Widget>[ListTile(title: Text('項目 1')),ListTile(title: Text('項目 2')),ListTile(title: Text('項目 3')),],);
}Widget _buildHorizontalLayout() {return GridView.count(crossAxisCount: 3,children: <Widget>[Container(color: Colors.red,child: const Center(child: Text('項目 1')),),Container(color: Colors.green,child: const Center(child: Text('項目 2')),),Container(color: Colors.blue,child: const Center(child: Text('項目 3')),),],);
}

OrientationBuilder

3. MediaQuery

  • 說明: 用于獲取設備的屏幕尺寸、方向、像素密度等信息。它可以幫助開發者創建響應式布局,以適應不同設備和屏幕尺寸

  • 規則:
    1. 獲取屏幕尺寸: 使用 MediaQuery.of(context).size 可以獲取屏幕的寬度和高度。
    2. 獲取方向: 使用 MediaQuery.of(context).orientation 可以獲取設備的方向(豎屏或橫屏)。
    3. 獲取文本縮放因子: 使用MediaQuery.of(context).textScaleFactor 可以獲取用戶的文本縮放設置。
    4. 獲取系統亮度: 使用 MediaQuery.of(context).platformBrightness 可以獲取系統的亮度模式(亮色或暗色)。
    5. 獲取系統遮擋區域: 使用 MediaQuery.of(context).padding 可以獲取系統遮擋區域的信息,如狀態欄和導航欄的高度。

  • 注意: 頻繁使用 MediaQuery.of(context).size 可能會導致不必要的小部件重建,影響性能。建議僅在需要的地方使用 MediaQuery,鍵盤彈出時,MediaQuery 的 viewInsets 屬性會變化,開發者需要處理這種情況以避免布局問題

  • 推薦: 根據用戶的文本縮放設置調整字體大小

var screenSize = MediaQuery.of(context).size;
var textScaleFactor = MediaQuery.of(context).textScaleFactor;
var orientation = MediaQuery.of(context).orientation;Column(mainAxisAlignment: MainAxisAlignment.center,children: [Text('Screen Size: ${screenSize.width} x ${screenSize.height}',style: TextStyle(fontSize: 24),),SizedBox(height: 10),Text('Text Scale Factor: $textScaleFactor',style: TextStyle(fontSize: 24),),SizedBox(height: 10),Text('Orientation: $orientation',style: TextStyle(fontSize: 24),),SizedBox(height: 20),if (orientation == Orientation.portrait)FilledButton(onPressed: () {},child: Text('Button for Portrait Mode'),)elseFilledButton(onPressed: () {},child: Text('Button for Landscape Mode'),),SizedBox(height: 20),FilledButton(onPressed: () {},child: Text('Custom Font Size Button',style: TextStyle(fontSize: 16 * textScaleFactor),),),],
)

MediaQuery

4. AspectRatio

  • 說明: 根據設備的方向(縱向或橫向)動態構建不同的 UI 布局。當設備的方向發生變化時,OrientationBuilder 會重新構建其子組件樹,以適應新的方向。這使得開發者能夠輕松地實現橫豎屏適配,它通過回調函數返回當前屏幕方向,無需依賴全局 MediaQuery

  • 規則: 根據設備的方向返回不同的布局組件,builder: 一個回調函數,接收兩個參數:BuildContext 和 Orientation。Orientation 是一個枚舉類型,包含兩個值:Orientation.portrait 和 Orientation.landscape

OrientationBuilder(builder: (BuildContext context, Orientation orientation) {// 根據 orientation 的值返回不同的布局return YourWidget();},
)
  • 注意: 避免在 builder 函數中執行耗時操作(如網絡請求),因其在屏幕旋轉時可能頻繁觸發,直接通過回調提供方向,更適合布局構建階段,僅監聽方向變化,若需響應尺寸變化(如分屏模式),應結合 LayoutBuilder

  • 推薦: 視頻播放器、表單輸入

AspectRatio(aspectRatio: 16 / 9,child: Container(color: Colors.blue),
)

AspectRatio

平臺適配布局控件

1. SafeArea

  • 說明: 用于規避系統 UI(如狀態欄、劉海屏、底部導航欄)的遮擋。它會根據設備屏幕的“安全區域”自動添加內邊距(padding),確保內容不被系統控件覆蓋,使 UI 動態且適應各種設備。在設計組件布局時,我們考慮了不同設備及其屏幕占用的約束,如狀態欄、劉海屏、導航欄等。然而,新設備不斷推出,設計各異,有時你的應用可能會覆蓋這些占用的約束。因此,為了使 UI 適應性強且無錯誤,我們使用 SafeArea 組件。SafeArea 實際上是一個填充組件,根據設備運行時的情況,為你的應用添加必要的填充。如果應用的組件覆蓋了系統特征,如劉海屏、狀態欄、相機孔等,SafeArea 會根據需要在周圍添加填充。SafeArea 內部使用 MediaQuery 檢查屏幕尺寸,并在必要時包含額外填充。構造函數允許你決定是否在特定方向上避免侵入,通過布爾值 true 或 false 來實現

  • 規則: 在需要避免系統侵入的區域包裹 SafeArea 組件,通過 left、top、right、bottom 屬性控制是否在相應方向上避免系統侵入;通過 minimum 屬性設置最小填充;通過 maintainBottomViewPadding 屬性決定是否保持底部填充不變,將需要保護的子組件放在 SafeArea 的 child 屬性中

  • 注意: 當軟鍵盤彈出時,底部填充可能會消失。可以通過設置 maintainBottomViewPadding 為 true 來保持底部填充不變,在頁面骨架中直接應用,避免全局遮擋,在沉浸式全屏(如視頻播放頁)中需關閉 SafeArea,否則頂部留白,若父容器已設置 padding(如 ListView),SafeArea 可能造成雙重邊距。此時需手動調整,強制取消安全邊距(如 padding: EdgeInsets.zero )可能導致內容被遮擋

  • 推薦: 頂部狀態欄區域、底部導航欄區域、劉海屏/挖孔屏設備、彈窗/浮層內容

SafeArea(// 2. 關閉底部安全區域(自定義底部欄時使用)bottom: false,child: Column(children: [// 3. 頂部標題欄(自動避開狀態欄)const AppHeader(),// 4. 內容區域(使用Expanded填充剩余空間)Expanded(child: ListView(children: List.generate(20, (i) => ListTile(title: Text("Item $i"))),),),// 5. 自定義底部導航欄(需手動避開系統欄)const CustomBottomBar(),],),
)// 自定義頂部組件 
class AppHeader extends StatelessWidget {const AppHeader({super.key}); @override Widget build(BuildContext context) {return Container(color: Colors.blue, padding: const EdgeInsets.all(16), child: const Text("SafeArea Demo", style: TextStyle(color: Colors.white)), );}
}// 自定義底部組件(需單獨處理安全邊距)
class CustomBottomBar extends StatelessWidget {const CustomBottomBar({super.key}); @overrideWidget build(BuildContext context) {// 6. 為底部欄單獨添加SafeArea return SafeArea(top: false, // 關閉頂部邊距minimum: const EdgeInsets.only(bottom:  10), // 追加額外外邊距child: Container(height: 50,color: Colors.green, child: const Center(child: Text("Bottom Bar")),),);}
}

SafeArea

2. PlatformView

  • 說明: 允許在 Flutter 應用中嵌入原生的 UI 組件,如 Android 的 View 或 iOS 的 UIView

  • 規則:

  1. 首先需要在原生代碼中創建一個自定義的 PlatformView。
  2. 創建一個 PlatformViewFactory 來創建 PlatformView 實例,并將其與 Flutter 應用關聯
  3. 通過 PlatformView Widget 將原生視圖嵌入到 Flutter 應用中
  4. 通過 MethodChannel 實現 Dart 與原生代碼的雙向通信
  • 注意: 由于 PlatformView 是原生視圖,頻繁的交互可能會影響性能,因此應盡量減少不必要的原生視圖嵌入,確保正確管理原生視圖的生命周期,避免內存泄漏,不同平臺的原生視圖實現方式不同,需要分別處理 Android 和 iOS 的實現,Dart 的 viewType(native_text_view)需與原生注冊的 ID 匹配

  • 推薦: 嵌入地圖、視頻播放、相機、文件選擇器、WebView、條形碼掃描

// dart
Column(mainAxisAlignment: MainAxisAlignment.center,children: [// Flutter 文本顯示原生視圖傳回的時間Text('Flutter 顯示: $_currentTime',style: const TextStyle(fontSize: 20)),const SizedBox(height: 30),// 原生視圖容器Container(width: 300,height: 200,decoration: BoxDecoration(border: Border.all(color: Colors.blue, width: 2),borderRadius: BorderRadius.circular(10),),child: Platform.isAndroid? const AndroidView(viewType: _viewType,creationParams: {'textColor': '#FF0000'}, // 紅色文本creationParamsCodec: StandardMessageCodec(),): const UiKitView(viewType: _viewType,creationParams: {'textSize': 24.0}, // iOS 文本大小creationParamsCodec: StandardMessageCodec(),),),],
)// swift
// TimeView.swift
class TimeView: NSObject, FlutterPlatformView {private var label: UILabelprivate var methodChannel: FlutterMethodChannel?init(frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, messenger: FlutterBinaryMessenger) {label = UILabel(frame: frame)label.textAlignment = .centerlabel.text = "iOS原生時間視圖"super.init()// 解析Flutter傳遞的參數if let params = args as? [String: Any], let textSize = params["textSize"] as? Double {label.font = UIFont.systemFont(ofSize: CGFloat(textSize))} else {label.font = UIFont.systemFont(ofSize: 20)}label.textColor = .blue// 創建方法通道methodChannel = FlutterMethodChannel(name: "com.example/time_channel", binaryMessenger: messenger)methodChannel?.setMethodCallHandler(handleMethodCall)}func view() -> UIView {return label}func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {switch call.method {case "getCurrentTime":// 返回當前時間給Flutterlet formatter = DateFormatter()formatter.dateFormat = "HH:mm:ss"result(formatter.string(from: Date()))default:result(FlutterMethodNotImplemented)}}
}// TimeViewFactory.swift
class TimeViewFactory: NSObject, FlutterPlatformViewFactory {private var messenger: FlutterBinaryMessengerinit(messenger: FlutterBinaryMessenger) {self.messenger = messengersuper.init()}func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {return FlutterStandardMessageCodec.sharedInstance()}func create(withFrame frame: CGRect,viewIdentifier viewId: Int64,arguments args: Any?) -> FlutterPlatformView {return TimeView(frame: frame,viewIdentifier: viewId,arguments: args,messenger: messenger)}
}// AppDelegate.swift (添加部分)import UIKit
import Flutter@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {override func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {let controller: FlutterViewController = window?.rootViewController as! FlutterViewController// 注冊PlatformViewlet factory = TimeViewFactory(messenger: controller.binaryMessenger)registrar(forPlugin: "TimeViewPlugin")?.register(factory,withId: "com.example/NativeTimeView")GeneratedPluginRegistrant.register(with: self)return super.application(application, didFinishLaunchingWithOptions: launchOptions)}
}// ios 配置
<!-- ios/Runner/Info.plist -->
<key>io.flutter.embedded_views_preview</key>
<true/>// android// TimeView.kt
package com.example.platformviewdemo;import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.platform.PlatformView;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;class TimeView implements PlatformView, MethodChannel.MethodCallHandler {private final TextView textView;private final MethodChannel methodChannel;TimeView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {textView = new TextView(context);// 解析Flutter傳遞的參數if (params != null && params.containsKey("textColor")) {String color = (String) params.get("textColor");textView.setTextColor(Color.parseColor(color));} else {textView.setTextColor(Color.BLUE);}textView.setTextSize(20);textView.setText("Android原生時間視圖");// 創建方法通道methodChannel = new MethodChannel(messenger, "com.example/time_channel");methodChannel.setMethodCallHandler(this);}@Overridepublic View getView() {return textView;}@Overridepublic void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {if (call.method.equals("getCurrentTime")) {// 返回當前時間給FlutterSimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());result.success(sdf.format(new Date()));} else {result.notImplemented();}}@Overridepublic void dispose() {methodChannel.setMethodCallHandler(null);}
}// TimeViewFactory.kt
package com.example.platformviewdemo;import android.content.Context;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.util.Map;public class TimeViewFactory extends PlatformViewFactory {private final BinaryMessenger messenger;public TimeViewFactory(BinaryMessenger messenger) {super(StandardMessageCodec.INSTANCE);this.messenger = messenger;}@Overridepublic PlatformView create(Context context, int viewId, Object args) {Map<String, Object> params = (Map<String, Object>) args;return new TimeView(context, messenger, viewId, params);}
}// android/app/src/main/java/com/example/platformviewdemo/MainActivity.kt
package com.example.platformviewdemo;import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;public class MainActivity extends FlutterActivity {private static final String CHANNEL = "com.example/time_channel";@Overridepublic void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {super.configureFlutterEngine(flutterEngine);// 注冊PlatformViewflutterEngine.getPlatformViewsController().getRegistry().registerViewFactory("com.example/NativeTimeView", new TimeViewFactory(flutterEngine.getDartExecutor()));}
}

AndroidView: 嵌入Android原生視圖

UiKitView: 嵌入iOS原生視圖

平臺適配:

Platform.isIOS? CupertinoButton(onPressed: () {}, child: Text('iOS')): ElevatedButton(onPressed: () {}, child: Text('Android'));

PlatformView

特殊布局控件

1. Offstage

  • 說明: 用于控制子組件是否參與布局和渲染的一個小部件,其核心功能是通過offstage屬性切換子組件的可見狀態

  • 規則: 當 offstage 屬性設置為 true 時,子組件會被隱藏,并且不會參與布局或渲染,也不會占用任何空間,當 offstage 為 false 時,子組件正常顯示

  • 注意: 如果子組件有動畫,應該手動停止動畫,因為 Offstage 不會停止動畫,Offstage 不會從渲染樹中移除子組件,只是不繪制和不響應點擊事件,因此在需要頻繁切換顯示狀態的場景中,Offstage 是一個高效的選擇,隱藏狀態的子組件不占用空間,可能導致父布局重新計算尺寸(如Column中的子組件隱藏后,其他子組件會向上移動)。需注意布局的穩定性,避免頻繁切換導致界面抖動

  • 推薦: 實現平滑的顯示/隱藏動畫(如淡入淡出、滑動入場)

bool _isHidden = false; // 控制文本是否隱藏Column(mainAxisAlignment: MainAxisAlignment.center,children: [// 使用 Offstage 控制文本顯示Offstage(offstage: _isHidden,child: const Text('Hello, Offstage!',style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),),),const SizedBox(height: 20),// 切換按鈕ElevatedButton(onPressed: () {setState(() => _isHidden = !_isHidden);},child: Text(_isHidden ? '顯示文本' : '隱藏文本'),),],
)

Offstage

2. Visibility

  • 說明: 用于控制子組件的可見性。當設置為“可見”時,子組件將顯示;設置為“隱藏”時,子組件將被隱藏。該組件還提供了多個可選屬性來控制子組件的狀態和行為,例如是否維持狀態、動畫、大小和交互性

  • 規則:

  1. child: 必填屬性,指定要控制可見性的子組件。
  2. visible: 可選屬性,布爾值,控制子組件的顯示或隱藏,默認為true。
  3. replacement: 可選屬性,當子組件不可見時顯示的替代小部件。
  4. maintainState: 可選屬性,布爾值,控制子組件狀態是否保持不變,默認為false。
  5. maintainAnimation: 可選屬性,布爾值,控制子組件動畫是否保持不變,默認為false。
  6. maintainSize: 可選屬性,布爾值,控制子組件空間是否保持不變,默認為false。
  7. maintainInteractivity: 可選屬性,布爾值,控制子組件在不可見時是否仍可交互,默認為false。
  8. maintainSemantics: 可選屬性,布爾值,控制子組件語義是否保持不變,默認為false。
  • 注意: 即使子組件被隱藏,其狀態也會被維持,這在需要保留用戶輸入或狀態的情況下非常有用,對高頻切換顯隱的組件(如列表項),避免啟用 maintainState,防止內存泄漏,

  • 推薦: 表單動態字段、Tab 切換內容、配合 AnimatedSwitcher 實現漸變/縮放效果

Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Visibility(visible: _isShown,child: Container(width: 100,height: 100,color: Colors.amber,),),const SizedBox(height: 20),ElevatedButton(onPressed: _toggleVisibility,child: Text(_isShown ? '隱藏盒子' : '顯示盒子'),),],),
)

Visibility

3. IgnorePointer

  • 說明: 用于忽略其子組件的指針事件(如點擊、拖動等)。這意味著包裹在 IgnorePointer 中的子組件將不會響應用戶的交互操作,但仍然會顯示在界面上。IgnorePointer 與 AbsorbPointer 類似,但 IgnorePointer 不會終止指針事件,而是讓這些事件傳遞到其下方的組件

  • 規則: ignoring: 布爾值,決定是否忽略指針事件。默認為 true,ignoringSemantics: 布爾值,決定是否忽略語義信息。默認為 null,即不忽略,可以嵌套在任何布局組件(如 Stack、Column)中。
    若多個 IgnorePointer 嵌套,外層 ignoring 為 true 時,內層設置無效,事件穿透:當 ignoring 為 true 時,子組件的點擊事件會穿透到父級組件。適用于需要隱藏交互但保留顯示效果的場景(如背景圖、裝飾元素)

  • 注意: 避免在復雜嵌套結構中濫用,可能導致不必要的渲染開銷。優先結合 Stack 和 Positioned 實現局部交互控制,需動態控制 ignoring 時,建議通過 StatefulWidget 管理狀態,配合 setState 更新,與 GestureDetector 結合使用時,需注意事件優先級。例如,外層 IgnorePointer 為 true 時,內層 GestureDetector 無法觸發事件

  • 推薦: 背景圖片、水印等無需交互的組件、表單提交時禁用按鈕點擊,防止重復提交、在 Stack 中疊加多個組件時,控制特定層的交互優先級

Column(mainAxisAlignment: MainAxisAlignment.center,children: [// 使用 IgnorePointer 包裹按鈕IgnorePointer(ignoring: _isIgnoring,child: ElevatedButton(onPressed: () {print('按鈕被點擊');},child: const Text('可點擊按鈕'),),),const SizedBox(height: 20),// 切換開關Switch(value: _isIgnoring,onChanged: (value) {setState(() {_isIgnoring = value;});},activeTrackColor: Colors.blueGrey,activeColor: Colors.blue,),const Text('開關開啟時按鈕不可點擊'),],
)

IgnorePointer

4. AbsorbPointer

  • 說明: 用于阻止子組件接收指針事件的布局組件,其核心功能是通過終止命中測試(HitTest)來禁用子樹的交互能力。它不會影響布局和繪制,僅控制事件傳遞

  • 規則: absorbing:布爾值,默認為 true。當設置為 true 時,子組件將無法接收用戶輸入事件,必須直接包裹需要禁用的子組件,且不影響父級或外部組件的交互

  • 注意: 雖然 AbsorbPointer 的使用不會顯著影響性能,但在復雜的布局中應謹慎使用,避免不必要的嵌套,需配合狀態管理(如 setState)動態控制 absorbing 屬性,實現交互開關,即使 AbsorbPointer 設置為吸收模式,它仍然會將點擊事件傳遞給其父組件。這意味著父組件的 GestureDetector 仍然能夠捕獲點擊事件

  • 推薦: 臨時禁用交互、批量禁用組件

Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [// 外層可點擊區域GestureDetector(// ignore: avoid_printonTap: () => print('外層點擊'),child: Container(color: Colors.grey,padding: const EdgeInsets.all(16),child: AbsorbPointer(absorbing: true, // 禁用子組件交互child: Row(children: [ElevatedButton(// ignore: avoid_printonPressed: () => print('按鈕點擊'),child: const Text('禁用按鈕'),),const SizedBox(width: 16),const Text('不可點擊區域'),],),),),),const SizedBox(height: 20),// 對比示例(交互正常)GestureDetector(// ignore: avoid_printonTap: () => print('外層點擊'),child: Container(color: Colors.grey,padding: const EdgeInsets.all(16),child: AbsorbPointer(absorbing: false, // 啟用子組件交互child: ElevatedButton(// ignore: avoid_printonPressed: () => print('正常按鈕'),child: const Text('可點擊按鈕'),),),),),],),
)

AbsorbPointer

Overlay 覆蓋層

Opacity 透明度效果

AnimatedOpacity 動畫透明度

ClipRRect 圓角裁剪

ClipOval 橢圓裁剪

ClipPath 路徑裁剪

ClipRect 矩形裁剪

BackdropFilter 背景濾鏡

DecoratedBox 裝飾盒子

RotatedBox 旋轉盒子

平臺特定布局控件

CupertinoPageScaffold iOS風格頁面腳手架

CupertinoTabScaffold iOS風格標簽欄腳手架

CupertinoNavigationBar iOS風格導航欄

CupertinoTabBar iOS風格標簽欄

CupertinoActionSheet iOS風格操作表

CupertinoAlertDialog iOS風格警告對話框

CupertinoContextMenu iOS風格上下文菜單

MaterialApp Material Design 應用容器

Scaffold Material Design 頁面腳手架

AppBar Material Design 應用欄

BottomAppBar Material Design 底部應用欄

TabBar Material Design 標簽欄

Drawer Material Design 抽屜

SnackBar. Material Design 底部消息條

BottomSheet Material Design 底部表單

Material Material Design 表面

Card Material Design 卡片

Chip Material Design 標簽

Divider Material Design 分割線:水平分割線組件,常見于列表項、板塊劃分等場景

ListTile Material Design 列表項:主要用于創建列表項(List Item),通常用于 ListView、Card 或 Drawer 等布局中

總結

雖然 Flutter 提供了超過 100+ 個布局相關控件,以上列舉覆蓋了幾乎所有官方核心布局控件,實際開發中最常用的約只有 15 個。掌握 Row/Column/Stack/Expanded/ListView這五個核心組件即可解決絕大多數布局需求。

基礎布局三巨頭: Row/Column(線性布局)、Stack(層疊布局)、ListView/GridView(滾動布局)滿足 80% 場景

彈性布局必用: Expanded 和 Flexible 是自適應布局的核心

性能關鍵: 長列表必用 ListView.builder,避免濫用 IntrinsicWidth/Height,復雜動畫使用 CustomMultiChildLayout

響應式最佳實踐: LayoutBuilder + MediaQuery + 約束條件判斷

DEMO

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/88153.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/88153.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/88153.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

EPLAN 電氣制圖:建立自己的部件庫,添加部件-加SQL Server安裝教程(三)上

在智能電氣設計領域&#xff0c;EPLAN 作為主流的設計軟件&#xff0c;其部件庫的完善程度直接影響項目設計的效率與質量。本文將從實際操作出發&#xff0c;詳細講解如何在 EPLAN 中建立專屬部件庫并添加部件&#xff0c;為電氣設計奠定堅實基礎。一、部件庫&#xff1a;電氣設…

靜態路由進階實戰全解

一、項目背景二、項目拓撲圖三、設備命名與IP地址規劃設備名接口編號IP地址規劃R1GE0/0192.168.1.1/24GE0/1172.16.1.1/24R2GE0/0192.168.1.2/24GE0/1192.168.2.2/24R3GE0/0192.168.2.3/24GE0/1192.168.3.3/24GE0/2192.168.4.3/24R4GE0/0192.168.3.4/24GE0/1192.168.4.4/24GE0/…

stm32hal模塊驅動(3)ssd1305 oled驅動

SD1305 OLED 驅動芯片詳細介紹SSD1305 是 Solomon Systech 公司生產的一款 OLED 顯示控制器/驅動器&#xff0c;專為 128x64 或 128x32 點陣的 OLED 顯示屏設計。下面我將從多個方面詳細介紹這款驅動芯片。一、SSD1305 基本特性顯示分辨率&#xff1a;最大支持 128 segments 6…

安全為先:如何在 Python 中安全處理數據庫連接與敏感信息

安全為先:如何在 Python 中安全處理數據庫連接與敏感信息 引言:Python 與安全的數據庫交互 自 1991 年誕生以來,Python 憑借其簡潔優雅的語法和強大的生態系統,成為 Web 開發、數據科學、人工智能和數據庫交互的首選語言。作為“膠水語言”,Python 不僅讓開發者能夠快速…

服務器經常出現藍屏是什么原因導致的?如何排查和修復?

服務器出現藍屏&#xff08;BSOD&#xff0c;Blue Screen of Death&#xff09;是一個嚴重的問題&#xff0c;通常表明系統內核或硬件發生了不可恢復的錯誤。藍屏不僅會導致服務器宕機&#xff0c;還可能對業務運行造成重大影響。要有效解決藍屏問題&#xff0c;需要先找到根本…

為什么elementui的<el-table-column label=“名稱“ prop=“name“ label不用寫成:label

在 Vue.js 中&#xff0c;label 和 prop 是 el-table-column 組件的普通屬性&#xff0c;而不是動態綁定的表達式。因此&#xff0c;不需要使用 : 來綁定它們。 1. Vue.js 中的屬性綁定 在 Vue.js 中&#xff0c;屬性綁定有兩種方式&#xff1a; 靜態屬性綁定&#xff1a;直接寫…

分布式光纖傳感:為儲能安全保駕護航

儲能系統是指一種能夠將電能、化學能、動能等形式的能量進行轉化、儲存和釋放的裝置&#xff0c;廣泛應用于可再生能源發電、智能電網、電動車等領域。儲能行業這幾年得到了穩步發展&#xff0c;受到政府機構、行業協會、大型能源企業、電網公司、系統集成商、檢測認證機構等業…

從歷史航拍圖像中去除陰影

在光學遙感中&#xff0c;陰影是影響土地覆蓋制圖精度和分辨率的一個因素&#xff0c;無論是歷史影像&#xff08;黑白影像&#xff09;還是近期影像&#xff08;全彩影像&#xff09;。陰影的產生取決于太陽光照&#xff08;太陽方位角和天頂角&#xff09;、相機視點&#xf…

UE material advance 學習筆記

如何體現輪胎速度的快速感&#xff1a;就是增加一個radial blur&#xff0c;會讓視覺效果感覺輪胎已經轉冒煙了&#xff0c;但是上面兩個輪胎的轉速其實是相同的這種磨砂的感覺&#xff0c;可以用上ditherAA來實現只看法線這一塊&#xff0c;ditherAA就是讓他的表面顏色有大量的…

Vue--2、Vue2 項目配置與組件化開發

一、Vue2 項目環境搭建1. 環境準備安裝 Node.js&#xff1a;推薦使用 nvm 管理多版本 Node# 安裝Node 16.20.2 nvm install 16.20.2 # 切換至指定版本 nvm use 16.20.2 # 驗證安裝 node -v && npm -v安裝 Vue CLI 腳手架&#xff1a;# 國內鏡像源安裝 npm install --re…

虛幻基礎:函數的返回節點

能幫到你的話&#xff0c;就給個贊吧 &#x1f618; 文章目錄函數的返回節點&#xff1a;返回執行后的值返回執行后的值若不執行第一次 返回參數的默認值第二次 返回上一次執行值示例函數的返回節點&#xff1a;返回執行后的值 返回執行后的值 若不執行 第一次 返回參數的默…

FFmpeg 升級指北

近期我參與了部門底層庫依賴的 FFmpeg 從 3.4 升級至 7.0.2 的工作&#xff0c;在此分享一些經驗和遇到的 API 變動。 將 FFmpeg 升級到高版本后&#xff0c;編譯過程中遇到大量報錯是常態。這些錯誤通常源于 API 接口變更或結構體字段調整。此時不必驚慌&#xff0c;核心解決…

RISCV Linux 虛擬內存精講系列三 -- setup_vm()

在 Linux 使用虛擬地址前&#xff0c;需要先配置頁表&#xff0c;這就是 setup_vm() 的作用。然而&#xff0c;Linux 的頁表配置&#xff0c;并不是一次過完成的&#xff0c;分了兩個階段&#xff0c;如下&#xff1a;在 setup_vm() 中&#xff0c;主要初始化了&#xff1a;1. …

創客匠人:解析創始人 IP 打造的底層邏輯與知識變現路徑

在數字經濟時代&#xff0c;創始人 IP 的價值被不斷放大&#xff0c;而知識變現作為 IP 商業閉環的核心環節&#xff0c;正成為無數創業者探索的方向。創客匠人深耕知識付費領域多年&#xff0c;見證了大量創始人從 0 到 1 打造 IP 并實現變現的全過程&#xff0c;其背后的邏輯…

Visual Studio 2022 MFC Dialog 添加Toolbar及Tips提示

主要步驟&#xff1a;在主框架類中添加消息處理函數聲明在 OnCreate 函數中啟用工具欄提示在消息映射中注冊 TTN_NEEDTEXT 消息使用 OnToolTipText 函數實現自定義提示文本1.在主程序的.h文件中加入afx_msg BOOL OnToolTipText(UINT id, NMHDR* pNMHDR, LRESULT* pResult); 2.在…

2025Q2大模型更新匯總(大語言模型篇)

摘要 2025年Q2大語言模型更新匯總&#xff1a; Qwen3&#xff0c;Deepseek-R1-0528&#xff0c;Doubao-Seed-1.6, MiniMax-M1, GPT4.1/O3/O4&#xff0c;Claude4/Gemini2.5 Qwen3 ? 開源MOE模型&#xff0c; ? MOE模型&#xff1a;Qwen3-235B-A22B&#xff0c;Qwen3-30B-…

【STM32】定時器中斷 + 含常用寄存器和庫函數配置(提供完整實例代碼)

通用定時器基礎知識 參考資料:STM32F1xx官方資料:《STM32中文參考手冊V10》-第14章通用定時器 通用定時器工作過程: 時鐘選擇 計數器時鐘可以由下列時鐘源提供: ① 內部時鐘(CK_INT) ② 外部時鐘模式1:外部輸入腳(TIx) ③ 外部時鐘模式2:外部觸發輸入(ETR) ④ 內部觸…

集群Redis

文章目錄前言一、Redis主從復制配置1.1.配置文件redis_master.conf,redis_slave.conf1.2.啟動服務1.3.檢查成果二、Redis集群配置2.1.服務器40.240.34.91集群配置2.2.其它服務器xxx.92,xxx.93集群配置2.3.啟動服務2.3.啟動集群服務2.4.檢查成果三、優劣四、結束前言 提示&…

ORA-600 kokiasg1故障分析---惜分飛

故障總結:客戶正常關閉數據庫,然后啟動報ORA-600 kokiasg1錯誤,通過對啟動分析確認是由于IDGEN1$序列丟失導致,修復該故障之后,數據庫啟動成功,但是后臺大量報ORA-600 12803,ORA-600 15264等錯誤,業務用戶無法登錄.經過深入分析,發現數據庫字典obj$中所有核心字典的序列全部被刪…

[RPA] 影刀RPA基本知識

1.應用的構成一個應用&#xff1a;由多條指令疊加組成一條指令代表了一個操作動作許多條指令按照一定的邏輯關系編排起來&#xff0c;就構成了一個應用(這里的應用可理解為軟件機器人RPA)一個應用 多個自動化指令的集合 2. 指令的一般構成在XXX對象上&#xff0c;對XXX元素執行…