Skip to content

Commit d6c25b5

Browse files
try integrating image cropper in MediaPickerDialog
1 parent a6d83a6 commit d6c25b5

4 files changed

Lines changed: 193 additions & 64 deletions

File tree

app/src/main/java/com/streamliners/feature/pickers_sample/comp/MediaPickerSample.kt

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.mr0xf00.easycrop.ui.ImageCropperDialog
3333
import com.streamliners.compose.comp.select.LabelledCheckBox
3434
import com.streamliners.compose.comp.select.RadioGroup
3535
import com.streamliners.pickers.media.FromGalleryType
36+
import com.streamliners.pickers.media.MediaPickerCropParams
3637
import com.streamliners.pickers.media.MediaPickerDialog
3738
import com.streamliners.pickers.media.MediaPickerDialogState
3839
import com.streamliners.pickers.media.MediaType
@@ -106,28 +107,14 @@ fun MediaPickerSample(
106107
type = type.value!!,
107108
allowMultiple = allowMultiple.value,
108109
fromGalleryType = fromGalleryType.value!!,
110+
cropParams = MediaPickerCropParams.Enabled(
111+
showAspectRatioSelectionButton = false,
112+
showShapeCropButton = false,
113+
lockAspectRatio = AspectRatio(1, 1)
114+
),
109115
callback = { getResult ->
110116
executeHandlingError {
111117
pickedMediaList.addAll(getResult())
112-
113-
val firstMedia = pickedMediaList.firstOrNull() ?: error("no media picked")
114-
if (firstMedia is PickedMedia.Image) {
115-
116-
val result = imageCropper.crop(firstMedia.uri.toUri(), context)
117-
when (result) {
118-
CropResult.Cancelled -> {
119-
120-
}
121-
is CropError -> {
122-
error("Crop error")
123-
}
124-
is CropResult.Success -> {
125-
val croppedImageUri = saveBitmapToFile(context, result.bitmap)
126-
pickedMediaList.remove(firstMedia)
127-
pickedMediaList.add(firstMedia.copy(uri = croppedImageUri.toString()))
128-
}
129-
}
130-
}
131118
}
132119
}
133120
)
@@ -149,27 +136,4 @@ fun MediaPickerSample(
149136
state = mediaPickerDialogState,
150137
authority = "com.streamliners.fileprovider"
151138
)
152-
153-
imageCropper.cropState?.let {
154-
155-
ImageCropperDialog(
156-
state = it,
157-
style = CropperStyle(
158-
autoZoom = false,
159-
guidelines = null
160-
),
161-
showAspectRatioSelectionButton = false,
162-
showShapeCropButton = false,
163-
lockAspectRatio = AspectRatio(1, 1)
164-
)
165-
}
166-
}
167-
168-
fun saveBitmapToFile(context: Context, bitmap: ImageBitmap): Uri {
169-
val file = createFile(context, "${System.currentTimeMillis()}.png", "capture")
170-
val fileOutputStream = FileOutputStream(file)
171-
bitmap.asAndroidBitmap().compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)
172-
fileOutputStream.flush()
173-
return file.getUri(context, "com.streamliners.fileprovider")
174-
}
175-
139+
}

pickers/src/main/java/com/streamliners/pickers/media/MediaPickerDialog.kt

Lines changed: 155 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import android.app.Activity
55
import android.content.Context
66
import android.content.Intent
77
import android.content.pm.PackageManager
8+
import android.graphics.Bitmap
9+
import android.graphics.BitmapFactory
10+
import android.net.Uri
811
import android.provider.MediaStore
912
import androidx.activity.compose.rememberLauncherForActivityResult
1013
import androidx.activity.result.PickVisualMediaRequest
@@ -23,22 +26,39 @@ import androidx.compose.material3.AlertDialog
2326
import androidx.compose.material3.Text
2427
import androidx.compose.material3.TextButton
2528
import androidx.compose.runtime.Composable
29+
import androidx.compose.runtime.LaunchedEffect
2630
import androidx.compose.runtime.MutableState
2731
import androidx.compose.runtime.mutableStateOf
2832
import androidx.compose.runtime.remember
33+
import androidx.compose.runtime.rememberCoroutineScope
2934
import androidx.compose.ui.Modifier
35+
import androidx.compose.ui.graphics.ImageBitmap
36+
import androidx.compose.ui.graphics.asImageBitmap
3037
import androidx.compose.ui.platform.LocalContext
3138
import androidx.compose.ui.unit.dp
3239
import androidx.compose.ui.window.DialogProperties
3340
import androidx.core.content.ContextCompat
41+
import androidx.core.net.toUri
42+
import com.mr0xf00.easycrop.CropError
43+
import com.mr0xf00.easycrop.CropResult
44+
import com.mr0xf00.easycrop.CropperStyle
45+
import com.mr0xf00.easycrop.ImageCropper
46+
import com.mr0xf00.easycrop.crop
47+
import com.mr0xf00.easycrop.rememberImageCropper
48+
import com.mr0xf00.easycrop.ui.ImageCropperDialog
3449
import com.streamliners.pickers.media.FromGalleryType.VisualMediaPicker
3550
import com.streamliners.pickers.media.MediaType.Image
3651
import com.streamliners.pickers.media.MediaType.Video
3752
import com.streamliners.pickers.media.comp.OptionButton
3853
import com.streamliners.pickers.media.util.VideoMetadataExtractor
3954
import com.streamliners.pickers.media.util.createFile
4055
import com.streamliners.pickers.media.util.getUri
56+
import com.streamliners.pickers.media.util.saveBitmapToFile
4157
import com.streamliners.utils.safeLet
58+
import kotlinx.coroutines.CoroutineScope
59+
import kotlinx.coroutines.launch
60+
import java.io.File
61+
4262

