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

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

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

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

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

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

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

فهم مصطلحات التمرير

التمرير هو كلمة يمكن أن تشير إلى معانٍ مختلفة في Android، وذلك حسب السياق.

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

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

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

المكوّنات التي تتضمّن عمليات تنفيذ التمرير المضمّنة

تحتوي مكوّنات Android التالية على دعم مدمج لسلوك التمرير والتمرير السريع:

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

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

إنشاء عملية تنفيذ مخصّصة للتمرير المستند إلى اللمس

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

مقتطف الرمز التالي مأخوذ من 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. يتم ذلك عادةً عندما يحرك عنصر فرعي في العرض عملية التمرير باستخدام عنصر تمرير، كما هو موضّح في المثال السابق.

تمرِّر معظم طرق العرض موضع 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، إذا تم تمرير قيم سالبة إلى onPull() أو onPullDistance() عندما تكون قيمة getDistance() هي 0، لن يتغيّر تأثير التمديد.deltaDistance في نظام التشغيل Android 11 والإصدارات الأقدم، تتيح السمة onPull() عرض تأثيرات التوهّج للقيم السالبة لإجمالي المسافة.

إيقاف التمرير الزائد

يمكنك إيقاف التمرير الزائد في ملف التصميم أو بشكل آلي.

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

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

لإيقاف عرض AMP آليًا، استخدِم رمزًا مثل ما يلي:

Kotlin

customView.overScrollMode = View.OVER_SCROLL_NEVER

Java

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

مراجع إضافية

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