🎅

【Flutter】CustomScrollView & Sliversで複数要素を一緒にスクロールできるようにする

2021/12/16に公開

ページ先頭にヘッダー画像などをつけて、スクロールしたらフェイドアウトしていくようなUIを作る。

ListViewだとリスト部分しかスクロールできないし、スクロールできるヘッダーみたいなものを入れたい!というときに便利だと思う。

使うのはCustomScrollViewとslivers。
slivers属性には仲間がたくさんいて、その中でも↑のような動きは3つのクラスを使っている。

  • SliverAppBar : 動きのあるAppBar
  • SliverList : スクロールできるリスト(要素によって高さ(大きさ)を指定できる)
  • SliverFixedExtentList : スクロールできるリスト(全ての要素で高さが一様)

今回の例ではSliverListを使う意味がなかったけれど(汗)、例えば高さの異なるボタンとテキストを並べて配置したい時などはSliverListを使うのが良さそう。
高さが一様でいい場合は、なるべくSliverFixedExtentListを使お。

sliverを包んでいるのはCustomScrollView

class SliverSamplePage extends StatelessWidget {
  const SliverSamplePage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return Scaffold(
          backgroundColor: Colors.white,
          body: CustomScrollView(
            slivers: [], //子にSliverたちを並べていく
          ),
        );
  }

sliversの中にはウィジェット(Sliver○○クラス)を入れていく。この中に入れたものたちは、連動してスクロールされる。

SliverAppBar

ヘッダー画像とアイコン画像、タイトル(例では太文字の"TORIsan")までの部分をSliverAppBarで作れる。

例で使用しているプロパティは以下。

leading

通常のAppBarのleadingと同じ役割。戻るボタンなどを設定する。

サンプル↓

        leading: Padding(
            padding: const EdgeInsets.all(8.0),
            child: ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
              },
              style: ElevatedButton.styleFrom(
                primary: Colors.white.withOpacity(0.6),
                shape: const CircleBorder(),
                padding: EdgeInsets.zero,
              ),
              child: const Icon(
                Icons.arrow_back_ios_new,
                color: Colors.black54,
                size: 16,
              ),
            ),
          ),

actions

通常のAppBarのactionsと同じ役割。

サンプル(押しても特に何も起こらない)↓

        actions: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: ElevatedButton(
                onPressed: () {},
                style: ElevatedButton.styleFrom(
                  primary: Colors.white.withOpacity(0.6),
                  shape: const CircleBorder(),
                  padding: const EdgeInsets.all(8),
                ),
                child: const Icon(
                  Icons.app_blocking,
                  color: Colors.black54,
                  size: 16,
                ),
              ),
            ),
          ],

expandedHeight

SliverAppBarの高さを指定。

backgroundColor

SliverAppBarの背景色を指定。

pinned

スクロールした時にAppBarを残したい時はtrue(冒頭の動画のような動きになる)、そうでなければfalseを指定。

elevation

スクロールした時に上部に残るAppBarのelevation。

flexibleSpace

SliverAppBar領域に表示するものを設定する。title("TORIsan"), background(今回でいえばヘッダー画像とアイコン)。

ヘッダー画像とアイコン画像を例のように重ねたいときはStackを使えば実現可能。

サンプル↓

            background: Stack(children: [
                SizedBox(
                  height: 200,
                  width: size.width, // sizeをMediaQuery.of(context).sizeなどで定義しておく
                ),
                Positioned(
                  top: 0,
                  child: SizedBox(
                    height: 150.0,
                    width: size.width,
                    child: Image(
                      image: NetworkImage(imageUrl),
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
                Positioned(
                  top: 110,
                  left: 20,
                  child: CircleAvatar(
                    radius: 40,
                    backgroundImage: NetworkImage(iconUrl),
                  ),
                ),
              ]),

SliverAppBar全体コードはこんな感じ↓。

        SliverAppBar(
            leading: //省略, 
            actions: [/*省略*/], 
            expandedHeight: 100 + kToolbarHeight, 
            backgroundColor: Colors.white,
            pinned: true,
            elevation: 2,
            flexibleSpace: FlexibleSpaceBar(
              title: /*省略*/,
              titlePadding: const EdgeInsets.all(8),
              collapseMode: CollapseMode.pin,
              centerTitle: true,
              background: /*省略*/,
            ),
          ),

SliverList

sliversの子に設定できる、スクロールできるクラス。
SliverListでは、delegateというプロパティの設定が必要。
delegateには『SliverChildListDelegate』と『SliverChildBuilderDelegate』を設定できる。

delegateってなに?についてはこちらがわかりやすい。

補足:delegateは直訳すると、「委譲」です。おまかせするという意味。
Viewを作るそのときに全てのアイテムをビルドするのではなく、アイテムが表示されそうになった>タイミングでそのアイテムをビルドする。そのアイテムのビルドはdelegageパラメータに指定し>た関数に「おまかせ」します、という意味です。
https://bukiyo-papa.com/sliverlist-slivergrid/

ここではリストの数が明確に決まっているので、SliverChildListDelegateを使う。
(後にSliverChildBuilderDelegateも使用。)

        SliverList(
              delegate: SliverChildListDelegate([
            const Center(
              child: Text(
                'region: Japan',
                style: TextStyle(color: Colors.grey, fontSize: 16.0),
              ),
            ),
            const Center(
              child: Text(
                'color: Brown',
                style: TextStyle(
                  color: Colors.grey,
                  fontSize: 16.0,
                ),
              ),
            ),
            const SizedBox(height: 10.0),
          ])),

SliverFixedExtentList

sliversの子に設定できる、スクロールできるクラス。
itemExtentにリストの高さを設定し、SliverListと同じようにdelegateも設定する。

SliverListとは違い、外部からとってきたリストデータ("data")をもとにウィジェットを作るパターンを想定し、SliverChildBuilderDelegateを使ってみる。

        SliverFixedExtentList(
            itemExtent: 100,
            delegate:
                SliverChildBuilderDelegate((BuildContext context, int index) {
              return CardWidget(index: index);
            }, childCount: data.length),
          ),

CardWidget↓

class CardWidget extends StatelessWidget {
  CardWidget({Key? key, required this.index}) : super(key: key);

  int index;

  
  Widget build(BuildContext context) {
    return Card(
      child: Center(
        child: Text(
          data[index],
          style: const TextStyle(color: Colors.grey, fontSize: 24.0),
        ),
      ),
      color: Colors.white.withOpacity(0.8),
    );
  }
}

SliverGridもあるよ

例で使っているのは、ListViewっぽい見た目のやつだけど、SliverGridというGridViewのような見た目で画面全体をスクロールできるようにするクラスもある。

Discussion