概要
Flutterで縦に長いページを作成する場合、一番上まで自動でスクロールさせたいユースケースがあると思います。
例えば、Twitter, Instagram, Facebook, Slackなど、そうそうたるアプリは、AppBarを押すと最上部に遷移する実装がなされています。
本記事では、それらのアプリが実現している【AppBarを押すと画面最上部まで自動でスクロール】の実装内容を整理してみました。
後編のみご覧いただくだけでも問題ありませんが、実現にはいくつかの要素を含むため、
本記事の内容が難解であると感じた場合、まずは前編をご覧いただくことをおすすめします。
前編に引き続き、今回も【scroll_to_index】を利用します。
スクロールについては、前後編で以下の内容で整理しています。
| 内容
---|---
前編|【同一Widget内】で、任意のスクロール場所へ遷移する方法
後編(本記事)|【表示と異なるWidget】で、任意のスクロール場所へ遷移する方法
※具体的にはAppBarを押すとページ最上部に遷移する
実装イメージ
AppBarをタップしたら、画面最上部に遷移するデモとなります。
(1回目は23まで手動でスクロール、その後AppBarタップ)
(2回目は99まで手動でスクロール、その後AppBarタップ)
環境
$ flutter --version
Flutter 2.5.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision ffb2ecea52 (3 weeks ago) • 2021-09-17 15:26:33 -0400
Engine • revision b3af521a05
Tools • Dart 2.14.2
pubspec.yaml
dependencies:
# ✨✨✨↓追加✨✨✨
scroll_to_index: ^2.1.0
hooks_riverpod: ^1.0.0-dev.10
実装
ポイントは、以下の3つです。
順に説明します。注意してほしい箇所はコメントアウトに記載しているので、その辺りも見ていただければと。
GitHubにも全文公開しているのでご参考まで
##スクロールのcontrollerの状態管理
本記事の状態管理はriverpodを採用しています。
riverpodあるあるとして、runAppのchildに【ProviderScope】をかませることを忘れないように注意してください。
※筆者はよく忘れます。自分への戒めとして明記しました。
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
/// scroll用のproviderを宣言
final scrollControllerProvider = Provider((_) => AutoScrollController());
void main() {
runApp(
/// ✨✨✨ProviderScopeは、かなり忘れがちなので注意✨✨✨
const ProviderScope(
child: MyApp(),
),
);
}
##AppBar押下のコールバック取得(+スクロールイベント発火)
デフォルトのAppBarは、Tap検出できないため、AppBarをカスタムします。
/// ✨✨✨デフォルトのAppBarは、Tap検出できないため、AppBarをカスタムする(GestureDetectorをかませた)
class ScrollAppBar extends StatelessWidget implements PreferredSizeWidget {
final VoidCallback onTap;
final AppBar appBar;
const ScrollAppBar({Key? key, required this.onTap, required this.appBar})
: super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(onTap: onTap, child: appBar);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
本クラスの呼び出し方法は、以下をご覧ください。
また、AppBarが押下(onTapがコール)されたら、先ほど宣言したcontrollerに対して、最上部へスクロールさせる実装します。
※ref.watchは必ずbuildメソッド内で実行してください。これも忘れがち。
class MyApp extends HookConsumerWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
/// riverpodからcontrollerを参照
final controller = ref.watch(scrollControllerProvider);
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
/// カスタムAppBarの呼び出し
appBar: ScrollAppBar(
appBar: AppBar(
title: const Text('Sample of Scroll to Jump'),
),
onTap: () {
/// ボタンを押したら先頭(=ListViewの0番目)にジャンプできる。
/// AutoScrollPosition.beginがListのindexの頭に表示される。他に、middleとendが存在する
controller.scrollToIndex(
0,
preferPosition: AutoScrollPosition.begin,
);
/// なお、【scroll_to_index】は、FlutterデフォルトAPIのScrollControllerを継承しているため、
/// 前編にてジャンプ使用不可と記載しましたが、先頭に遷移させるだけであれば、実はjumpToが簡単に使えます。
/// TwitterやInstagramは、アニメーション方式を採用している、且つユーザ体験もanimationの方が好ましいため、今回はコメントアウトしますが、お知りおきを。
// controller.jumpTo(0);
},
),
/// 呼び出し元のWidget
body: const ScrollWidget(),
),
);
}
}
##スクロールさせたい画面表示
こちらは、ほぼ前編と同様の内容になっています。
変更点としては、
- AppBarでスクロール動作させるようになったので、ListView内のボタンを削除
- スクロール用のcontrollerをriverpodから参照
となります。
※ref.watchは必ずbuildメソッド内で実行してください。これも忘れがち。
class ScrollWidget extends HookConsumerWidget {
const ScrollWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
/// riverpodからcontrollerを参照
final controller = ref.watch(scrollControllerProvider);
return ListView.builder(
controller: controller,
/// とりあえず100個だけ表示するように実装
itemCount: 100,
itemBuilder: (context, index) {
/// AutoScrollTagをかませる
return AutoScrollTag(
key: ValueKey(index),
controller: controller,
index: index,
child: Column(
children: [
SizedBox(
width: double.infinity,
height: 80,
/// 行頭に遷移されることがわかりやすいようにCardウィジェットを採用しています。適宜変更してください。
child: Card(
child: Text(
'$index',
style: const TextStyle(fontSize: 24),
),
),
),
],
),
);
},
);
}
}
#おまけ
お気づきの方もいらっしゃるかもしれませんが、画面最上部に持ってくるだけなら、scroll_to_indexのプラグインは正直不要です。
ただし、任意の場所まで遷移させることを考えた時に、拡張性があるという点でscroll_to_index使えば良いという点で採用しています。
※前後編の繋がりも考えて、採用したというのも大いにあったりしますが、、、