Skip to content

Commit e115120

Browse files
update down, move, up gestures for DynamicCropState
1 parent bdd2aaf commit e115120

File tree

1 file changed

+228
-63
lines changed

1 file changed

+228
-63
lines changed

cropper/src/main/java/com/smarttoolfactory/cropper/state/DynamicCropState.kt

Lines changed: 228 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
88
import androidx.compose.ui.unit.IntSize
99
import com.smarttoolfactory.cropper.TouchRegion
1010
import com.smarttoolfactory.cropper.model.AspectRatio
11-
import com.smarttoolfactory.cropper.util.getDistanceToEdgeFromTouch
12-
import com.smarttoolfactory.cropper.util.getTouchRegion
13-
import com.smarttoolfactory.cropper.util.updateOverlayRect
1411
import kotlinx.coroutines.coroutineScope
1512

1613
/**
@@ -33,24 +30,25 @@ import kotlinx.coroutines.coroutineScope
3330
* empty space on sides or edges of parent
3431
*/
3532
class DynamicCropState internal constructor(
36-
private val handleSize: Float,
37-
private val minOverlaySize: Float,
33+
private var handleSize: Float,
34+
private var minOverlaySize: Float,
3835
imageSize: IntSize,
3936
containerSize: IntSize,
37+
drawAreaSize: IntSize,
4038
aspectRatio: AspectRatio,
41-
maxZoom: Float = 5f,
42-
fling: Boolean = false,
43-
zoomable: Boolean = true,
44-
pannable: Boolean = true,
45-
rotatable: Boolean = false,
46-
limitPan: Boolean = false
39+
maxZoom: Float,
40+
fling: Boolean,
41+
zoomable: Boolean,
42+
pannable: Boolean,
43+
rotatable: Boolean,
44+
limitPan: Boolean
4745
) : CropState(
4846
imageSize = imageSize,
4947
containerSize = containerSize,
48+
drawAreaSize = drawAreaSize,
5049
aspectRatio = aspectRatio,
5150
maxZoom = maxZoom,
5251
fling = fling,
53-
moveToBounds = true,
5452
zoomable = zoomable,
5553
pannable = pannable,
5654
rotatable = rotatable,
@@ -114,88 +112,107 @@ class DynamicCropState internal constructor(
114112
change = change
115113
)
116114

117-
val bounds = getBounds()
118-
val positionChange = change.positionChangeIgnoreConsumed()
119-
120-
// When zoom is bigger than 100% and dynamic overlay is not at any edge of
121-
// image we can pan in the same direction motion goes towards when touch region
122-
// of rectangle is not one of the handles but region inside
123-
val isPanRequired = touchRegion == TouchRegion.Inside && zoom > 1f
124-
125-
// Overlay moving right
126-
if (isPanRequired && -pan.x < bounds.x && newRect.right >= containerSize.width) {
127-
snapOverlayRectTo(newRect.translate(-positionChange.x, 0f))
128-
snapPanXto(pan.x - positionChange.x * zoom)
129-
// Overlay moving left
130-
} else if (isPanRequired && pan.x < bounds.x && newRect.left <= 0f) {
131-
snapOverlayRectTo(newRect.translate(-positionChange.x, 0f))
132-
snapPanXto(pan.x - positionChange.x * zoom)
133-
} else if (isPanRequired && pan.y < bounds.y && newRect.top <= 0f) {
134-
// Overlay moving top
135-
snapOverlayRectTo(newRect.translate(0f, -positionChange.y))
136-
snapPanYto(pan.y - positionChange.y * zoom)
137-
} else if (isPanRequired && -pan.y < bounds.y && newRect.bottom >= containerSize.height) {
138-
// Overlay moving bottom
139-
snapOverlayRectTo(newRect.translate(0f, -positionChange.y))
140-
snapPanYto(pan.y - positionChange.y * zoom)
141-
} else {
142-
snapOverlayRectTo(newRect)
143-
}
144-
if (touchRegion != TouchRegion.None) {
145-
change.consume()
146-
}
115+
snapOverlayRectTo(newRect)
147116
}
148117

149118
override suspend fun onUp(change: PointerInputChange) = coroutineScope {
150119
touchRegion = TouchRegion.None
120+
121+
// Update overlay if it's out of Container bounds
151122
rectTemp = moveOverlayRectToBounds(rectBounds, overlayRect)
152123
animateOverlayRectTo(rectTemp)
124+
125+
// Update and animate pan, zoom and image draw area after overlay position is updated
126+
animateTransformationToOverlayBounds()
127+
128+
// Update image draw area after animating pan, zoom or rotation is completed
129+
updateImageDrawRectFromTransformation()
153130
}
154131

132+
// TODO Write gestures for dynamic crop state
155133
override suspend fun onGesture(
156134
centroid: Offset,
157-
pan: Offset,
158-
zoom: Float,
159-
rotation: Float,
135+
panChange: Offset,
136+
zoomChange: Float,
137+
rotationChange: Float,
160138
mainPointer: PointerInputChange,
161139
changes: List<PointerInputChange>
162140
) {
163-
if (touchRegion == TouchRegion.None) {
164-
updateTransformState(
165-
centroid = centroid,
166-
zoomChange = zoom,
167-
panChange = pan,
168-
rotationChange = 0f
169-
)
170-
}
141+
// if (touchRegion == TouchRegion.None) {
142+
// updateTransformState(
143+
// centroid = centroid,
144+
// zoomChange = zoomChange,
145+
// panChange = panChange,
146+
// rotationChange = 0f
147+
// )
148+
// }
171149
}
172150

151+
// TODO This function changes translation of image are when zoom is bigger than 1f
152+
// private fun moveOverlayToBounds() {
153+
// val bounds = getBounds()
154+
// val positionChange = change.positionChangeIgnoreConsumed()
155+
//
156+
// // When zoom is bigger than 100% and dynamic overlay is not at any edge of
157+
// // image we can pan in the same direction motion goes towards when touch region
158+
// // of rectangle is not one of the handles but region inside
159+
// val isPanRequired = touchRegion == TouchRegion.Inside && zoom > 1f
160+
//
161+
// // Overlay moving right
162+
// if (isPanRequired && -pan.x < bounds.x && newRect.right >= containerSize.width) {
163+
// snapOverlayRectTo(newRect.translate(-positionChange.x, 0f))
164+
// snapPanXto(pan.x - positionChange.x * zoom)
165+
// // Overlay moving left
166+
// } else if (isPanRequired && pan.x < bounds.x && newRect.left <= 0f) {
167+
// snapOverlayRectTo(newRect.translate(-positionChange.x, 0f))
168+
// snapPanXto(pan.x - positionChange.x * zoom)
169+
// } else if (isPanRequired && pan.y < bounds.y && newRect.top <= 0f) {
170+
// // Overlay moving top
171+
// snapOverlayRectTo(newRect.translate(0f, -positionChange.y))
172+
// snapPanYto(pan.y - positionChange.y * zoom)
173+
// } else if (isPanRequired && -pan.y < bounds.y && newRect.bottom >= containerSize.height) {
174+
// // Overlay moving bottom
175+
// snapOverlayRectTo(newRect.translate(0f, -positionChange.y))
176+
// snapPanYto(pan.y - positionChange.y * zoom)
177+
// } else {
178+
// snapOverlayRectTo(newRect)
179+
// }
180+
// if (touchRegion != TouchRegion.None) {
181+
// change.consume()
182+
// }
183+
// }
184+
173185
override suspend fun onGestureStart() = Unit
174186
override suspend fun onGestureEnd(onBoundsCalculated: () -> Unit) = Unit
175187

188+
// TODO Write double tap for dynamic crop state
176189
override suspend fun onDoubleTap(
177190
pan: Offset,
178191
zoom: Float,
179192
rotation: Float,
180193
onAnimationEnd: () -> Unit
181194
) {
182-
doubleTapped = true
183-
184-
if (fling) {
185-
resetTracking()
186-
}
187-
resetWithAnimation(pan = pan, zoom = zoom, rotation = rotation)
188-
onAnimationEnd()
195+
// doubleTapped = true
196+
//
197+
// if (fling) {
198+
// resetTracking()
199+
// }
200+
// resetWithAnimation(pan = pan, zoom = zoom, rotation = rotation)
201+
// onAnimationEnd()
189202
}
190203

191204
/**
192-
* When pointer is up calculate valid position and size overlay can be updated to
205+
* When pointer is up calculate valid position and size overlay can be updated to inside
206+
* a virtual rect between `topLeft = (0,0)` to `bottomRight=(containerWidth, containerHeight)`
207+
*
208+
* [overlayRect] might be shrunk or moved up/down/left/right to container bounds when
209+
* it's out of Composable region
193210
*/
194211
private fun moveOverlayRectToBounds(rectBounds: Rect, rectCurrent: Rect): Rect {
212+
195213
var width = rectCurrent.width
196214
var height = rectCurrent.height
197215

198-
199216
if (width > rectBounds.width) {
200217
width = rectBounds.width
201218
}
@@ -224,4 +241,152 @@ class DynamicCropState internal constructor(
224241

225242
return rect
226243
}
227-
}
244+
245+
/**
246+
* Update overlay rectangle based on touch gesture
247+
*/
248+
private fun updateOverlayRect(
249+
distanceToEdgeFromTouch: Offset,
250+
touchRegion: TouchRegion,
251+
minDimension: Float,
252+
rectTemp: Rect,
253+
overlayRect: Rect,
254+
change: PointerInputChange
255+
): Rect {
256+
257+
val position = change.position
258+
// Get screen coordinates from touch position inside composable
259+
// and add how far it's from corner to not jump edge to user's touch position
260+
val screenPositionX = position.x + distanceToEdgeFromTouch.x
261+
val screenPositionY = position.y + distanceToEdgeFromTouch.y
262+
263+
return when (touchRegion) {
264+
265+
// Corners
266+
TouchRegion.TopLeft -> {
267+
268+
// Set position of top left while moving with top left handle and
269+
// limit position to not intersect other handles
270+
val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension)
271+
val top = screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
272+
Rect(
273+
left = left,
274+
top = top,
275+
right = rectTemp.right,
276+
bottom = rectTemp.bottom
277+
)
278+
}
279+
280+
TouchRegion.BottomLeft -> {
281+
282+
// Set position of top left while moving with bottom left handle and
283+
// limit position to not intersect other handles
284+
val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension)
285+
val bottom = screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
286+
Rect(
287+
left = left,
288+
top = rectTemp.top,
289+
right = rectTemp.right,
290+
bottom = bottom,
291+
)
292+
}
293+
294+
TouchRegion.TopRight -> {
295+
296+
// Set position of top left while moving with top right handle and
297+
// limit position to not intersect other handles
298+
val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension)
299+
val top = screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
300+
301+
Rect(
302+
left = rectTemp.left,
303+
top = top,
304+
right = right,
305+
bottom = rectTemp.bottom,
306+
)
307+
308+
}
309+
310+
TouchRegion.BottomRight -> {
311+
312+
// Set position of top left while moving with bottom right handle and
313+
// limit position to not intersect other handles
314+
val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension)
315+
val bottom = screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
316+
317+
Rect(
318+
left = rectTemp.left,
319+
top = rectTemp.top,
320+
right = right,
321+
bottom = bottom
322+
)
323+
}
324+
325+
TouchRegion.Inside -> {
326+
val drag = change.positionChangeIgnoreConsumed()
327+
val scaledDragX = drag.x
328+
val scaledDragY = drag.y
329+
overlayRect.translate(scaledDragX, scaledDragY)
330+
}
331+
332+
else -> overlayRect
333+
}
334+
}
335+
336+
/**
337+
* get touch region inside this rectangle based on touch position.
338+
*/
339+
private fun getTouchRegion(
340+
position: Offset,
341+
rect: Rect,
342+
threshold: Float
343+
): TouchRegion {
344+
345+
return when {
346+
347+
position.x - rect.left in 0.0f..threshold &&
348+
position.y - rect.top in 0.0f..threshold -> TouchRegion.TopLeft
349+
350+
rect.right - position.x in 0f..threshold &&
351+
position.y - rect.top in 0.0f..threshold -> TouchRegion.TopRight
352+
353+
rect.right - position.x in 0f..threshold &&
354+
rect.bottom - position.y in 0.0f..threshold -> TouchRegion.BottomRight
355+
356+
position.x - rect.left in 0.0f..threshold &&
357+
rect.bottom - position.y in 0.0f..threshold -> TouchRegion.BottomLeft
358+
359+
360+
rect.contains(offset = position) -> TouchRegion.Inside
361+
else -> TouchRegion.None
362+
}
363+
}
364+
365+
/**
366+
* Returns how far user touched to corner or center of sides of the screen. [TouchRegion]
367+
* where user exactly has touched is already passed to this function. For instance user
368+
* touched top left then this function returns distance to top left from user's position so
369+
* we can add an offset to not jump edge to position user touched.
370+
*/
371+
private fun getDistanceToEdgeFromTouch(
372+
touchRegion: TouchRegion,
373+
rect: Rect,
374+
touchPosition: Offset
375+
) = when (touchRegion) {
376+
TouchRegion.TopLeft -> {
377+
rect.topLeft - touchPosition
378+
}
379+
TouchRegion.TopRight -> {
380+
rect.topRight - touchPosition
381+
}
382+
TouchRegion.BottomLeft -> {
383+
rect.bottomLeft - touchPosition
384+
}
385+
TouchRegion.BottomRight -> {
386+
rect.bottomRight - touchPosition
387+
}
388+
else -> {
389+
Offset.Zero
390+
}
391+
}
392+
}

0 commit comments

Comments
 (0)