Blog Infos
Author
Published
Topics
,
Author
Published

For my first article in medium, I would like to start by showing you how to create a color picker in pure compose.

Photo by Frans van Heerden on Pexels

 

Usually color pickers will have 3 components,

  1. Hue — The color of the picker
  2. Saturation — The intensity of the color
  3. Value — The lightness of the color

We are going to have 2 panels. One will contain the hue selector and other panel will have both saturation and value in it.

First we will start by having hue panel. The output should look something like this.

(Since it would not look nice to have to write android.graphics.Color or androidx.compose.ui.graphics.Color in all the places that we use. We will import android.graphics.Color as AndroidColor)

For that we would have to draw in canvas.

Canvas(
modifier = Modifier
.height(40.dp)
.width(300.dp)
.clip(RoundedCornerShape(50))
) {
...
}
view raw MainActivity.kt hosted with ❤ by GitHub

We will be drawing this hue gradient in bitmap using android.graphics.Canvas.

val bitmap = Bitmap.createBitmap(size.width.toInt(), size.height.toInt(), Bitmap.Config.ARGB_8888)
val hueCanvas = Canvas(bitmap)
val huePanel = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
view raw MainActivity.kt hosted with ❤ by GitHub

Now we will split these colors across the width of the huePanel.

val hueColors = IntArray((huePanel.width()).toInt())
var hue = 0f
for (i in hueColors.indices) {
hueColors[i] = AndroidColor.HSVToColor(floatArrayOf(hue, 1f, 1f))
hue += 360f / hueColors.size
}
view raw MainActivity.kt hosted with ❤ by GitHub

Drawing each line of color in canvas would look like

val linePaint = Paint()
linePaint.strokeWidth = 0F
for (i in hueColors.indices) {
linePaint.color = hueColors[i]
hueCanvas.drawLine(i.toFloat(), 0F, i.toFloat(), huePanel.bottom, linePaint)
}
view raw MainActivity.kt hosted with ❤ by GitHub

Finally drawing this into our compose canvas

drawIntoCanvas {
it.nativeCanvas.drawBitmap(
bitmap,
null,
panel.toRect(),
null
)
}
view raw MainActivity.kt hosted with ❤ by GitHub

Next step is to have a offset state to observe where user is touching and drawing a circle to that area.

Note: We have to draw this circle after drawing bitmap, else it will just be overdrawn by bitmap.

val pressOffset = remember {
mutableStateOf(Offset.Zero)
}
Canvas(
modifier = Modifier
.height(40.dp)
.width(300.dp)
.clip(RoundedCornerShape(50))
) {
.....
drawCircle(
Color.White,
radius = size.height/2,
center = Offset(pressOffset.value.x, size.height/2),
style = Stroke(
width = 2.dp.toPx()
)
)
}
view raw MainActivity.kt hosted with ❤ by GitHub

We have to send a callback for when hue is changed with click and also with drag.

Luckily we can not only get click position using InteractionSource but also send pressPosition to it. This way we only need to observe for it in one place.

We will write a custom Modifier to achieve this.

