تحريك إيماءة التمرير

تجربة طريقة ComposeAllowed
‫Jetpack Compose هي مجموعة أدوات واجهة المستخدم المُقترَحة لنظام التشغيل Android. تعرَّف على كيفية استخدام اللمس والإدخال في تطبيق Compose.

في Android، يتم عادةً الانتقال للأعلى أو للأسفل باستخدام فئة ScrollView. يمكنك تضمين أي تنسيق عادي قد يتجاوز حدود الحاوية في ScrollView لتوفير عرض قابل للتمرير يُدار من قِبل الإطار. يعد تنفيذ شريط تمرير مخصص ضروريًا فقط في سيناريوهات خاصة. يوضّح هذا المستند طريقة عرض تأثير التمرير استجابةً لإيماءات اللمس باستخدام أشرطة التمرير.

يمكن لتطبيقك استخدام أشرطة التمرير،Scroller أو OverScroller، لجمع البيانات اللازمة لإنشاء صورة متحركة قابلة للتمرير استجابةً لحدث لمس. هما متشابهان، ولكن يتضمّن OverScroller أيضًا طرقًا للإشارة إلى المستخدمين عندما يصلون إلى حواف المحتوى بعد التحريك أو إيماءة الانزلاق.

  • بدءًا من Android 12 (المستوى 31 من واجهة برمجة التطبيقات)، تتم تمديد العناصر المرئية وارتدادها عند تنفيذ حدث سحب، ثم ارتدادها وارتدادها مرّة أخرى عند ارتداد حدث.
  • في نظام التشغيل Android 11 (المستوى 30 من واجهة برمجة التطبيقات) والإصدارات الأقدم، تعرِض الحدود تأثير "توهج" بعد إيماءة السحب أو التمرير السريع إلى الحافة.

يستخدم نموذج InteractiveChart في هذا المستند فئة EdgeEffect لعرض تأثيرات التمرير السريع هذه.

يمكنك استخدام شريط تمرير لتحريك المحتوى بشكل متحرك بمرور الوقت، باستخدام قوانين الفيزياء العادية الخاصة بالتنقّل على المنصة، مثل الاحتكاك والسرعة وغيرها من الخصائص. لا يرسم شريط التمرير نفسه أي شيء. تتتبّع أشرطة التمرير Offsets التمرير لك بمرور الوقت، ولكنّها لا تطبّق هذه المواضع تلقائيًا على عرضك. يجب الحصول على إحداثيات جديدة وتطبيقها بمعدّل يجعل المؤثر المتحرك للانتقال إلى أعلى أو أسفل يبدو سلسًا.

فهم مصطلحات الانتقال إلى الأسفل أو الأعلى

التمرير كلمة قد تعني أشياء مختلفة في Android، بناءً على السياق.

التمرير هو العملية العامة لنقل مساحة العرض، أي "نافذة" المحتوى الذي تشاهده. عندما يكون الانتقال للأعلى أو للأسفل في كلا محورَي x وy، يُعرف ذلك باسم التكبير/التصغير. يوضّح نموذج تطبيق "InteractiveChart" في هذا المستند نوعَين مختلفَين من التمرير والسحب والتنقّل:

  • السحب: هذا هو نوع الانتقال الذي يحدث عندما يسحِب المستخدم إصبعه على الشاشة اللمسية. يمكنك تنفيذ ميزة السحب من خلال تجاوز onScroll() في GestureDetector.OnGestureListener. لمزيد من المعلومات حول السحب، يمكنك الاطّلاع على مقالة السحب وتغيير الحجم.
  • التحريك السريع: هذا هو نوع الانتقال الذي يحدث عندما يجرّ المستخدم يده ويرفعها بسرعة. بعد رفع إصبع المستخدم، يجب بشكل عام مواصلة تحريك إطار العرض، ولكن مع إبطاء السرعة إلى أن يتوقّف إطار العرض عن التحرك. يمكنك تنفيذ ميزة الرمي السريع من خلال إلغاء onFling() في GestureDetector.OnGestureListener واستخدام كائن scroller.
  • تمويه الصورة: يُطلق على التمرير في الوقت نفسه على طول محورَي س ص اسم تمويه الصورة.

من الشائع استخدام عناصر شريط التمرير مع إيماءة التمرير السريع، ولكن يمكنك استخدامها في أي سياق تريد فيه أن تعرض واجهة المستخدم شريط التمرير في استجابة لحدث لمس. على سبيل المثال، يمكنك تجاوز onTouchEvent() لمعالجة أحداث اللمس مباشرةً وإنتاج تأثير تمرير أو صورة متحركة "محاذاة إلى صفحة" استجابةً لأحداث اللمس هذه.

العناصر التي تحتوي على عمليات تنفيذ مدمجة لميزة الانتقال للأعلى أو للأسفل

تحتوي مكوّنات Android التالية على إمكانات مدمجة لسلوك الانتقال للأعلى أو للأسفل والانتقال سريعًا للأعلى أو للأسفل:

إذا كان تطبيقك يحتاج إلى إتاحة الانتقال للأعلى أو للأسفل والانتقال إلى أعلى الصفحة أو أسفلها داخل عنصر مختلف، أكمِل الخطوات التالية:

  1. إنشاء تنفيذ تمرير مخصّص مستند إلى اللمس:
  2. لتتوافق مع الأجهزة التي تعمل بنظام التشغيل Android 12 والإصدارات الأحدث، طبِّق تأثير التمدد عند الانتقال إلى أعلى الصفحة أو أسفلها.

إنشاء تنفيذ مخصّص للتمرير باللمس

يوضّح هذا القسم كيفية إنشاء أداة التمرير الخاصة بك إذا كان تطبيقك يستخدم مكونات لا تتضمّن ميزات مدمجة للتمرير والتمرير السريع.

يستند المقتطف التالي إلى نموذج InteractiveChart. وهي تستخدِم GestureDetector وتلغي GestureDetector.SimpleOnGestureListener الطريقة onFling(). وهو يستخدم OverScroller لتتبُّع إيماءة الانتقال. إذا وصل المستخدم إلى حواف المحتوى بعد تنفيذ لفتة الرمي، تشير الحاوية إلى وصول المستخدم إلى نهاية المحتوى. يعتمد المؤشر على إصدار Android الذي يعمل عليه الجهاز:

  • على نظام التشغيل Android 12 والإصدارات الأحدث، يتم تمديد العناصر المرئية ثم عودتها إلى مكانها الأصلي.
  • في الإصدار 11 من نظام التشغيل Android والإصدارات الأقدم، تعرض العناصر المرئية تأثيرًا ملفِّقًا.

يعرض الجزء الأول من المقتطف التالي تنفيذ 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 لكائن شريط التمرير مباشرةً إلى 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% في كلا الاتجاهين، فسيكون الحجم المعروض ضعف الحجم أفقيًا وعموديًا.

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

للحصول على مثال آخر على استخدام شريط التمرير، يمكنك الاطّلاع على الرمز المصدر للصف ViewPager. ويتم الانتقال للأعلى أو للأسفل استجابةً للحركات السريعة، ويستخدم العنصر الانتقال للأعلى أو للأسفل لتنفيذ الصورة المتحركة "الانتقال السريع إلى الصفحة".

تنفيذ تأثير التمرير الزائد عن الحد

بدءًا من Android 12، يضيف EdgeEffect واجهات برمجة التطبيقات التالية لتنفيذ تأثير التمدد عند الانتقال إلى أعلى الصفحة أو أسفلها:

  • 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() لمعرفة مقدار المسافة التي يجب سحب التأثير الحالي إليها.

على عكس onPull()، يعرض onPullDistance() الكمية التي تم استهلاكها من القيمة المتغيرة التي تم تمريرها. اعتبارًا من Android 12، إذا تم تمرير قيم deltaDistance سالبة لملف onPull() أو onPullDistance() عندما يكون getDistance() 0، لا يتغيّر تأثير التمدد. في الإصدار onPull() من نظام التشغيل Android والإصدارات الأقدم، تتيح القيم السالبة لإجمالي المسافة عرض تأثيرات الإضاءة.

إيقاف ميزة "الانتقال السريع للأسفل أو للأعلى"

يمكنك إيقاف ميزة "الانتقال السريع للأسفل أو للأعلى" في ملف التنسيق أو آليًا.

لإيقاف هذه الميزة في ملف التنسيق، اضبط android:overScrollMode على النحو التالي: كما هو موضّح في المثال التالي:

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

لإيقاف هذه الميزة آليًا، استخدِم رمزًا مثل ما يلي:

Kotlin

customView.overScrollMode = View.OVER_SCROLL_NEVER

Java

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

مصادر إضافية

يُرجى الرجوع إلى المراجع ذات الصلة التالية: