Open3
NestedScrollViewとは?
NestedScrollViewとは?
NestedScrollViewは、Flutterで提供される特殊なスクロールビューウィジェットです。その最大の特徴は、複数のスクロール可能なビューを入れ子にでき、それらのスクロール位置が互いにリンクされることです。
基本的な使用例
最も一般的な使用例は以下のような構成です:
NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text('NestedScrollViewのデモ'),
floating: true,
// ヘッダーの設定
),
];
},
body: TabBarView(
// タブの内容
),
)
なぜNestedScrollViewが必要なのか?
従来のScrollViewの課題
通常のScrollViewでは、以下のような問題が発生していました:
- 内部のスクロールビューと外部のスクロールビューが独立して動作
- SliverAppBarの展開/収縮が内部のスクロールに連動しない
- ユーザー体験が分断される
NestedScrollViewによる解決
NestedScrollViewは以下の機能を提供することでこれらの問題を解決します:
- 外部・内部のScrollControllerのカスタム連携
- スクロール位置の同期
- シームレスなユーザー体験の提供
重要な設定項目
1. floatHeaderSlivers
NestedScrollView(
floatHeaderSlivers: true, // ヘッダーのフロート動作を有効化
// ...
)
このプロパティは、ヘッダーのSliverAppBarのフロート動作を制御します。
2. SliverOverlapAbsorberの使用
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
// ...
),
)
これにより、ヘッダーと本文のスクロール位置が正しく連携します。
主な使用パターン
1. ピン留めされたSliverAppBar
SliverAppBar(
pinned: true, // 常に表示されるように固定
// ...
)
2. フローティングSliverAppBar
SliverAppBar(
floating: true, // スクロールに応じて表示/非表示
// ...
)
サンプルコード
class NestedScrollViewExample extends StatelessWidget {
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
body: NestedScrollView(
// ヘッダーの設定
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text('NestedScrollViewデモ'),
pinned: true,
floating: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.list), text: 'リスト'),
Tab(icon: Icon(Icons.grid_on), text: 'グリッド'),
Tab(icon: Icon(Icons.person), text: 'プロフィール'),
],
),
),
),
];
},
// 本文の設定
body: TabBarView(
children: [
_buildListTab(),
_buildGridTab(),
_buildProfileTab(),
],
),
),
),
);
}
// リストタブの内容
Widget _buildListTab() {
return Builder(
builder: (BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
backgroundColor: Colors.blue[100],
),
title: Text('リストアイテム ${index + 1}'),
subtitle: Text('説明テキスト'),
onTap: () {},
);
},
childCount: 30,
),
),
],
);
},
);
}
// グリッドタブの内容
Widget _buildGridTab() {
return Builder(
builder: (BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 1.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(
color: Colors.blue[100],
child: Center(
child: Text(
'アイテム ${index + 1}',
style: TextStyle(fontSize: 16.0),
),
),
);
},
childCount: 20,
),
),
],
);
},
);
}
// プロフィールタブの内容
Widget _buildProfileTab() {
return Builder(
builder: (BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverList(
delegate: SliverChildListDelegate([
ListTile(
leading: Icon(Icons.person),
title: Text('ユーザー名'),
subtitle: Text('ユーザーID: 12345'),
),
ListTile(
leading: Icon(Icons.email),
title: Text('メールアドレス'),
subtitle: Text('example@email.com'),
),
ListTile(
leading: Icon(Icons.phone),
title: Text('電話番号'),
subtitle: Text('090-1234-5678'),
),
// 他のプロフィール情報
]),
),
],
);
},
);
}
}