🤖

Flutterで特定の要素のところにスクロールさせる時に気をつけること

2024/04/10に公開

はじめに

最近はFlutterばっかりでkotlinに触れていない加藤です。&AIでインターンしてます。
今日はScrollableクラスensureVisibleの簡単な使い方紹介と実際使ってみて詰まったポイントについてまとめます。
めちゃくちゃニッチなテーマかと思いきや、特定の要素のところまでスクロールさせたいというタイミングは結構あるので、知っておくと割と使えます。
(こっから丁寧語じゃなくなります。)

ensureVisibleとは

ScrollableクラスのensureVisibleは、指定した要素が表示されるようにスクロールするメソッド。
指定したい要素にGlobalKeyを渡して、そのGlobalKeyを目印にしてスクロールさせることが可能。
ただスクロールするだけではなく、ちょっとずらしたり、ゆっくりスクロールさせたり、スクロールに緩急をつけることもできる。

使う時のイメージはこんな感じ。

globalKeyを渡す

~~~
SizedBox(
  key: _hogehogeKey,
  height: 100,
  ~~~
),
~~~

呼び出す

~~~
    final context = _hogehogeKey.currentContext;
    if (context != null) {
      Scrollable.ensureVisible(
        context,
        alignment: 0.5,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
      );
    }
  }
~~~

ここでensureVisibleにいくつか引数を渡しているがそれぞれの意味は以下

context

スクロールさせたい要素に渡したglobalKeyを目印に、currentContextを取得して渡す。
どこにスクロールさせるかの肝

alignment

その要素が画面内のどこら辺に見えていて欲しいかを指定するもの。
該当の要素を画面の一番上に表示させたい場合は0.0、真ん中に表示させたい場合は0.5、一番下に表示させたい場合は1.0を指定する。

durationとcurve

アニメーションの設定をするもの。
durationはどのくらいの時間をかけてスクロールさせるかを指定し、curveはスピードの緩急を指定する。

alignmentPolicy

その要素が画面内のどこら辺に見えていて欲しいかを指定するもの。
デフォルトはexplicitで、指定したalignmentに従う。
keepVisibleAtEndを指定すると、画面の一番下に表示されるようにスクロールする。
keepVisibleAtStartを指定すると、画面の一番上に表示されるようにスクロールする。

注意点1 alignmentとalignmentPolicyの違い

指定する引数の部分で、混乱すると思う。
alignmentとalignmentPolicyの違いが分かりにくい。

alignmentPolicyの内部の実装を見ると少しわかる

    switch (alignmentPolicy) {
      case ScrollPositionAlignmentPolicy.explicit:
        target = clampDouble(viewport.getOffsetToReveal(object, alignment, rect: targetRect).offset, minScrollExtent, maxScrollExtent);
      case ScrollPositionAlignmentPolicy.keepVisibleAtEnd:
        target = clampDouble(viewport.getOffsetToReveal(object, 1.0, rect: targetRect).offset, minScrollExtent, maxScrollExtent);
        if (target < pixels) {
          target = pixels;
        }
      case ScrollPositionAlignmentPolicy.keepVisibleAtStart:
        target = clampDouble(viewport.getOffsetToReveal(object, 0.0, rect: targetRect).offset, minScrollExtent, maxScrollExtent);
        if (target > pixels) {
          target = pixels;
        }
    }

alignmentPolicyにexplicitを指定した場合は、alignmentに指定した値をそのまま使う。
というこれはデフォルト値なので、alignmentを使いたい時にはalignmentPolicyを指定しなくても良い。
keepVisibleAtEndの場合は、alignmentを1.0とした時と同じだが、pixelsよりも小さい場合はpixelsになる。つまり画面からはみ出ないようことを確約する。
keepVisibleAtStartの場合は、alignmentを0.0とした時と同じだが、pixelsよりも大きい場合はpixelsになる。つまり画面からはみ出ないようことを確約する。
ということなのだろう。
知らんけど。

自分の理解だと、alignmentとalignmentPolicyはどっちかを使えばよく、
細かく位置を指定したいならaligmentを使う。
画面の一番上か下に表示するだけでよければalignmentPolicyのkeepVisibleAtEndかkeepVisibleAtStartを使う。

注意点2 ListViewだと使えないことが多い

以下の二つの理由でListViewとEnsureVisibleは相性が悪い

  1. 同じGlobalKeyを複数の要素に割り当ててしまう可能性がある
  2. ListViewはデフォルトで画面外の要素を描画しない。

一つ目は正しくコードを書けば防げるが二つ目が致命的。
まだ描画されてない画面外にある要素のcurrentContextを取得したくても取得できず、どこにスクロールすれば良いかわからなくなってしまう。

これを防ぐのに一番簡単なのはSingleChildScrollViewを使って中にColumnをおくこと。
https://stackoverflow.com/questions/49153087/flutter-scrolling-to-a-widget-in-listview

どっちにしろEnsureVisibleを使いたいならListViewは避けたほうが良いと思う。

余談: どうしてもListViewを使いたい場合

該当の要素のところにスクロールさせたいという要件を満たしたいなら
Googleさんが作っているscrollable_positioned_listを使うという方法もある。

しかも、scrollable_positioned_listはListViewのように、画面外に出た要素を再利用するリサイクルビューの特性を持っている。
要素数が大きいとかパフォーマンスが気になるようなページだとそっちを使ったほうが良いだろう。
パッケージを追加することを避けたいとかパフォーマンスは気にならないページならensureVisibleで十分だと思う。

注意点3 描画が終わってから呼び出さないとずれる可能性がある

よく見る実装例だとinitStateあたりでensureVisibleを呼び出しているが、これだと描画が終わっていないので、正しくスクロールされない可能性がある。
非同期で描画処理を行っているところなど、まだ描画が終わっていないのにスクロールが始まると、正しい位置にスクロールしない。
個人的にはこれが一番ハマりやすいポイントだと思う。(のに大体の記事には書いてなくて困った。)

ベストプラクティスかは不明だが、buildメソッド内、WidgetsBinding.instance.addPostFrameCallbackの中で呼んであげれば良い。

Discussion