Skip to content

Commit db257da

Browse files
update CropState with touchRegion param
touchRegion param is used in DynamicCropState and cropper to change transparent color when handles are touched
1 parent 6aa0557 commit db257da

3 files changed

Lines changed: 180 additions & 101 deletions

File tree

Lines changed: 171 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
@file:OptIn(ExperimentalAnimationApi::class)
21

32
package com.smarttoolfactory.cropper
43

54
import androidx.compose.animation.AnimatedVisibility
65
import androidx.compose.animation.ExperimentalAnimationApi
6+
import androidx.compose.animation.animateColorAsState
77
import androidx.compose.animation.core.tween
88
import androidx.compose.animation.scaleIn
99
import androidx.compose.foundation.background
@@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.Color
1919
import androidx.compose.ui.graphics.FilterQuality
2020
import androidx.compose.ui.graphics.ImageBitmap
2121
import androidx.compose.ui.graphics.drawscope.DrawScope
22+
import androidx.compose.ui.layout.ContentScale
2223
import androidx.compose.ui.platform.LocalDensity
2324
import androidx.compose.ui.platform.LocalLayoutDirection
2425
import androidx.compose.ui.unit.Dp
@@ -33,6 +34,7 @@ import com.smarttoolfactory.cropper.settings.CropDefaults
3334
import com.smarttoolfactory.cropper.settings.CropProperties
3435
import com.smarttoolfactory.cropper.settings.CropStyle
3536
import com.smarttoolfactory.cropper.settings.CropType
37+
import com.smarttoolfactory.cropper.state.DynamicCropState
3638
import com.smarttoolfactory.cropper.state.rememberCropState
3739
import kotlinx.coroutines.Dispatchers
3840
import kotlinx.coroutines.delay
@@ -62,23 +64,21 @@ fun ImageCropper(
6264

6365
// No crop operation is applied by ScalableImage so rect points to bounds of original
6466
// bitmap
65-
val scaledImageBitmap =
66-
getScaledImageBitmap(
67-
imageWidth = imageWidth,
68-
imageHeight = imageHeight,
69-
rect = rect,
70-
bitmap = imageBitmap,
71-
contentScale = cropProperties.contentScale,
72-
)
73-
74-
val cropAgent = remember {
75-
CropAgent()
76-
}
67+
val scaledImageBitmap = getScaledImageBitmap(
68+
imageWidth = imageWidth,
69+
imageHeight = imageHeight,
70+
rect = rect,
71+
bitmap = imageBitmap,
72+
contentScale = cropProperties.contentScale,
73+
)
7774

7875
// Container Dimensions
7976
val containerWidthPx = constraints.maxWidth
8077
val containerHeightPx = constraints.maxHeight
8178

79+
val containerWidth: Dp
80+
val containerHeight: Dp
81+
8282
// Bitmap Dimensions
8383
val bitmapWidth = scaledImageBitmap.width
8484
val bitmapHeight = scaledImageBitmap.height
@@ -87,9 +87,6 @@ fun ImageCropper(
8787
val imageWidthPx: Int
8888
val imageHeightPx: Int
8989

90-
val containerWidth: Dp
91-
val containerHeight: Dp
92-
9390
with(LocalDensity.current) {
9491
imageWidthPx = imageWidth.roundToPx()
9592
imageHeightPx = imageHeight.roundToPx()
@@ -104,21 +101,8 @@ fun ImageCropper(
104101

105102
// these keys are for resetting cropper when image width/height, contentScale or
106103
// overlay aspect ratio changes
107-
val resetKeys = remember(
108-
scaledImageBitmap,
109-
imageWidthPx,
110-
imageHeightPx,
111-
contentScale,
112-
cropType
113-
) {
114-
arrayOf(
115-
scaledImageBitmap,
116-
imageWidthPx,
117-
imageHeightPx,
118-
contentScale,
119-
cropType
120-
)
121-
}
104+
val resetKeys =
105+
getResetKeys(scaledImageBitmap, imageWidthPx, imageHeightPx, contentScale, cropType)
122106

123107
val cropState = rememberCropState(
124108
imageSize = IntSize(bitmapWidth, bitmapHeight),
@@ -128,86 +112,109 @@ fun ImageCropper(
128112
keys = resetKeys
129113
)
130114

131-
LaunchedEffect(key1 = cropProperties) {
132-
cropState.updateProperties(cropProperties)
133-
}
134-
135-
/**
136-
* Rectangle that is used for cropping image, this rectangle is not the
137-
* one that draws on screen. We might have 4000x3000 rect while we
138-
* draw 1000x750px Composable on screen
139-
*/
140-
val rectCrop = cropState.cropRect
141-
142-
val density = LocalDensity.current
143-
val layoutDirection = LocalLayoutDirection.current
144-
145-
LaunchedEffect(crop) {
146-
if (crop) {
147-
flow {
148-
emit(
149-
cropAgent.crop(
150-
scaledImageBitmap,
151-
rectCrop,
152-
cropOutline,
153-
layoutDirection,
154-
density
155-
)
156-
)
157-
}
158-
.flowOn(Dispatchers.Default)
159-
.onStart {
160-
onCropStart()
161-
delay(400)
162-
}
163-
.onEach {
164-
onCropSuccess(it)
165-
}
166-
.launchIn(this)
115+
val isTouched = remember(cropState) {
116+
derivedStateOf {
117+
cropState is DynamicCropState && handlesTouched(cropState.touchRegion)
167118
}
168119
}
169120

121+
val transparentColor by animateColorAsState(
122+
targetValue = if (isTouched.value) Color(0x77000000) else Color(0xAA000000)
123+
)
124+
125+
// Crops image when user invokes crop operation
126+
Crop(
127+
crop,
128+
scaledImageBitmap,
129+
cropState.cropRect,
130+
cropOutline,
131+
onCropStart,
132+
onCropSuccess
133+
)
134+
170135
val imageModifier = Modifier
171136
.size(containerWidth, containerHeight)
172137
.crop(
173138
keys = resetKeys,
174139
cropState = cropState
175140
)
176141

177-
Box(modifier = Modifier
142+
LaunchedEffect(key1 = cropProperties) {
143+
cropState.updateProperties(cropProperties)
144+
}
145+
146+
/// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
147+
var visible by remember { mutableStateOf(false) }
148+
149+
LaunchedEffect(Unit) {
150+
delay(100)
151+
visible = true
152+
}
153+
154+
ImageCropper(
155+
modifier = imageModifier,
156+
visible = visible,
157+
imageBitmap = imageBitmap,
158+
containerWidth = containerWidth,
159+
containerHeight = containerHeight,
160+
imageWidthPx = imageWidthPx,
161+
imageHeightPx = imageHeightPx,
162+
handleSize = cropProperties.handleSize,
163+
overlayRect = cropState.overlayRect,
164+
cropType = cropType,
165+
cropOutline = cropOutline,
166+
cropStyle = cropStyle,
167+
transparentColor = transparentColor
168+
)
169+
}
170+
}
171+
172+
@OptIn(ExperimentalAnimationApi::class)
173+
@Composable
174+
private fun ImageCropper(
175+
modifier: Modifier,
176+
visible: Boolean,
177+
imageBitmap: ImageBitmap,
178+
containerWidth: Dp,
179+
containerHeight: Dp,
180+
imageWidthPx: Int,
181+
imageHeightPx: Int,
182+
handleSize: Dp,
183+
cropType: CropType,
184+
cropOutline: CropOutline,
185+
cropStyle: CropStyle,
186+
overlayRect: Rect,
187+
transparentColor: Color,
188+
) {
189+
Box(
190+
modifier = Modifier
178191
.fillMaxSize()
179192
.background(Color.Black)
180-
) {
193+
) {
181194

182-
/// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
183-
var visible by remember { mutableStateOf(false) }
195+
AnimatedVisibility(
196+
visible = visible,
197+
enter = scaleIn(tween(500))
198+
) {
184199

185-
LaunchedEffect(Unit) {
186-
delay(100)
187-
visible = true
188-
}
200+
ImageCropperImpl(
201+
modifier = modifier,
202+
imageBitmap = imageBitmap,
203+
containerWidth = containerWidth,
204+
containerHeight = containerHeight,
205+
imageWidthPx = imageWidthPx,
206+
imageHeightPx = imageHeightPx,
207+
cropType = cropType,
208+
cropOutline = cropOutline,
209+
handleSize = handleSize,
210+
cropStyle = cropStyle,
211+
rectOverlay = overlayRect,
212+
transparentColor = transparentColor
213+
)
214+
}
189215

190-
AnimatedVisibility(
191-
visible = visible,
192-
enter = scaleIn(tween(500))
193-
) {
194-
195-
ImageCropperImpl(
196-
modifier = imageModifier,
197-
imageBitmap = scaledImageBitmap,
198-
containerWidth = containerWidth,
199-
containerHeight = containerHeight,
200-
imageWidthPx = imageWidthPx,
201-
imageHeightPx = imageHeightPx,
202-
cropType = cropType,
203-
cropOutline = cropOutline,
204-
handleSize = cropProperties.handleSize,
205-
cropStyle = cropStyle,
206-
rectOverlay = cropState.overlayRect
207-
)
208-
}
209-
210-
// TODO Remove this text when cropper is complete. This is for debugging
216+
// TODO Remove this text when cropper is complete. This is for debugging
217+
// val rectCrop = cropState.cropRect
211218
// val drawAreaRect = cropState.drawAreaRect
212219
// val pan = cropState.pan
213220
// val zoom = cropState.zoom
@@ -222,7 +229,6 @@ fun ImageCropper(
222229
// "overlayRect: ${cropState.overlayRect}, size: ${cropState.overlayRect.size}\n" +
223230
// "cropRect: $rectCrop, size: ${rectCrop.size}"
224231
// )
225-
}
226232
}
227233
}
228234

@@ -238,6 +244,7 @@ private fun ImageCropperImpl(
238244
cropOutline: CropOutline,
239245
handleSize: Dp,
240246
cropStyle: CropStyle,
247+
transparentColor: Color,
241248
rectOverlay: Rect
242249
) {
243250

@@ -271,8 +278,74 @@ private fun ImageCropperImpl(
271278
handleColor = handleColor,
272279
strokeWidth = strokeWidth,
273280
drawHandles = drawHandles,
274-
handleSize = handleSizeInPx
281+
handleSize = handleSizeInPx,
282+
transparentColor = transparentColor,
275283
)
276284

277285
}
278286
}
287+
288+
@Composable
289+
private fun Crop(
290+
crop: Boolean,
291+
scaledImageBitmap: ImageBitmap,
292+
cropRect: Rect,
293+
cropOutline: CropOutline,
294+
onCropStart: () -> Unit,
295+
onCropSuccess: (ImageBitmap) -> Unit
296+
) {
297+
298+
val density = LocalDensity.current
299+
val layoutDirection = LocalLayoutDirection.current
300+
301+
// Crop Agent is responsible for cropping image
302+
val cropAgent = remember { CropAgent() }
303+
304+
LaunchedEffect(crop) {
305+
if (crop) {
306+
flow {
307+
emit(
308+
cropAgent.crop(
309+
scaledImageBitmap,
310+
cropRect,
311+
cropOutline,
312+
layoutDirection,
313+
density
314+
)
315+
)
316+
}
317+
.flowOn(Dispatchers.Default)
318+
.onStart {
319+
onCropStart()
320+
delay(400)
321+
}
322+
.onEach {
323+
onCropSuccess(it)
324+
}
325+
.launchIn(this)
326+
}
327+
}
328+
}
329+
330+
@Composable
331+
private fun getResetKeys(
332+
scaledImageBitmap: ImageBitmap,
333+
imageWidthPx: Int,
334+
imageHeightPx: Int,
335+
contentScale: ContentScale,
336+
cropType: CropType
337+
) = remember(
338+
scaledImageBitmap,
339+
imageWidthPx,
340+
imageHeightPx,
341+
contentScale,
342+
cropType
343+
) {
344+
arrayOf(
345+
scaledImageBitmap,
346+
imageWidthPx,
347+
imageHeightPx,
348+
contentScale,
349+
cropType
350+
)
351+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import androidx.compose.animation.core.Animatable
44
import androidx.compose.animation.core.AnimationSpec
55
import androidx.compose.animation.core.VectorConverter
66
import androidx.compose.animation.core.tween
7+
import androidx.compose.runtime.getValue
8+
import androidx.compose.runtime.mutableStateOf
9+
import androidx.compose.runtime.setValue
710
import androidx.compose.ui.geometry.Offset
811
import androidx.compose.ui.geometry.Rect
912
import androidx.compose.ui.geometry.Size
1013
import androidx.compose.ui.input.pointer.PointerInputChange
1114
import androidx.compose.ui.unit.IntSize
15+
import com.smarttoolfactory.cropper.TouchRegion
1216
import com.smarttoolfactory.cropper.model.AspectRatio
1317
import com.smarttoolfactory.cropper.model.CropData
1418
import com.smarttoolfactory.cropper.settings.CropProperties
@@ -90,6 +94,11 @@ abstract class CropState internal constructor(
9094

9195
private var initialized: Boolean = false
9296

97+
/**
98+
* Region of touch inside, corners of or outside of overlay rectangle
99+
*/
100+
var touchRegion by mutableStateOf(TouchRegion.None)
101+
93102
internal suspend fun init() {
94103
// When initial aspect ratio doesn't match drawable area
95104
// overlay gets updated so update draw area as well

0 commit comments

Comments
 (0)