في 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 التالية على دعم مدمج لسلوك التمرير والتمرير الزائد:
GridViewHorizontalScrollViewListViewNestedScrollViewRecyclerViewScrollViewViewPagerViewPager2
إذا كان تطبيقكم بحاجة إلى دعم التمرير والتمرير الزائد داخل مكوّن مختلف، يُرجى إكمال الخطوات التالية:
- إنشاء عملية تنفيذ مخصّصة للتمرير المستند إلى اللمس.
- لتوفير الدعم للأجهزة التي تعمل بنظام التشغيل Android 12 والإصدارات الأحدث، تنفيذ تأثير تجاوز حد التمرير الممتد.
إنشاء عملية تنفيذ مخصّصة للتمرير المستند إلى اللمس
يأتي المقتطف التالي من نموذج
InteractiveChart. يستخدم
GestureDetector
ويلغي طريقة onFling()في
GestureDetector.SimpleOnGestureListener. ويستخدم OverScroller لتتبُّع إيماءة الإفلات السريع. إذا وصل المستخدم إلى حواف المحتوى بعد إجراء إيماءة التمرير السريع، تشير الحاوية إلى الوقت الذي يصل فيه المستخدم إلى نهاية المحتوى. يعتمد هذا المؤشر على إصدار Android الذي يعمل به الجهاز:
- في Android 12 والإصدارات الأحدث، تتمدّد العناصر المرئية وترتدّ.
- في Android 11 والإصدارات الأقدم، تعرض العناصر المرئية تأثير توهّج.
يعرض الجزء الأول من المقتطف التالي عملية تنفيذ onFling():
Kotlin
// Viewport extremes. See currentViewport for a discussion of the viewport. private val AXIS_X_MIN = -1f private val AXIS_X_MAX = 1f private val AXIS_Y_MIN = -1f private val AXIS_Y_MAX = 1f // The current viewport. This rectangle represents the visible chart // domain and range. The viewport is the part of the app that the // user manipulates via touch gestures. private val currentViewport = RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX) // The current destination rectangle—in pixel coordinates—into which // the chart data must be drawn. private lateinit var contentRect: Rect private lateinit var scroller: OverScroller private lateinit var scrollerStartViewport: RectF ... private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { override fun onDown(e: MotionEvent): Boolean { // Initiates the decay phase of any active edge effects. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { releaseEdgeEffects() } scrollerStartViewport.set(currentViewport) // Aborts any active scroll animations and invalidates. scroller.forceFinished(true) ViewCompat.postInvalidateOnAnimation(this@InteractiveLineGraphView) return true } ... override fun onFling( e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float ): Boolean { fling((-velocityX).toInt(), (-velocityY).toInt()) return true } } private fun fling(velocityX: Int, velocityY: Int) { // Initiates the decay phase of any active edge effects. // On Android 12 and later, the edge effect (stretch) must // continue. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { releaseEdgeEffects() } // Flings use math in pixels, as opposed to math based on the viewport. val surfaceSize: Point = computeScrollSurfaceSize() val (startX: Int, startY: Int) = scrollerStartViewport.run { set(currentViewport) (surfaceSize.x * (left - AXIS_X_MIN) / (AXIS_X_MAX - AXIS_X_MIN)).toInt() to (surfaceSize.y * (AXIS_Y_MAX - bottom) / (AXIS_Y_MAX - AXIS_Y_MIN)).toInt() } // Before flinging, stops the current animation. scroller.forceFinished(true) // Begins the animation. scroller.fling( // Current scroll position. startX, startY, velocityX, velocityY, /* * Minimum and maximum scroll positions. The minimum scroll * position is generally 0 and the maximum scroll position * is generally the content size less the screen size. So if the * content width is 1000 pixels and the screen width is 200 * pixels, the maximum scroll offset is 800 pixels. */ 0, surfaceSize.x - contentRect.width(), 0, surfaceSize.y - contentRect.height(), // The edges of the content. This comes into play when using // the EdgeEffect class to draw "glow" overlays. contentRect.width() / 2, contentRect.height() / 2 ) // Invalidates to trigger computeScroll(). ViewCompat.postInvalidateOnAnimation(this) }
Java
// Viewport extremes. See currentViewport for a discussion of the viewport. private static final float AXIS_X_MIN = -1f; private static final float AXIS_X_MAX = 1f; private static final float AXIS_Y_MIN = -1f; private static final float AXIS_Y_MAX = 1f; // The current viewport. This rectangle represents the visible chart // domain and range. The viewport is the part of the app that the // user manipulates via touch gestures. private RectF currentViewport = new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX); // The current destination rectangle—in pixel coordinates—into which // the chart data must be drawn. private final Rect contentRect = new Rect(); private final OverScroller scroller; private final RectF scrollerStartViewport = new RectF(); // Used only for zooms and flings. ... private final GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { releaseEdgeEffects(); } scrollerStartViewport.set(currentViewport); scroller.forceFinished(true); ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this); return true; } ... @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { fling((int) -velocityX, (int) -velocityY); return true; } }; private void fling(int velocityX, int velocityY) { // Initiates the decay phase of any active edge effects. // On Android 12 and later, the edge effect (stretch) must // continue. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { releaseEdgeEffects(); } // Flings use math in pixels, as opposed to math based on the viewport. Point surfaceSize = computeScrollSurfaceSize(); scrollerStartViewport.set(currentViewport); int startX = (int) (surfaceSize.x * (scrollerStartViewport.left - AXIS_X_MIN) / ( AXIS_X_MAX - AXIS_X_MIN)); int startY = (int) (surfaceSize.y * (AXIS_Y_MAX - scrollerStartViewport.bottom) / ( AXIS_Y_MAX - AXIS_Y_MIN)); // Before flinging, stops the current animation. scroller.forceFinished(true); // Begins the animation. scroller.fling( // Current scroll position. startX, startY, velocityX, velocityY, /* * Minimum and maximum scroll positions. The minimum scroll * position is generally 0 and the maximum scroll position * is generally the content size less the screen size. So if the * content width is 1000 pixels and the screen width is 200 * pixels, the maximum scroll offset is 800 pixels. */ 0, surfaceSize.x - contentRect.width(), 0, surfaceSize.y - contentRect.height(), // The edges of the content. This comes into play when using // the EdgeEffect class to draw "glow" overlays. contentRect.width() / 2, contentRect.height() / 2); // Invalidates to trigger computeScroll(). ViewCompat.postInvalidateOnAnimation(this); }
عندما تستدعي onFling() طريقة postInvalidateOnAnimation()، فإنّها تشغّل computeScroll() لتعديل قيمتَي x وy. يحدث ذلك عادةً عندما تحرّك طريقة عرض فرعية التمرير باستخدام عنصر محتوى قابل للتمرير، كما هو موضّح في المثال السابق.
تُمرِّر معظم طرق العرض موضعَي x وy لعنصر أداة التمرير مباشرةً إلى scrollTo().
تتّبع عملية تنفيذ computeScroll() التالية أسلوبًا مختلفًا: فهي تستدعي computeScrollOffset() للحصول على الموضع الحالي لـ x وy. عند استيفاء معايير عرض تأثير حافة تجاوز حد التمرير "توهّج"، أي أن يكون العرض مكبَّرًا، وأن تكون x أو y خارج الحدود، وألا يكون التطبيق يعرض حاليًا تجاوز حد التمرير، فإنّ الرمز البرمجي يضبط تأثير توهّج تجاوز حد التمرير ويستدعي postInvalidateOnAnimation() لتشغيل عملية إبطال في طريقة العرض.
Kotlin
// Edge effect/overscroll tracking objects. private lateinit var edgeEffectTop: EdgeEffect private lateinit var edgeEffectBottom: EdgeEffect private lateinit var edgeEffectLeft: EdgeEffect private lateinit var edgeEffectRight: EdgeEffect private var edgeEffectTopActive: Boolean = false private var edgeEffectBottomActive: Boolean = false private var edgeEffectLeftActive: Boolean = false private var edgeEffectRightActive: Boolean = false override fun computeScroll() { super.computeScroll() var needsInvalidate = false // The scroller isn't finished, meaning a fling or // programmatic pan operation is active. if (scroller.computeScrollOffset()) { val surfaceSize: Point = computeScrollSurfaceSize() val currX: Int = scroller.currX val currY: Int = scroller.currY val (canScrollX: Boolean, canScrollY: Boolean) = currentViewport.run { (left > AXIS_X_MIN || right < AXIS_X_MAX) to (top > AXIS_Y_MIN || bottom < AXIS_Y_MAX) } /* * If you are zoomed in, currX or currY is * outside of bounds, and you aren't already * showing overscroll, then render the overscroll * glow edge effect. */ if (canScrollX && currX < 0 && edgeEffectLeft.isFinished && !edgeEffectLeftActive) { edgeEffectLeft.onAbsorb(scroller.currVelocity.toInt()) edgeEffectLeftActive = true needsInvalidate = true } else if (canScrollX && currX > surfaceSize.x - contentRect.width() && edgeEffectRight.isFinished && !edgeEffectRightActive) { edgeEffectRight.onAbsorb(scroller.currVelocity.toInt()) edgeEffectRightActive = true needsInvalidate = true } if (canScrollY && currY < 0 && edgeEffectTop.isFinished && !edgeEffectTopActive) { edgeEffectTop.onAbsorb(scroller.currVelocity.toInt()) edgeEffectTopActive = true needsInvalidate = true } else if (canScrollY && currY > surfaceSize.y - contentRect.height() && edgeEffectBottom.isFinished && !edgeEffectBottomActive) { edgeEffectBottom.onAbsorb(scroller.currVelocity.toInt()) edgeEffectBottomActive = true needsInvalidate = true } ... } }
Java
// Edge effect/overscroll tracking objects. private EdgeEffectCompat edgeEffectTop; private EdgeEffectCompat edgeEffectBottom; private EdgeEffectCompat edgeEffectLeft; private EdgeEffectCompat edgeEffectRight; private boolean edgeEffectTopActive; private boolean edgeEffectBottomActive; private boolean edgeEffectLeftActive; private boolean edgeEffectRightActive; @Override public void computeScroll() { super.computeScroll(); boolean needsInvalidate = false; // The scroller isn't finished, meaning a fling or // programmatic pan operation is active. if (scroller.computeScrollOffset()) { Point surfaceSize = computeScrollSurfaceSize(); int currX = scroller.getCurrX(); int currY = scroller.getCurrY(); boolean canScrollX = (currentViewport.left > AXIS_X_MIN || currentViewport.right < AXIS_X_MAX); boolean canScrollY = (currentViewport.top > AXIS_Y_MIN || currentViewport.bottom < AXIS_Y_MAX); /* * If you are zoomed in, currX or currY is * outside of bounds, and you aren't already * showing overscroll, then render the overscroll * glow edge effect. */ if (canScrollX && currX < 0 && edgeEffectLeft.isFinished() && !edgeEffectLeftActive) { edgeEffectLeft.onAbsorb((int)mScroller.getCurrVelocity()); edgeEffectLeftActive = true; needsInvalidate = true; } else if (canScrollX && currX > (surfaceSize.x - contentRect.width()) && edgeEffectRight.isFinished() && !edgeEffectRightActive) { edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity()); edgeEffectRightActive = true; needsInvalidate = true; } if (canScrollY && currY < 0 && edgeEffectTop.isFinished() && !edgeEffectTopActive) { edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity()); edgeEffectTopActive = true; needsInvalidate = true; } else if (canScrollY && currY > (surfaceSize.y - contentRect.height()) && edgeEffectBottom.isFinished() && !edgeEffectBottomActive) { edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity()); edgeEffectBottomActive = true; needsInvalidate = true; } ... }
إليكم قسم الرمز البرمجي الذي ينفّذ عملية التكبير الفعلية:
Kotlin
lateinit var zoomer: Zoomer val zoomFocalPoint = PointF() ... // If a zoom is in progress—either programmatically // or through double touch—this performs the zoom. if (zoomer.computeZoom()) { val newWidth: Float = (1f - zoomer.currZoom) * scrollerStartViewport.width() val newHeight: Float = (1f - zoomer.currZoom) * scrollerStartViewport.height() val pointWithinViewportX: Float = (zoomFocalPoint.x - scrollerStartViewport.left) / scrollerStartViewport.width() val pointWithinViewportY: Float = (zoomFocalPoint.y - scrollerStartViewport.top) / scrollerStartViewport.height() currentViewport.set( zoomFocalPoint.x - newWidth * pointWithinViewportX, zoomFocalPoint.y - newHeight * pointWithinViewportY, zoomFocalPoint.x + newWidth * (1 - pointWithinViewportX), zoomFocalPoint.y + newHeight * (1 - pointWithinViewportY) ) constrainViewport() needsInvalidate = true } if (needsInvalidate) { ViewCompat.postInvalidateOnAnimation(this) }
Java
// Custom object that is functionally similar to Scroller. Zoomer zoomer; private PointF zoomFocalPoint = new PointF(); ... // If a zoom is in progress—either programmatically // or through double touch—this performs the zoom. if (zoomer.computeZoom()) { float newWidth = (1f - zoomer.getCurrZoom()) * scrollerStartViewport.width(); float newHeight = (1f - zoomer.getCurrZoom()) * scrollerStartViewport.height(); float pointWithinViewportX = (zoomFocalPoint.x - scrollerStartViewport.left) / scrollerStartViewport.width(); float pointWithinViewportY = (zoomFocalPoint.y - scrollerStartViewport.top) / scrollerStartViewport.height(); currentViewport.set( zoomFocalPoint.x - newWidth * pointWithinViewportX, zoomFocalPoint.y - newHeight * pointWithinViewportY, zoomFocalPoint.x + newWidth * (1 - pointWithinViewportX), zoomFocalPoint.y + newHeight * (1 - pointWithinViewportY)); constrainViewport(); needsInvalidate = true; } if (needsInvalidate) { ViewCompat.postInvalidateOnAnimation(this); }
هذه هي طريقة computeScrollSurfaceSize() التي يتم استدعاؤها في المقتطف السابق. تحسب هذه الطريقة حجم السطح الحالي القابل للتمرير بوحدات البكسل. على سبيل المثال، إذا كانت منطقة الرسم البياني بأكملها مرئية، يكون هذا هو الحجم الحالي لـ mContentRect. إذا تم تكبير الرسم البياني بنسبة% 200 في كلا الاتجاهَين، يكون الحجم المعروض أكبر بمرتين أفقيًا وعموديًا.
Kotlin
private fun computeScrollSurfaceSize(): Point { return Point( (contentRect.width() * (AXIS_X_MAX - AXIS_X_MIN) / currentViewport.width()).toInt(), (contentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN) / currentViewport.height()).toInt() ) }
Java
private Point computeScrollSurfaceSize() { return new Point( (int) (contentRect.width() * (AXIS_X_MAX - AXIS_X_MIN) / currentViewport.width()), (int) (contentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN) / currentViewport.height())); }
لمعرفة مثال آخر على استخدام أداة التمرير، يُرجى الاطّلاع على
رمز المصدر
للفئة ViewPager. تتولى هذه الفئة التمرير استجابةً للإيماءات السريعة وتستخدم التمرير لتنفيذ الصورة المتحركة "الانتقال إلى الصفحة".
تنفيذ تأثير تجاوز حد التمرير الممتد
اعتبارًا من Android 12، تضيف EdgeEffect واجهات برمجة التطبيقات التالية لتنفيذ تأثير تجاوز حد التمرير الممتد:
getDistance()onPullDistance()
لتقديم أفضل تجربة للمستخدم مع تجاوز حد التمرير الممتد، يُرجى اتّباع الخطوات التالية:
- عندما تكون الصورة المتحركة للتمديد سارية عندما يلمس المستخدم الـ محتوى، سجِّلوا اللمسة على أنّها "إيقاف". يوقف المستخدم الصورة المتحركة و يبدأ في معالجة التمديد مرة أخرى.
- عندما يحرك المستخدم إصبعه في الاتجاه المعاكس للتمديد، حرِّروا التمديد إلى أن يختفي تمامًا، ثم ابدأوا التمرير.
- عندما يجري المستخدم إيماءة سريعة أثناء التمديد، حرِّكوا
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() مقدار الدلتا المستهلكة من `deltaDistance` التي تم تمريرها. اعتبارًا من Android 12، إذا تم تمرير قيم deltaDistance سالبة إلى onPull() أو onPullDistance() عندما تكون 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);
مراجع إضافية
يُرجى الرجوع إلى المراجع ذات الصلة التالية: