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

تجربة طريقة 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);

مصادر إضافية

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