0

My app shows buttons depending on logged user.

Details and Conditions of the app:

  • The number of buttons that will be shown is unknown (could be 1, 20, 100, etc.)
  • A different color must be set for each button
  • I want some control over color values, as text is always white, so text must always be readable

How can I create a dynamic color value under these conditions?

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
  • 1
    [This](https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color) would help – Zain Nov 18 '22 at 18:48
  • Thanks! A lot to read and try there, there should be a solution there – Leonel Fusco Nov 18 '22 at 19:11

1 Answers1

1

First you need to decide how "readable" the text should be. WCAG 2.1 is a common standard for accessibility requirements, and its minimum contrast requirement is 4.5:1. (The spec definition is here, or for a lighter overview with some nice examples there's this.)

That amount of contrast will guarantee your text can be read by people with "moderately low vision". 3:1 is recommended for "standard text and standard vision", but I'd always recommend going with the accessible ratio - especially since you're using white and random colours, which will vary in readability quite a bit!

WCAG 2.1 also allows for that 3:1 ratio for large-scale text, which is 18pt or 14pt bold. That works out to about 40dp for regular text and 31dp for bold. Depends on the font too, and also since you often use sp instead the user can control how big the fonts are, so it complicates things. But basically, big text = lower contrast requirements


Now you have your contrast level, you can check whether your colour combo meets it or not. There's a nice tool in ColorUtils that does this for you - it uses the WCAG formula for calculating contrast:

fun meetsMinContrast(@ColorInt foreground: Int, @ColorInt background: Int): Boolean {
    val minContrast = 4.5
    val actual = ColorUtils.calculateContrast(foreground, background)
    return actual >= minContrast
}

As for actually generating colours, I don't know the "smart" way to do it, if there is one - you could possibly generate a colour space of valid colours when paired with white, and pick from that, but I don't really know anything about it - maybe someone else can chime in with a better solution!

So for a purely naive random approach:

val colours = generateSequence {
    Color.valueOf(
        Random.nextInt(0, 255),
        Random.nextInt(0, 255),
        Random.nextInt(0, 255)
    )
}

val accessibleBackgrounds = colours.filter { background ->
    meetsMinContrast(Color.WHITE, background)
}

and then you have a stream of valid, random colours you can set on your buttons.

If you don't like the "just keep generating randomly until you hit one that works" approach (which is pretty hacky and could be slow if you're unlucky), you could work with HSV instead:

fun getAccessibleBackground(): Color {
    val hsv = floatArrayOf(
        Random.nextFloat() * 360f,
        Random.nextFloat(),
        Random.nextFloat()
    )
    var colour: Color

    while(true) {
        colour = Color.HSVtoColor(hsv)
        if (meetsMinContrast(Color.WHITE, colour)) return colour
        // darken the colour a bit (subtract 1% value) and try again
        hsv[2] -= 0.01f
    }
}

(I'd be more explicit about error checking and providing a fallback there, especially if you made it work with a foreground colour parameter, but that should always return a valid colour before you need to worry about subtracting from value too much - it's just a simple example)

cactustictacs
  • 17,935
  • 2
  • 14
  • 25