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)