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

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

في 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.
  • تمويه الصورة: يُطلق على التمرير في الوقت نفسه على طول محورَي س ص اسم تمويه الصورة.

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

العناصر التي تحتوي على عمليات تنفيذ مدمجة لميزة الانتقال للأعلى أو للأسفل

تحتوي مكوّنات Android التالية على إمكانات مدمجة لسلوك الانتقال للأعلى أو للأسفل والانتقال السريع للأعلى أو للأسفل:

إذا كان تطبيقك يحتاج إلى إتاحة الانتقال للأعلى أو للأسفل والانتقال إلى أعلى الصفحة أو أسفلها داخل عنصر مختلف، أكمِل الخطوات التالية:

  1. أنشئ عملية تنفيذ مخصّصة للانتقال باللمس.
  2. لتتوافق مع الأجهزة التي تعمل بنظام التشغيل Android 12 والإصدارات الأحدث، طبِّق تأثير التمدد عند الانتقال إلى أعلى الصفحة أو أسفلها.

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

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

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

  • في الإصدار 12 من نظام التشغيل Android والإصدارات الأحدث، يتم تمديد العناصر المرئية ثم عودتها إلى مكانها الأصلي.
  • في الإصدار 11 من نظام التشغيل Android والإصدارات الأقدم، تعرض العناصر المرئية تأثيرًا ملفّقًا.

يعرض الجزء الأول من المقتطف التالي تنفيذ 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، إذا تم تمرير قيم 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);

مصادر إضافية

يُرجى الرجوع إلى المراجع ذات الصلة التالية: