Android에서 스크롤은 일반적으로 ScrollView
클래스를 사용하여 구현합니다. ScrollView
에 컨테이너 경계를 넘어 확장될 수 있는 표준 레이아웃을 중첩하여 프레임워크에서 관리하는 스크롤 가능한 뷰를 제공합니다. 맞춤 스크롤러를 구현하는 것은 특수한 시나리오에만 필요합니다. 이 문서에서는 스크롤러를 사용하여 터치 동작에 응답하여 스크롤 효과를 표시하는 방법을 설명합니다.
앱은 스크롤러(Scroller
또는 OverScroller
)를 사용하여 터치 이벤트에 응답하여 스크롤 애니메이션을 생성하는 데 필요한 데이터를 수집할 수 있습니다. 이러한 스크롤러는 비슷하지만, OverScroller
에는 화면 이동 동작이나 살짝 튕기기 동작 이후 콘텐츠 가장자리에 도달했음을 사용자에게 알리는 메서드도 포함되어 있습니다.
- Android 12 (API 수준 31)부터 시각적 요소가 드래그 이벤트에 늘어났다가 다시 돌아오고 플링 이벤트에 플링되었다가 다시 돌아옵니다.
- Android 11 (API 수준 30) 이하에서는 가장자리로 드래그하거나 살짝 튕기기 동작을 한 후 경계가 '발광 효과'를 표시합니다.
이 문서의 InteractiveChart
샘플은 EdgeEffect
클래스를 사용하여 이러한 오버스크롤 효과를 표시합니다.
스크롤러를 사용하여 마찰, 속도, 기타 품질과 같은 플랫폼 표준 스크롤 물리 특성을 활용하여 시간 경과에 따른 스크롤을 애니메이션 처리할 수 있습니다. 스크롤러 자체는 아무것도 그리지 않습니다. 스크롤러는 시간 경과에 따라 스크롤 오프셋을 자동으로 추적하지만 스크롤 오프셋 위치를 뷰에 자동으로 적용하지는 않습니다. 스크롤 애니메이션을 부드럽게 표현하는 속도로 새 좌표를 가져와 적용해야 합니다.
스크롤 용어 이해하기
스크롤은 Android에서 컨텍스트에 따라 서로 다른 의미를 가질 수 있는 단어입니다.
스크롤은 표시 영역, 즉 보고 있는 콘텐츠의 '창'을 이동하는 일반적인 프로세스입니다. 스크롤이 x축과 y축 모두를 따라 이루어질 때 이를 화면 이동이라고 합니다. 이 문서의 InteractiveChart
샘플 앱은 두 가지 유형의 스크롤, 즉 드래그와 플링을 보여줍니다.
- 드래그: 사용자가 터치 스크린에서 손가락을 드래그할 때 발생하는 스크롤 유형입니다.
GestureDetector.OnGestureListener
에서onScroll()
를 재정의하여 드래그를 구현할 수 있습니다. 드래그에 관한 자세한 내용은 드래그 및 크기 조정을 참고하세요. - 살짝 튕기기: 사용자가 재빨리 드래그한 후 손가락을 뗄 때 발생하는 스크롤 유형입니다. 일반적으로 사용자가 손가락을 뗀 후에도 뷰포트 이동이 계속되지만 속도가 느려진 후 뷰포트 이동이 멈추기를 원합니다.
GestureDetector.OnGestureListener
의onFling()
를 재정의하고 스크롤러 객체를 사용하여 플링을 구현할 수 있습니다. - 화면 이동: x축과 y축을 동시에 스크롤하는 것을 화면 이동이라고 합니다.
스크롤러 객체는 살짝 튕기기 동작과 함께 사용하는 것이 일반적이지만 터치 이벤트에 응답해 UI에 스크롤을 표시하려는 모든 컨텍스트에서 사용할 수 있습니다. 예를 들어 onTouchEvent()
를 재정의하여 터치 이벤트를 직접 처리하고 이러한 터치 이벤트에 응답하여 스크롤 효과 또는 '페이지에 맞추기' 애니메이션을 생성할 수 있습니다.
내장 스크롤 구현이 포함된 구성요소
다음 Android 구성요소에는 스크롤 및 오버스크롤 동작에 관한 기본 제공 지원이 포함되어 있습니다.
GridView
HorizontalScrollView
ListView
NestedScrollView
RecyclerView
ScrollView
ViewPager
ViewPager2
앱에서 다른 구성요소 내에서 스크롤 및 오버스크롤을 지원해야 하는 경우 다음 단계를 완료하세요.
- 맞춤 터치 기반 스크롤 구현 만들기
- 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) }
자바
// 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 } ... } }
자바
// 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) }
자바
// 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% 확대된다면 가로와 세로로 2배의 크기가 반환됩니다.
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() ) }
자바
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
는 스트레치 오버스크롤 효과를 구현하기 위해 다음 API를 추가합니다.
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 }
자바
@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()
는 mIsBeingDragged
가 true
일 때 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); } ... }
자바
@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()
를 사용하여 현재 효과를 해제하는 데 필요한 pull 거리를 알 수 있습니다.
onPull()
와 달리 onPullDistance()
은 전달된 델타의 사용량을 반환합니다. Android 12부터 getDistance()
가 0
일 때 onPull()
또는 onPullDistance()
에 음수 deltaDistance
값이 전달되면 스트레치 효과가 변경되지 않습니다. Android 11 이하에서는 onPull()
를 사용하면 총 거리에 음수 값을 지정하여 발광 효과를 표시할 수 있습니다.
오버스크롤 선택 해제
레이아웃 파일에서 또는 프로그래매틱 방식으로 오버스크롤을 선택 해제할 수 있습니다.
레이아웃 파일에서 선택 해제하려면 다음 예와 같이 android:overScrollMode
를 설정합니다.
<MyCustomView android:overScrollMode="never"> ... </MyCustomView>
프로그래매틱 방식으로 선택 해제하려면 다음과 같은 코드를 사용하세요.
Kotlin
customView.overScrollMode = View.OVER_SCROLL_NEVER
Java
customView.setOverScrollMode(View.OVER_SCROLL_NEVER);
추가 리소스
다음 관련 리소스를 참조하세요.