@@ -23,11 +23,13 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
2323 private val gestureDetector = GestureDetector (context, GestureListener ())
2424
2525 // Zoom and pan state
26+ private var renderQuality = RenderQuality .NORMAL
2627 private var scaleFactor = 1f
2728 private var isZoomEnabled = true
28- private var maxZoom = MAX_ZOOM
29+ private val maxZoom get() = MAX_ZOOM * renderQuality.qualityMultiplier
2930 private var zoomDuration = ZOOM_DURATION
3031 private var isZoomingInProgress = false
32+ private var isOnTop = true
3133
3234 // Panning offsets and touch memory
3335 private var lastTouchX = 0f
@@ -36,6 +38,7 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
3638 private var posY = 0f
3739
3840 private var zoomChangeListener: ((Boolean , Float ) -> Unit )? = null
41+ private var scrollListener: ((Boolean ) -> Unit )? = null
3942
4043 init {
4144 setWillNotDraw(false )
@@ -53,6 +56,14 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
5356 zoomChangeListener = listener
5457 }
5558
59+ fun setScrollListener (listener : (isScrolledToTop: Boolean ) -> Unit ) {
60+ scrollListener = listener
61+ }
62+
63+ fun setRenderQuality (quality : RenderQuality ) {
64+ renderQuality = quality
65+ }
66+
5667 /* *
5768 * Handles touch interactions — zoom, pan, and scroll.
5869 */
@@ -67,42 +78,10 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
6778 }
6879
6980 when (ev.actionMasked) {
70- MotionEvent .ACTION_DOWN -> {
71- lastTouchX = ev.x
72- lastTouchY = ev.y
73- activePointerId = ev.getPointerId(0 )
74- }
75- MotionEvent .ACTION_MOVE -> {
76- if (! scaleDetector.isInProgress && scaleFactor > 1f ) {
77- val pointerIndex = ev.findPointerIndex(activePointerId)
78- if (pointerIndex != - 1 ) {
79- val x = ev.getX(pointerIndex)
80- val y = ev.getY(pointerIndex)
81- val dx = x - lastTouchX
82- val dy = y - lastTouchY
83- posX + = dx
84- posY + = dy
85- clampPosition()
86- invalidate()
87-
88- lastTouchX = x
89- lastTouchY = y
90- }
91- }
92- }
93- MotionEvent .ACTION_POINTER_UP -> {
94- val pointerIndex = ev.actionIndex
95- val pointerId = ev.getPointerId(pointerIndex)
96- if (pointerId == activePointerId) {
97- val newPointerIndex = if (pointerIndex == 0 ) 1 else 0
98- lastTouchX = ev.getX(newPointerIndex)
99- lastTouchY = ev.getY(newPointerIndex)
100- activePointerId = ev.getPointerId(newPointerIndex)
101- }
102- }
103- MotionEvent .ACTION_CANCEL -> {
104- activePointerId = INVALID_POINTER_ID
105- }
81+ MotionEvent .ACTION_DOWN -> onDown(ev = ev)
82+ MotionEvent .ACTION_MOVE -> onMove(ev = ev)
83+ MotionEvent .ACTION_POINTER_UP -> onUp(ev = ev)
84+ MotionEvent .ACTION_CANCEL -> onCancel(ev = ev)
10685 }
10786
10887 return if (scaleFactor > 1f ) true else super .onTouchEvent(ev)
@@ -171,13 +150,65 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
171150 return (averageHeight * itemCount * scaleFactor).toInt()
172151 }
173152
153+ private fun onDown (ev : MotionEvent ) {
154+ lastTouchX = ev.x
155+ lastTouchY = ev.y
156+ activePointerId = ev.getPointerId(0 )
157+ }
158+
159+ private fun onMove (ev : MotionEvent ) {
160+ val pointerIndex = ev.findPointerIndex(activePointerId)
161+ if (pointerIndex != - 1 ) {
162+ if (! scaleDetector.isInProgress && scaleFactor > 1f ) {
163+ val x = ev.getX(pointerIndex)
164+ val y = ev.getY(pointerIndex)
165+ val dx = x - lastTouchX
166+ val dy = y - lastTouchY
167+ posX + = dx
168+ posY + = dy
169+ clampPosition()
170+ invalidate()
171+
172+ lastTouchX = x
173+ lastTouchY = y
174+ }
175+
176+ val isScrolledOut = ! scaleDetector.isInProgress && scaleFactor == 1f
177+ val currentScrollOffset = computeVerticalScrollOffset()
178+ if (currentScrollOffset == 0 && isScrolledOut && ! isOnTop) {
179+ scrollListener?.invoke(true )
180+ isOnTop = true
181+ } else if ((currentScrollOffset != 0 || isScrolledOut.not ()) && isOnTop) {
182+ scrollListener?.invoke(false )
183+ isOnTop = false
184+ }
185+ }
186+ }
187+
188+ private fun onUp (ev : MotionEvent ) {
189+ val pointerIndex = ev.actionIndex
190+ val pointerId = ev.getPointerId(pointerIndex)
191+ if (pointerId == activePointerId) {
192+ val newPointerIndex = if (pointerIndex == 0 ) 1 else 0
193+ lastTouchX = ev.getX(newPointerIndex)
194+ lastTouchY = ev.getY(newPointerIndex)
195+ activePointerId = ev.getPointerId(newPointerIndex)
196+ }
197+ }
198+
199+ private fun onCancel (ev : MotionEvent ) {
200+ activePointerId = INVALID_POINTER_ID
201+ }
202+
174203 /* *
175204 * Handles pinch-to-zoom scaling with focal-point centering.
176205 */
177206 private inner class ScaleListener : ScaleGestureDetector .SimpleOnScaleGestureListener () {
178207 override fun onScaleBegin (detector : ScaleGestureDetector ): Boolean {
179208 isZoomingInProgress = true
180209 suppressLayout(true )
210+ scrollListener?.invoke(false )
211+ isOnTop = false
181212 return true
182213 }
183214
0 commit comments