4363
@Composable
4464
fun MediaPickerDialog(
@@ -47,6 +67,13 @@ fun MediaPickerDialog(
4767
) {
4868
val data = state.value as? MediaPickerDialogState.Visible ?: return
4969

70+
LaunchedEffect(key1 = Unit) {
71+
if (data.cropParams is MediaPickerCropParams.Enabled) {
72+
if (data.type == Video) error("cropParams are not allowed for MediaType.Video")
73+
if (data.allowMultiple) error("cropParams are not allowed when allowMultiple = true")
74+
}
75+
}
76+
5077
val context = LocalContext.current
5178

5279
val cameraPermissionIsGranted = {
@@ -55,6 +82,8 @@ fun MediaPickerDialog(
5582
) == PackageManager.PERMISSION_GRANTED
5683
}
5784

85+
val imageCropper = rememberImageCropper()
86+
5887
AlertDialog(
5988
modifier = Modifier
6089
.padding(28.dp)
@@ -79,12 +108,12 @@ fun MediaPickerDialog(
79108
) {
80109
FromCameraButton(
81110
modifier = Modifier.weight(1f),
82-
state, data, authority, cameraPermissionIsGranted
111+
state, data, authority, cameraPermissionIsGranted, imageCropper
83112
)
84113

85114
FromGalleryButton(
86115
modifier = Modifier.weight(1f),
87-
state, data
116+
state, data, imageCropper, authority
88117
)
89118
}
90119

@@ -99,6 +128,20 @@ fun MediaPickerDialog(
99128
}
100129
}
101130
)
131+
132+
imageCropper.cropState?.let {
133+
134+
ImageCropperDialog(
135+
state = it,
136+
style = CropperStyle(
137+
autoZoom = false,
138+
guidelines = null
139+
),
140+
showAspectRatioSelectionButton = (data.cropParams as? MediaPickerCropParams.Enabled)?.showAspectRatioSelectionButton ?: true,
141+
showShapeCropButton = (data.cropParams as? MediaPickerCropParams.Enabled)?.showAspectRatioSelectionButton ?: true,
142+
lockAspectRatio = (data.cropParams as? MediaPickerCropParams.Enabled)?.lockAspectRatio
143+
)
144+
}
102145
}
103146

