Below I will show you how to create a simple color picker in Android Studio with Jetpack Compose.
The element used is a Canvas that captures the touch motion.
This is a rough sketch I just put together. There is more customization needed to implement into your own project / use case.
I included some basic debugging information in the form of visual Text elements.
You can create a callback in the ColorPicker Composable and return the value captured if needed for further processing.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SampleColorPickerTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ColorPicker(
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun ColorPicker(modifier: Modifier = Modifier) {
var dragOffset by remember {
mutableStateOf(Offset.Zero)
}
var mSize by remember {
// preset size of view
mutableStateOf(IntSize(360, 100))
}
var color: List<Int> = (0..mSize.width).toList()
val colorList = color.map { color[it].toFloat() }
var hueList: List<Color> = emptyList()
//reset view size to fill maxWidth after composition
if (mSize != IntSize.Zero) {
hueList =
colorList.map { Color.hsl(getFloatFromWidth(it, mSize.width), 1f, 0.5f) }
}
Column(modifier = modifier) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.onGloballyPositioned {
mSize = it.size
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
dragOffset = change.position
}
},
onDraw = {
drawRoundRect(
brush = Brush.horizontalGradient(
hueList,
),
topLeft = Offset.Zero,
size = size
)
drawCircle(
color = Color.White,
radius = 35f,
center = dragOffset,
style = Stroke(width = 5f)
)
}
)
Text(text = "${dragOffset.x}, ${dragOffset.y}")
if (hueList.isNotEmpty()) {
Row {
Text(text = "${dragOffset.x.roundToInt()}", textAlign = TextAlign.End)
}
Row(
modifier = Modifier
.size(100.dp)
.padding(15.dp)
.background(
hueList[dragOffset.x.roundToInt()],
shape = RoundedCornerShape(50.dp)
)
) {
}
}
}
}
fun getFloatFromWidth(it: Float, mSize: Int): Float {
return ((it / mSize) * 360f)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
SampleColorPickerTheme {
ColorPicker()
}
}

UPDATE


I wanted to elaborate a little more on this. I wanted to be able to quickly add the saturation and the light adjustment. As pictured above, the old version is on the left and the new one is on the right.
After creation, I realized I overlooked the fact that there is no dark or light version of colors including full black or full white.
The simplest way I figured to do this is create a separate Rect above the color picker Rect in the canvas. This has a verticalGradient applied to it with color stops of
drawRoundRect(
brush = Brush.verticalGradient(
0.0f to Color.Black,
0.5f to Color.Transparent,
1.0f to Color.White,
startY = Float.MIN_VALUE,
endY = Float.POSITIVE_INFINITY,
tileMode = TileMode.Clamp
),
topLeft = Offset.Zero,
size = size
)
This is FAR from a proper way to implement but I wanted to create a quick fix without changing too much of the existing code.
A better way to implement is create a separate entity to hold the change for saturation and also another one to hold the change in light.
I did however create a callback to show how you can retrieve the Color value.
Here is the new version
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SampleColorPickerTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ColorPicker(
modifier = Modifier.padding(innerPadding)
) { retrieveSelectedColor ->
}
}
}
}
}
}
@Composable
fun ColorPicker(modifier: Modifier = Modifier, retrieveSelectedColor: (Color) -> Unit) {
var dragOffset by remember {
mutableStateOf(Offset.Zero)
}
var mSize by remember {
mutableStateOf(IntSize(360, 100))
}
var activeColor by remember {
mutableStateOf(Color.White)
}
var color: List<Int> = (0..mSize.width).toList()
val colorList = color.map { color[it].toFloat() }
var hueList: List<Color> = emptyList()
if (mSize != IntSize.Zero) {
hueList =
colorList.map { Color.hsl(getFloatFromWidth(it, mSize.width), 1f, 0.5f) }
}
Column(modifier = modifier) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.onGloballyPositioned {
mSize = it.size
}
.pointerInput(Unit) {
detectTapGestures(
onPress = { position ->
dragOffset = position
activeColor = Color.hsl(
(dragOffset.x / mSize.width) * 360f,
getFloatFromHeight(dragOffset.y, mSize.height.toFloat()),
getFloatFromHeight(dragOffset.y, mSize.height.toFloat()),
1.0f
)
retrieveSelectedColor(activeColor)
}
)
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
dragOffset = change.position
//below catches if the current position is out of bounds and keeps it in the Rect
if (dragOffset.x > size.width) dragOffset = dragOffset.copy(x = size.width.toFloat())
if (dragOffset.x < 0) dragOffset = dragOffset.copy(x = 0f)
if (dragOffset.y > size.height) dragOffset = dragOffset.copy(y = size.height.toFloat())
if (dragOffset.y < 0) dragOffset = dragOffset.copy(y = 0f)
//here is the change, its a rough sketch that should be implemented better but this works
activeColor = Color.hsl(
(dragOffset.x / mSize.width) * 360f,
getFloatFromHeight(dragOffset.y, mSize.height.toFloat()),
getFloatFromHeight(dragOffset.y, mSize.height.toFloat()),
1.0f
)
//callback to hand current color to hand off current color
retrieveSelectedColor(activeColor)
}
},
onDraw = {
drawRoundRect(
brush = Brush.horizontalGradient(
hueList,
),
topLeft = Offset.Zero,
size = size
)
//change to simulate the saturation and light transition, could be done better with another Compoosable function
drawRoundRect(
brush = Brush.verticalGradient(
0.0f to Color.Black,
0.5f to Color.Transparent,
1.0f to Color.White,
startY = Float.MIN_VALUE,
endY = Float.POSITIVE_INFINITY,
tileMode = TileMode.Clamp
),
topLeft = Offset.Zero,
size = size
)
drawCircle(
color = Color.White,
radius = 35f,
center = dragOffset,
style = Stroke(width = 5f)
)
}
)
Text(text = "${dragOffset.x}, ${dragOffset.y}")
if (hueList.isNotEmpty()) {
Row {
Text(text = "${dragOffset.x.roundToInt()}", textAlign = TextAlign.End)
}
Row(
modifier = Modifier
.size(100.dp)
.padding(15.dp)
.background(
activeColor,
shape = RoundedCornerShape(50.dp)
)
) {
}
}
}
}
fun getFloatFromWidth(it: Float, mSize: Int): Float {
return ((it / mSize) * 360f)
}
fun getFloatFromHeight(it: Float, mSize: Float): Float {
var sat_lght = (it / mSize)
//keep variable in line between 0.0f and 1.0f
if (sat_lght > 1.0f) sat_lght = 1.0f
if (sat_lght < 0f) sat_lght = 0f
return sat_lght
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
SampleColorPickerTheme {
ColorPicker() { retrieveSelectedColor ->
}
}
}
