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:
- #RGB
- #ARGB
- #RRGGBB
- #AARRGGBB
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.