ListView 基本上是以線性排列元件,如果想以網格的方式排列元件,並具有捲動效果,雖然可以透過 Row、Column 與 SingleChildScrollView 的組合來達成,不過 Flutter 提供了現成的 GridView,可以方便地達到這項需求。
GridView 與 ListView 都是 BoxScrollView 的子類,而 BoxScrollView 是 ScrollView 的子類,因此會有一些捲動行為方面的共通選項,實際上在使用 GridView 時,應該注意的反而不是這些捲動行為方面的共通選項,而是網格的排列行為。
想想看,如果是自行使用 Row、Column 來建立網格,必然要注意 main axis 與 cross axis 的設定,在使用 GridView 時也是如此,對 GridView 來說,捲動的方向是 main axis,垂直於捲動方向的是 cross axis,至於網格的元件如何排列,是藉由 gridDelegate 特性來設定,從參數名稱來看,這個行為是委外的,而 gridDelegate 的型態是 SliverGridDelegate,嗯?Slivers 是?
在官方文件的〈Slivers〉中第一句就談到:
A sliver is a portion of a scrollable area.
就概念而言,就只是這樣!Silver 代表一塊可捲動的區域。當然,為了控制這塊可捲動區域的行為,Flutter 必須定義出一些協定,畢竟必須可以捲動,這協定與〈OVERFLOW 是啥?〉不同,然而捲動的實作有其複雜性,因此 Flutter 也有一些元件,實作了這些協定,事實上,Flutter 的一些捲動元件,像是 ListView、GridView 等,底層都是 Silver 的相關實作在處理。
gridDelegate 的型態是 SliverGridDelegate,在 Flutter 中主要有兩個實現類別:SliverGridDelegateWithFixedCrossAxisCount 與 SliverGridDelegateWithMaxCrossAxisExtent。
SliverGridDelegateWithFixedCrossAxisCount 提供了 cross axis 方向上固定元素的排列,這是藉由 SliverGridDelegateWithFixedCrossAxisCount 的 crossAxisCount 設定,例如:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Openhome.cc')),
body: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
childAspectRatio: 1.5
),
children: demoChildren(32)
)
)
)
);
}
List<Widget> demoChildren(num) {
return List.generate(num, (i) => Center(
child: Ink(
decoration: const ShapeDecoration(
color: Colors.lightBlue,
shape: CircleBorder(),
),
child: IconButton(
icon: Icon(
IconData(59677 + i, fontFamily: 'MaterialIcons')
),
color: Colors.white,
onPressed: () {},
),
),
));
}
在這邊的範例,crossAxisCount 設定為 5,表示 cross axis 固定有五個子元件,至於 childAspectRatio,指的是每個子元件 cross axis / main axis 範圍比例,預設是 1.0,簡單來說就是依 cross axis 可獲得的範圍決定子元件的寬後,高就等於寬,childAspectRatio 設定越大,main axis 方向的元件就越密,主要就是看你的版面想要如何安排來設定,也更細部地調整 mainAxisSpacing、crossAxisSpacing 等特性。
範例執行後的畫面如下,如果你轉動了手機,cross axis 方向依然只會有五個元件:
SliverGridDelegateWithMaxCrossAxisExtent 則提供了 cross axis 方向每個子元件可用的最大範圍,例如:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Openhome.cc')),
body: GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120
),
children: demoChildren(32)
)
)
)
);
}
List<Widget> demoChildren(num) {
return List.generate(num, (i) => Center(
child: Ink(
decoration: const ShapeDecoration(
color: Colors.lightBlue,
shape: CircleBorder(),
),
child: IconButton(
icon: Icon(
IconData(59677 + i, fontFamily: 'MaterialIcons')
),
color: Colors.white,
onPressed: () {},
),
),
));
}
cross axis 可獲得的範圍沒辦法容納的子元件,就會往 main axis 的方向排列,例如,底下是執行畫面之一:
將手機擺直後的畫面會是:
GridView.count 建構式內部使用了 SliverGridDelegateWithFixedCrossAxisCount,而 GridView.extent 建構式內部使用了 SliverGridDelegateWithMaxCrossAxisExtent,怎麼使用應該就不必多做說明了吧!
不過以上的範例,因為必須準備好 children,也就沒有延遲載入的效果,類似地,有個 GridView.builder 建構式可以達到這個需求:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Openhome.cc')),
body: GridView.builder(
itemCount: 30,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120
),
itemBuilder: (context, i) {
return Center(
child: Ink(
decoration: const ShapeDecoration(
color: Colors.lightBlue,
shape: CircleBorder(),
),
child: IconButton(
icon: Icon(
IconData(59677 + i, fontFamily: 'MaterialIcons')
),
color: Colors.white,
onPressed: () {},
),
),
);
}
)
)
)
);
}
執行後的結果與前一個範例是相同的,只不過是延遲載入。

