ב-Android, בדרך כלל גוללים באמצעות המחלקה ScrollView. כדי לספק תצוגה עם גלילה שמנוהלת על ידי ה-framework, צריך להוסיף כל פריסה רגילה שעשויה לחרוג מהגבולות של הקונטיינר שלה ב-ScrollView. צריך להטמיע גלילה בהתאמה אישית רק בתרחישים מיוחדים. במאמר הזה מוסבר איך להציג אפקט גלילה בתגובה לתנועות מגע באמצעות רכיבי גלילה.
האפליקציה יכולה להשתמש ברכיבי גלילה – Scroller או OverScroller – כדי לאסוף את הנתונים שדרושים ליצירת אנימציית גלילה בתגובה לאירוע מגע. הם דומים, אבל OverScroller כולל גם שיטות שמציינות למשתמשים כשהם מגיעים לקצוות התוכן אחרי תנועת הזזה או תנועת החלקה מהירה.
- החל מ-Android 12 (רמת API 31), הרכיבים החזותיים נמתחים וחוזרים למצב הקודם באירוע גרירה, ומוקפצים וחוזרים למצב הקודם באירוע הקפצה.
- ב-Android 11 (רמת API 30) ומטה, הגבולות מציגים אפקט "זוהר" אחרי תנועת גרירה או החלקה מהירה לקצה.
בדוגמה של InteractiveChart במסמך הזה נעשה שימוש במחלקה EdgeEffect כדי להציג את האפקטים האלה של גלילה מעבר לקצה.
אתם יכולים להשתמש ב-scroller כדי ליצור אנימציה של גלילה לאורך זמן, באמצעות פיזיקת גלילה סטנדרטית בפלטפורמה כמו חיכוך, מהירות ומאפיינים אחרים. הגלילה עצמה לא מציירת שום דבר. הגלילה עוקבת אחרי היסט הגלילה לאורך זמן, אבל לא מחילה את המיקומים האלה באופן אוטומטי על התצוגה. צריך לקבל ולהחיל קואורדינטות חדשות בקצב שגורם לאנימציית הגלילה להיראות חלקה.
הסבר על המונחים שקשורים לגלילה
למילה "גלילה" יכולות להיות משמעויות שונות ב-Android, בהתאם להקשר.
גלילה היא התהליך הכללי של הזזת אזור התצוגה, כלומר "חלון" התוכן שבו אתם צופים. כשגוללים גם בציר x וגם בציר y, הפעולה נקראת הזזה. InteractiveChartהאפליקציה לדוגמה במסמך הזה ממחישה שני סוגים שונים של גלילה, גרירה והחלקה:
- גרירה: סוג הגלילה שמתרחש כשמשתמש גורר את האצבע על מסך המגע. כדי להטמיע גרירה, אפשר לשנות את
onScroll()ב-GestureDetector.OnGestureListener. מידע נוסף על גרירה זמין במאמר גרירה ושינוי קנה מידה. - גלילה מהירה: סוג הגלילה שמתרחש כשמשתמש גורר את האצבע ומרים אותה במהירות. אחרי שהמשתמש מרים את האצבע, בדרך כלל כדאי להמשיך להזיז את אזור התצוגה, אבל להאט עד שאזור התצוגה מפסיק לזוז. כדי להטמיע את התנועה המהירה, אפשר לשנות את
onFling()ב-GestureDetector.OnGestureListenerולהשתמש באובייקט גלילה. - הזזה: גלילה בו-זמנית לאורך ציר ה-X וציר ה-Y נקראת הזזה.
בדרך כלל משתמשים באובייקטים של גלילה בשילוב עם תנועת החלקה, אבל אפשר להשתמש בהם בכל הקשר שבו רוצים שממשק המשתמש יציג גלילה בתגובה לאירוע מגע. לדוגמה, אפשר לשנות את onTouchEvent() כדי לעבד אירועי מגע ישירות וליצור אפקט גלילה או אנימציה של "הצמדה לדף" בתגובה לאירועי המגע האלה.
רכיבים שמכילים הטמעות מובנות של גלילה
רכיבי Android הבאים מכילים תמיכה מובנית בהתנהגות של גלילה וגלילת יתר:
GridViewHorizontalScrollViewListViewNestedScrollViewRecyclerViewScrollViewViewPagerViewPager2
אם האפליקציה צריכה לתמוך בגלילה ובגלילת יתר בתוך רכיב אחר, צריך לבצע את השלבים הבאים:
- יצירת הטמעה מותאמת אישית של גלילה באמצעות מגע.
- כדי לתמוך במכשירים עם Android מגרסה 12 ואילך, מטמיעים את אפקט המתיחה בגלילה מעבר לקצה.
יצירת הטמעה מותאמת אישית של גלילה באמצעות מגע
בקטע הזה מוסבר איך ליצור גלילה משלכם אם האפליקציה משתמשת ברכיב שלא מכיל תמיכה מובנית בגלילה ובגלילת יתר.
קטע הקוד הבא מגיע מהדוגמה InteractiveChart. היא משתמשת ב-GestureDetector ומבטלת את ה-method 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); }
זוהי ה-method 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())); }
דוגמה נוספת לשימוש ב-scroller מופיעה בקוד המקור של המחלקה ViewPager. הוא גולל בתגובה לתנועות מהירות ומשתמש בגלילה כדי להטמיע את האנימציה "הצמדה לדף".
הטמעת אפקט המתיחה בגלילה מעבר לקצה
החל מ-Android 12, EdgeEffect מוסיף את ממשקי ה-API הבאים להטמעת אפקט המתיחה בגלילה מעבר לקצה:
getDistance()onPullDistance()
כדי לספק את חוויית השימוש הטובה ביותר עם גלילה מעבר לקצה במתיחה, צריך לבצע את הפעולות הבאות:
- כשאנימציית המתיחה פועלת והמשתמש נוגע בתוכן, צריך לרשום את הנגיעה כ "תפיסה". המשתמש עוצר את האנימציה ומתחיל שוב לשנות את המתיחה.
- כשהמשתמש מזיז את האצבע בכיוון ההפוך למתיחה, צריך לשחרר את המתיחה עד שהיא נעלמת לגמרי, ואז להתחיל לגלול.
- כשהמשתמש מבצע תנועת החלקה מהירה במהלך מתיחה, צריך להחליק במהירות את
EdgeEffectכדי לשפר את אפקט המתיחה.
תפיסת האנימציה
כשמשתמש תופס אנימציית מתיחה פעילה,
EdgeEffect.getDistance() מחזיר את הערך 0. התנאי הזה מציין שצריך לשנות את המתיחה באמצעות תנועת המגע. ברוב הקונטיינרים, ה-catch מזוהה ב-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, אפקט המתיחה לא משתנה. ב-Android מגרסה 11 ומטה, onPull() מאפשר להציג אפקטים של זוהר בערכים שליליים של המרחק הכולל.
ביטול ההסכמה לשימוש בגלילת יתר
אפשר לבטל את ההסכמה לגלילה מעבר לקצה בקובץ הפריסה או באופן פרוגרמטי.
כדי לבטל את ההסכמה בקובץ הפריסה, מגדירים את android:overScrollMode כמו בדוגמה הבאה:
<MyCustomView android:overScrollMode="never"> ... </MyCustomView>
כדי לבטל את ההסכמה באופן פרוגרמטי, משתמשים בקוד כמו זה:
Kotlin
customView.overScrollMode = View.OVER_SCROLL_NEVER
Java
customView.setOverScrollMode(View.OVER_SCROLL_NEVER);
מקורות מידע נוספים
מקורות מידע שקשורים לנושא: