[Flutter] 指定のウィジェットまで画面をスクロールする

アイキャッチ画像 Flutter

特定のウィジェットまでスムーズにスクロールさせたい場面ってありませんか?
例えばフォームのエラー箇所まで自動で移動したり、特定のセクションにユーザーを案内したりするケースなどです。
今回はColumnやListviewで指定のウィジェットまで画面をスクロールする方法をご紹介します!

ListViewやColumnの子要素のウィジェットにGlobalKeyを指定する

特定のウィジェットまでスクロールするためには、childrenの要素になるWidgetにGlobalKeyを指定します。GlobalKeyを指定することで目的のウィジェットの描画状態や描画位置を取得することができるようになります。
ContainerやTextなどFlutterで元々用意されているウィジェットはkeyプロパティがあるのでそれを利用しましょう!自分で作ったStatefulWidgetやStatelessWidgetはコンストラクタでKeyを受け取ります。const <Class名> ({Key? super.key}); をつければOKです!
↓はTextを描画する自作クラスの例です。

/// ---------- リストに描画するWidget ----------
class ListItem extends StatelessWidget {
  /// ---------- かならずKeyを受け取る ----------
  const ListItem({Key? super.key, required String this.text});
  final String text;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox(
        height: 50,
        width: 300,
        child: Center(
          child: Text(
            this.text,
            style: const TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }
}

GlobalKeyと子要素を作成する

↑の例でGlobalKeyを指定したウィジェットを作成できるようになりました!
あとは指定したGlobalKeyを再利用できるようにウィジェットを作成します。
以下の例のようにGlobalKeyとウィジェットを作成し、それをメンバ変数に格納することで再利用可能になります。今回は配列で指定していますが、スクロール先となるウィジェットの個数が少ない場合などは無理に配列を使わなくても大丈夫です。

class ScrollToWidgetView extends State<ScrollToWidgetExample> {
  /// スクロールしたいリストに付けるkey
  final List<GlobalKey> _listKeys = [];

  /// スクロールしたいリストに描画するウィジェット
  final List<Widget> _listItems = [];

  /// スクロールを制御するコントローラー
  final ScrollController _controller = ScrollController();

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

    // リストアイテムを用意
    for (int i = 0; i <= 50; ++i) {
      // アイテム1個ずつkeyを割り当てる
      var key = GlobalKey();
      this._listItems.add(ListItem(key: key, text: "Item${i}"));
      this._listKeys.add(key);
    }
  }
}

スクロールメソッドを作成する

子要素にGlobalKeyを設定できたら、あとは目的のウィジェットまでスクロールするメソッド作るだけです!
実は先程の例にScrollControllerが出ていたことにはお気づきでしょうか?
これを利用することで特定の座標までスクロールすることが可能になります。
あとは、最初に設定したGlobalKeyを利用して目的のウィジェットが描画されている座標を取得すれば完成ですね!
このコードを写して実際に試してみてください。位置が少しずれるようでしたら -80 の値を変更してoffsetを調整しましょう!

/// ---------- 指定したウィジェットの位置までスクロール ----------
void _scrollToTarget(GlobalKey targetKey) {
  final context = targetKey.currentContext;
  if (context == null) return;

  // ウィジェットのグローバル位置を取得
  final RenderObject? renderObject = context.findRenderObject();
  if (renderObject == null) return;

  // 対象ウィジェットが現在の画面から見てどこに描画されているか
  final RenderBox renderBox = renderObject as RenderBox;
  final offset = renderBox.localToGlobal(Offset.zero);

  // リストの現在のスクロール位置を取得
  final scrollOffset = this._controller.offset;

  // スクロール位置を計算
  final targetOffset = scrollOffset + offset.dy - 80; // 任意でオフセットを調整

  // 目的のスクロール位置までスクロールする
  this._controller.animateTo(
        targetOffset,
        duration: const Duration(seconds: 1),
        curve: Curves.easeInOut,
      );
}

全体コード

最後に実行可能な全体のコード例を紹介します。
これで流れを追っていただければと思います!

import "package:flutter/material.dart";

void main() {
  runApp(
    MaterialApp(
      title: "Flutter Shadow Examples",
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ScrollToWidgetExample(),
    ),
  );
}

/// ---------- リストに描画するWidget ----------
class ListItem extends StatelessWidget {
  /// ---------- かならずKeyを受け取る ----------
  const ListItem({Key? super.key, required String this.text});
  final String text;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox(
        height: 50,
        width: 300,
        child: Center(
          child: Text(
            this.text,
            style: const TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }
}

class ScrollToWidgetExample extends StatefulWidget {
  const ScrollToWidgetExample({Key? super.key});

  @override
  State<ScrollToWidgetExample> createState() => ScrollToWidgetView();
}

class ScrollToWidgetView extends State<ScrollToWidgetExample> {
  /// スクロールしたいリストに付けるkey
  final List<GlobalKey> _listKeys = [];

  /// スクロールしたいリストに描画するウィジェット
  final List<Widget> _listItems = [];

  final ScrollController _controller = ScrollController();

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

    // リストアイテムを用意
    for (int i = 0; i <= 50; ++i) {
      // アイテム1個ずつkeyを割り当てる
      var key = GlobalKey();
      this._listItems.add(ListItem(key: key, text: "Item${i}"));
      this._listKeys.add(key);
    }
  }

  /// ---------- 指定したウィジェットの位置までスクロール ----------
  void _scrollToTarget(GlobalKey targetKey) {
    final context = targetKey.currentContext;
    if (context == null) return;

    // ウィジェットのグローバル位置を取得
    final RenderObject? renderObject = context.findRenderObject();
    if (renderObject == null) return;

    // 対象ウィジェットが現在の画面から見てどこに描画されているか
    final RenderBox renderBox = renderObject as RenderBox;
    final offset = renderBox.localToGlobal(Offset.zero);

    // リストの現在のスクロール位置を取得
    final scrollOffset = this._controller.offset;

    // スクロール位置を計算
    final targetOffset = scrollOffset + offset.dy - 80; // 任意でオフセットを調整

    // 目的のスクロール位置までスクロールする
    this._controller.animateTo(
          targetOffset,
          duration: const Duration(seconds: 1),
          curve: Curves.easeInOut,
        );
  }

  /// ---------- スクロール用ボタン ----------
  Widget _scrollButton(int itemIndex) {
    return InkWell(
      onTap: () => this._scrollToTarget(this._listKeys[itemIndex]),
      child: SizedBox(
        height: 50,
        width: 100,
        child: Text(
          "${itemIndex}に移動",
          style: const TextStyle(fontSize: 20),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Padding(
          padding: const EdgeInsets.only(top: 15),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              this._scrollButton(5),
              this._scrollButton(20),
              this._scrollButton(50),
            ],
          ),
        ),
      ),
      body: Scrollbar(
        thumbVisibility: true,
        thickness: 10,
        controller: this._controller,
        child: SingleChildScrollView(
          controller: this._controller,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: this._listItems,
          ),
        ),
      ),
    );
  }
}

この記事が役に立ちましたらぜひ左下のGoodボタンをお願いします!
皆様のGoodが執筆の励みになります。

コメント

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