From a0cdc9d987a1ef1094e56bfd2ac630541ca579b2 Mon Sep 17 00:00:00 2001 From: skydoves Date: Sat, 29 Jun 2024 21:19:49 +0900 Subject: [PATCH] Implement setPaletteImageBitmap and sync imageBitmap changes --- app/api/app.api | 2 +- app/build.gradle.kts | 6 +-- .../colorpickercomposedemo/PhotoPickerIcon.kt | 46 +++++++---------- .../screens/ImageColorPickerScreen.kt | 6 ++- .../colorpicker/compose/Configuration.kt | 1 + .../api/android/colorpicker-compose.api | 2 + .../api/desktop/colorpicker-compose.api | 2 + .../colorpicker/compose/ColorPicker.kt | 13 +++-- .../compose/ColorPickerController.kt | 49 ++++++++++++++++--- .../colorpicker/compose/ImageColorPicker.kt | 41 +++++++++++++--- gradle/libs.versions.toml | 4 +- 11 files changed, 114 insertions(+), 58 deletions(-) diff --git a/app/api/app.api b/app/api/app.api index 7a7f573..9d0cc3f 100644 --- a/app/api/app.api +++ b/app/api/app.api @@ -42,7 +42,7 @@ public final class com/github/skydoves/colorpickercomposedemo/MainScreenKt { } public final class com/github/skydoves/colorpickercomposedemo/PhotoPickerIconKt { - public static final fun PhotoPickerIcon (Landroidx/compose/foundation/layout/ColumnScope;Lcom/github/skydoves/colorpicker/compose/ColorPickerController;Landroidx/compose/runtime/Composer;I)V + public static final fun PhotoPickerIcon (Landroidx/compose/foundation/layout/ColumnScope;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)V } public abstract class com/github/skydoves/colorpickercomposedemo/Screen { diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 41c2ba0..11cab65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,11 +34,11 @@ kotlin { implementation(compose.components.uiToolingPreview) implementation(compose.material) implementation(compose.components.resources) + implementation(libs.androidx.compose.navigation) + implementation(libs.image.picker) implementation(project(":colorpicker-compose")) - - // TODO: implementation(libs.photo.picker) } androidMain.dependencies { @@ -56,7 +56,7 @@ android { defaultConfig { applicationId = "com.github.skydoves.colorpickercomposedemo" - minSdk = Configuration.minSdk + minSdk = Configuration.demoMinSdk targetSdk = Configuration.targetSdk versionCode = Configuration.versionCode versionName = Configuration.versionName diff --git a/app/src/commonMain/kotlin/com/github/skydoves/colorpickercomposedemo/PhotoPickerIcon.kt b/app/src/commonMain/kotlin/com/github/skydoves/colorpickercomposedemo/PhotoPickerIcon.kt index b36b57f..7729570 100644 --- a/app/src/commonMain/kotlin/com/github/skydoves/colorpickercomposedemo/PhotoPickerIcon.kt +++ b/app/src/commonMain/kotlin/com/github/skydoves/colorpickercomposedemo/PhotoPickerIcon.kt @@ -15,12 +15,6 @@ */ package com.github.skydoves.colorpickercomposedemo -// import android.graphics.ImageDecoder -// import android.os.Build -// import android.provider.MediaStore -// import androidx.activity.compose.rememberLauncherForActivityResult -// import androidx.compose.ui.graphics.asImageBitmap -// import androidx.compose.ui.res.vectorResource import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -28,33 +22,34 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.unit.dp import colorpickercomposedemo.app.generated.resources.Res import colorpickercomposedemo.app.generated.resources.ic_gallery -import com.github.skydoves.colorpicker.compose.ColorPickerController +import com.preat.peekaboo.image.picker.SelectionMode +import com.preat.peekaboo.image.picker.rememberImagePickerLauncher +import com.preat.peekaboo.image.picker.toImageBitmap import org.jetbrains.compose.resources.vectorResource -// import com.google.modernstorage.photopicker.PhotoPicker - @Composable fun ColumnScope.PhotoPickerIcon( - controller: ColorPickerController, + onImageSelected: (ImageBitmap?) -> Unit, ) { -// val context = LocalContext.current -// val photoPicker = -// rememberLauncherForActivityResult(PhotoPicker()) { uris -> -// val uri = uris.firstOrNull() ?: return@rememberLauncherForActivityResult -// -// val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { -// ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri)) -// } else { -// MediaStore.Images.Media.getBitmap(context.contentResolver, uri) -// } -// -// controller.setPaletteImageBitmap(bitmap.asImageBitmap()) -// } + val scope = rememberCoroutineScope() + + val singleImagePicker = rememberImagePickerLauncher( + selectionMode = SelectionMode.Single, + scope = scope, + onResult = { byteArrays -> + byteArrays.firstOrNull()?.let { + val imageBitmap = it.toImageBitmap() + onImageSelected.invoke(imageBitmap) + } + }, + ) Box( modifier = Modifier @@ -64,10 +59,7 @@ fun ColumnScope.PhotoPickerIcon( Image( modifier = Modifier .size(42.dp) - .clickable { - // Launch the picker with only one image selectable - // photoPicker.launch(PhotoPicker.Args(PhotoPicker.Type.IMAGES_ONLY, 1)) - }, + .clickable { singleImagePicker.launch() }, imageVector = vectorResource(Res.drawable.ic_gallery), contentDescription = null, ) diff --git a/app/src/commonMain/kotlin/com/github/skydoves/colorpickercomposedemo/screens/ImageColorPickerScreen.kt b/app/src/commonMain/kotlin/com/github/skydoves/colorpickercomposedemo/screens/ImageColorPickerScreen.kt index cbe5ba8..204bf12 100644 --- a/app/src/commonMain/kotlin/com/github/skydoves/colorpickercomposedemo/screens/ImageColorPickerScreen.kt +++ b/app/src/commonMain/kotlin/com/github/skydoves/colorpickercomposedemo/screens/ImageColorPickerScreen.kt @@ -59,13 +59,15 @@ fun ImageColorPickerScreen() { Column { Spacer(modifier = Modifier.weight(1f)) - PhotoPickerIcon(controller) + PhotoPickerIcon { imageBitmap -> + imageBitmap?.let { controller.setPaletteImageBitmap(it) } + } ImageColorPicker( modifier = Modifier .testTag("ImageColorPicker") .fillMaxWidth() - .height(200.dp) + .height(300.dp) .padding(10.dp), controller = controller, paletteImageBitmap = imageResource(Res.drawable.palettebar), diff --git a/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Configuration.kt b/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Configuration.kt index 96ece00..e999115 100644 --- a/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Configuration.kt +++ b/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Configuration.kt @@ -4,6 +4,7 @@ object Configuration { const val compileSdk = 34 const val targetSdk = 34 const val minSdk = 21 + const val demoMinSdk = 24 const val majorVersion = 1 const val minorVersion = 0 const val patchVersion = 9 diff --git a/colorpicker-compose/api/android/colorpicker-compose.api b/colorpicker-compose/api/android/colorpicker-compose.api index 17d0a4d..6158c20 100644 --- a/colorpicker-compose/api/android/colorpicker-compose.api +++ b/colorpicker-compose/api/android/colorpicker-compose.api @@ -34,6 +34,7 @@ public final class com/github/skydoves/colorpicker/compose/ColorPickerController public final fun getColorFlow (J)Lkotlinx/coroutines/flow/Flow; public static synthetic fun getColorFlow$default (Lcom/github/skydoves/colorpicker/compose/ColorPickerController;JILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public final fun getEnabled ()Z + public final fun getPaletteBitmap ()Lkotlinx/coroutines/flow/StateFlow; public final fun getSelectedColor ()Landroidx/compose/runtime/State; public final fun getSelectedPoint ()Landroidx/compose/runtime/State; public final fun getWheelAlpha ()F @@ -50,6 +51,7 @@ public final class com/github/skydoves/colorpicker/compose/ColorPickerController public final fun setAlpha (FZ)V public final fun setBrightness (FZ)V public final fun setEnabled (Z)V + public final fun setPaletteImageBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)V public final fun setWheelAlpha (F)V public final fun setWheelBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)V public final fun setWheelColor-8_81llA (J)V diff --git a/colorpicker-compose/api/desktop/colorpicker-compose.api b/colorpicker-compose/api/desktop/colorpicker-compose.api index 17d0a4d..6158c20 100644 --- a/colorpicker-compose/api/desktop/colorpicker-compose.api +++ b/colorpicker-compose/api/desktop/colorpicker-compose.api @@ -34,6 +34,7 @@ public final class com/github/skydoves/colorpicker/compose/ColorPickerController public final fun getColorFlow (J)Lkotlinx/coroutines/flow/Flow; public static synthetic fun getColorFlow$default (Lcom/github/skydoves/colorpicker/compose/ColorPickerController;JILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public final fun getEnabled ()Z + public final fun getPaletteBitmap ()Lkotlinx/coroutines/flow/StateFlow; public final fun getSelectedColor ()Landroidx/compose/runtime/State; public final fun getSelectedPoint ()Landroidx/compose/runtime/State; public final fun getWheelAlpha ()F @@ -50,6 +51,7 @@ public final class com/github/skydoves/colorpicker/compose/ColorPickerController public final fun setAlpha (FZ)V public final fun setBrightness (FZ)V public final fun setEnabled (Z)V + public final fun setPaletteImageBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)V public final fun setWheelAlpha (F)V public final fun setWheelBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)V public final fun setWheelColor-8_81llA (J)V diff --git a/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ColorPicker.kt b/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ColorPicker.kt index 0bd9a78..74d2d9b 100644 --- a/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ColorPicker.kt +++ b/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ColorPicker.kt @@ -58,14 +58,13 @@ internal fun ColorPicker( drawOnPosSelected: (DrawScope.() -> Unit)? = null, drawDefaultWheelIndicator: Boolean = wheelImageBitmap == null && drawOnPosSelected == null, onColorChanged: (colorEnvelope: ColorEnvelope) -> Unit = {}, - sizeChanged: (IntSize) -> Unit = { _ -> }, setup: ColorPickerController.() -> Unit, draw: Canvas.(size: Size) -> Unit, ) { var initialized by remember { mutableStateOf(false) } - DisposableEffect(controller) { + DisposableEffect(key1 = controller) { controller.coroutineScope.launch(Dispatchers.Main) { controller.getColorFlow().collect { onColorChanged(it) } // TODO: debounce parameter } @@ -105,11 +104,11 @@ internal fun ColorPicker( // draw wheel bitmap on the canvas. canvas.drawWheel( - controller.selectedPoint.value, - controller.wheelBitmap, - drawDefaultWheelIndicator, - controller.wheelRadius.toPx(), - controller.wheelPaint, + point = controller.selectedPoint.value, + wheelBitmap = controller.wheelBitmap, + drawDefaultWheelIndicator = drawDefaultWheelIndicator, + wheelRadiusPx = controller.wheelRadius.toPx(), + wheelPaint = controller.wheelPaint, ) drawOnPosSelected?.let { it() } diff --git a/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ColorPickerController.kt b/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ColorPickerController.kt index 48bdec6..2af934d 100644 --- a/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ColorPickerController.kt +++ b/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ColorPickerController.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filterNotNull @@ -63,9 +64,12 @@ constructor( ) { internal var canvasSize: Size = Size.Zero set(value) { - if (value == field) { return } + if (value == field) { + return + } val cur = _selectedPoint.value - _selectedPoint.value = Offset( // TODO: check this for aspect ratio preservation + _selectedPoint.value = Offset( + // TODO: check this for aspect ratio preservation cur.x * value.width / field.width, cur.y * value.height / field.height, ) @@ -91,6 +95,10 @@ constructor( /** Brightness value to be applied with the selected color. */ internal var brightness: MutableState = mutableFloatStateOf(1.0f) + /** An [ImageBitmap] to be drawn on the canvas as a palette. */ + private var _paletteBitmap: MutableStateFlow = MutableStateFlow(null) + public val paletteBitmap: StateFlow = _paletteBitmap + /** An [ImageBitmap] to be drawn on the canvas as a wheel. */ public var wheelBitmap: ImageBitmap? = null @@ -129,7 +137,9 @@ constructor( /** Enable or not color selection. */ public var enabled: Boolean get() = _enabled.value - set(value) { _enabled.value = value } + set(value) { + _enabled.value = value + } /** Indicates if the alpha slider has been attached. */ internal var isAttachedAlphaSlider: Boolean = false @@ -233,17 +243,23 @@ constructor( var changed = selectByCoordinate(hsvToCoord(h, s, canvasSize.center)) changed = setAlpha(alpha) || changed changed = setBrightness(v) || changed - if (changed) { notifyColorChanged(fromUser) } + if (changed) { + notifyColorChanged(fromUser) + } } /** Combine the alpha value to the selected pure color. */ public fun setAlpha(alpha: Float, fromUser: Boolean) { - if (setAlpha(alpha)) { notifyColorChanged(fromUser) } + if (setAlpha(alpha)) { + notifyColorChanged(fromUser) + } } /** Combine the brightness value to the selected pure color. */ public fun setBrightness(brightness: Float, fromUser: Boolean) { - if (setBrightness(brightness)) { notifyColorChanged(fromUser) } + if (setBrightness(brightness)) { + notifyColorChanged(fromUser) + } } /** Notify color changes to the color picker and other subcomponents. */ @@ -272,7 +288,9 @@ constructor( /** Combine the alpha value to the selected pure color. */ private fun setAlpha(alpha: Float): Boolean { - if (!enabled || this.alpha.value == alpha) { return false } + if (!enabled || this.alpha.value == alpha) { + return false + } this.alpha.value = alpha _selectedColor.value = selectedColor.value.copy(alpha = alpha) return true @@ -280,7 +298,9 @@ constructor( /** Combine the brightness value to the selected pure color. */ private fun setBrightness(brightness: Float): Boolean { - if (!enabled || this.brightness.value == brightness) { return false } + if (!enabled || this.brightness.value == brightness) { + return false + } this.brightness.value = brightness val (h, s, _) = pureSelectedColor.value.toHSV() _selectedColor.value = Color.hsv(h, s, brightness, alpha.value) @@ -294,7 +314,20 @@ constructor( return Color.hsv(h, s, actualV, if (isAttachedAlphaSlider) alpha.value else 1f) } + /** Set an [ImageBitmap] to draw on the canvas as a palette. */ + public fun setPaletteImageBitmap(imageBitmap: ImageBitmap) { + val targetSize = imageBitmap.takeIf { it.width != 0 && it.height != 0 } + ?: throw RuntimeException( + "Can't set an ImageBitmap before initializing the canvas", + ) + canvasSize = Size(targetSize.width.toFloat(), targetSize.height.toFloat()) + _paletteBitmap.value = imageBitmap + selectCenter(fromUser = false) + reviseTick.intValue++ + } + internal fun releaseBitmap() { wheelBitmap = null + _paletteBitmap.value = null } } diff --git a/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ImageColorPicker.kt b/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ImageColorPicker.kt index ac9c175..a935681 100644 --- a/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ImageColorPicker.kt +++ b/colorpicker-compose/src/commonMain/kotlin/com/github/skydoves/colorpicker/compose/ImageColorPicker.kt @@ -18,6 +18,8 @@ package com.github.skydoves.colorpicker.compose import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -64,11 +66,30 @@ public fun ImageColorPicker( return } - val width = remember { paletteImageBitmap.width } - val height = remember { paletteImageBitmap.height } + val controllerBitmap: ImageBitmap? by controller.paletteBitmap.collectAsState( + initial = paletteImageBitmap, + ) + val imageBitmap = controllerBitmap ?: paletteImageBitmap + + val width = remember(imageBitmap) { imageBitmap.width } + val height = remember(imageBitmap) { imageBitmap.height } var offset by remember { mutableStateOf(Offset.Zero) } var scale by remember { mutableStateOf(1f) } + LaunchedEffect(key1 = imageBitmap) { + controller.setup { point -> + val origPoint = (point - offset) / scale + val imPoint = Offset( + origPoint.x.coerceIn(0f, width - 1f), + origPoint.y.coerceIn(0f, height - 1f), + ) + // TODO: transparent pixel handling + val px = imageBitmap.getPixel(imPoint.roundToInt()) + val newPoint = imPoint * scale + offset + px to newPoint + } + } + ColorPicker( modifier = modifier, controller = controller, @@ -76,11 +97,15 @@ public fun ImageColorPicker( drawOnPosSelected = drawOnPosSelected, drawDefaultWheelIndicator = drawDefaultWheelIndicator, onColorChanged = onColorChanged, - sizeChanged = { size -> val metrics = when (paletteContentScale) { - PaletteContentScale.FIT -> { getMetricsForFit(paletteImageBitmap.size, size) } - PaletteContentScale.CROP -> { getMetricsForCrop(paletteImageBitmap.size, size) } + PaletteContentScale.FIT -> { + getMetricsForFit(imageBitmap.size, size) + } + + PaletteContentScale.CROP -> { + getMetricsForCrop(imageBitmap.size, size) + } } scale = metrics.first offset = metrics.second @@ -93,16 +118,16 @@ public fun ImageColorPicker( origPoint.y.coerceIn(0f, height - 1f), ) // TODO: transparent pixel handling - val px = paletteImageBitmap.getPixel(imPoint.roundToInt()) + val px = imageBitmap.getPixel(imPoint.roundToInt()) val newPoint = imPoint * scale + offset px to newPoint } }, draw = { drawImageRect( - paletteImageBitmap, + image = imageBitmap, dstOffset = offset.roundToInt(), - dstSize = paletteImageBitmap.size * scale, + dstSize = imageBitmap.size * scale, paint = emptyPaint, ) }, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index be9321f..61080de 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ jvmTarget = "11" androidxActivity = "1.9.0" androidxTest = "1.5.2" androidxNavigation = "2.7.0-alpha07" -photoPicker = "1.0.0-alpha06" +imagePicker = "0.5.2" baselineProfiles = "1.3.1" macroBenchmark = "1.2.4" uiAutomator = "2.3.0" @@ -37,7 +37,7 @@ baseline-profile = { id = "androidx.baselineprofile", version.ref = "androidxMac [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-compose-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidxNavigation" } -photo-picker = { group = "com.google.modernstorage", name = "modernstorage-photopicker", version.ref = "photoPicker" } +image-picker = { group = "io.github.onseok", name = "peekaboo-image-picker", version.ref = "imagePicker" } # unit test androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTest" }