104147
@Composable
@@ -107,25 +150,41 @@ fun FromCameraButton(
107150
state: MutableState<MediaPickerDialogState>,
108151
data: MediaPickerDialogState.Visible,
109152
authority: String,
110-
cameraPermissionIsGranted: () -> Boolean
153+
cameraPermissionIsGranted: () -> Boolean,
154+
imageCropper: ImageCropper
111155
) {
112156
val context = LocalContext.current
113157

114158
val filePath = remember { mutableStateOf<String?>(null) }
115159
val fileUri = remember { mutableStateOf<String?>(null) }
160+
val scope = rememberCoroutineScope()
116161

117162
val cameraLauncher = rememberLauncherForActivityResult(
118163
contract = ActivityResultContracts.StartActivityForResult(),
119164
onResult = { result ->
120165
if (result.resultCode == Activity.RESULT_OK) {
121166
safeLet(filePath.value, fileUri.value) { path, uri ->
122-
data.callback {
123-
listOf(
124-
when (data.type) {
125-
Image -> PickedMedia.Image(uri, path)
126-
Video -> processVideo(context, uri, path)
167+
168+
when (data.type) {
169+
Image -> {
170+
showImageCropperIfRequired(
171+
data,
172+
PickedMedia.Image(uri, path),
173+
imageCropper,
174+
context,
175+
scope,
176+
authority
177+
) {
178+
data.callback { listOf(it) }
179+
}
180+
}
181+
Video -> {
182+
data.callback {
183+
listOf(
184+
processVideo(context, uri, path)
185+
)
127186
}
128-
)
187+
}
129188
}
130189
}
131190
state.dismiss()
@@ -187,10 +246,14 @@ fun FromCameraButton(
187246
fun FromGalleryButton(
188247
modifier: Modifier,
189248
state: MutableState<MediaPickerDialogState>,
190-
data: MediaPickerDialogState.Visible
249+
data: MediaPickerDialogState.Visible,
250+
imageCropper: ImageCropper,
251+
authority: String
191252
) {
192253
val context = LocalContext.current
193254

255+
val scope = rememberCoroutineScope()
256+
194257
val documentPickerLauncher = rememberLauncherForActivityResult(
195258
contract = ActivityResultContracts.StartActivityForResult(),
196259
onResult = { result ->
@@ -208,11 +271,32 @@ fun FromGalleryButton(
208271
}
209272
}.filterNotNull()
210273

211-
data.callback {
212-
items.map { uri ->
213-
when (data.type) {
214-
Image -> PickedMedia.Image(uri.toString())
215-
Video -> processVideo(context, uri.toString())
274+
when (data.type) {
275+
Image -> {
276+
if (data.allowMultiple) {
277+
data.callback {
278+
items.map { uri ->
279+
PickedMedia.Image(uri.toString())
280+
}
281+
}
282+
} else {
283+
showImageCropperIfRequired(
284+
data,
285+
PickedMedia.Image(items.first().toString()),
286+
imageCropper,
287+
context,
288+
scope,
289+
authority
290+
) {
291+
data.callback { listOf(it) }
292+
}
293+
}
294+
}
295+
Video -> {
296+
data.callback {
297+
items.map { uri ->
298+
processVideo(context, uri.toString())
299+
}
216300
}
217301
}
218302
}
@@ -230,13 +314,27 @@ fun FromGalleryButton(
230314
uri,
231315
Intent.FLAG_GRANT_READ_URI_PERMISSION
232316
)
233-
data.callback {
234-
listOf(
235-
when (data.type) {
236-
Image -> PickedMedia.Image(uri.toString())
237-
Video -> processVideo(context, uri.toString())
317+
318+
when (data.type) {
319+
Image -> {
320+
showImageCropperIfRequired(
321+
data,
322+
PickedMedia.Image(uri.toString()),
323+
imageCropper,
324+
context,
325+
scope,
326+
authority
327+
) {
328+
data.callback { listOf(it) }
238329
}
239-
)
330+
}
331+
Video -> {
332+
data.callback {
333+
listOf(
334+
processVideo(context, uri.toString())
335+
)
336+
}
337+
}
240338
}
241339
}
242340
state.dismiss()
@@ -306,4 +404,40 @@ suspend fun processVideo(
306404
duration = VideoMetadataExtractor.getDuration(context, uri),
307405
thumbnailUri = VideoMetadataExtractor.getThumbnailUri(context, uri)
308406
)
407+
}
408+
409+
fun showImageCropperIfRequired(
410+
data: MediaPickerDialogState.Visible,
411+
image: PickedMedia.Image,
412+
imageCropper: ImageCropper,
413+
context: Context,
414+
scope: CoroutineScope,
415+
authority: String,
416+
onReady: (PickedMedia.Image) -> Unit
417+
) {
418+
data.cropParams as? MediaPickerCropParams.Enabled ?: run {
419+
onReady(image)
420+
return
421+
}
422+
423+
scope.launch {
424+
425+
// val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, Uri.parse(image.uri))
426+
427+
when (
428+
// val result = imageCropper.crop(bmp = bitmap.asImageBitmap())
429+
val result = imageCropper.crop(image.uri.toUri(), context, cacheBeforeUse = false)
430+
) {
431+
CropResult.Cancelled -> {
432+
error("Crop cancelled")
433+
}
434+
is CropError -> {
435+
error("Crop error : ${result.name}")
436+
}
437+
is CropResult.Success -> {
438+
val croppedImageUri = saveBitmapToFile(context, result.bitmap, authority)
439+
onReady(PickedMedia.Image(croppedImageUri.toString()))
440+
}
441+
}
442+
}
309443
}

pickers/src/main/java/com/streamliners/pickers/media/MediaPickerState.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.MutableState
55
import androidx.compose.runtime.mutableStateOf
66
import androidx.compose.runtime.remember
7+
import com.mr0xf00.easycrop.AspectRatio
78

89
sealed class MediaPickerDialogState {
910

@@ -13,6 +14,7 @@ sealed class MediaPickerDialogState {
1314
val type: MediaType,
1415
val allowMultiple: Boolean,
1516
val fromGalleryType: FromGalleryType,
17+
val cropParams: MediaPickerCropParams = MediaPickerCropParams.Disabled,
1618
val callback: (suspend () -> List<PickedMedia>) -> Unit
1719
): MediaPickerDialogState() {
1820

@@ -68,6 +70,18 @@ enum class FromGalleryType {
6870
DocumentPicker, VisualMediaPicker
6971
}
7072

73+
sealed class MediaPickerCropParams {
74+
75+
data object Disabled: MediaPickerCropParams()
76+
77+
data class Enabled(
78+
val showAspectRatioSelectionButton: Boolean = true,
79+
val showShapeCropButton: Boolean = true,
80+
val lockAspectRatio: AspectRatio? = null
81+
): MediaPickerCropParams()
82+
83+
}
84+
7185
@Composable
7286
fun rememberMediaPickerDialogState(): MutableState<MediaPickerDialogState> {
7387
return remember {

0 commit comments

Comments
 (0)