Animasikan isyarat menggulir

Mencoba cara Compose
Jetpack Compose adalah toolkit UI yang direkomendasikan untuk Android. Pelajari cara menggunakan sentuhan dan input di Compose.

Di Android, scroll biasanya dicapai menggunakan class ScrollView. Tempatkan tata letak standar apa pun yang mungkin melampaui batas penampungnya dalam ScrollView untuk memberikan tampilan yang dapat di-scroll yang dikelola oleh framework. Penerapan scroller kustom hanya diperlukan untuk skenario khusus. Dokumen ini menjelaskan cara menampilkan efek scroll sebagai respons terhadap gestur sentuh menggunakan scroller.

Aplikasi Anda dapat menggunakan scroller—Scroller atau OverScroller—untuk mengumpulkan data yang diperlukan dalam menghasilkan animasi scroll sebagai respons terhadap peristiwa sentuh. Keduanya serupa, tetapi OverScroller juga menyertakan metode untuk menunjukkan kepada pengguna kapan mereka mencapai tepi konten setelah gestur geser atau fling.

  • Mulai Android 12 (API level 31), elemen visual meregang dan memantul kembali pada peristiwa tarik, lalu mengayunkan dan memantul kembali saat peristiwa fling.
  • Di Android 11 (API level 30) dan yang lebih lama, batas menampilkan efek "glow" setelah gestur tarik atau ayunkan jari ke tepi.

Contoh InteractiveChart dalam dokumen ini menggunakan class EdgeEffect untuk menampilkan efek overscroll ini.

Anda dapat menggunakan scroller untuk menganimasikan scroll dari waktu ke waktu, menggunakan fisika scroll standar platform seperti gesekan, kecepatan, dan kualitas lainnya. Scroller itu sendiri tidak menggambar apa pun. Scroller melacak offset scroll untuk Anda dari waktu ke waktu, tetapi tidak otomatis menerapkan posisi tersebut ke tampilan Anda. Anda harus mendapatkan dan menerapkan koordinat baru pada kecepatan yang membuat animasi scroll terlihat lancar.

Memahami terminologi scrolling

Scroll adalah kata yang memiliki arti yang berbeda di Android, bergantung pada konteksnya.

Men-scroll adalah proses umum untuk memindahkan area tampilan—yaitu, "jendela" konten yang Anda lihat. Scrolling dalam sumbu x- dan y disebut panning. Aplikasi contoh InteractiveChart dalam dokumen ini menggambarkan dua jenis scroll, tarik, dan ayunkan jari yang berbeda:

  • Menarik: ini adalah jenis scroll yang terjadi ketika pengguna menarik jari di layar sentuh. Anda dapat mengimplementasikan penarikan dengan mengganti onScroll() di GestureDetector.OnGestureListener. Untuk mengetahui informasi selengkapnya tentang menarik, lihat Menarik dan menskalakan.
  • Flinging: ini adalah jenis scroll yang terjadi saat pengguna menarik dan mengangkat jari dengan cepat. Setelah pengguna mengangkat jarinya, biasanya Anda ingin terus memindahkan area tampilan, tetapi perlambat hingga area pandang berhenti bergerak. Anda dapat mengimplementasikan flinging dengan mengganti onFling() di GestureDetector.OnGestureListener dan menggunakan objek scroller.
  • Penggeseran: men-scroll secara bersamaan di sepanjang sumbu x dan y disebut geser.

Penggunaan objek scroller bersamaan dengan gestur ayunkan jari adalah hal yang umum, tetapi Anda dapat menggunakannya dalam konteks apa pun saat Anda ingin UI menampilkan scroll sebagai respons terhadap peristiwa sentuh. Misalnya, Anda dapat mengganti onTouchEvent() untuk memproses peristiwa sentuh secara langsung dan menghasilkan efek scroll atau animasi "paskan ke halaman" sebagai respons terhadap peristiwa sentuh tersebut.

Komponen yang berisi implementasi scroll bawaan

Komponen Android berikut berisi dukungan bawaan untuk perilaku scroll dan overscroll:

