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

تجربة طريقة 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. تتوافق مع الأجهزة التي تعمل بنظام التشغيل 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. يتم ذلك عادةً عندما "عرض الطفل" (viewchild) هو حركة لتمرير باستخدام كائن شريط التمرير، كما هو موضح في السابق مثال.

تجتاز معظم طرق العرض الموضعين 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);

مصادر إضافية

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