The Missing Bit

Managing colors on Android with HSLuv

Android do not provide HSLuv support out of the box, here is a little trick to generate Android colors from an HSLuv source.

HSLuv

I usually work with HSLuv to manage my colors, and I will share my approach for Android app here.

I explain the benefits of HSLuv in a separate article. If you don't know what HSLuv is, you should read it first.

Colors on Android

Android uses color resources which support colors in the following formats:

Managing color in this format directly is not very practical. Android Studio has tools to manage colors, but I don't use Android Studio so I need a text based solution.

Typically, color resources are contained into colors.xml, like so:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<resources>
    <color name="red">#FF0000</color>
    <color name="pink">#FF00FF</color>
</resources>

Managing XML by hand is very cumbersome, and writing colors in RGB is also unpractical.

While we could create colors in Java or Kotlin, having color resources is the way Android is supposed to work as there are a few places where you can only reference color resources.

Defining colors

I tried a few formats, TOML, JSON… to finally select CSV. The reason is two folds, it's easy to parse (well, not really easy as CSV is surprisingly convoluted, but the library support is good) and it's easy to write, especially with editor plug-ins like Tabularize for vim.

Here is an example file:

name             , hue , saturation , luminance
error            , 10  , 100        , 50
ok               , 122 , 100        , 50
background       , 160 , 5          , 10
textPrimary      , 160 , 5          , 85
pink             , 290 , 100        , 50

Your mileage may vary, as your editor plug-in might work differently.

Converting colors

To avoid relying on an external tool while working on Android, I wrote the convert task in Java, as a Gradle task.

The task look like this and lives in buildSrc/src/main/kotlin/ColorTask.kt in your project:


import org.gradle.api.*
import org.gradle.api.tasks.*
import org.hsluv.HUSLColorConverter
import java.io.*
import org.apache.commons.csv.*
import javax.xml.parsers.*
import javax.xml.transform.*
import javax.xml.transform.dom.*
import javax.xml.transform.stream.*


open class ColorTask : DefaultTask() {
    @InputFile
    var source: File? = null

    @OutputFile
    var dest: File? = null

    fun from(path: Any) {
        source = getProject().file(path)
    }

    fun into(path: Any) {
        dest = getProject().file(path)
    }

    @TaskAction
    fun convert() {
        val parser = CSVParser(FileReader(source), CSVFormat.DEFAULT.withHeader())
        val colorRows = parser.getRecords()
        val docFactory = DocumentBuilderFactory.newInstance()
        val docBuilder = docFactory.newDocumentBuilder()
        val doc = docBuilder.newDocument()
        val rootElement = doc.createElement("resources")
        doc.appendChild(rootElement)

        for (row in colorRows) {
            val name = row.get(0).trim()
            val hue = row.get(1).toDouble()
            val saturation = row.get(2).toDouble()
            val luminance = row.get(3).toDouble()
            val colorHex = HUSLColorConverter.hsluvToHex(
                doubleArrayOf(hue, saturation, luminance)
            )

            val colorEl = doc.createElement("color")
            rootElement.appendChild(colorEl)

            val attr = doc.createAttribute("name")
            attr.setValue(name)
            colorEl.setAttributeNode(attr)
            colorEl.appendChild(doc.createTextNode(colorHex));
        }

        val transformerFactory = TransformerFactory.newInstance()
        val transformer = transformerFactory.newTransformer()
        transformer.setOutputProperty(OutputKeys.INDENT, "yes")
        transformer.setOutputProperty(
            "{http://xml.apache.org/xslt}indent-amount",
            "4"
        )
        val source = DOMSource(doc)
        val result = StreamResult(dest)

        transformer.transform(source, result)
    }
}

It is very crude, but it does the work. You can tune it to your liking.

To use it, add this to your App's build.gradle.kts:


tasks.register<ColorTask>("colors") {
    from("$projectDir/assets/colors.csv")
    into("$projectDir/src/main/res/values/colors.xml")
}

tasks.preBuild {
    dependsOn("colors")
}

The task will then generate your colors.xml file every time it runs. You can then reference those colors as usual.

I have not required alpha yet, but the task can easily be extended with an extra column for alpha channel.