網格排列的 GridView


ListView 基本上是以線性排列元件,如果想以網格的方式排列元件,並具有捲動效果,雖然可以透過 RowColumnSingleChildScrollView 的組合來達成,不過 Flutter 提供了現成的 GridView,可以方便地達到這項需求。

GridViewListView 都是 BoxScrollView 的子類,而 BoxScrollViewScrollView 的子類,因此會有一些捲動行為方面的共通選項,實際上在使用 GridView 時,應該注意的反而不是這些捲動行為方面的共通選項,而是網格的排列行為。

想想看,如果是自行使用 RowColumn 來建立網格,必然要注意 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 的一些捲動元件,像是 ListViewGridView 等,底層都是 Silver 的相關實作在處理。

gridDelegate 的型態是 SliverGridDelegate,在 Flutter 中主要有兩個實現類別:SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent

SliverGridDelegateWithFixedCrossAxisCount 提供了 cross axis 方向上固定元素的排列,這是藉由 SliverGridDelegateWithFixedCrossAxisCountcrossAxisCount 設定,例如:

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 方向的元件就越密,主要就是看你的版面想要如何安排來設定,也更細部地調整 mainAxisSpacingcrossAxisSpacing 等特性。

範例執行後的畫面如下,如果你轉動了手機,cross axis 方向依然只會有五個元件:

網格排列的 GridView

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

將手機擺直後的畫面會是:

網格排列的 GridView

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: () {},
                ),
              ),
            );
          }
        )
      )
    )
  );
}

執行後的結果與前一個範例是相同的,只不過是延遲載入。