Skip to content

Commit a9fba71

Browse files
committed
Add pinch-to-zoom and double-tap zoom in full screen viewer
1 parent 3aabba8 commit a9fba71

2 files changed

Lines changed: 222 additions & 2 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package com.degard.imagecompressor
2+
3+
import android.content.Context
4+
import android.graphics.Matrix
5+
import android.graphics.PointF
6+
import android.graphics.drawable.Drawable
7+
import android.util.AttributeSet
8+
import android.view.GestureDetector
9+
import android.view.MotionEvent
10+
import android.view.ScaleGestureDetector
11+
import android.view.animation.DecelerateInterpolator
12+
import android.widget.OverScroller
13+
import androidx.appcompat.widget.AppCompatImageView
14+
15+
class ZoomableImageView @JvmOverloads constructor(
16+
context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
17+
) : AppCompatImageView(context, attrs, defStyle) {
18+
19+
private val matrix = Matrix()
20+
private val savedMatrix = Matrix()
21+
private val matrixValues = FloatArray(9)
22+
23+
private var mode = NONE
24+
private val startPoint = PointF()
25+
private val midPoint = PointF()
26+
private var oldDist = 1f
27+
28+
private var minScale = 1f
29+
private var maxScale = 5f
30+
private var currentScale = 1f
31+
32+
private val scaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
33+
override fun onScale(detector: ScaleGestureDetector): Boolean {
34+
val scaleFactor = detector.scaleFactor
35+
currentScale *= scaleFactor
36+
currentScale = currentScale.coerceIn(minScale, maxScale)
37+
matrix.postScale(scaleFactor, scaleFactor, detector.focusX, detector.focusY)
38+
constrainMatrix()
39+
imageMatrix = matrix
40+
return true
41+
}
42+
})
43+
44+
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
45+
override fun onDoubleTap(e: MotionEvent): Boolean {
46+
if (currentScale > 1.5f) {
47+
resetZoom()
48+
} else {
49+
zoomTo(3f, e.x, e.y)
50+
}
51+
return true
52+
}
53+
54+
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float): Boolean {
55+
if (currentScale > 1f) {
56+
matrix.postTranslate(-dx, -dy)
57+
constrainMatrix()
58+
imageMatrix = matrix
59+
}
60+
return true
61+
}
62+
})
63+
64+
private val scroller = OverScroller(context)
65+
private var scrollAction: Runnable? = null
66+
67+
init {
68+
scaleType = ScaleType.MATRIX
69+
}
70+
71+
override fun setImageDrawable(drawable: Drawable?) {
72+
super.setImageDrawable(drawable)
73+
post { resetZoom() }
74+
}
75+
76+
override fun onTouchEvent(event: MotionEvent): Boolean {
77+
scaleDetector.onTouchEvent(event)
78+
gestureDetector.onTouchEvent(event)
79+
80+
when (event.actionMasked) {
81+
MotionEvent.ACTION_DOWN -> {
82+
mode = DRAG
83+
startPoint.set(event.x, event.y)
84+
savedMatrix.set(matrix)
85+
stopScroll()
86+
parent.requestDisallowInterceptTouchEvent(true)
87+
}
88+
MotionEvent.ACTION_POINTER_DOWN -> {
89+
mode = ZOOM
90+
oldDist = spacing(event)
91+
midPoint = mid(event)
92+
savedMatrix.set(matrix)
93+
parent.requestDisallowInterceptTouchEvent(true)
94+
}
95+
MotionEvent.ACTION_MOVE -> {
96+
if (mode == ZOOM && event.pointerCount >= 2) {
97+
val newDist = spacing(event)
98+
if (newDist > 10f) {
99+
val scale = newDist / oldDist
100+
matrix.set(savedMatrix)
101+
currentScale *= scale
102+
currentScale = currentScale.coerceIn(minScale, maxScale)
103+
matrix.postScale(scale, scale, midPoint.x, midPoint.y)
104+
constrainMatrix()
105+
imageMatrix = matrix
106+
oldDist = newDist
107+
}
108+
}
109+
}
110+
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
111+
mode = NONE
112+
if (currentScale < minScale) {
113+
resetZoom()
114+
}
115+
if (currentScale <= 1f) {
116+
parent.requestDisallowInterceptTouchEvent(false)
117+
}
118+
}
119+
}
120+
return true
121+
}
122+
123+
private fun resetZoom() {
124+
currentScale = minScale
125+
matrix.reset()
126+
centerImage()
127+
imageMatrix = matrix
128+
}
129+
130+
private fun zoomTo(targetScale: Float, focusX: Float, focusY: Float) {
131+
val startScale = currentScale
132+
val startTime = System.currentTimeMillis()
133+
val duration = 300L
134+
135+
scrollAction?.let { removeCallbacks(it) }
136+
scrollAction = object : Runnable {
137+
override fun run() {
138+
val elapsed = System.currentTimeMillis() - startTime
139+
val t = (elapsed.toFloat() / duration).coerceAtMost(1f)
140+
val eased = 1f - (1f - t) * (1f - t)
141+
val newScale = startScale + (targetScale - startScale) * eased
142+
val factor = newScale / currentScale
143+
currentScale = newScale
144+
matrix.postScale(factor, factor, focusX, focusY)
145+
constrainMatrix()
146+
imageMatrix = matrix
147+
if (t < 1f) postOnAnimation(this)
148+
}
149+
}
150+
postOnAnimation(scrollAction!!)
151+
}
152+
153+
private fun centerImage() {
154+
val d = drawable ?: return
155+
val viewWidth = width.toFloat()
156+
val viewHeight = height.toFloat()
157+
val imgWidth = d.intrinsicWidth.toFloat()
158+
val imgHeight = d.intrinsicHeight.toFloat()
159+
160+
val scale = minOf(viewWidth / imgWidth, viewHeight / imgHeight, 1f)
161+
currentScale = scale
162+
minScale = scale
163+
164+
val dx = (viewWidth - imgWidth * scale) / 2f
165+
val dy = (viewHeight - imgHeight * scale) / 2f
166+
matrix.setScale(scale, scale)
167+
matrix.postTranslate(dx, dy)
168+
}
169+
170+
private fun constrainMatrix() {
171+
val d = drawable ?: return
172+
val values = FloatArray(9)
173+
matrix.getValues(values)
174+
val scaleX = values[Matrix.MSCALE_X]
175+
val imgWidth = d.intrinsicWidth * scaleX
176+
val imgHeight = d.intrinsicHeight * scaleX
177+
178+
var transX = values[Matrix.MTRANS_X]
179+
var transY = values[Matrix.MTRANS_Y]
180+
181+
val viewWidth = width.toFloat()
182+
val viewHeight = height.toFloat()
183+
184+
if (imgWidth <= viewWidth) {
185+
transX = (viewWidth - imgWidth) / 2f
186+
} else {
187+
transX = transX.coerceIn(viewWidth - imgWidth, 0f)
188+
}
189+
190+
if (imgHeight <= viewHeight) {
191+
transY = (viewHeight - imgHeight) / 2f
192+
} else {
193+
transY = transY.coerceIn(viewHeight - imgHeight, 0f)
194+
}
195+
196+
matrix.postTranslate(transX - values[Matrix.MTRANS_X], transY - values[Matrix.MTRANS_Y])
197+
}
198+
199+
private fun spacing(event: MotionEvent): Float {
200+
val x = event.getX(0) - event.getX(1)
201+
val y = event.getY(0) - event.getY(1)
202+
return Math.sqrt((x * x + y * y).toDouble()).toFloat()
203+
}
204+
205+
private fun mid(event: MotionEvent): PointF {
206+
val x = event.getX(0) + event.getX(1)
207+
val y = event.getY(0) + event.getY(1)
208+
return PointF(x / 2f, y / 2f)
209+
}
210+
211+
private fun stopScroll() {
212+
scrollAction?.let { removeCallbacks(it) }
213+
scroller.forceFinished(true)
214+
}
215+
216+
companion object {
217+
private const val NONE = 0
218+
private const val DRAG = 1
219+
private const val ZOOM = 2
220+
}
221+
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
2+
<com.degard.imagecompressor.ZoomableImageView xmlns:android="http://schemas.android.com/apk/res/android"
33
android:id="@+id/ivFull"
44
android:layout_width="match_parent"
55
android:layout_height="match_parent"
6-
android:scaleType="fitCenter"
76
android:background="#FF000000" />

0 commit comments

Comments
 (0)