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: () {},
),
),
);
}
)
)
)
);
}
執行後的結果與前一個範例是相同的,只不過是延遲載入。