Dart

38.【AppleMusicクローン】SliverAppBarとSliverListでヘッダーの伸び縮みを再現する

今回からApple Musicの真ん中のタブに存在する「見つける」の画面を実装していきます。

このヘッダーの部分の実装

今回、使うウィジェットは

  • SliverAppBar
  • SliverFixedExtentList
  • ScrollController

この3つがメインになります。

Slivers について

SliverAppBar は Slivers の一部のウィジェットらしいです。

Slivers の説明

A sliver is a portion of a scrollable area. You can use slivers to achieve custom scrolling effects.

Sliver はスクロール可能領域をカスタムにすることができるウィジェットとのことです。

おおまかな使い方はCustomScrollViewにSliverXXX と付くウィジェットを乗せていきます。

CustomScrollView(
        shrinkWrap: false,
        slivers: <Widget>[
          /// Sliverの付くウィジェットを乗せる
        ],
      )

SliversのAppBar に該当するウィジェットはSliverAppBar、body に該当するウィジェットはSliverListSliverGridなどが存在します。
それらをCustomScrollViewのslivers に乗せていきます。

今回のヘッダーを作成する場合にはSliverAppBarとSliverFixedExtentListを使います。

SliverAppBar(
            pinned: false,
            expandedHeight: 40.0,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(
                '見つける',
              ),
            ),
          )

SliverFixedExtentList(
            itemExtent: 200.0,
            delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                return Container(
                  alignment: Alignment.center,
                  color: Colors.lightBlue[100 * (index % 9)],
                  child: Text('list item $index', style: TextStyle(fontSize: 30),),
                );
              },
            ),
          )

このように使います。

ScrollController について

ですが、Sliversを使っても「見つける」文字がスクロールして左側から中央に表示させることが難しかったです。
上級者であればスムーズに実装することができるのかもしれませんが、私は今回は別のアプローチを取りました。
スクロール量に応じて表示の切り替えをするアプローチを取ります。

Flutter でスクロール量を計算できるものにScrollControllerが存在するのでこれを使います。

使い方は

ScrollController _scrollController;

@override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
  }

として初期化します。
初期化した_scrollControllerをCustomScrollViewのcontrollerに設定します。

CustomScrollView(
        shrinkWrap: false,
        controller: _scrollController,
        slivers: <Widget>[
        ],
      )

そして、スクロールしたときに処理する内容をメソッドにしてaddListenerに入れます。

@override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListener);
  }

  void _scrollListener() {
    print(_scrollController.offset);
  }

_scrollController.offset は現在のスクロール位置を出力します。
int型で数字なのでif文を使って制御します。

void _scrollListener() {
    print(_scrollController.offset);

    if (_scrollController.offset > 100) {
      setState(() {
      });
    } else {
      setState(() {
      });
    }
  }

これでスクロール位置によって何かを制御できるようになりました。

ソースコードについて

それではこれらの機能をもとにして「見つける」の伸び縮みを再現したソースコードを書きます。
今回はmain.dart だけになります。

main.dart

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'サンプルアプリ',
      theme: ThemeData(),
      home: HomePage(title: '見つける'),
    );
  }
}

class HomePage extends StatefulWidget {
  HomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _isVisibleHeader;
  ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListener);
    _isVisibleHeader = true;
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  void _scrollListener() {
    print(_scrollController.offset);

    if (_scrollController.offset > 100) {
      setState(() {
        _isVisibleHeader = true;
      });
    } else {
      setState(() {
        _isVisibleHeader = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: _isVisibleHeader ? 1.0 : 0.0,
        backgroundColor: Colors.white,
        title: Visibility(visible: _isVisibleHeader, child: Text('見つける', style: TextStyle(color: Colors.black),)),
      ),
      body: CustomScrollView(
        shrinkWrap: false,
        controller: _scrollController,
        slivers: <Widget>[
          SliverAppBar(
            pinned: false,
            backgroundColor: Colors.white,
            expandedHeight: 40.0,
            flexibleSpace: FlexibleSpaceBar(
              titlePadding: EdgeInsets.only(left: 20),
              centerTitle: false,
              title: Text(
                '見つける',
                style: TextStyle(color: Colors.black),
              ),
            ),
          ),

          SliverFixedExtentList(
            itemExtent: 200.0,
            delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                return Container(
                  alignment: Alignment.center,
                  color: Colors.lightBlue[100 * (index % 9)],
                  child: Text('list item $index', style: TextStyle(fontSize: 30),),
                );
              },
            ),
          )
        ],
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

SliverFixedExtentList は今後別のウィジェットにする予定ですが、仮置きで書きました。
これで一番最初の難関を突破した気分です。

ABOUT ME
tamappe
都内で働くiOSアプリエンジニアのTamappeです。 当ブログではモバイルアプリの開発手法について紹介しています。メインはiOS、サブでFlutter, Android も対応できます。 執筆・講演のご相談は tamapppe@gmail.com までお問い合わせください。