Jika aplikasi Anda perlu mendukung scroll dan overscrolling di dalam komponen yang berbeda, selesaikan langkah-langkah berikut:

  1. Buat penerapan scroll berbasis sentuhan kustom.
  2. Untuk mendukung perangkat yang menjalankan Android 12 dan yang lebih baru, implementasikan efek overscroll peregangan.

Membuat implementasi scroll kustom berbasis sentuhan

Bagian ini menjelaskan cara membuat scroller Anda sendiri jika aplikasi menggunakan komponen yang tidak berisi dukungan bawaan untuk men-scroll dan melakukan overscroll.

Cuplikan berikut berasal dari contoh InteractiveChart. Class tersebut menggunakan GestureDetector dan mengganti metode GestureDetector.SimpleOnGestureListener onFling(). OverScroller digunakan untuk melacak gestur ayunkan jari. Jika pengguna mencapai tepi konten setelah melakukan gestur ayunkan jari, penampung akan menunjukkan kapan pengguna mencapai akhir konten. Indikasinya bergantung pada versi Android yang dijalankan perangkat:

  • Di Android 12 dan yang lebih baru, elemen visual meregang dan memantul kembali.
  • Di Android 11 dan yang lebih lama, elemen visual menampilkan efek glow.

Bagian pertama cuplikan berikut menunjukkan implementasi onFling():

Kotlin

// Viewport extremes. See currentViewport for a discussion of the viewport.
private val AXIS_X_MIN = -1f
private val AXIS_X_MAX = 1f
private val AXIS_Y_MIN = -1f
private val AXIS_Y_MAX = 1f

// The current viewport. This rectangle represents the visible chart
// domain and range. The viewport is the part of the app that the
// user manipulates via touch gestures.
private val currentViewport = RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX)

// The current destination rectangle—in pixel coordinates—into which
// the chart data must be drawn.
private lateinit var contentRect: Rect

private lateinit var scroller: OverScroller
private lateinit var scrollerStartViewport: RectF
...
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {

    override fun onDown(e: MotionEvent): Boolean {
        // Initiates the decay phase of any active edge effects.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects()
        }
        scrollerStartViewport.set(currentViewport)
        // Aborts any active scroll animations and invalidates.
        scroller.forceFinished(true)
        ViewCompat.postInvalidateOnAnimation(this@InteractiveLineGraphView)
        return true
    }
    ...
    override fun onFling(
            e1: MotionEvent,
            e2: MotionEvent,
            velocityX: Float,
            velocityY: Float
    ): Boolean {
        fling((-velocityX).toInt(), (-velocityY).toInt())
        return true
    }
}

private fun fling(velocityX: Int, velocityY: Int) {
    // Initiates the decay phase of any active edge effects.
    // On Android 12 and later, the edge effect (stretch) must
    // continue.
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects()
    }
    // Flings use math in pixels, as opposed to math based on the viewport.
    val surfaceSize: Point = computeScrollSurfaceSize()
    val (startX: Int, startY: Int) = scrollerStartViewport.run {
        set(currentViewport)
        (surfaceSize.x * (left - AXIS_X_MIN) / (AXIS_X_MAX - AXIS_X_MIN)).toInt() to
                (surfaceSize.y * (AXIS_Y_MAX - bottom) / (AXIS_Y_MAX - AXIS_Y_MIN)).toInt()
    }
    // Before flinging, stops the current animation.
    scroller.forceFinished(true)
    // Begins the animation.
    scroller.fling(
            // Current scroll position.
            startX,
            startY,
            velocityX,
            velocityY,
            /*
             * Minimum and maximum scroll positions. The minimum scroll
             * position is generally 0 and the maximum scroll position
             * is generally the content size less the screen size. So if the
             * content width is 1000 pixels and the screen width is 200
             * pixels, the maximum scroll offset is 800 pixels.
             */
            0, surfaceSize.x - contentRect.width(),
            0, surfaceSize.y - contentRect.height(),
            // The edges of the content. This comes into play when using
            // the EdgeEffect class to draw "glow" overlays.
            contentRect.width() / 2,
            contentRect.height() / 2
    )
    // Invalidates to trigger computeScroll().
    ViewCompat.postInvalidateOnAnimation(this)
}

