Skip to content

Commit b353e51

Browse files
add EnhancedZoomState and EnhancedZoomModifier
1 parent 18ae618 commit b353e51

File tree

2 files changed

+580
-0
lines changed

2 files changed

+580
-0
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
package com.smarttoolfactory.image.zoom
2+
3+
import androidx.compose.foundation.gestures.detectTapGestures
4+
import androidx.compose.runtime.rememberCoroutineScope
5+
import androidx.compose.ui.Modifier
6+
import androidx.compose.ui.composed
7+
import androidx.compose.ui.geometry.Offset
8+
import androidx.compose.ui.geometry.Rect
9+
import androidx.compose.ui.geometry.Size
10+
import androidx.compose.ui.graphics.Color
11+
import androidx.compose.ui.graphics.drawscope.DrawScope
12+
import androidx.compose.ui.graphics.drawscope.Stroke
13+
import androidx.compose.ui.graphics.graphicsLayer
14+
import androidx.compose.ui.input.pointer.PointerInputChange
15+
import androidx.compose.ui.input.pointer.pointerInput
16+
import androidx.compose.ui.input.pointer.positionChange
17+
import androidx.compose.ui.unit.dp
18+
import com.smarttoolfactory.gesture.detectTransformGestures
19+
import com.smarttoolfactory.image.transform.TouchRegion
20+
import com.smarttoolfactory.image.util.update
21+
import kotlinx.coroutines.launch
22+
23+
fun Modifier.enhancedZoom(
24+
vararg keys: Any?,
25+
touchRegionSize: Float,
26+
minDimension: Float,
27+
enhancedZoomState: EnhancedZoomState,
28+
onDown: (EnhancedZoomData) -> Unit,
29+
onMove: (EnhancedZoomData) -> Unit,
30+
onUp: (EnhancedZoomData) -> Unit,
31+
) = composed(
32+
33+
factory = {
34+
35+
val coroutineScope = rememberCoroutineScope()
36+
37+
val transformModifier = Modifier.pointerInput(keys) {
38+
39+
detectTransformGestures(
40+
consume = true,
41+
onGestureStart = {
42+
onDown(enhancedZoomState.enhancedZoomData)
43+
},
44+
onGestureEnd = {
45+
46+
// Reset to bounds gesture
47+
val zoom = enhancedZoomState.zoom.coerceAtLeast(1f)
48+
val pan = enhancedZoomState.pan
49+
50+
val bounds = enhancedZoomState.getBounds(size)
51+
52+
// val newOffset = Offset(
53+
// pan.x.coerceIn(-bounds.x, bounds.x),
54+
// pan.y.coerceIn(-bounds.y, bounds.y)
55+
// )
56+
//
57+
// coroutineScope.run {
58+
// enhancedZoomState.resetTracking()
59+
// launch { enhancedZoomState.animateZoomTo(zoom) }
60+
// launch { enhancedZoomState.animatePanTo(newOffset) }
61+
// }
62+
63+
// Fling Gesture
64+
coroutineScope.launch {
65+
enhancedZoomState.onGestureEnd()
66+
}
67+
68+
onUp(enhancedZoomState.enhancedZoomData)
69+
},
70+
onGesture = { _, gesturePan, gestureZoom, gestureRotate, mainPointer, pointerList ->
71+
72+
coroutineScope.launch {
73+
74+
enhancedZoomState.updateZoomState(
75+
size = size,
76+
gestureZoom = gestureZoom,
77+
gesturePan = gesturePan,
78+
gestureRotate = gestureRotate
79+
)
80+
81+
// Fling Gesture
82+
if (pointerList.size == 1) {
83+
84+
// val bounds = enhancedZoomState.getBounds(size)
85+
//
86+
// enhancedZoomState.boundPan(
87+
// lowerBound = Offset(-bounds.x, -bounds.y),
88+
// upperBound = Offset(bounds.x, bounds.y)
89+
// )
90+
91+
enhancedZoomState.addPosition(
92+
mainPointer.uptimeMillis,
93+
mainPointer.position
94+
)
95+
}
96+
97+
onMove(enhancedZoomState.enhancedZoomData)
98+
}
99+
}
100+
)
101+
}
102+
103+
val tapModifier = Modifier.pointerInput(keys) {
104+
detectTapGestures(
105+
onDoubleTap = {
106+
coroutineScope.launch {
107+
enhancedZoomState.onDoubleTap()
108+
}
109+
}
110+
)
111+
}
112+
113+
val graphicsModifier = Modifier.graphicsLayer {
114+
this.update(enhancedZoomState)
115+
}
116+
117+
this.then(
118+
Modifier
119+
.then(tapModifier)
120+
.then(transformModifier)
121+
.then(graphicsModifier)
122+
)
123+
},
124+
inspectorInfo = {
125+
name = "enhancedZoom"
126+
// add name and value of each argument
127+
properties["touchRegionRadius"] = touchRegionSize
128+
properties["minDimension"] = minDimension
129+
properties["onDown"] = onDown
130+
properties["onMove"] = onMove
131+
properties["onUp"] = onUp
132+
}
133+
)
134+
135+
internal fun moveIntoBounds(rectBounds: Rect, rectCurrent: Rect): Rect {
136+
var width = rectCurrent.width
137+
var height = rectCurrent.height
138+
139+
140+
if (width > rectBounds.width) {
141+
width = rectBounds.width
142+
}
143+
144+
if (height > rectBounds.height) {
145+
height = rectBounds.height
146+
}
147+
148+
var rect = Rect(offset = rectCurrent.topLeft, size = Size(width, height))
149+
150+
if (rect.left < rectBounds.left) {
151+
rect = rect.translate(rectBounds.left - rect.left, 0f)
152+
}
153+
154+
if (rect.top < rectBounds.top) {
155+
rect = rect.translate(0f, rectBounds.top - rect.top)
156+
}
157+
158+
if (rect.right > rectBounds.right) {
159+
rect = rect.translate(rectBounds.right - rect.right, 0f)
160+
}
161+
162+
if (rect.bottom > rectBounds.bottom) {
163+
rect = rect.translate(0f, rectBounds.bottom - rect.bottom)
164+
}
165+
166+
return rect
167+
}
168+
169+
/**
170+
* Update draw rect based on user touch
171+
*/
172+
fun updateDrawRect(
173+
distanceToEdgeFromTouch: Offset,
174+
touchRegion: TouchRegion,
175+
minDimension: Float,
176+
rectTemp: Rect,
177+
rectDraw: Rect,
178+
change: PointerInputChange
179+
): Rect {
180+
181+
val position = change.position
182+
// Get screen coordinates from touch position inside composable
183+
// and add how far it's from corner to not jump edge to user's touch position
184+
val screenPositionX = position.x + distanceToEdgeFromTouch.x
185+
val screenPositionY = position.y + distanceToEdgeFromTouch.y
186+
187+
return when (touchRegion) {
188+
189+
// Corners
190+
TouchRegion.TopLeft -> {
191+
192+
// Set position of top left while moving with top left handle and
193+
// limit position to not intersect other handles
194+
val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension)
195+
val top = screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
196+
Rect(
197+
left = left,
198+
top = top,
199+
right = rectTemp.right,
200+
bottom = rectTemp.bottom
201+
)
202+
}
203+
204+
TouchRegion.BottomLeft -> {
205+
206+
// Set position of top left while moving with bottom left handle and
207+
// limit position to not intersect other handles
208+
val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension)
209+
val bottom = screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
210+
Rect(
211+
left = left,
212+
top = rectTemp.top,
213+
right = rectTemp.right,
214+
bottom = bottom,
215+
)
216+
217+
}
218+
219+
TouchRegion.TopRight -> {
220+
221+
// Set position of top left while moving with top right handle and
222+
// limit position to not intersect other handles
223+
val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension)
224+
val top = screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
225+
226+
Rect(
227+
left = rectTemp.left,
228+
top = top,
229+
right = right,
230+
bottom = rectTemp.bottom,
231+
)
232+
233+
}
234+
235+
TouchRegion.BottomRight -> {
236+
237+
// Set position of top left while moving with bottom right handle and
238+
// limit position to not intersect other handles
239+
val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension)
240+
val bottom = screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
241+
242+
Rect(
243+
left = rectTemp.left,
244+
top = rectTemp.top,
245+
right = right,
246+
bottom = bottom
247+
)
248+
}
249+
250+
TouchRegion.Inside -> {
251+
val drag = change.positionChange()
252+
253+
val scaledDragX = drag.x
254+
val scaledDragY = drag.y
255+
256+
rectDraw.translate(scaledDragX, scaledDragY)
257+
}
258+
259+
else -> rectDraw
260+
}
261+
}
262+
263+
264+
fun DrawScope.drawGrid(rect: Rect, color: Color = Color.White) {
265+
266+
val width = rect.width
267+
val height = rect.height
268+
val gridWidth = width / 3
269+
val gridHeight = height / 3
270+
271+
272+
drawRect(
273+
color = color,
274+
topLeft = rect.topLeft,
275+
size = rect.size,
276+
style = Stroke(width = 2.dp.toPx())
277+
)
278+
279+
// Horizontal lines
280+
for (i in 1..2) {
281+
drawLine(
282+
color = color,
283+
start = Offset(rect.left, rect.top + i * gridHeight),
284+
end = Offset(rect.right, rect.top + i * gridHeight),
285+
strokeWidth = .7.dp.toPx()
286+
)
287+
}
288+
289+
// Vertical lines
290+
for (i in 1..2) {
291+
drawLine(
292+
color,
293+
start = Offset(rect.left + i * gridWidth, rect.top),
294+
end = Offset(rect.left + i * gridWidth, rect.bottom),
295+
strokeWidth = .7.dp.toPx()
296+
)
297+
}
298+
}
299+

0 commit comments

Comments
 (0)