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

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

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

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

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

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

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

فهم مصطلحات التنقّل

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

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

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

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

المكوّنات التي تحتوي على عمليات تنفيذ مدمجة للتمرير

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

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

إيقاف تجاوز حد التمرير

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

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

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

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

Kotlin

customView.overScrollMode = View.OVER_SCROLL_NEVER

Java

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

مراجع إضافية

يُرجى الاطّلاع على المراجع التالية ذات الصلة: