Flutterで複数のPageViewを同期スクロールできるlinked_pageviewパッケージの解説

Flutterで複数のPageViewを配置し、それぞれのスクロールを連動させたいケースがあります。

例えば、

  • 画像カルーセルと情報パネルを連動させる
  • 複数のデータ表示を同期させる
  • パララックス効果を実装する
  • ドキュメントを左右で比較する
  • 複数のレイヤーを連動してスクロールさせる
  • ページごとに異なるサイズのコンテンツを表示する

といった場面です。

通常のPageViewでは、それぞれ独立したスクロールになります。

今回紹介するlinked_pageviewは、複数のPageViewのスクロールを同期できるFlutterパッケージです。

複数のPageViewを1つのジェスチャーで連動させられるほか、それぞれ異なるviewportFractionを設定することもできます。

さらに、ページのコンテンツサイズに合わせて自動的にサイズを変更するExpandableLinkedPageViewも提供されています。

この記事では、linked_pageviewの基本的な使い方から、複数のPageViewを同期する方法、viewportFractionの設定、ページサイズに合わせた自動リサイズ、Controllerの管理方法まで詳しく解説します。

linked_pageviewとは?

linked_pageviewは、複数のPageViewのスクロール操作を同期させるためのFlutterパッケージです。

通常のPageViewでは、

PageView A
    ↓
独立してスクロール

PageView B
    ↓
独立してスクロール

となります。

linked_pageviewを利用すると、

        1つのスクロール操作
                ↓
      ┌─────────┴─────────┐
      ↓                   ↓
 PageView A            PageView B
      ↓                   ↓
   同じ位置へ同期スクロール

という構成を作れます。

さらに、それぞれのPageViewに異なるviewportFractionを設定することもできます。

linked_pageviewの特徴

主な特徴は以下の通りです。

機能対応
複数PageViewの同期
1つのジェスチャーによる同期スクロール
異なるviewportFraction
スムーズなスクロール同期
ピクセル単位の同期
Expandable PageView
childrenによるページ生成
builderによるページ生成
水平方向のスクロール
垂直方向のスクロール
Android
iOS
Web
Windows
macOS
Linux

インストール

pubspec.yamlに追加します。

YAML

dependencies:
  linked_pageview: ^1.1.5

追加したら、以下を実行します。

Bash

flutter pub get

コードから利用する場合はインポートします。

Dart

import 'package:linked_pageview/linked_pageview.dart';

基本的な使い方

linked_pageviewでは、LinkedPageControllerGroupを作成し、そのグループから複数のControllerを生成します。

Dart

final controllerGroup = LinkedPageControllerGroup();

final controller1 = controllerGroup.create(
  viewportFraction: 0.7,
);

final controller2 = controllerGroup.create(
  viewportFraction: 0.4,
);

同じLinkedPageControllerGroupから作成されたControllerをLinkedPageViewに設定することで、複数のPageViewを同期できます。

複数のPageViewを同期する

例えば、上下に2つのPageViewを配置して同期させる場合は以下のように実装できます。

Dart

final controllerGroup = LinkedPageControllerGroup();

final controller1 = controllerGroup.create(
  viewportFraction: 0.7,
);

final controller2 = controllerGroup.create(
  viewportFraction: 0.4,
);

class SyncScrollDemo extends StatelessWidget {
  const SyncScrollDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: LinkedPageView(
              controller: controller1,
              children: [
                Container(),
                Container(),
                Container(),
              ],
            ),
          ),
          Expanded(
            child: LinkedPageView(
              controller: controller2,
              children: [
                Container(),
                Container(),
                Container(),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

一方のPageViewをスクロールすると、もう一方も同じページ位置へ同期してスクロールします。

viewportFractionを個別に設定する

linked_pageviewの特徴の1つが、同期するPageViewごとに異なるviewportFractionを指定できることです。

例えば、

Dart

final controller1 = controllerGroup.create(
  viewportFraction: 0.7,
);

final controller2 = controllerGroup.create(
  viewportFraction: 0.4,
);

とすると、一方はページ幅の70%、もう一方は40%を基準にした表示になります。

このように、表示サイズが異なるPageView同士でもスクロール位置を同期できるのが特徴です。

LinkedPageControllerGroupとは?

LinkedPageControllerGroupは、同期対象となるLinkedPageControllerを管理するためのクラスです。

Controllerは、

Dart

final controllerGroup = LinkedPageControllerGroup();

final controller1 = controllerGroup.create();
final controller2 = controllerGroup.create();
final controller3 = controllerGroup.create();

のように作成できます。

同じController Groupから生成したControllerをそれぞれのLinkedPageViewに渡すことで、スクロールを同期できます。

animateToで同期スクロールする

Controller GroupにはanimateTo()が用意されています。

これを利用すると、指定した位置までアニメーションしながら同期スクロールできます。

Dart

controllerGroup.animateTo(
  2,
);

複数のPageViewをまとめて特定のページへ移動させたい場合に利用できます。

jumpToで同期スクロールする

アニメーションを使わず、指定した位置へ即座に移動させたい場合はjumpTo()を利用します。

Dart

controllerGroup.jumpTo(
  2,
);

animateTo()と違って即座に移動するため、アニメーションが必要ないケースに適しています。

scrollDirectionを変更する

LinkedPageViewでは、スクロール方向も指定できます。

デフォルトでは水平方向です。

Dart

LinkedPageView(
  controller: controller1,
  scrollDirection: Axis.horizontal,
  children: [
    Container(),
    Container(),
    Container(),
  ],
)

垂直方向にすることもできます。

Dart

LinkedPageView(
  controller: controller1,
  scrollDirection: Axis.vertical,
  children: [
    Container(),
    Container(),
    Container(),
  ],
)

ExpandableLinkedPageViewでもscrollDirectionに対応しており、垂直方向なら高さ、水平方向なら幅をコンテンツに合わせて変更できます。

pageSnappingを設定する

LinkedPageViewではpageSnappingを設定できます。

デフォルトではtrueです。

Dart

LinkedPageView(
  controller: controller1,
  pageSnapping: true,
  children: [
    Container(),
    Container(),
    Container(),
  ],
)

ページ単位でスナップさせる標準的なPageViewの挙動を利用できます。

physicsを設定する

スクロール時の物理挙動はphysicsから設定できます。

Dart

LinkedPageView(
  controller: controller1,
  physics: const PageScrollPhysics(),
  children: [
    Container(),
    Container(),
    Container(),
  ],
)

ExpandableLinkedPageView

linked_pageviewには、通常のLinkedPageViewとは別にExpandableLinkedPageViewが用意されています。

これは、現在表示されているページのコンテンツサイズに合わせてPageView自体のサイズを変更するWidgetです。

例えば、

ページ1
高さ:200px

      ↓ スクロール

ページ2
高さ:400px

      ↓ スクロール

ページ3
高さ:300px

というようにページごとにサイズが違っていても、表示中のページに合わせてPageViewの高さを変更できます。

ExpandableLinkedPageViewを使う

基本的な使い方は以下の通りです。

Dart

ExpandableLinkedPageView(
  controller: controller1,
  children: [
    Container(
      height: 200,
    ),
    Container(
      height: 400,
    ),
    Container(
      height: 300,
    ),
  ],
)

表示されているページのコンテンツサイズに合わせて、PageViewのサイズが変更されます。

ExpandableLinkedPageView.builder

ページ数が多い場合や、必要になったタイミングでページを生成したい場合は、.builderコンストラクタを利用できます。

Dart

ExpandableLinkedPageView.builder(
  controller: controller2,
  itemCount: 3,
  itemBuilder: (context, index) {
    return Container(
      height: 200.0 + index * 100,
    );
  },
)

builderを利用することで、ページを遅延生成する構成にできます。

estimatedPageSizeを設定する

ExpandableLinkedPageViewにはestimatedPageSizeという設定があります。

これは初期表示時のページサイズを推定するための値です。

Dart

ExpandableLinkedPageView(
  controller: controller1,
  estimatedPageSize: 300,
  children: [
    Container(
      height: 200,
    ),
    Container(
      height: 400,
    ),
  ],
)

初回レンダリング時のサイズを推定することで、サイズ計測によるちらつきを抑える用途に利用できます。

画像カルーセルに利用する

linked_pageviewの代表的な用途の1つが、複数の画像カルーセルの同期です。

例えば、

┌──────────────────┐
│                  │
│   メイン画像      │
│                  │
└──────────────────┘

○ ○ ○ ○ ○
サムネイル

というUIを作り、メイン画像とサムネイルを同期させることができます。

メイン画像用のPageViewとサムネイル用のPageViewで同じController Groupを使用することで、スクロール位置を連動させられます。

データビジュアライゼーションに利用する

複数のデータ表示を同じスクロール位置で動かしたい場合にも利用できます。

例えば、

グラフ
  ↓
PageView

詳細データ
  ↓
PageView

という構成にして、同じページを表示するように同期できます。

パララックス効果に利用する

複数のPageViewを異なるviewportFractionで同期できるため、複数レイヤーを組み合わせたパララックス効果にも利用できます。

例えば、

背景レイヤー
    ↓
PageView A

メインコンテンツ
    ↓
PageView B

前景レイヤー
    ↓
PageView C

というような構成です。

ドキュメント比較に利用する

2つのドキュメントを左右に並べて、同じ位置をスクロールさせるようなUIにも利用できます。

┌────────────┬────────────┐
│ Document A │ Document B │
│            │            │
│ Page 1     │ Page 1     │
│            │            │
│ Page 2     │ Page 2     │
└────────────┴────────────┘

片方を操作すると、もう一方も同じページ位置へ移動させられます。

オンボーディング画面に利用する

複雑なオンボーディング画面にも利用できます。

例えば、説明文とイラストを別々のPageViewとして用意し、それぞれを同期させることで、

イラスト
   ↕
説明文
   ↕
ページインジケーター

のように複数の要素を連動させられます。

Controller Groupをdisposeする

LinkedPageControllerGroupを利用する場合は、不要になったタイミングでdispose()を呼び出します。

例えばStatefulWidgetなら、

Dart

@override
void dispose() {
  controllerGroup.dispose();

  super.dispose();
}

とします。

LinkedPageControllerGroupをdisposeすると、そのGroupから作成されたLinked Controllerもまとめてdisposeされます。

StatefulWidgetで管理する

実際のアプリでは、Controller GroupをWidgetのStateとして管理すると扱いやすくなります。

Dart

class SyncPage extends StatefulWidget {
  const SyncPage({super.key});

  @override
  State<SyncPage> createState() => _SyncPageState();
}

class _SyncPageState extends State<SyncPage> {
  late final LinkedPageControllerGroup controllerGroup;
  late final LinkedPageController controller1;
  late final LinkedPageController controller2;

  @override
  void initState() {
    super.initState();

    controllerGroup = LinkedPageControllerGroup();

    controller1 = controllerGroup.create(
      viewportFraction: 0.7,
    );

    controller2 = controllerGroup.create(
      viewportFraction: 0.4,
    );
  }

  @override
  void dispose() {
    controllerGroup.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: LinkedPageView(
            controller: controller1,
            children: [
              Container(),
              Container(),
              Container(),
            ],
          ),
        ),
        Expanded(
          child: LinkedPageView(
            controller: controller2,
            children: [
              Container(),
              Container(),
              Container(),
            ],
          ),
        ),
      ],
    );
  }
}

