🗂

【Flutter】Sliverでスクロールした時に上にくっつくTabBarの実装方法

2021/12/12に公開

上にくっつくTabBarとは

そもそも上にくっつくTabBarってなんやねんと思う方もいるかもしれません
ここではいわゆるこんな感じのやつです。(正しい呼び方があれば教えてください)

SliverAppBar SliverPersistentHeader
SliverAppBar SliverPersistentHeader

https://note.com/kitoko552/n/n7da7c55ebab6
こちらの記事ではStickyTabBarと呼んでいました!

この実装方法を主に2つ紹介します

実装方法

  1. SliverAppBarによる実装
  2. SliverPersistentHeaderによる実装

SliverAppBarによる実装

SliverAppBar

SliverAppBarについてはこちらの公式ドキュメントによくまとまっています。いろんな例も詳しく載っているのでぜひご覧ください!
https://api.flutter.dev/flutter/material/SliverAppBar-class.html

NestedScrollViewを使用することによりSliverAppBar+TabBarViewの実装を可能にしています。
ドキュメントによるとNestedScrollViewのheaderにSliverAppBar、bodyにTabBarViewを設定するという実装が一般的だそう。

実装例
//DefaultTabControllerでTabの数を設定する
 DefaultTabController(
        length: 2,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
	  //headerSliverBuilder内でSliverAppBarを設定
            return <Widget>[
              const SliverAppBar(
                pinned: true,//trueの場合、スクロースしても上にAppBarが残る
                expandedHeight: 150,//拡大されている際のAppBarの高さ
                bottom: TabBar(
                  //AppBarと同様にbottomにTabBarを設定
                ),
              ),
            ];
          },
	  //bodyにTabBarViewを設定
          body: TabBarView(
            children: [
		//Tabごとにウィジェットを設定する
            ],
          ),
        ),
      ),

SliverPersistentHeaderによる実装

SliverPersistentHeader

そこのあなた!AppBarとTabBarの間に何か挟みたい時、ありますよね??
そんな時に使うのがSliverPersistentHeaderです!! SliverPersistentHeaderを使うと色々自由にUIを組めそうなのでいいと思います!

ということでまずは全体的に見ていきましょう

Scaffold(
      //AppBarを設定する
      appBar: AppBar(),
      body: DefaultTabController(
        length: 2,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              _headerSection(),//オレンジ色の部分
              _tabSection(),//TabBarの部分
            ];
          },
          body: TabBarView(
            children: [
            //省略
            ],
          ),
        ),
      ),
    );

_headerSection()について

NestedScrollViewのheaderSliverBuilder内ではSliverとつくWidget群を入れる必要があり、それ以外を入れるとエラーが発生します(TabBarを試しに入れてみるとエラーが出ます)↓
エラー

_headerSectionはSliverListを用いて作成しました。

_headerSection
Widget _headerSection() {
  return SliverList(
    delegate: SliverChildListDelegate(
      [
      //ここをカスタマイズする
        Container(
          color: Colors.orangeAccent,
          height: 100,
          child: const Center(
            child: Text('headerSection'),
          ),
        ),
      ],
    ),
  );
}

_tabSection()について

SliverとつくWidget群を入れる必要があるためSliverPersistentHeaderを用いてTabBarを設定するのですが、SliverPersistentHeaderDelegateが必要 となります

SliverPersistentHeader(
    delegate: //SliverPersistentHeaderDelegateが必要(TabBar部分)
  );

そこでSliverPersistentHeaderを継承したクラスを作成します

SliverPersistentHeaderDelegateを継承したTabBar
class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  const _StickyTabBarDelegate({required this.tabBar});

  final TabBar tabBar;

  
  double get minExtent => tabBar.preferredSize.height;

  
  double get maxExtent => tabBar.preferredSize.height;

  
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return Container(
      color: Colors.white,
      child: tabBar,
    );
  }

  
  bool shouldRebuild(_StickyTabBarDelegate oldDelegate) {
    return tabBar != oldDelegate.tabBar;
  }
}

以上からSliverで使用できるTabBarができたので、_tabSectionはこのようになります

_tabSection()
Widget _tabSection() {
  return const SliverPersistentHeader(
    pinned: true,
    delegate: _StickyTabBarDelegate(
      tabBar: TabBar(
        tabs: [
          Tab(
            text: '1',
          ),
          Tab(
            text: '2',
          )
        ],
      ),
    ),
  );
}

全体のコード

こちらにSliverPersistentHeaderによる実装のサンプルコードを置いておくので色々試してみてください!

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: DefaultTabController(
        length: 2,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              _headerSection(),
              _tabSection(),
            ];
          },
          body: TabBarView(
            children: [
              ListView.builder(
                itemCount: 10,
                itemBuilder: (context, index) {
                  return Center(
                    child: Text(
                      index.toString(),
                      style: const TextStyle(
                        fontSize: 100,
                      ),
                    ),
                  );
                },
              ),
              ListView.builder(
                itemCount: 10,
                itemBuilder: (context, index) {
                  return Center(
                    child: Text(
                      index.toString(),
                      style: const TextStyle(
                        fontSize: 100,
                      ),
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

//header部分
Widget _headerSection() {
  return SliverList(
    delegate: SliverChildListDelegate(
      [
        Container(
          color: Colors.orangeAccent,
          height: 100,
          child: const Center(
            child: Text('headerSection'),
          ),
        ),
      ],
    ),
  );
}

//TabBar部分
Widget _tabSection() {
  return const SliverPersistentHeader(
    pinned: true,
    delegate: _StickyTabBarDelegate(
      tabBar: TabBar(
        labelColor: Colors.black,
        tabs: [
          Tab(
            text: '1',
          ),
          Tab(
            text: '2',
          )
        ],
      ),
    ),
  );
}

//SliverPersistentHeaderDelegateを継承したTabBarを作る
class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  const _StickyTabBarDelegate({required this.tabBar});

  final TabBar tabBar;

  
  double get minExtent => tabBar.preferredSize.height;

  
  double get maxExtent => tabBar.preferredSize.height;

  
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return Container(
      color: Colors.white,
      child: tabBar,
    );
  }

  
  bool shouldRebuild(_StickyTabBarDelegate oldDelegate) {
    return tabBar != oldDelegate.tabBar;
  }
}

Discussion