private fun Modifier.emitDragGesture(
interactionSource: MutableInteractionSource
): Modifier = composed {
val scope = rememberCoroutineScope()
pointerInput(Unit) {
detectDragGestures { input, _ ->
scope.launch {
interactionSource.emit(PressInteraction.Press(input.position))
}
}
}.clickable(interactionSource, null) {
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

We have to observe for click and drag to find out exactly which hue is user picking.

scope.launch {
interactionSource.interactions.collect { interaction ->
when(interaction) {
PressInteraction.Press -> {
val pressPos = pressPosition.x.coerceIn(0f..drawScopeSize.width)
pressOffset.value = Offset(pressPos, 0f)
val selectedHue = pointToHue(pressPos)
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Now the hue panel is all set. After a little bit of cleaning code will look something like this.

@Composable
fun HueBar(
setColor: (Float) -> Unit
) {
val scope = rememberCoroutineScope()
val interactionSource = remember {
MutableInteractionSource()
}
val pressOffset = remember {
mutableStateOf(Offset.Zero)
}
Canvas(
modifier = Modifier
.height(40.dp)
.width(300.dp)
.clip(RoundedCornerShape(50))
.emitDragGesture(interactionSource)
) {
val drawScopeSize = size
val bitmap = Bitmap.createBitmap(size.width.toInt(), size.height.toInt(), Bitmap.Config.ARGB_8888)
val hueCanvas = Canvas(bitmap)
val huePanel = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
val hueColors = IntArray((huePanel.width()).toInt())
var hue = 0f
for (i in hueColors.indices) {
hueColors[i] = AndroidColor.HSVToColor(floatArrayOf(hue, 1f, 1f))
hue += 360f / hueColors.size
}
val linePaint = Paint()
linePaint.strokeWidth = 0F
for (i in hueColors.indices) {
linePaint.color = hueColors[i]
hueCanvas.drawLine(i.toFloat(), 0F, i.toFloat(), huePanel.bottom, linePaint)
}
drawBitmap(
bitmap = bitmap,
panel = huePanel
)
fun pointToHue(pointX: Float): Float {
val width = huePanel.width()
val x = when {
pointX < huePanel.left -> 0F
pointX > huePanel.right -> width
else -> pointX - huePanel.left
}
return x * 360f / width
}
scope.collectForPress(interactionSource) { pressPosition ->
val pressPos = pressPosition.x.coerceIn(0f..drawScopeSize.width)
pressOffset.value = Offset(pressPos, 0f)
val selectedHue = pointToHue(pressPos)
setColor(selectedHue)
}
drawCircle(
Color.White,
radius = size.height/2,
center = Offset(pressOffset.value.x, size.height/2),
style = Stroke(
width = 2.dp.toPx()
)
)
}
}
fun CoroutineScope.collectForPress(
interactionSource: InteractionSource,
setOffset: (Offset) -> Unit
) {
launch {
interactionSource.interactions.collect { interaction ->
(interaction as? PressInteraction.Press)
?.pressPosition
?.let(setOffset)
}
}
}
private fun Modifier.emitDragGesture(
interactionSource: MutableInteractionSource
): Modifier = composed {
val scope = rememberCoroutineScope()
pointerInput(Unit) {
detectDragGestures { input, _ ->
scope.launch {
interactionSource.emit(PressInteraction.Press(input.position))
}
}
}.clickable(interactionSource, null) {
}
}
private fun DrawScope.drawBitmap(
bitmap: Bitmap,
panel: RectF
) {
drawIntoCanvas {
it.nativeCanvas.drawBitmap(
bitmap,
null,
panel.toRect(),
null
)
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Meet Jewel:Create IDE plugins in Compose

Jetpack Compose is the declarative UI toolkit for Android that makes it easy to create beautiful, responsive apps. However, until recently, there was no easy way to use Compose to create IDE plugins without too…
Watch Video

Meet Jewel:Create IDE plugins in Compose

Sebastiano Poggi & Chris Sinco
UX Engineer & UX Design Lead
Google

Meet Jewel:Create IDE plugins in Compose

Sebastiano Poggi & ...
UX Engineer & UX Des ...
Google

Meet Jewel:Create IDE plugins in Compose

Sebastiano Poggi ...
UX Engineer & UX Design L ...
Google

Jobs

Next is a panel which is used to select saturation and value.

The basic upto creating a Canvas for bitmap is same as that of HuePanel.

val interactionSource = remember {
MutableInteractionSource()
}
val scope = rememberCoroutineScope()
var sat: Float
var value: Float
val pressOffset = remember {
mutableStateOf(Offset.Zero)
}
Canvas(
modifier = Modifier
.size(300.dp)
.emitDragGesture(interactionSource)
.clip(RoundedCornerShape(12.dp))
) {
val cornerRadius = 12.dp.toPx()
val satValSize = size
val bitmap = Bitmap.createBitmap(size.width.toInt(), size.height.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val satValPanel = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
}
view raw MainActivity.kt hosted with ❤ by GitHub

Since we are creating a panel that contains both saturation and value, we will create saturation gradient from top-left to top-right and value gradient from top-left to bottom-left.

val rgb = AndroidColor.HSVToColor(floatArrayOf(hue, 1f, 1f))
val satShader = LinearGradient(
satValPanel.left, satValPanel.top, satValPanel.right, satValPanel.top,
-0x1, rgb, Shader.TileMode.CLAMP
)
val valShader = LinearGradient(
satValPanel.left, satValPanel.top, satValPanel.left, satValPanel.bottom,
-0x1, -0x1000000, Shader.TileMode.CLAMP
)
view raw MainActivity.kt hosted with ❤ by GitHub

The rgb value is calculated under the assumption that hue is passed to this composable.

Next we will draw a rounded rectangle by combining the satShader and valShader using Compose Shader.

canvas.drawRoundRect(
satValPanel,
cornerRadius,
cornerRadius,
Paint().apply {
shader = ComposeShader(
valShader,
satShader,
PorterDuff.Mode.MULTIPLY
)
}
)
view raw MainActivity.kt hosted with ❤ by GitHub

Drawing the Saturation + Value panel is over. Now we need to observe and draw a circle where our user will click/drag and then convert that position into saturation and value.

fun pointToSatVal(pointX: Float, pointY: Float): Pair<Float, Float> {
val width = satValPanel.width()
val height = satValPanel.height()
val x = when {
pointX < satValPanel.left -> 0f
pointX > satValPanel.right -> width
else -> pointX - satValPanel.left
}
val y = when {
pointY < satValPanel.top -> 0f
pointY > satValPanel.bottom -> height
else -> pointY - satValPanel.top
}
val satPoint = 1f / width * x
val valuePoint = 1f - 1f / height * y
return satPoint to valuePoint
}
scope.collectForPress(interactionSource) { pressPosition ->
val pressPositionOffset = Offset(
pressPosition.x.coerceIn(0f..satValSize.width),
pressPosition.y.coerceIn(0f..satValSize.height)
)
pressOffset.value = pressPositionOffset
val (satPoint, valuePoint) = pointToSatVal(pressPositionOffset.x, pressPositionOffset.y)
sat = satPoint
value = valuePoint
setSatVal(sat, value)
}
drawCircle(
color = Color.White,
radius = 8.dp.toPx(),
center = pressOffset.value,
style = Stroke(
width = 2.dp.toPx()
)
)
drawCircle(
color = Color.White,
radius = 2.dp.toPx(),
center = pressOffset.value,
)
view raw MainActivity.kt hosted with ❤ by GitHub

Finally our composable will look like this.

@Composable
fun SatValPanel(
hue: Float,
setSatVal: (Float, Float) -> Unit
) {
val interactionSource = remember {
MutableInteractionSource()
}
val scope = rememberCoroutineScope()
var sat: Float
var value: Float
val pressOffset = remember {
mutableStateOf(Offset.Zero)
}
Canvas(
modifier = Modifier
.size(300.dp)
.emitDragGesture(interactionSource)
.clip(RoundedCornerShape(12.dp))
) {
val cornerRadius = 12.dp.toPx()
val satValSize = size
val bitmap = Bitmap.createBitmap(size.width.toInt(), size.height.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val satValPanel = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
val rgb = AndroidColor.HSVToColor(floatArrayOf(hue, 1f, 1f))
val satShader = LinearGradient(
satValPanel.left, satValPanel.top, satValPanel.right, satValPanel.top,
-0x1, rgb, Shader.TileMode.CLAMP
)
val valShader = LinearGradient(
satValPanel.left, satValPanel.top, satValPanel.left, satValPanel.bottom,
-0x1, -0x1000000, Shader.TileMode.CLAMP
)
canvas.drawRoundRect(
satValPanel,
cornerRadius,
cornerRadius,
Paint().apply {
shader = ComposeShader(
valShader,
satShader,
PorterDuff.Mode.MULTIPLY
)
}
)
drawBitmap(
bitmap = bitmap,
panel = satValPanel
)
fun pointToSatVal(pointX: Float, pointY: Float): Pair<Float, Float> {
val width = satValPanel.width()
val height = satValPanel.height()
val x = when {
pointX < satValPanel.left -> 0f
pointX > satValPanel.right -> width
else -> pointX - satValPanel.left
}
val y = when {
pointY < satValPanel.top -> 0f
pointY > satValPanel.bottom -> height
else -> pointY - satValPanel.top
}
val satPoint = 1f / width * x
val valuePoint = 1f - 1f / height * y
return satPoint to valuePoint
}
scope.collectForPress(interactionSource) { pressPosition ->
val pressPositionOffset = Offset(
pressPosition.x.coerceIn(0f..satValSize.width),
pressPosition.y.coerceIn(0f..satValSize.height)
)
pressOffset.value = pressPositionOffset
val (satPoint, valuePoint) = pointToSatVal(pressPositionOffset.x, pressPositionOffset.y)
sat = satPoint
value = valuePoint
setSatVal(sat, value)
}
drawCircle(
color = Color.White,
radius = 8.dp.toPx(),
center = pressOffset.value,
style = Stroke(
width = 2.dp.toPx()
)
)
drawCircle(
color = Color.White,
radius = 2.dp.toPx(),
center = pressOffset.value,
)
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Getting value from both of those panel and set it where ever you like.

Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val hsv = remember {
val hsv = floatArrayOf(0f, 0f, 0f)
AndroidColor.colorToHSV(Color.Blue.toArgb(), hsv)
mutableStateOf(
Triple(hsv[0], hsv[1], hsv[2])
)
}
val backgroundColor = remember(hsv.value) {
mutableStateOf(Color.hsv(hsv.value.first, hsv.value.second, hsv.value.third))
}
SatValPanel(hue = hsv.value.first) { sat, value ->
hsv.value = Triple(hsv.value.first, sat, value)
}
Spacer(modifier = Modifier.height(32.dp))
HueBar { hue ->
hsv.value = Triple(hue, hsv.value.second, hsv.value.third)
}
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.size(100.dp)
.background(backgroundColor.value)
)
}
view raw MainActivity.kt hosted with ❤ by GitHub

Our output will look like this.

Compose Color picker in Emulator

 

Any criticism is welcomed. Kindly post it in comments.

Check out the code at my github.

#HappyCoding

 

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Compose is a relatively young technology for writing declarative UI. Many developers don’t even…
READ MORE
blog
When it comes to the contentDescription-attribute, I’ve noticed a couple of things Android devs…
READ MORE
blog
In this article we’ll go through how to own a legacy code that is…
READ MORE
blog
Compose is part of the Jetpack Library released by Android last spring. Create Android…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu