1- @file:OptIn(ExperimentalAnimationApi ::class )
21
32package com.smarttoolfactory.cropper
43
54import androidx.compose.animation.AnimatedVisibility
65import androidx.compose.animation.ExperimentalAnimationApi
6+ import androidx.compose.animation.animateColorAsState
77import androidx.compose.animation.core.tween
88import androidx.compose.animation.scaleIn
99import androidx.compose.foundation.background
@@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.Color
1919import androidx.compose.ui.graphics.FilterQuality
2020import androidx.compose.ui.graphics.ImageBitmap
2121import androidx.compose.ui.graphics.drawscope.DrawScope
22+ import androidx.compose.ui.layout.ContentScale
2223import androidx.compose.ui.platform.LocalDensity
2324import androidx.compose.ui.platform.LocalLayoutDirection
2425import androidx.compose.ui.unit.Dp
@@ -33,6 +34,7 @@ import com.smarttoolfactory.cropper.settings.CropDefaults
3334import com.smarttoolfactory.cropper.settings.CropProperties
3435import com.smarttoolfactory.cropper.settings.CropStyle
3536import com.smarttoolfactory.cropper.settings.CropType
37+ import com.smarttoolfactory.cropper.state.DynamicCropState
3638import com.smarttoolfactory.cropper.state.rememberCropState
3739import kotlinx.coroutines.Dispatchers
3840import 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+ }
0 commit comments