Java

// Viewport extremes. See currentViewport for a discussion of the viewport.
private static final float AXIS_X_MIN = -1f;
private static final float AXIS_X_MAX = 1f;
private static final float AXIS_Y_MIN = -1f;
private static final float AXIS_Y_MAX = 1f;

// The current viewport. This rectangle represents the visible chart
// domain and range. The viewport is the part of the app that the
// user manipulates via touch gestures.
private RectF currentViewport =
  new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);

// The current destination rectangle—in pixel coordinates—into which
// the chart data must be drawn.
private final Rect contentRect = new Rect();

private final OverScroller scroller;
private final RectF scrollerStartViewport =
  new RectF(); // Used only for zooms and flings.
...
private final GestureDetector.SimpleOnGestureListener gestureListener
        = new GestureDetector.SimpleOnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects();
        }
        scrollerStartViewport.set(currentViewport);
        scroller.forceFinished(true);
        ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
        return true;
    }
...
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        fling((int) -velocityX, (int) -velocityY);
        return true;
    }
};

private void fling(int velocityX, int velocityY) {
    // Initiates the decay phase of any active edge effects.
    // On Android 12 and later, the edge effect (stretch) must
    // continue.
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects();
    }
    // Flings use math in pixels, as opposed to math based on the viewport.
    Point surfaceSize = computeScrollSurfaceSize();
    scrollerStartViewport.set(currentViewport);
    int startX = (int) (surfaceSize.x * (scrollerStartViewport.left -
            AXIS_X_MIN) / (
            AXIS_X_MAX - AXIS_X_MIN));
    int startY = (int) (surfaceSize.y * (AXIS_Y_MAX -
            scrollerStartViewport.bottom) / (
            AXIS_Y_MAX - AXIS_Y_MIN));
    // Before flinging, stops the current animation.
    scroller.forceFinished(true);
    // Begins the animation.
    scroller.fling(
            // Current scroll position.
            startX,
            startY,
            velocityX,
            velocityY,
            /*
             * Minimum and maximum scroll positions. The minimum scroll
             * position is generally 0 and the maximum scroll position
             * is generally the content size less the screen size. So if the
             * content width is 1000 pixels and the screen width is 200
             * pixels, the maximum scroll offset is 800 pixels.
             */
            0, surfaceSize.x - contentRect.width(),
            0, surfaceSize.y - contentRect.height(),
            // The edges of the content. This comes into play when using
            // the EdgeEffect class to draw "glow" overlays.
            contentRect.width() / 2,
            contentRect.height() / 2);
    // Invalidates to trigger computeScroll().
    ViewCompat.postInvalidateOnAnimation(this);
}

Saat onFling() memanggil postInvalidateOnAnimation(), ini akan memicu computeScroll() untuk memperbarui nilai x dan y. Hal ini biasanya dilakukan saat turunan tampilan menganimasikan scroll menggunakan objek scroller, seperti ditunjukkan pada contoh sebelumnya.

Sebagian besar tampilan meneruskan posisi x dan y objek scroller langsung ke scrollTo(). Implementasi computeScroll() berikut menggunakan pendekatan berbeda: memanggil computeScrollOffset() untuk mendapatkan lokasi x dan y saat ini. Jika kriteria untuk menampilkan efek tepi "glow" overscroll terpenuhi—yaitu, tampilan diperbesar, x atau y berada di luar batas, dan aplikasi belum menampilkan overscroll—kode akan menyiapkan efek glow overscroll dan memanggil postInvalidateOnAnimation() untuk memicu Pembatalan pada tampilan.

Kotlin

// Edge effect/overscroll tracking objects.
private lateinit var edgeEffectTop: EdgeEffect
private lateinit var edgeEffectBottom: EdgeEffect
private lateinit var edgeEffectLeft: EdgeEffect
private lateinit var edgeEffectRight: EdgeEffect

private var edgeEffectTopActive: Boolean = false
private var edgeEffectBottomActive: Boolean = false
private var edgeEffectLeftActive: Boolean = false
private var edgeEffectRightActive: Boolean = false

override fun computeScroll() {
    super.computeScroll()

    var needsInvalidate = false

    // The scroller isn't finished, meaning a fling or
    // programmatic pan operation is active.
    if (scroller.computeScrollOffset()) {
        val surfaceSize: Point = computeScrollSurfaceSize()
        val currX: Int = scroller.currX
        val currY: Int = scroller.currY

        val (canScrollX: Boolean, canScrollY: Boolean) = currentViewport.run {
            (left > AXIS_X_MIN || right < AXIS_X_MAX) to (top > AXIS_Y_MIN || bottom < AXIS_Y_MAX)
        }

        /*
         * If you are zoomed in, currX or currY is
         * outside of bounds, and you aren't already
         * showing overscroll, then render the overscroll
         * glow edge effect.
         */
        if (canScrollX
                && currX < 0
                && edgeEffectLeft.isFinished
                && !edgeEffectLeftActive) {
            edgeEffectLeft.onAbsorb(scroller.currVelocity.toInt())
            edgeEffectLeftActive = true
            needsInvalidate = true
        } else if (canScrollX
                && currX > surfaceSize.x - contentRect.width()
                && edgeEffectRight.isFinished
                && !edgeEffectRightActive) {
            edgeEffectRight.onAbsorb(scroller.currVelocity.toInt())
            edgeEffectRightActive = true
            needsInvalidate = true
        }

        if (canScrollY
                && currY < 0
                && edgeEffectTop.isFinished
                && !edgeEffectTopActive) {
            edgeEffectTop.onAbsorb(scroller.currVelocity.toInt())
            edgeEffectTopActive = true
            needsInvalidate = true
        } else if (canScrollY
                && currY > surfaceSize.y - contentRect.height()
                && edgeEffectBottom.isFinished
                && !edgeEffectBottomActive) {
            edgeEffectBottom.onAbsorb(scroller.currVelocity.toInt())
            edgeEffectBottomActive = true
            needsInvalidate = true
        }
        ...
    }
}

Java

// Edge effect/overscroll tracking objects.
private EdgeEffectCompat edgeEffectTop;
private EdgeEffectCompat edgeEffectBottom;
private EdgeEffectCompat edgeEffectLeft;
private EdgeEffectCompat edgeEffectRight;

private boolean edgeEffectTopActive;
private boolean edgeEffectBottomActive;
private boolean edgeEffectLeftActive;
private boolean edgeEffectRightActive;

@Override
public void computeScroll() {
    super.computeScroll();

    boolean needsInvalidate = false;

    // The scroller isn't finished, meaning a fling or
    // programmatic pan operation is active.
    if (scroller.computeScrollOffset()) {
        Point surfaceSize = computeScrollSurfaceSize();
        int currX = scroller.getCurrX();
        int currY = scroller.getCurrY();

        boolean canScrollX = (currentViewport.left > AXIS_X_MIN
                || currentViewport.right < AXIS_X_MAX);
        boolean canScrollY = (currentViewport.top > AXIS_Y_MIN
                || currentViewport.bottom < AXIS_Y_MAX);

        /*
         * If you are zoomed in, currX or currY is
         * outside of bounds, and you aren't already
         * showing overscroll, then render the overscroll
         * glow edge effect.
         */
        if (canScrollX
                && currX < 0
                && edgeEffectLeft.isFinished()
                && !edgeEffectLeftActive) {
            edgeEffectLeft.onAbsorb((int)mScroller.getCurrVelocity());
            edgeEffectLeftActive = true;
            needsInvalidate = true;
        } else if (canScrollX
                && currX > (surfaceSize.x - contentRect.width())
                && edgeEffectRight.isFinished()
                && !edgeEffectRightActive) {
            edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity());
            edgeEffectRightActive = true;
            needsInvalidate = true;
        }

        if (canScrollY
                && currY < 0
                && edgeEffectTop.isFinished()
                && !edgeEffectTopActive) {
            edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity());
            edgeEffectTopActive = true;
            needsInvalidate = true;
        } else if (canScrollY
                && currY > (surfaceSize.y - contentRect.height())
                && edgeEffectBottom.isFinished()
                && !edgeEffectBottomActive) {
            edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity());
            edgeEffectBottomActive = true;
            needsInvalidate = true;
        }
        ...
    }

Berikut adalah bagian dari kode yang melakukan zoom aktual:

Kotlin

lateinit var zoomer: Zoomer
val zoomFocalPoint = PointF()
...
// If a zoom is in progress—either programmatically
// or through double touch—this performs the zoom.
if (zoomer.computeZoom()) {
    val newWidth: Float = (1f - zoomer.currZoom) * scrollerStartViewport.width()
    val newHeight: Float = (1f - zoomer.currZoom) * scrollerStartViewport.height()
    val pointWithinViewportX: Float =
            (zoomFocalPoint.x - scrollerStartViewport.left) / scrollerStartViewport.width()
    val pointWithinViewportY: Float =
            (zoomFocalPoint.y - scrollerStartViewport.top) / scrollerStartViewport.height()
    currentViewport.set(
            zoomFocalPoint.x - newWidth * pointWithinViewportX,
            zoomFocalPoint.y - newHeight * pointWithinViewportY,
            zoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
            zoomFocalPoint.y + newHeight * (1 - pointWithinViewportY)
    )
    constrainViewport()
    needsInvalidate = true
}
if (needsInvalidate) {
    ViewCompat.postInvalidateOnAnimation(this)
}

Java

// Custom object that is functionally similar to Scroller.
Zoomer zoomer;
private PointF zoomFocalPoint = new PointF();
...
// If a zoom is in progress—either programmatically
// or through double touch—this performs the zoom.
if (zoomer.computeZoom()) {
    float newWidth = (1f - zoomer.getCurrZoom()) *
            scrollerStartViewport.width();
    float newHeight = (1f - zoomer.getCurrZoom()) *
            scrollerStartViewport.height();
    float pointWithinViewportX = (zoomFocalPoint.x -
            scrollerStartViewport.left)
            / scrollerStartViewport.width();
    float pointWithinViewportY = (zoomFocalPoint.y -
            scrollerStartViewport.top)
            / scrollerStartViewport.height();
    currentViewport.set(
            zoomFocalPoint.x - newWidth * pointWithinViewportX,
            zoomFocalPoint.y - newHeight * pointWithinViewportY,
            zoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
            zoomFocalPoint.y + newHeight * (1 - pointWithinViewportY));
    constrainViewport();
    needsInvalidate = true;
}
if (needsInvalidate) {
    ViewCompat.postInvalidateOnAnimation(this);
}

Ini adalah metode computeScrollSurfaceSize() yang dipanggil dalam cuplikan sebelumnya. Metode ini menghitung ukuran platform yang dapat di-scroll saat ini dalam piksel. Misalnya, jika seluruh area diagram terlihat, ini adalah ukuran mContentRect saat ini. Jika diagram diperbesar 200% ke kedua arah, ukuran yang ditampilkan akan menjadi dua kali lebih besar secara horizontal dan vertikal.

Kotlin

private fun computeScrollSurfaceSize(): Point {
    return Point(
            (contentRect.width() * (AXIS_X_MAX - AXIS_X_MIN) / currentViewport.width()).toInt(),
            (contentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN) / currentViewport.height()).toInt()
    )
}

Java

private Point computeScrollSurfaceSize() {
    return new Point(
            (int) (contentRect.width() * (AXIS_X_MAX - AXIS_X_MIN)
                    / currentViewport.width()),
            (int) (contentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN)
                    / currentViewport.height()));
}

Untuk contoh lain penggunaan scroller, lihat kode sumber untuk class ViewPager. Fitur ini men-scroll sebagai respons terhadap fling dan menggunakan scroll untuk mengimplementasikan animasi "snap-to-page".

Mengimplementasikan efek overscroll regangan

Mulai Android 12, EdgeEffect menambahkan API berikut untuk menerapkan efek overscroll regangan:

  • getDistance()
  • onPullDistance()

Untuk memberikan pengalaman pengguna terbaik dengan overscroll regangan, lakukan hal berikut:

  1. Saat animasi regangan diterapkan saat pengguna menyentuh konten, daftarkan sentuhan sebagai "tangkap". Pengguna menghentikan animasi dan mulai memanipulasi regangan lagi.
  2. Saat pengguna menggerakkan jari ke arah yang berlawanan dari regangan, lepaskan regangan hingga benar-benar hilang, lalu mulailah men-scroll.
  3. Jika pengguna mengayunkan jari saat meregangkan, ayunkan EdgeEffect untuk meningkatkan efek regangan.

Menonton animasi

Saat pengguna menonton animasi regangan aktif, EdgeEffect.getDistance() akan menampilkan 0. Kondisi ini menunjukkan bahwa regangan harus dimanipulasi oleh gerakan sentuh. Di sebagian besar penampung, tangkapan terdeteksi di onInterceptTouchEvent(), seperti yang ditunjukkan pada cuplikan kode berikut:

Kotlin

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

Java

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  ...
  switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
      ...
      isBeingDragged = EdgeEffectCompat.getDistance(edgeEffectBottom) > 0
          || EdgeEffectCompat.getDistance(edgeEffectTop) > 0;
      ...
  }
}

Pada contoh sebelumnya, onInterceptTouchEvent() menampilkan true jika mIsBeingDragged adalah true, sehingga cukup menggunakan peristiwa tersebut sebelum turunan memiliki kesempatan untuk memakainya.

Merilis efek overscroll

Sebaiknya lepaskan efek regangan sebelum men-scroll untuk mencegah penyebaran diterapkan ke konten scroll. Contoh kode berikut menerapkan praktik terbaik ini:

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 - lastMotionY
      val pullDistance = deltaY / height
      val displacement = x / width

      if (deltaY < 0f && EdgeEffectCompat.getDistance(edgeEffectTop) > 0f) {
        deltaY -= height * EdgeEffectCompat.onPullDistance(edgeEffectTop,
            pullDistance, displacement);
      }
      if (deltaY > 0f && EdgeEffectCompat.getDistance(edgeEffectBottom) > 0f) {
        deltaY += height * EdgeEffectCompat.onPullDistance(edgeEffectBottom,
            -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 - lastMotionY;
      float pullDistance = deltaY / getHeight();
      float displacement = x / getWidth();

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

Saat pengguna menarik, gunakan jarak pull EdgeEffect sebelum Anda meneruskan peristiwa sentuh ke penampung scroll bertingkat atau tarik scroll. Pada contoh kode sebelumnya, getDistance() menampilkan nilai positif saat efek tepi ditampilkan dan dapat dilepaskan dengan gerakan. Saat melepaskan regangan, peristiwa sentuh pertama kali digunakan oleh EdgeEffect sehingga sepenuhnya dilepaskan sebelum efek lainnya, seperti scroll bertingkat, ditampilkan. Anda dapat menggunakan getDistance() untuk mempelajari seberapa jauh jarak tarik yang diperlukan untuk melepaskan efek saat ini.

Tidak seperti onPull(), onPullDistance() menampilkan jumlah yang digunakan dari delta yang diteruskan. Mulai Android 12, jika onPull() atau onPullDistance() diteruskan nilai deltaDistance negatif saat getDistance() adalah 0, efek regangan tidak berubah. Di Android 11 dan yang lebih lama, onPull() memungkinkan nilai negatif untuk total jarak menampilkan efek cahaya.

Nonaktifkan overscroll

Anda dapat memilih untuk tidak menggunakan overscroll dalam file tata letak atau secara terprogram.

Untuk memilih tidak ikut dalam file tata letak, tetapkan android:overScrollMode seperti yang ditunjukkan pada contoh berikut:

<MyCustomView android:overScrollMode="never">
    ...
</MyCustomView>

Untuk memilih tidak ikut secara terprogram, gunakan kode seperti berikut:

Kotlin

customView.overScrollMode = View.OVER_SCROLL_NEVER

Java

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

Referensi tambahan

Lihat referensi terkait berikut ini: