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

تجربة طريقة الإنشاء
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 وتلغي طريقة 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() بالسالب إلى قيمة سالبة 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);

مراجع إضافية

راجع الموارد ذات الصلة التالية: