Made this color picker today in just 200 lines (th...
# compose
l
Made this color picker today in just 200 lines (there could be less if I didn't need some color conversion utils). It can be updated with touch events or hex input
👍 20
🎉 4
❤️ 1
Here's the code. The implementation isn't the best but I thought it'd be nice to share
Copy code
@OptIn(ExperimentalLayout::class)
@Composable
fun ColorPalette(
  initialColor: Color = Color.White,
  onColorChanged: (Color) -> Unit = {}
) {
  var selectedColor by remember { mutableStateOf(initialColor) }
  var selectedColorHex by remember { mutableStateOf(selectedColor.toHexString()) }

  var hue by remember { mutableStateOf(initialColor.toHsv()[0]) }
  var hueCursor by remember { mutableStateOf(0f) }

  var matrixSize by remember { mutableStateOf(IntSize(0, 0)) }
  var matrixCursor by remember { mutableStateOf(Offset(0f, 0f)) }

  val saturationGradient = remember(hue, matrixSize) {
    LinearGradient(
      colors = listOf(Color(0xFFFFFFFF), hueToColor(hue)),
      startX = 0f, startY = 0f, endX = matrixSize.width.toFloat(), endY = 0f
    )
  }
  val valueGradient = remember(matrixSize) {
    LinearGradient(
      colors = listOf(Color(0xFFFFFFFF), Color(0xFF000000)),
      startX = 0f, startY = 0f, endX = 0f, endY = matrixSize.height.toFloat())
  }

  fun setNewColor(color: Color, invalidate: Boolean = false) {
    selectedColor = color
    selectedColorHex = color.toHexString()
    if (invalidate) {
      val hsv = color.toHsv()
      hue = hsv[0]
      matrixCursor = satValToCoordinates(hsv[1], hsv[2], matrixSize)
      hueCursor = hueToCoordinate(hsv[0], matrixSize)
    }
    onColorChanged(color)
  }

  Column {
    Row(Modifier.preferredHeight(IntrinsicSize.Max)) {
      Box(Modifier
        .padding(8.dp)
        .aspectRatio(1f)
        .weight(1f)
        .onSizeChanged {
          matrixSize = it
          val hsv = selectedColor.toHsv()
          matrixCursor = satValToCoordinates(hsv[1], hsv[2], it)
          hueCursor = hueToCoordinate(hue, it)
        }
        .drawWithContent {
          drawRect(brush = valueGradient)
          drawRect(brush = saturationGradient, blendMode = BlendMode.Multiply)
          drawCircle(Color.White, radius = 15f, center = matrixCursor, style = Stroke(10f))
        }
        .pointerInteropFilter { ev ->
          if (ev.action == MotionEvent.ACTION_DOWN || ev.action == MotionEvent.ACTION_MOVE) {
            val safeX = ev.x.coerceIn(0f, matrixSize.width.toFloat())
            val safeY = ev.y.coerceIn(0f, matrixSize.height.toFloat())
            matrixCursor = Offset(safeX, safeY)
            val newColor = matrixCoordinatesToColor(hue, safeX, safeY, matrixSize)
            setNewColor(newColor)
          }

          true
        })

      Box(Modifier.fillMaxHeight().width(48.dp).padding(8.dp).drawWithCache {
        var h = 360f
        val colors = MutableList(size.height.toInt()) {
          hsvToColor(h, 1f, 1f).also {
            h -= 360f / size.height
          }
        }

        onDraw {
          colors.fastForEachIndexed { i, color ->
            val pos = i.toFloat()
            drawLine(color, Offset(0f, pos), Offset(size.width, pos))
          }
          drawRect(Color.White, topLeft = Offset(0f, hueCursor), size = Size(size.width, 10f),
            style = Stroke(4f))
        }
      }.pointerInteropFilter { ev ->
        if (ev.action == MotionEvent.ACTION_DOWN || ev.action == MotionEvent.ACTION_MOVE) {
          val safeY = ev.y.coerceIn(0f, matrixSize.height.toFloat())
          hueCursor = safeY
          hue = hueCoordinatesToHue(safeY, matrixSize)
          val newColor = matrixCoordinatesToColor(hue, matrixCursor.x, matrixCursor.y, matrixSize)
          setNewColor(newColor)
        }
        true
      })
    }

    Row(Modifier.padding(32.dp)) {
      Box(Modifier.size(100.dp, 56.dp).background(selectedColor))
      TextField(
        value = selectedColorHex,
        modifier = Modifier.padding(start = 16.dp).width(124.dp),
        onValueChange = {
          val newColor = hexStringToColor(it)
          if (newColor != null) {
            setNewColor(newColor, invalidate = true)
          } else {
            selectedColorHex = it
          }
        }
      )
    }
  }
}

// Coordinates <-> Color

private fun matrixCoordinatesToColor(hue: Float, x: Float, y: Float, size: IntSize): Color {
  val saturation = 1f / size.width * x
  val value = 1f - (1f / size.height * y)
  return hsvToColor(hue, saturation, value)
}

private fun hueCoordinatesToHue(y: Float, size: IntSize): Float {
  val hue = 360f - y * 360f / size.height
  return hsvToColor(hue, 1f, 1f).toHsv()[0]
}

private fun satValToCoordinates(saturation: Float, value: Float, size: IntSize): Offset {
  return Offset(saturation * size.width, ((1f - value) * size.height))
}

private fun hueToCoordinate(hue: Float, size: IntSize): Float {
  return size.height - (hue * size.height / 360f)
}

// Color space conversions

private fun hsvToColor(hue: Float, saturation: Float, value: Float): Color {
  val f = floatArrayOf(hue, saturation, value)
  return Color(android.graphics.Color.HSVToColor(f))
}

private fun Color.toHsv(): FloatArray {
  val result = floatArrayOf(0f, 0f, 0f)
  android.graphics.Color.colorToHSV(toArgb(), result)
  return result
}

private fun hueToColor(hue: Float): Color {
  return hsvToColor(hue, 1f, 1f)
}

private fun Color.toHexString(): String {
  return String.format("#%06X", (0xFFFFFF and toArgb()))
}

private fun hexStringToColor(hex: String): Color? {
  return try {
    Color(android.graphics.Color.parseColor(hex))
  } catch (e: Exception) {
    null
  }
}
a
would be better if you share a gist with it
j
Even a repo on GitHub to give a star 😛