Skip to content

Commit 11c5495

Browse files
update multiple pointer inside DynamicCropState
Touching with multiple pointers inside overlay when DynamicCropState is selected zooms in/out
1 parent 4de82b9 commit 11c5495

File tree

6 files changed

+85
-64
lines changed

6 files changed

+85
-64
lines changed

cropper/src/main/java/com/smarttoolfactory/cropper/CropModifier.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import com.smarttoolfactory.cropper.state.cropData
1515
import com.smarttoolfactory.cropper.util.ZoomLevel
1616
import com.smarttoolfactory.cropper.util.getNextZoomLevel
1717
import com.smarttoolfactory.cropper.util.update
18-
import com.smarttoolfactory.gesture.detectMotionEvents
18+
import com.smarttoolfactory.gesture.detectMotionEventsAsList
1919
import com.smarttoolfactory.gesture.detectTransformGestures
2020
import kotlinx.coroutines.launch
2121

@@ -100,7 +100,7 @@ fun Modifier.crop(
100100
}
101101

102102
val touchModifier = Modifier.pointerInput(*keys) {
103-
detectMotionEvents(
103+
detectMotionEventsAsList(
104104
onDown = {
105105
coroutineScope.launch {
106106
cropState.onDown(it)

cropper/src/main/java/com/smarttoolfactory/cropper/Cropper.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ fun ImageCropper(
128128
if (crop) {
129129
val croppedBitmap = Bitmap.createBitmap(
130130
scaledImageBitmap.asAndroidBitmap(),
131-
rectCrop.left,
132-
rectCrop.top,
133-
rectCrop.width,
134-
rectCrop.height
131+
rectCrop.left.toInt(),
132+
rectCrop.top.toInt(),
133+
rectCrop.width.toInt(),
134+
rectCrop.height.toInt()
135135
).asImageBitmap()
136136

137137
onCropSuccess(croppedBitmap)

cropper/src/main/java/com/smarttoolfactory/cropper/model/CropData.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.smarttoolfactory.cropper.model
33
import androidx.compose.runtime.Immutable
44
import androidx.compose.ui.geometry.Offset
55
import androidx.compose.ui.geometry.Rect
6-
import androidx.compose.ui.unit.IntRect
76

87

98
/**
@@ -18,5 +17,5 @@ data class CropData(
1817
val pan: Offset = Offset.Zero,
1918
val rotation: Float = 0f,
2019
val overlayRect: Rect,
21-
val cropRect: IntRect
20+
val cropRect: Rect
2221
)

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

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import androidx.compose.ui.geometry.Offset
77
import androidx.compose.ui.geometry.Rect
88
import androidx.compose.ui.geometry.Size
99
import androidx.compose.ui.input.pointer.PointerInputChange
10-
import androidx.compose.ui.unit.IntOffset
11-
import androidx.compose.ui.unit.IntRect
1210
import androidx.compose.ui.unit.IntSize
1311
import com.smarttoolfactory.cropper.model.AspectRatio
1412
import com.smarttoolfactory.cropper.model.CropData
@@ -79,26 +77,25 @@ abstract class CropState internal constructor(
7977
val overlayRect: Rect
8078
get() = animatableRectOverlay.value
8179

82-
var cropRect: IntRect = IntRect.Zero
80+
var cropRect: Rect = Rect.Zero
8381
get() = getCropRectangle(
8482
imageSize.width,
8583
imageSize.height,
8684
drawAreaRect,
87-
overlayRect
85+
animatableRectOverlay.targetValue
8886
)
8987
private set
9088

9189

9290
/**
9391
* Update properties of [CropState] and animate to valid intervals if required
9492
*/
95-
suspend fun updateProperties(cropProperties: CropProperties) {
93+
internal open suspend fun updateProperties(cropProperties: CropProperties) {
9694
fling = cropProperties.fling
9795
pannable = cropProperties.pannable
9896
zoomable = cropProperties.zoomable
9997
rotatable = cropProperties.rotatable
10098

101-
// TODO Fix zoom reset
10299
val maxZoom = cropProperties.maxZoom
103100

104101
// Update overlay rectangle
@@ -125,16 +122,16 @@ abstract class CropState internal constructor(
125122
}
126123

127124
// Update image draw area
128-
updateImageDrawRectFromTransformation()
125+
drawAreaRect = updateImageDrawRectFromTransformation()
129126
}
130127

131128
/**
132129
* Animate overlay rectangle to target value
133130
*/
134-
suspend fun animateOverlayRectTo(rect: Rect) {
131+
internal suspend fun animateOverlayRectTo(rect: Rect) {
135132
animatableRectOverlay.animateTo(
136133
targetValue = rect,
137-
animationSpec = tween(500)
134+
animationSpec = tween(400)
138135
)
139136
}
140137

@@ -148,16 +145,16 @@ abstract class CropState internal constructor(
148145
/*
149146
Touch gestures
150147
*/
151-
abstract suspend fun onDown(change: PointerInputChange)
148+
internal abstract suspend fun onDown(change: PointerInputChange)
152149

153-
abstract suspend fun onMove(change: PointerInputChange)
150+
internal abstract suspend fun onMove(changes: List<PointerInputChange>)
154151

155-
abstract suspend fun onUp(change: PointerInputChange)
152+
internal abstract suspend fun onUp(change: PointerInputChange)
156153

157154
/*
158155
Transform gestures
159156
*/
160-
abstract suspend fun onGesture(
157+
internal abstract suspend fun onGesture(
161158
centroid: Offset,
162159
panChange: Offset,
163160
zoomChange: Float,
@@ -166,12 +163,12 @@ abstract class CropState internal constructor(
166163
changes: List<PointerInputChange>
167164
)
168165

169-
abstract suspend fun onGestureStart()
166+
internal abstract suspend fun onGestureStart()
170167

171-
abstract suspend fun onGestureEnd(onBoundsCalculated: () -> Unit)
168+
internal abstract suspend fun onGestureEnd(onBoundsCalculated: () -> Unit)
172169

173170
// Double Tap
174-
abstract suspend fun onDoubleTap(
171+
internal abstract suspend fun onDoubleTap(
175172
pan: Offset = Offset.Zero,
176173
zoom: Float = 1f,
177174
rotation: Float = 0f,
@@ -194,7 +191,7 @@ abstract class CropState internal constructor(
194191
* changed and animated if it's out of [containerSize] bounds or its grow
195192
* bigger than previous size
196193
*/
197-
internal fun updateImageDrawRectFromTransformation() {
194+
internal fun updateImageDrawRectFromTransformation(): Rect {
198195
val containerWidth = containerSize.width
199196
val containerHeight = containerSize.height
200197

@@ -212,7 +209,7 @@ abstract class CropState internal constructor(
212209
val newWidth = originalDrawWidth * zoom
213210
val newHeight = originalDrawHeight * zoom
214211

215-
drawAreaRect = Rect(
212+
return Rect(
216213
offset = Offset(
217214
left - (newWidth - originalDrawWidth) / 2 + panX,
218215
top - (newHeight - originalDrawHeight) / 2 + panY,
@@ -355,7 +352,7 @@ abstract class CropState internal constructor(
355352
bitmapHeight: Int,
356353
drawAreaRect: Rect,
357354
overlayRect: Rect
358-
): IntRect {
355+
): Rect {
359356

360357
// Calculate latest image draw area based on overlay position
361358
// This is valid rectangle that contains crop area inside overlay
@@ -373,18 +370,15 @@ abstract class CropState internal constructor(
373370
val diffLeft = overlayRect.left - newRect.left
374371
val diffTop = overlayRect.top - newRect.top
375372

376-
val croppedBitmapLeft = (diffLeft * (bitmapWidth / drawAreaWidth)).toInt()
377-
val croppedBitmapTop = (diffTop * (bitmapHeight / drawAreaHeight)).toInt()
373+
val croppedBitmapLeft = (diffLeft * (bitmapWidth / drawAreaWidth))
374+
val croppedBitmapTop = (diffTop * (bitmapHeight / drawAreaHeight))
378375

379-
val croppedBitmapWidth = (bitmapWidth * widthRatio).toInt()
380-
.coerceAtMost(bitmapWidth - croppedBitmapLeft)
381-
val croppedBitmapHeight =
382-
(bitmapHeight * heightRatio).toInt()
383-
.coerceAtMost(bitmapHeight - croppedBitmapTop)
376+
val croppedBitmapWidth = bitmapWidth * widthRatio
377+
val croppedBitmapHeight = bitmapHeight * heightRatio
384378

385-
return IntRect(
386-
offset = IntOffset(croppedBitmapLeft, croppedBitmapTop),
387-
size = IntSize(croppedBitmapWidth, croppedBitmapHeight)
379+
return Rect(
380+
offset = Offset(croppedBitmapLeft, croppedBitmapTop),
381+
size = Size(croppedBitmapWidth, croppedBitmapHeight)
388382
)
389383
}
390384
}

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

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ class DynamicCropState internal constructor(
5555
limitPan = limitPan
5656
) {
5757

58-
// Rectangle that covers Image composable
58+
/**
59+
* Rectangle that covers Image composable
60+
*/
61+
5962
private val rectBounds = Rect(
6063
offset = Offset.Zero,
6164
size = Size(containerSize.width.toFloat(), containerSize.height.toFloat())
@@ -76,9 +79,14 @@ class DynamicCropState internal constructor(
7679

7780
private var doubleTapped = false
7881

82+
// Check if transform gesture has been invoked
83+
// inside overlay but with multiple pointers to zoom
84+
private var gestureInvoked = false
85+
7986
override suspend fun onDown(change: PointerInputChange) {
8087

8188
rectTemp = overlayRect.copy()
89+
8290
val position = change.position
8391
val touchPositionScreenX = position.x
8492
val touchPositionScreenY = position.y
@@ -99,21 +107,35 @@ class DynamicCropState internal constructor(
99107
getDistanceToEdgeFromTouch(touchRegion, rectTemp, touchPositionOnScreen)
100108
}
101109

102-
override suspend fun onMove(change: PointerInputChange) {
103-
104-
// update overlay rectangle based on where its touched and touch position to corners
105-
// This function moves and/or scales overlay rectangle
106-
val newRect = updateOverlayRect(
107-
distanceToEdgeFromTouch = distanceToEdgeFromTouch,
108-
touchRegion = touchRegion,
109-
minDimension = minOverlaySize,
110-
rectTemp = rectTemp,
111-
overlayRect = overlayRect,
112-
change = change
113-
)
110+
override suspend fun onMove(changes: List<PointerInputChange>) {
111+
112+
if (changes.isEmpty()) {
113+
touchRegion = TouchRegion.None
114+
return
115+
}
116+
117+
gestureInvoked = changes.size > 1 && (touchRegion == TouchRegion.Inside)
114118

115-
snapOverlayRectTo(newRect)
119+
// If overlay is touched and pointer size is one update
120+
// or pointer size is bigger than one but touched any handles update
121+
if (changes.size == 1 && !gestureInvoked) {
122+
123+
val change = changes.first()
124+
125+
// update overlay rectangle based on where its touched and touch position to corners
126+
// This function moves and/or scales overlay rectangle
127+
val newRect = updateOverlayRect(
128+
distanceToEdgeFromTouch = distanceToEdgeFromTouch,
129+
touchRegion = touchRegion,
130+
minDimension = minOverlaySize,
131+
rectTemp = rectTemp,
132+
overlayRect = overlayRect,
133+
change = change
134+
)
135+
136+
snapOverlayRectTo(newRect)
116137
// moveOverlayToBounds(change = change, newRect = newRect)
138+
}
117139
}
118140

119141
override suspend fun onUp(change: PointerInputChange) = coroutineScope {
@@ -126,9 +148,11 @@ class DynamicCropState internal constructor(
126148
animateTransformationToOverlayBounds()
127149

128150
// Update image draw area after animating pan, zoom or rotation is completed
129-
updateImageDrawRectFromTransformation()
151+
drawAreaRect = updateImageDrawRectFromTransformation()
130152
touchRegion = TouchRegion.None
131153
}
154+
155+
gestureInvoked = false
132156
}
133157

134158
override suspend fun onGesture(
@@ -139,21 +163,24 @@ class DynamicCropState internal constructor(
139163
mainPointer: PointerInputChange,
140164
changes: List<PointerInputChange>
141165
) {
142-
if (touchRegion == TouchRegion.None) {
166+
167+
if (touchRegion == TouchRegion.None || gestureInvoked) {
143168
doubleTapped = false
144169

170+
val newPan = if (gestureInvoked) Offset.Zero else panChange
171+
145172
updateTransformState(
146173
centroid = centroid,
147174
zoomChange = zoomChange,
148-
panChange = panChange,
175+
panChange = newPan,
149176
rotationChange = rotationChange
150177
)
151178

152179
// Update image draw rectangle based on pan, zoom or rotation change
153-
updateImageDrawRectFromTransformation()
180+
drawAreaRect = updateImageDrawRectFromTransformation()
154181

155182
// Fling Gesture
156-
if (fling) {
183+
if (pannable && fling) {
157184
if (changes.size == 1) {
158185
addPosition(mainPointer.uptimeMillis, mainPointer.position)
159186
}
@@ -165,17 +192,17 @@ class DynamicCropState internal constructor(
165192

166193
override suspend fun onGestureEnd(onBoundsCalculated: () -> Unit) {
167194

168-
if (touchRegion == TouchRegion.None) {
195+
if (touchRegion == TouchRegion.None || gestureInvoked) {
169196

170197
// Gesture end might be called after second tap and we don't want to fling
171198
// or animate back to valid bounds when doubled tapped
172199
if (!doubleTapped) {
173200

174-
if (fling && zoom > 1) {
201+
if (pannable && fling && !gestureInvoked && zoom > 1) {
175202
fling {
176203
// We get target value on start instead of updating bounds after
177204
// gesture has finished
178-
updateImageDrawRectFromTransformation()
205+
drawAreaRect = updateImageDrawRectFromTransformation()
179206
onBoundsCalculated()
180207
}
181208
} else {
@@ -203,6 +230,7 @@ class DynamicCropState internal constructor(
203230
animateOverlayRectTo(
204231
calculateOverlayRectInBounds(rectBounds, overlayRect)
205232
)
233+
animateTransformationToOverlayBounds()
206234
onAnimationEnd()
207235
}
208236

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class StaticCropState internal constructor(
4747
) {
4848

4949
override suspend fun onDown(change: PointerInputChange) = Unit
50-
override suspend fun onMove(change: PointerInputChange) = Unit
50+
override suspend fun onMove(changes: List<PointerInputChange>) = Unit
5151
override suspend fun onUp(change: PointerInputChange) = Unit
5252

5353
private var doubleTapped = false
@@ -73,10 +73,10 @@ class StaticCropState internal constructor(
7373
)
7474

7575
// Update image draw rectangle based on pan, zoom or rotation change
76-
updateImageDrawRectFromTransformation()
76+
drawAreaRect = updateImageDrawRectFromTransformation()
7777

7878
// Fling Gesture
79-
if (fling) {
79+
if (pannable && fling) {
8080
if (changes.size == 1) {
8181
addPosition(mainPointer.uptimeMillis, mainPointer.position)
8282
}
@@ -91,11 +91,11 @@ class StaticCropState internal constructor(
9191
// or animate back to valid bounds when doubled tapped
9292
if (!doubleTapped) {
9393

94-
if (fling && zoom > 1) {
94+
if (pannable && fling && zoom > 1) {
9595
fling {
9696
// We get target value on start instead of updating bounds after
9797
// gesture has finished
98-
updateImageDrawRectFromTransformation()
98+
drawAreaRect = updateImageDrawRectFromTransformation()
9999
onBoundsCalculated()
100100
}
101101
} else {

0 commit comments

Comments
 (0)