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

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

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

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

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

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

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

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

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

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

  • السحب: هذا هو نوع الانتقال الذي يحدث عندما يسحِب المستخدم إصبعه على الشاشة اللمسية. يمكنك تنفيذ السحب بواسطة إلغاء onScroll() بوصة GestureDetector.OnGestureListener لمزيد من المعلومات عن السحب، راجع السحب والتعديل:
  • التحريك السريع: هو نوع التمرير الذي يحدث عندما يجرّ المستخدم يده ويرفعها بسرعة. بعد رفع إصبع المستخدم، يجب بشكل عام مواصلة تحريك إطار العرض، ولكن مع إبطاء السرعة إلى أن يتوقّف إطار العرض عن التحرك. يمكنك تنفيذ ميزة الرمي السريع من خلال إلغاء onFling() في GestureDetector.OnGestureListener واستخدام كائن scroller.
  • التحريك: التمرير في وقتٍ واحد على طول كلٍّ من الرمزين 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. يتم ذلك عادةً عندما يُنشئ عنصر عرض فرعي حركة انتقالية لعنصر التمرير باستخدام عنصر scroller، كما هو موضّح في المثال السابق .

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

إيقاف ميزة "الانتقال السريع للأسفل أو للأعلى"

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

لإيقاف هذا الخيار في ملف التنسيق، اضبط android:overScrollMode على كما هو موضح في المثال التالي:

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

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

Kotlin

customView.overScrollMode = View.OVER_SCROLL_NEVER

Java

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

مصادر إضافية

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