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

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

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

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

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

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

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

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

الانتقال للأعلى أو للأسفل هو مصطلح يمكن أن يعني أشياء مختلفة في Android، حسب السياق.

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

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

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

المكوّنات التي تحتوي على عمليات تنفيذ مضمّنة للانتقال للأعلى أو للأسفل

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

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

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

إنشاء عملية تنفيذ مخصّصة للانتقال للأعلى أو للأسفل استنادًا إلى اللمس

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

يأتي المقتطف التالي من نموذج InteractiveChart. يستخدم GestureDetector ويلغي طريقة onFling()في GestureDetector.SimpleOnGestureListener. ويستخدم 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. يحدث ذلك عادةً عندما تحرّك طريقة عرض فرعية الانتقال للأعلى أو للأسفل باستخدام عنصر محتوى قابل للتمرير، كما هو موضّح في المثال السابق.

تُمرِّر معظم طرق العرض موضعَي 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() مقدار السمة `delta` التي تم استهلاكها. بدءًا من 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);

مراجع إضافية

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