สร้างภาพเคลื่อนไหวการเลื่อนสัมผัส

ลองใช้วิธีการเขียน
Jetpack Compose เป็นชุดเครื่องมือ UI ที่แนะนำสำหรับ Android ดูวิธีใช้การแตะและการป้อนข้อมูลในการเขียน

ใน Android โดยทั่วไปการเลื่อนทำได้โดยการใช้คลาส ScrollView ฝังเลย์เอาต์มาตรฐานที่อาจขยายเกินขอบเขตของคอนเทนเนอร์ใน ScrollView เพื่อให้มีมุมมองที่เลื่อนได้ซึ่งจัดการโดยเฟรมเวิร์ก การใช้แถบเลื่อนที่กําหนดเองจําเป็นเฉพาะในกรณีที่พิเศษเท่านั้น เอกสารนี้อธิบายวิธีแสดงเอฟเฟกต์การเลื่อนเพื่อตอบสนองต่อท่าทางสัมผัสโดยใช้แถบเลื่อน

แอปของคุณสามารถใช้แถบเลื่อน Scroller หรือ OverScroller เพื่อรวบรวมข้อมูลที่จําเป็นในการสร้างภาพเคลื่อนไหวแบบเลื่อนเพื่อตอบสนองต่อเหตุการณ์การสัมผัส ทั้งสองมีความคล้ายคลึงกัน แต่ OverScroller ยังมีวิธีการสำหรับบ่งบอกให้ผู้ใช้ทราบเมื่อถึงขอบของเนื้อหาหลังจากใช้ท่าทางสัมผัสการเลื่อนหรือปัด

  • เริ่มตั้งแต่ Android 12 (API ระดับ 31) องค์ประกอบภาพจะขยายและย้อนกลับไปยังเหตุการณ์การลาก รวมถึงการสะบัดและตีกลับเมื่อเกิดเหตุการณ์การสะบัด
  • ใน Android 11 (API ระดับ 30) และเวอร์ชันก่อนหน้า ขอบเขตจะแสดงเอฟเฟกต์ "เรืองแสง" หลังจากที่ผู้ใช้ใช้ท่าทางสัมผัสลากหรือปัดไปยังขอบ

ตัวอย่าง InteractiveChart ในเอกสารนี้ใช้คลาส EdgeEffect เพื่อแสดงเอฟเฟกต์การเลื่อนผ่าน

คุณใช้แถบเลื่อนเพื่อทำให้การเลื่อนเคลื่อนไหวเมื่อเวลาผ่านไปได้โดยใช้ฟิสิกส์การเลื่อนตามมาตรฐานแพลตฟอร์ม เช่น การเสียดสี ความเร็ว และคุณภาพอื่นๆ ตัวเลื่อนจะไม่วาดอะไรเลย ตัวเลื่อนจะติดตามออฟเซตการเลื่อนให้คุณเมื่อเวลาผ่านไป แต่จะไม่ใช้ตำแหน่งเหล่านั้นกับมุมมองของคุณโดยอัตโนมัติ คุณต้องรับและใช้พิกัดใหม่ในอัตราที่ทำให้ภาพเคลื่อนไหวการเลื่อนดูดูราบรื่น

ทําความเข้าใจคําศัพท์เกี่ยวกับการเลื่อน

การเลื่อนคือคำที่อาจมีความหมายแตกต่างกันไปใน Android ขึ้นอยู่กับบริบท

การเลื่อนเป็นขั้นตอนทั่วไปของการย้ายวิวพอร์ต กล่าวคือ "หน้าต่าง" ของเนื้อหาที่คุณกำลังดูอยู่ เมื่อเลื่อนทั้งในแกนx และy เรียกว่าการเลื่อน ตัวอย่างแอป InteractiveChart ในเอกสารนี้จะแสดงการเลื่อน การลากและสะบัด 2 ประเภทที่แตกต่างกัน ดังนี้

  • การลาก: เป็นการเลื่อนประเภทหนึ่งที่จะเกิดขึ้นเมื่อผู้ใช้ลากนิ้วบนหน้าจอสัมผัส คุณใช้การลากได้โดย overriding onScroll() ใน GestureDetector.OnGestureListener ดูข้อมูลเพิ่มเติมเกี่ยวกับการลากได้ที่ลากและวาง
  • การปัด: เป็นการเลื่อนประเภทหนึ่งที่จะเกิดขึ้นเมื่อผู้ใช้ลากนิ้วแล้วยกขึ้นอย่างรวดเร็ว หลังจากผู้ใช้ยกนิ้วขึ้นแล้ว โดยทั่วไปคุณควรเลื่อนวิดเจ็ตการแสดงผลต่อไป แต่ให้ลดความเร็วลงจนกว่าวิดเจ็ตการแสดงผลจะหยุดเลื่อน คุณสามารถใช้การปัดโดยลบล้าง onFling() ใน GestureDetector.OnGestureListener และใช้ออบเจ็กต์ scroller
  • การเลื่อน: การเลื่อนพร้อมกันทั้งแกน x และ y เรียกว่าการเลื่อน

การใช้ออบเจ็กต์ Scroller ร่วมกับท่าทางสัมผัสแบบฟลิงเป็นเรื่องปกติ แต่คุณใช้ออบเจ็กต์เหล่านี้ได้ในบริบทใดก็ได้ที่ต้องการให้ UI แสดงการเลื่อนเพื่อตอบสนองต่อเหตุการณ์การสัมผัส ตัวอย่างเช่น คุณสามารถลบล้าง onTouchEvent() เพื่อประมวลผลเหตุการณ์การแตะโดยตรง และสร้างเอฟเฟกต์เลื่อนหรือภาพเคลื่อนไหว "snap-to-page" เพื่อตอบสนองต่อเหตุการณ์การแตะเหล่านั้น

คอมโพเนนต์ที่มีการใช้งานการเลื่อนในตัว

คอมโพเนนต์ Android ต่อไปนี้รองรับการเลื่อนและลักษณะการเลื่อนผ่านโดยอัตโนมัติในตัว

หากแอปต้องรองรับการเลื่อนและการเลื่อนมากเกินไปภายในคอมโพเนนต์อื่น ให้ทำตามขั้นตอนต่อไปนี้

  1. สร้างการใช้งานการเลื่อนแบบสัมผัสที่กําหนดเอง
  2. หากต้องการรองรับอุปกรณ์ที่ใช้ Android 12 ขึ้นไป ให้ใช้เอฟเฟกต์การเลื่อนแบบยืด

สร้างการใช้งานการเลื่อนแบบสัมผัสที่กําหนดเอง

ส่วนนี้จะอธิบายวิธีสร้างตัวเลื่อนของคุณเองในกรณีที่แอปใช้คอมโพเนนต์ที่ไม่มีการสนับสนุนในตัวสำหรับการเลื่อนและการเลื่อนผ่าน

ข้อมูลโค้ดต่อไปนี้มาจากตัวอย่าง InteractiveChart โดยใช้ GestureDetector และลบล้างเมธอด GestureDetector.SimpleOnGestureListener onFling() โดยจะใช้ OverScroller เพื่อติดตาม ท่าทางการสะบัดนิ้ว หากผู้ใช้ไปถึงขอบของเนื้อหาหลังจากใช้ท่าทางสัมผัสการปัด คอนเทนเนอร์จะระบุเมื่อผู้ใช้ไปถึงส่วนท้ายของเนื้อหา การแสดงผลจะขึ้นอยู่กับเวอร์ชัน Android ที่อุปกรณ์ใช้ ดังนี้

  • ใน Android 12 ขึ้นไป องค์ประกอบภาพจะยืดออกและดีดตัวกลับ
  • ใน Android 11 และเวอร์ชันก่อนหน้า องค์ประกอบภาพจะแสดงเอฟเฟกต์เรืองแสง

ส่วนแรกของข้อมูลโค้ดต่อไปนี้แสดงการใช้งาน 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);
}

เมื่อ onFling() เรียกใช้ postInvalidateOnAnimation() ระบบจะทริกเกอร์ computeScroll() ให้อัปเดตค่าสำหรับ x และ y ซึ่งโดยปกติแล้วจะทำเมื่อบุตรหลานของมุมมองแสดงภาพเคลื่อนไหวการเลื่อนโดยใช้ออบเจ็กต์ scroller ดังที่แสดงในตัวอย่างก่อนหน้านี้

มุมมองส่วนใหญ่จะส่งตำแหน่ง x และ y ของออบเจ็กต์ Scroller โดยตรงไปที่ scrollTo() การใช้งาน computeScroll() ต่อไปนี้ใช้วิธีการอื่น โดยจะเรียกใช้ computeScrollOffset() เพื่อรับข้อมูลตำแหน่งปัจจุบันของ x และ y เมื่อเป็นไปตามเกณฑ์สำหรับการแสดงเอฟเฟกต์ขอบ "เรืองแสง" ของการเลื่อนเกินขอบ กล่าวคือ การแสดงผลมีการซูมเข้า, x หรือ y อยู่นอกขอบเขต และแอปไม่ได้แสดงการเลื่อนเกินขอบอยู่แล้ว โค้ดจะตั้งค่าเอฟเฟกต์เรืองแสงของการเลื่อนเกินขอบและเรียกใช้ postInvalidateOnAnimation() เพื่อทริกเกอร์การทำให้ข้อมูลในมุมมองเป็นโมฆะ

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;
        }
        ...
    }

ส่วนโค้ดที่ทำการซูมจริงมีดังนี้

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);
}

นี่คือเมธอด computeScrollSurfaceSize() ที่เรียกในข้อมูลโค้ดก่อนหน้านี้ โดยจะคํานวณขนาดพื้นผิวที่เลื่อนได้ในปัจจุบันเป็นพิกเซล ตัวอย่างเช่น หากมองเห็นพื้นที่ทั้งหมดของแผนภูมิได้ จะเห็นได้ว่าขนาดปัจจุบันคือ mContentRect หากแผนภูมิซูมเข้า 200% ในทั้ง 2 ทิศทาง ขนาดที่แสดงผลจะมีขนาดใหญ่เป็น 2 เท่าทั้งในแนวตั้งและแนวนอน

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()));
}

ดูตัวอย่างการใช้งาน Scroller อื่นๆ ได้ที่ซอร์สโค้ดของคลาส ViewPager โดยจะเลื่อนตามการแตะแล้วลากและใช้การเลื่อนเพื่อแสดงภาพเคลื่อนไหว "พอดีกับหน้า"

ใช้เอฟเฟกต์การเลื่อนไปจนสุด

ใน Android 12 เป็นต้นไป EdgeEffect จะเพิ่ม API ต่อไปนี้เพื่อใช้เอฟเฟกต์การเลื่อนซ้อนทับ

  • getDistance()
  • onPullDistance()

ทําตามขั้นตอนต่อไปนี้เพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดีที่สุดเมื่อใช้การเลื่อนแบบยืด

  1. เมื่อภาพเคลื่อนไหวแบบยืดมีผลเมื่อผู้ใช้แตะเนื้อหา ให้บันทึกการแตะเป็น "การจับ" ผู้ใช้หยุดภาพเคลื่อนไหวและเริ่มดึงภาพให้ยืดออกอีกครั้ง
  2. เมื่อผู้ใช้เลื่อนนิ้วไปในทิศทางตรงกันข้ามกับส่วนที่ยืดออก ให้ปล่อยนิ้วเมื่อยืดออกจนสุด จากนั้นจึงเริ่มเลื่อน
  3. เมื่อผู้ใช้ยืดเส้นยืดสาย ให้สะบัด EdgeEffect เพื่อเพิ่มเอฟเฟกต์การยืด

จับภาพเคลื่อนไหว

เมื่อผู้ใช้จับภาพภาพเคลื่อนไหวการยืดแบบทำงานอยู่ EdgeEffect.getDistance() จะแสดงผลเป็น 0 เงื่อนไขนี้บ่งบอกว่าการยืดนั้นต้องได้รับการจัดการโดยการเคลื่อนไหวการแตะ ในคอนเทนเนอร์ส่วนใหญ่ ระบบตรวจพบการตรวจจับใน onInterceptTouchEvent() ดังที่แสดงในข้อมูลโค้ดต่อไปนี้

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;
      ...
  }
}

ในตัวอย่างก่อนหน้านี้ onInterceptTouchEvent() จะแสดงผลเป็น true เมื่อ mIsBeingDragged เป็น 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 - 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);
      }
            ...

เมื่อผู้ใช้ลาก ให้ใช้EdgeEffectระยะการดึง ก่อนที่จะส่งเหตุการณ์การสัมผัสไปยังคอนเทนเนอร์การเลื่อนที่ฝังอยู่หรือลากการเลื่อน ในตัวอย่างโค้ดก่อนหน้านี้ getDistance() จะแสดงผลเป็นค่าบวกเมื่อแสดงเอฟเฟกต์ขอบและสามารถปล่อยด้วยการเคลื่อนไหว เมื่อเหตุการณ์การสัมผัสปล่อยการยืด EdgeEffect จะใช้เหตุการณ์ดังกล่าวก่อนเพื่อให้ปล่อยการยืดออกจนเสร็จสมบูรณ์ก่อนที่จะแสดงเอฟเฟกต์อื่นๆ เช่น การสไลด์ที่ซ้อนกัน คุณใช้ getDistance() เพื่อเรียนรู้ว่าต้องใช้ระยะพุลเท่าใดจึงจะปล่อยเอฟเฟกต์ปัจจุบันได้

onPullDistance() จะแสดงผลจำนวนเดลต้าที่ใช้ไปทั้งหมดซึ่งต่างจาก onPull() ตั้งแต่ Android 12 เป็นต้นไป หากมีการส่งค่า deltaDistance เชิงลบให้กับ onPull() หรือ onPullDistance() เมื่อ getDistance() มีค่าเป็น 0 ผลของการยืดจะไม่เปลี่ยนแปลง ใน Android 11 และเวอร์ชันก่อนหน้า onPull() จะให้ค่าลบสำหรับระยะทางรวมแสดงเอฟเฟกต์การเรืองแสง

เลือกไม่ใช้การเลื่อนผ่าน

คุณสามารถเลือกไม่ใช้การเลื่อนเกินในไฟล์เลย์เอาต์หรือใช้แบบเป็นโปรแกรมก็ได้

หากต้องการเลือกไม่ใช้ในไฟล์เลย์เอาต์ ให้ตั้งค่า android:overScrollMode ตามที่แสดงในตัวอย่างต่อไปนี้

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

หากต้องการเลือกไม่ใช้แบบเป็นโปรแกรม ให้ใช้โค้ดต่อไปนี้

Kotlin

customView.overScrollMode = View.OVER_SCROLL_NEVER

Java

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

แหล่งข้อมูลเพิ่มเติม

โปรดดูแหล่งข้อมูลที่เกี่ยวข้องต่อไปนี้