Flutterで複数のPageViewを配置し、それぞれのスクロールを連動させたいケースがあります。
例えば、
- 画像カルーセルと情報パネルを連動させる
- 複数のデータ表示を同期させる
- パララックス効果を実装する
- ドキュメントを左右で比較する
- 複数のレイヤーを連動してスクロールさせる
- ページごとに異なるサイズのコンテンツを表示する
といった場面です。
通常のPageViewでは、それぞれ独立したスクロールになります。
今回紹介するlinked_pageviewは、複数のPageViewのスクロールを同期できるFlutterパッケージです。
複数のPageViewを1つのジェスチャーで連動させられるほか、それぞれ異なるviewportFractionを設定することもできます。
さらに、ページのコンテンツサイズに合わせて自動的にサイズを変更するExpandableLinkedPageViewも提供されています。
この記事では、linked_pageviewの基本的な使い方から、複数のPageViewを同期する方法、viewportFractionの設定、ページサイズに合わせた自動リサイズ、Controllerの管理方法まで詳しく解説します。
- linked_pageviewとは?
- linked_pageviewの特徴
- インストール
- 基本的な使い方
- 複数のPageViewを同期する
- viewportFractionを個別に設定する
- LinkedPageControllerGroupとは?
- animateToで同期スクロールする
- jumpToで同期スクロールする
- scrollDirectionを変更する
- pageSnappingを設定する
- physicsを設定する
- ExpandableLinkedPageView
- ExpandableLinkedPageViewを使う
- ExpandableLinkedPageView.builder
- estimatedPageSizeを設定する
- 画像カルーセルに利用する
- データビジュアライゼーションに利用する
- パララックス効果に利用する
- ドキュメント比較に利用する
- オンボーディング画面に利用する
- Controller Groupをdisposeする
- StatefulWidgetで管理する
- 注意点
- メリット
- まとめ
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を連動させたい場合に、候補として検討しやすいパッケージです。