このようにすると、Widgetのライフサイクルに合わせてController Groupを管理できます。

注意点

Controller Groupを適切にdisposeする

Controller Groupを不要になった後も保持すると、リソース管理上の問題につながる可能性があります。

StatefulWidgetで利用する場合はdispose()で、

Dart

controllerGroup.dispose();

を呼び出すようにしましょう。

ページ数を揃える

複数のPageViewを同期する場合、同期対象となるページ構成を意識して設計する必要があります。

例えば、一方が3ページ、もう一方が5ページという構成では、アプリ側でどのように対応させるかをあらかじめ決めておくとよいでしょう。

ExpandableLinkedPageViewのサイズ変化を考慮する

ExpandableLinkedPageViewは現在表示されているページのサイズに合わせてPageViewのサイズを変更します。

そのため、周囲のWidgetのレイアウトによっては、ページ切り替え時のサイズ変更を考慮する必要があります。

builderを活用する

大量のページや動的に生成するコンテンツでは、ExpandableLinkedPageView.builderを利用することで、必要なページを遅延生成する構成にできます。

メリット

複数のPageViewを簡単に同期できる

通常のPageControllerを個別に操作するよりも、LinkedPageControllerGroupを利用することで同期スクロールをまとめて扱えます。

viewportFractionを個別に設定できる

同期対象のPageViewごとに異なる表示サイズを設定できます。

ExpandableLinkedPageViewが便利

ページごとに高さや幅が異なるUIでも、現在のページに合わせてPageViewを自動的にリサイズできます。

childrenとbuilderを選択できる

固定されたコンテンツならchildren、動的・大量のコンテンツならbuilderと使い分けられます。

Flutterの複数プラットフォームに対応

Android、iOSだけでなく、Linux、macOS、Web、Windowsにも対応しています。

まとめ

linked_pageviewは、複数のPageViewを同期してスクロールさせたい場合に便利なFlutterパッケージです。

基本的には、

Dart

final controllerGroup = LinkedPageControllerGroup();

final controller1 = controllerGroup.create();
final controller2 = controllerGroup.create();

のように同じLinkedPageControllerGroupからControllerを作成し、それぞれのLinkedPageViewに設定します。

これによって、

  • 複数PageViewの同期
  • 異なるviewportFraction
  • animateTo()による同期アニメーション
  • jumpTo()による即時移動
  • 水平・垂直スクロール
  • Expandable PageView
  • children / builderによるページ生成

などを実装できます。

特に、画像カルーセル、データビジュアライゼーション、パララックスUI、ドキュメント比較、オンボーディング画面など、複数のPageViewを連動させたいUIとの相性が良いでしょう。

また、ExpandableLinkedPageViewを利用すれば、ページごとにコンテンツサイズが異なるケースにも対応できます。

複数のPageViewを個別に制御するのではなく、同じスクロール位置を共有しながら複数のUIを連動させたい場合に、候補として検討しやすいパッケージです。

参考: https://pub.dev/packages/linked_pageview

タイトルとURLをコピーしました