Android Color Picker
Process
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 composables.
You can create a callback in the Android custom color picker composable and return the value captured if needed for further processing. An example of a callback implementation is located at bottom of post.
The color creation is an array based on the size of the screen. The colors are added to it using Color.hsl()Β with the Float value calculated from the given part of the screen. Meaning, the first part of the function requires a float value 0.0F to 1.0F, coordinate of middle of screen is 0.5F. Then the function would be used as Color.hsl(0.5f, 1f, 0.5f)Β to get the color.
Code
MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SampleColorPickerTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ColorPicker(
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}ColorPicker
@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)
}Update
After creation, I realized I overlooked the fact that there is no dark or light version of colors including full black or full white. As pictured below, the old version is on the left and the new one is on the right.
I needed to quickly add the saturation and the light adjustment of the colors. This would add the dark and light variations of the displayed colors.
Fix
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 shown below.
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.
Android Color Picker - 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 ->
//retrieveSelectedColor is the value of the selected color
}
}
}
}
}
}
@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
}Hope this was useful.



