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

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

في 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. بالنسبة إلى الأجهزة التي تعمل بالإصدار 12 من نظام التشغيل Android أو الإصدارات الأحدث، يمكنك تنفيذ تأثير الانتقال بشكل زائد عن الحد.

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

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

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

  • في الإصدار 12 من نظام التشغيل Android والإصدارات الأحدث، تتم إطالة العناصر المرئية وارتدادها.
  • على نظام التشغيل 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() على قيمة سالبة deltaDistance عندما كانت قيمة 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);

مصادر إضافية

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