タイトルがわかりにくいですが、下のgifのようなUIをFlutterでどのように実現するかという話です。
実装方法
全体の構造
NestedScrollViewを使って以下のようなViewの構造にします。
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (context, hoge) {
return [
SliverPersistentHeader(
pinned: true,
// floating: true,
delegate: MySliverPersistentHeaderDelegate())
];
},
body: TabBarView(
children: [
ListView.builder(
key: PageStorageKey(0), // keep scroll position
itemBuilder: (context, index) {
return ListTile(
title: Text("page one:$index"),
);
},
itemCount: 50,
),
ListView.builder(
key: PageStorageKey(1),
itemBuilder: (context, index) {
return ListTile(
title: Text("page two:$index"),
);
},
itemCount: 80,
),
],
),
),
),
),
);
}
SliverAppBarが便利そうだったのですが、bottomのタブだけ残すという動作ができなかったのでヘッダは SliverPersistentHeader
を使って、タブだけ残るようにしています。
MySliverPersistentHeaderDelegate
MySliverPersistentHeaderDelegateは以下のような実装にしています。
スクロールによって緑の部分の高さを減らしていって、最終的にタブの部分だけ残るようにしています。
サンプルなので常にリビルドしています。
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
@override
double get maxExtent => 200;
@override
double get minExtent => 48; // from tab height
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
print("so:$shrinkOffset, oc:$overlapsContent");
return Container(
color: Colors.pinkAccent,
height: max(maxExtent - shrinkOffset, minExtent),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: Colors.green,
height: max(maxExtent - shrinkOffset - minExtent, 0),
),
TabBar(tabs: [
Tab(
text: "page1",
),
Tab(
text: "page2",
),
]),
],
),
);
}
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
まとめ
この手のUIをiOS/Androidで両方実現しようとすると、どちらにしても結構コスト掛かりそうですがFlutterでそれなりに簡単に両OSに対応できるのは素晴らしいですね。
サンプルプロジェクトはこちら
参考