NestedScrollViewを使ってSticky TabBarを実現する

FlutterにはTabBarというWidgetがあります。Material DesignのTabs UIを実現するためのWidgetで、AppBar.bottomにTabBarをセットし、TabControllerと一緒に使用することでこのUIを実現するのが一般的な使い方です。こうすることでとても簡単にTabs UIを実現することが可能です。

しかし、アプリ開発に求められる要件はシンプルなものばかりではありません。上記のTabs UIに関しても同様で、例えば、AppBarとTabBarの間にコンテンツを挟んで、スクロールしたら通常のAppBar+TabBarのようにスクロールにくっついてきて欲しいなどです。この記事で言う「Sticky TabBar」はこれのことを指します。つまり、通常のTabBarとは異なり、AppBarとTabBarの間に何かしらのWidgetが存在していて、下にスクロールしていくとAppBarの直下にTabBarがくっつくようなUIのことをこの記事ではSticky TabBarと言います。

画像1

このSticky TabBarを実装しようと思ったとき、どのように実装すればいいかパッと思いつくでしょうか?残念ながら当時の僕はパッとは思いつかず、NestedScrollView使えばいけそうだなとなんとなく思う程度でした。

結論から言うと、NestedScrollView.headerSliverBuilderにSliverListとSliverPersistentHeaderを突っ込むことで僕はSticky TabBarを実現させました。この記事では、その具体的な実装方法を順を追って紹介します。

ちなみにデモアプリをGitHubに置いているので、実際に動かしてみたい方や実装だけ見たい方はこちらからどうぞ。

NestedScrollViewについて

NestedScrollViewは、スクロール位置がリンクされる他のScrollViewを内部にネストすることができるScrollViewです。和訳は得意ではないので原文も載せておきます。

A scrolling view inside of which can be nested other scrolling views, with their scroll positions being intrinsically linked.

これを使ってSticky TabBarを実現するわけですが、NestedScrollView自体メジャーなWidgetであるとは言えないと思うので簡単に解説していきます。NestedScrollViewをかじったことがある方はすでに見たことがあるかもですが、まずはドキュメントに出てくるサンプルに目を通していただきたいです。(以下ドキュメントより抜粋引用)

​headerSliverBuilderとbodyの2つのプロパティが出てきています。このたったの2つのプロパティを使うだけでnestedなScrollViewを実現できることがわかります。更にNestedScrollViewの実装をのぞいてみると、内部的にCustomScrollViewの子クラスを使用しており、そのsliversにheaderSliverBuilderで渡したWidgets(Slivers)とbodyをSliverFillRemainingのchildにセットしたものを渡していることがわかります。SliverFillRemainingは、ViewPortの残りのスペースをchildで埋めるSliverです。つまり、Widgetの構成だけ見て要約すると、CustomScrollViewのsliversに、上にheaderSliverBuilderで渡すSlivers、下にSliverFillRemaining(child: body)を渡しているものになります。(下のコードはあくまで理解しやすくするための要約です)

これを踏まえてサンプルのコードに戻ると、このサンプルはSliverAppBarを用いて、スクロールによって高さが可変なAppBarとTabBarの組み合わせを実現していることがわかります。これは実現したいSticky TabBarに非常に近いUIになっています。

画像2

実際にはAppBarの高さが可変で、あくまでTabBarはAppBarのbottomであり、AppBarとTabBarの間に別のWidgetは存在していないためSticky TabBarではありません。しかし実現したいことが似ていることと上記のWidgetの構成を考えると、このサンプルコードを少しいじればSticky TabBarが実現できてしまうことがわかります。

Sticky TabBarを順に実装

ここからは上記のNestedScrollViewのWidget構成を思い出しつつ、サンプルコードを少しずつ変えていくことでSticky TabBarの実装をしていきます。NestedScrollViewのWidget構成を考えると、bodyはTabBarViewで変える必要がなく、headerSliverBuilderを変えればSticky TabBarが実現できそうということがわかります。なので変更するところはheaderSliverBuilderだけです(コードも省略します)。

早速変えていきます。まず、AppBarの高さが可変である必要ないのでSliverAppBarが不要になります。

こうすることでTabBarがなくなってしまったので、TabBarを追加します。しかし、TabBarと言ってもただのTabBarでなく、AppBar.bottomにセットした時のようにスクロールした時にAppBar直下にくっついてくるTabBarが必要です。

これを実現するためにSliverPersistentHeaderを使います。SliverPersistentHeaderはViewPortの端(leading edge)までスクロールされるとサイズを変化させることができるSliverで、SliverAppBarも内部的にはこれを使っています。これだけ聞くとなぜこれを使うのか疑問に思うかと思いますが、SliverPersistentHeaderには、スクロール時にViewPortの端(leading edge)にそのWidgetを置いておくことができる機能があるのです。正直この機能がSliverPersistentHeaderを使うメインの目的で、サイズは変化しなくていいので実は機能としてはtoo muchです。この機能だけを持つSliverがあればそれだけ使えばいいのですが、現状(Flutter v1.9.1+hotfix.6)はpublicなSliverでこの機能を持っているのはSliverPersistentHeaderだけなのでこちらを使用します(もちろん自作できる方は作ってもいいでしょう)。

SliverPersistentHeaderはdelegateプロパティにSliverPersistentHeaderDelegateをセットしなければならないので、SliverPersistentHeaderDelegateを継承した_StickyTabBarDelegateというものをdelegateに渡しています。前述したように、SliverPersistentHeaderは本来ViewPortの端(leading edge)までスクロールされるとサイズを変化させることができるSliverなので、それらのサイズの設定などをこのSliverPersistentHeaderDelegateで書くことができます。今回はサイズは変わらなくていいので、PreferredSizeWidgetをimplementsしているTabBarをプロパティとして持ち、その高さをminExtent/maxExtentそれぞれに設定することで高さが変わらないSliverPersistentHeaderDelegateを実現しています。

これでStickyなTabBarは実現されたはずです。あとはSliverPersistentHeaderの上にWidgetを入れればそれがAppBarとTabBarの間のWidgetとなります。例えば、SliverListなどを入れてコンテンツを自由に表現できるかと思います。

以上でSticky TabBarの実現ができました。再度になりますが、GitHubにデモアプリを置いてあるので是非実際に触ってみてください。


Twitterはこちら https://twitter.com/kitoko552