オーバースクロール効果

Android 12 以降を搭載したデバイスでは、オーバースクロール イベントの視覚的な動作が変更されます。

Android 11 以前では、オーバースクロール イベントによって視覚要素にグローが表示されます。Android 12 以降の場合、ドラッグ イベントでは視覚要素がストレッチして元に戻り、フリング イベントではフリングして元に戻ります。

オーバースクロール時の新しい動作は、ドラッグとフリングのアニメーションに影響します。

この動作は、EdgeEffect を使用するすべてのアプリと、次のクラス内に存在するすべてのコンテンツに適用されます。

この視覚効果は、垂直スクロールと水平スクロールの両方で機能します。オーバースクロールが無効になっていないすべてのアプリにデフォルトで適用されるため、一貫性のある UI エクスペリエンスを提供できます。

おすすめの方法

新しいオーバースクロール エクスペリエンスがアプリで正常に機能するよう、次のおすすめの方法に従ってください。

ストレッチ EdgeEffect の使用

EdgeEffect には、オーバースクロールのストレッチ効果を実装するための 2 つの API が追加されています。

float getDistance()
float onPullDistance(float deltaDistance, float displacement)

ストレッチ オーバースクロールを使って最適なユーザー エクスペリエンスを実現するには、次の手順を行います。

  • ユーザーがコンテンツを解放し、解放アニメーション中にそのコンテンツをタップしたときに、そのタップを「キャッチ」として登録します。ユーザーはアニメーションを停止し、ストレッチの操作を再開します。
  • ユーザーがストレッチの反対方向に指を動かした場合、ストレッチが完全に終わるまで解放し、その後スクロールを開始します。
  • ユーザーがストレッチ中にフリングした場合、EdgeEffect をフリングしてストレッチ効果を強化します。

アニメーションをキャッチする

ユーザーがアクティブなストレッチ アニメーションをキャッチすると、EdgeEffect.isFinished()false を返します。これは、ストレッチをタップ モーションで操作する必要があることを示します。次のコード スニペットに示すように、ほとんどのコンテナでは onInterceptTouchEvent() でキャッチが検出されます。

Kotlin

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
  ...
  when (action and MotionEvent.ACTION_MASK) {
    MotionEvent.ACTION_DOWN ->
      ...
      isBeingDragged = !edgeEffectBottom.isFinished() ||
          !edgeEffectTop.isFinished()
      ...
  }
  return isBeingDragged
}

Java

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  ...
  switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
      ...
      mIsBeingDragged = !mEdgeEffectBottom.isFinished()
          || !mEdgeEffectTop.isFinished();
      ...

上記の例では、mIsBeingDraggedtrue の場合、onInterceptTouchEvent()true を返すため、子がイベントを消費する前にイベントを消費できます。

オーバースクロール効果を解放する

ストレッチ効果がスクロールするコンテンツに適用されないように、スクロールする前にストレッチ効果を解放することが重要です。次のコードサンプルは、その方法を示しています。

Kotlin

override fun onTouchEvent(ev: MotionEvent): Boolean {
  val activePointerIndex = ev.actionIndex

  when (ev.getActionMasked()) {
    MotionEvent.ACTION_MOVE ->
      val x = ev.getX(activePointerIndex)
      val y = ev.getY(activePointerIndex)
      var deltaY = y - mLastMotionY
      val pullDistance = deltaY / height
      val displacement = x / width

      if (deltaY < 0f && mEdgeEffectTop.distance > 0f) {
        deltaY -= height * mEdgeEffectTop
            .onPullDistance(pullDistance, displacement);
      }
      if (deltaY > 0f && mEdgeEffectBottom.distance > 0f) {
        deltaY += height * mEdgeEffectBottom
            .onPullDistance(-pullDistance, 1 - displacement);
      }

      ...
  }

Java

@Override
public boolean onTouchEvent(MotionEvent ev) {

  final int actionMasked = ev.getActionMasked();

  switch (actionMasked) {
    case MotionEvent.ACTION_MOVE:
      final float x = ev.getX(activePointerIndex);
      final float y = ev.getY(activePointerIndex);
      float deltaY = y - mLastMotionY;
      float pullDistance = deltaY / getHeight();
      float displacement = x / getWidth();

      if (deltaY < 0 && mEdgeEffectTop.getDistance() > 0) {
        deltaY -= getHeight() * mEdgeEffectTop
            .onPullDistance(pullDistance, displacement);
      }
      if (deltaY > 0 && mEdgeEffectBottom.getDistance() > 0) {
        deltaY += getHeight() * mEdgeEffectBottom
            .onPullDistance(-pullDistance, 1 - displacement);
      }
            ...

ネスト スクロールにタッチイベントを渡す前や、スクロールをドラッグする前にドラッグする場合は、EdgeEffect の pull 距離を消費する必要があります。上記のコードサンプルでは、getDistance() は、エッジ効果が表示されていて、モーションを使用して解放できる場合に正の値を返します。タッチイベントがストレッチを解放すると、そのイベントはまず EdgeEffect によって消費されるため、ネスト スクロールなどの他の効果が表示される前に完全に解放されます。getDistance() を使用すると、現在の効果を解放するために必要な pull 距離を確認できます。

onPullDistance() は、渡されたデルタの消費量を返す点で onPull() とは異なります。onPull() にはこれまで、グロー効果の合計距離として負の値を渡すことができました。Android 12 以降では、getDistance() が 0 のときに onPull() または onPullDistance() に負の deltaDistance 値を渡した場合、ストレッチは変化しません。

無効にする

オーバースクロールは、XML レイアウト ファイルで、またはプログラムによって無効にできます。次の XML コードは、レイアウト ファイルで設定された android:overScrollMode を示しています。

<!-- Via markup -->
<ScrollView
  ...
  android:overScrollMode="never"
  ...
>

プログラムで無効にする場合は、次のコード スニペットのようにします。

Kotlin

<!-- Programmatically-->
...
recyclerview.overScrollMode = View.OVER_SCROLL_NEVER
...

Java

<!-- Programmatically-->
...
recyclerview.setOverScrollMode(View.OVER_SCROLL_NEVER);
...

フィードバックを送信する

皆様からのフィードバックをお待ちしております。問題が見つかった場合や、この機能を改善するアイデアをお持ちの場合は、お知らせください。新しい問題を報告していただく前に、既存の問題をご確認ください。スターボタンをクリックすると、既存の問題に投票できます。

新しい問題を報告する

詳細については、Issue Tracker のドキュメントをご覧ください。