Trong Android, thao tác cuộn thường được thực hiện bằng cách sử dụng lớp ScrollView
. Lồng bất kỳ bố cục tiêu chuẩn nào có thể mở rộng ra ngoài giới hạn của vùng chứa trong ScrollView
để cung cấp chế độ xem có thể cuộn do khung quản lý. Bạn chỉ cần triển khai thanh cuộn tuỳ chỉnh cho các trường hợp đặc biệt. Tài liệu này mô tả cách hiển thị hiệu ứng cuộn để phản hồi các cử chỉ chạm bằng thanh cuộn.
Ứng dụng của bạn có thể sử dụng thanh cuộn – Scroller
hoặc OverScroller
– để thu thập dữ liệu cần thiết nhằm tạo ảnh động cuộn để phản hồi một sự kiện chạm. Các phương thức này tương tự nhau, nhưng OverScroller
cũng bao gồm các phương thức để cho người dùng biết khi họ chạm đến các cạnh nội dung sau một cử chỉ kéo hoặc hất.
- Kể từ Android 12 (API cấp 31), các thành phần hình ảnh sẽ kéo giãn và bật lại trong sự kiện kéo, đồng thời hất và bật lại trong sự kiện hất.
- Trên Android 11 (API cấp 30) trở xuống, các ranh giới sẽ hiển thị hiệu ứng "sáng" sau khi người dùng kéo hoặc hất cử chỉ đến cạnh.
Mẫu InteractiveChart
trong tài liệu này sử dụng lớp EdgeEffect
để hiển thị các hiệu ứng cuộn quá mức này.
Bạn có thể sử dụng thanh cuộn để tạo ảnh động cuộn theo thời gian, sử dụng các tính năng vật lý cuộn theo tiêu chuẩn nền tảng như ma sát, tốc độ và các đặc tính khác. Bản thân thanh cuộn không vẽ bất kỳ nội dung nào. Thanh cuộn theo dõi độ dời cuộn cho bạn theo thời gian, nhưng không tự động áp dụng các vị trí đó cho chế độ xem. Bạn phải lấy và áp dụng toạ độ mới ở tốc độ giúp ảnh động cuộn trông mượt mà.
Tìm hiểu thuật ngữ cuộn
Cuộn là một từ có thể có nhiều nghĩa trong Android, tuỳ thuộc vào ngữ cảnh.
Cuộn là quy trình chung để di chuyển khung nhìn, tức là "cửa sổ" nội dung bạn đang xem. Khi cuộn ở cả trục x và y, thao tác này được gọi là kéo. Ứng dụng mẫu InteractiveChart
trong tài liệu này minh hoạ hai loại thao tác cuộn, kéo và hất:
- Kéo: đây là loại thao tác cuộn xảy ra khi người dùng kéo ngón tay trên màn hình cảm ứng. Bạn có thể triển khai thao tác kéo bằng cách ghi đè
onScroll()
trongGestureDetector.OnGestureListener
. Để biết thêm thông tin về thao tác kéo, hãy xem phần Kéo và điều chỉnh theo tỷ lệ. - Thả: đây là loại thao tác cuộn xảy ra khi người dùng kéo và nhấc ngón tay lên nhanh chóng. Sau khi người dùng nhấc ngón tay lên, bạn thường muốn tiếp tục di chuyển khung nhìn, nhưng giảm tốc cho đến khi khung nhìn ngừng di chuyển. Bạn có thể triển khai thao tác hất bằng cách ghi đè
onFling()
trongGestureDetector.OnGestureListener
và sử dụng đối tượng thanh cuộn. - Di chuyển: việc cuộn đồng thời dọc theo cả trục x và y được gọi là di chuyển.
Thông thường, bạn sẽ sử dụng các đối tượng thanh cuộn kết hợp với cử chỉ hất, nhưng bạn có thể sử dụng các đối tượng này trong bất kỳ ngữ cảnh nào mà bạn muốn giao diện người dùng hiển thị thao tác cuộn để phản hồi một sự kiện chạm. Ví dụ: bạn có thể ghi đè onTouchEvent()
để trực tiếp xử lý các sự kiện chạm và tạo hiệu ứng cuộn hoặc ảnh động "chuyển đến trang" để phản hồi các sự kiện chạm đó.
Các thành phần chứa các phương thức triển khai cuộn tích hợp
Các thành phần Android sau đây có chức năng hỗ trợ tích hợp cho hành vi cuộn và cuộn quá mức:
GridView
HorizontalScrollView
ListView
NestedScrollView
RecyclerView
ScrollView
ViewPager
ViewPager2
Nếu ứng dụng của bạn cần hỗ trợ tính năng cuộn và cuộn xuống bên trong một thành phần khác, hãy hoàn tất các bước sau:
- Tạo một phương thức triển khai cuộn dựa trên thao tác chạm tuỳ chỉnh.
- Để hỗ trợ các thiết bị chạy Android 12 trở lên, hãy triển khai hiệu ứng kéo giãn khi cuộn quá mức.
Tạo một phương thức triển khai cuộn dựa trên thao tác chạm tuỳ chỉnh
Phần này mô tả cách tạo thanh cuộn của riêng bạn nếu ứng dụng của bạn sử dụng một thành phần không chứa tính năng hỗ trợ tích hợp để cuộn và cuộn quá mức.
Đoạn mã sau đây được lấy từ mẫu InteractiveChart
. Phương thức này sử dụng GestureDetector
và ghi đè phương thức GestureDetector.SimpleOnGestureListener
onFling()
. Phương thức này sử dụng OverScroller
để theo dõi cử chỉ hất. Nếu người dùng chạm đến các cạnh của nội dung sau khi thực hiện cử chỉ hất, thì vùng chứa sẽ cho biết thời điểm người dùng chạm đến cuối nội dung. Thông báo này tuỳ thuộc vào phiên bản Android mà thiết bị chạy:
- Trên Android 12 trở lên, các thành phần hình ảnh sẽ kéo giãn và bật lại.
- Trên Android 11 trở xuống, các thành phần hình ảnh sẽ hiển thị hiệu ứng phát sáng.
Phần đầu của đoạn mã sau đây cho thấy cách triển khai 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); }
Khi onFling()
gọi postInvalidateOnAnimation()
, thao tác này sẽ kích hoạt computeScroll()
để cập nhật các giá trị cho x và y. Việc này thường được thực hiện khi một thành phần hiển thị con đang tạo ảnh động cuộn bằng đối tượng cuộn, như trong ví dụ trước.
Hầu hết các thành phần hiển thị đều truyền trực tiếp vị trí x và y của đối tượng thanh cuộn đến scrollTo()
.
Cách triển khai computeScroll()
sau đây sẽ sử dụng một phương pháp khác: phương thức này gọi computeScrollOffset()
để lấy vị trí hiện tại của x và y. Khi đáp ứng các tiêu chí để hiển thị hiệu ứng cạnh "hiệu ứng phát sáng" khi cuộn quá mức, tức là màn hình được phóng to, x hoặc y nằm ngoài giới hạn và ứng dụng chưa hiển thị hiệu ứng cuộn quá mức, mã sẽ thiết lập hiệu ứng phát sáng khi cuộn quá mức và gọi postInvalidateOnAnimation()
để kích hoạt một hiệu ứng vô hiệu trên thành phần hiển thị.
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; } ... }
Dưới đây là phần mã thực hiện thao tác thu phóng thực tế:
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); }
Đây là phương thức computeScrollSurfaceSize()
được gọi trong đoạn mã trước. Hàm này tính toán kích thước bề mặt có thể cuộn hiện tại tính bằng pixel. Ví dụ: nếu toàn bộ khu vực biểu đồ hiển thị, thì đây là kích thước hiện tại của mContentRect
. Nếu biểu đồ được phóng to 200% theo cả hai hướng, kích thước được trả về sẽ lớn gấp đôi theo chiều ngang và chiều dọc.
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())); }
Để biết ví dụ khác về cách sử dụng thanh cuộn, hãy xem mã nguồn cho lớp ViewPager
. Thành phần này cuộn để phản hồi các thao tác hất và sử dụng tính năng cuộn để triển khai ảnh động "đưa đến trang".
Triển khai hiệu ứng kéo giãn khi cuộn xuống cuối cùng
Kể từ Android 12, EdgeEffect
sẽ thêm các API sau để triển khai hiệu ứng cuộn quá mức kéo giãn:
getDistance()
onPullDistance()
Để mang lại trải nghiệm tốt nhất cho người dùng khi cuộn giãn, hãy làm như sau:
- Khi ảnh động kéo giãn có hiệu lực khi người dùng chạm vào nội dung, hãy đăng ký thao tác chạm đó dưới dạng "bắt". Người dùng dừng ảnh động và bắt đầu thao tác lại thao tác kéo giãn.
- Khi người dùng di chuyển ngón tay theo hướng ngược lại với phần kéo giãn, hãy thả phần kéo giãn cho đến khi phần này biến mất hoàn toàn, sau đó bắt đầu cuộn.
- Khi người dùng hất trong khi kéo giãn, hãy hất
EdgeEffect
để tăng cường hiệu ứng kéo giãn.
Nắm bắt ảnh động
Khi người dùng bắt gặp một ảnh động kéo giãn đang hoạt động, EdgeEffect.getDistance()
sẽ trả về 0
. Điều kiện này cho biết rằng bạn phải thao tác kéo giãn bằng chuyển động chạm. Trong hầu hết các vùng chứa, lỗi phát hiện được trong onInterceptTouchEvent()
, như minh hoạ trong đoạn mã sau:
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; ... } }
Trong ví dụ trước, onInterceptTouchEvent()
trả về true
khi mIsBeingDragged
là true
, vì vậy, bạn chỉ cần sử dụng sự kiện trước khi thành phần con có cơ hội sử dụng sự kiện đó.
Phát hành hiệu ứng cuộn hết
Điều quan trọng là phải huỷ hiệu ứng kéo giãn trước khi cuộn để tránh việc hiệu ứng kéo giãn được áp dụng cho nội dung cuộn. Mẫu mã sau đây áp dụng phương pháp hay nhất này:
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); } ...
Khi người dùng đang kéo, hãy sử dụng khoảng cách kéo EdgeEffect
trước khi bạn truyền sự kiện chạm đến vùng chứa cuộn lồng nhau hoặc kéo thanh cuộn. Trong mẫu mã trước, getDistance()
trả về một giá trị dương khi hiệu ứng cạnh đang hiển thị và có thể được phát hành bằng chuyển động. Khi sự kiện chạm giải phóng tính năng kéo giãn, trước tiên, sự kiện này sẽ được EdgeEffect
sử dụng để giải phóng hoàn toàn trước khi các hiệu ứng khác (chẳng hạn như cuộn lồng nhau) hiển thị. Bạn có thể sử dụng getDistance()
để tìm hiểu khoảng cách cần kéo để phát hành hiệu ứng hiện tại.
Không giống như onPull()
, onPullDistance()
trả về lượng đã tiêu thụ của delta đã truyền. Kể từ Android 12, nếu onPull()
hoặc onPullDistance()
được truyền giá trị deltaDistance
âm khi getDistance()
là 0
, thì hiệu ứng kéo giãn sẽ không thay đổi. Trên Android 11
và các phiên bản trước, onPull()
cho phép các giá trị âm cho tổng quãng đường
hiển thị hiệu ứng phát sáng.
Chọn không sử dụng tính năng cuộn xuống cuối cùng
Bạn có thể chọn không sử dụng tính năng cuộn quá mức trong tệp bố cục hoặc theo phương thức lập trình.
Để chọn không sử dụng trong tệp bố cục, hãy đặt android:overScrollMode
như trong ví dụ sau:
<MyCustomView android:overScrollMode="never"> ... </MyCustomView>
Để chọn không sử dụng theo phương thức lập trình, hãy sử dụng mã như sau:
Kotlin
customView.overScrollMode = View.OVER_SCROLL_NEVER
Java
customView.setOverScrollMode(View.OVER_SCROLL_NEVER);
Tài nguyên khác
Hãy tham khảo các tài nguyên liên quan sau: