Open3

NestedScrollViewとは?

muranakarmuranakar

NestedScrollViewとは?

NestedScrollViewは、Flutterで提供される特殊なスクロールビューウィジェットです。その最大の特徴は、複数のスクロール可能なビューを入れ子にでき、それらのスクロール位置が互いにリンクされることです。

基本的な使用例

最も一般的な使用例は以下のような構成です:

NestedScrollView(
  headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
    return <Widget>[
      SliverAppBar(
        title: Text('NestedScrollViewのデモ'),
        floating: true,
        // ヘッダーの設定
      ),
    ];
  },
  body: TabBarView(
    // タブの内容
  ),
)

なぜNestedScrollViewが必要なのか?

従来のScrollViewの課題

通常のScrollViewでは、以下のような問題が発生していました:

  1. 内部のスクロールビューと外部のスクロールビューが独立して動作
  2. SliverAppBarの展開/収縮が内部のスクロールに連動しない
  3. ユーザー体験が分断される

NestedScrollViewによる解決

NestedScrollViewは以下の機能を提供することでこれらの問題を解決します:

  1. 外部・内部のScrollControllerのカスタム連携
  2. スクロール位置の同期
  3. シームレスなユーザー体験の提供

重要な設定項目

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,  // スクロールに応じて表示/非表示
  // ...
)
muranakarmuranakar


サンプルコード


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'),
                ),
                // 他のプロフィール情報
              ]),
            ),
          ],
        );
      },
    );
  }
}