Skip to content

Commit

Permalink
Merge pull request #66 from skydoves/fix/controller-imagebitmap
Browse files Browse the repository at this point in the history
Implement setPaletteImageBitmap and sync imageBitmap changes
  • Loading branch information
skydoves authored Jun 29, 2024
2 parents d36360a + a0cdc9d commit b388dd6
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 58 deletions.
2 changes: 1 addition & 1 deletion app/api/app.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,41 @@
*/
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
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
Expand All @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions colorpicker-compose/api/android/colorpicker-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions colorpicker-compose/api/desktop/colorpicker-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
)
Expand All @@ -91,6 +95,10 @@ constructor(
/** Brightness value to be applied with the selected color. */
internal var brightness: MutableState<Float> = mutableFloatStateOf(1.0f)

/** An [ImageBitmap] to be drawn on the canvas as a palette. */
private var _paletteBitmap: MutableStateFlow<ImageBitmap?> = MutableStateFlow(null)
public val paletteBitmap: StateFlow<ImageBitmap?> = _paletteBitmap

/** An [ImageBitmap] to be drawn on the canvas as a wheel. */
public var wheelBitmap: ImageBitmap? = null

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -272,15 +288,19 @@ 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
}

/** 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)
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,23 +66,46 @@ 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,
wheelImageBitmap = wheelImageBitmap,
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
Expand All @@ -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,
)
},
Expand Down
Loading

0 comments on commit b388dd6

Please sign in to comment.