@@ -8,9 +8,6 @@ import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
88import androidx.compose.ui.unit.IntSize
99import com.smarttoolfactory.cropper.TouchRegion
1010import 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
1411import kotlinx.coroutines.coroutineScope
1512
1613/* *
@@ -33,24 +30,25 @@ import kotlinx.coroutines.coroutineScope
3330 * empty space on sides or edges of parent
3431 */
3532class 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