From 9e045a34b1e07fb233e2ef2b8900c10f075f1bb9 Mon Sep 17 00:00:00 2001 From: Robert Ros Date: Thu, 12 Feb 2026 20:58:30 +0100 Subject: [PATCH 1/3] Calculate the better contrasting color for translucent backgrounds using a generalized heuristic. --- .../org/fossify/commons/extensions/Int.kt | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt b/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt index 42de9253a..3b6e37d31 100644 --- a/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt +++ b/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt @@ -15,16 +15,51 @@ import java.util.Locale import java.util.Random import kotlin.math.log10 import kotlin.math.pow +import kotlin.math.sqrt fun Int.getContrastColor(): Int { return getContrastColor(DARK_GREY, Color.WHITE) } -fun Int.getContrastColor(colorFirst: Int, colorSecond: Int): Int { - val contrastFirst = ColorUtils.calculateContrast(colorFirst, this) - val contrastSecond = ColorUtils.calculateContrast(colorSecond, this) +fun Int.getContrastColor(firstColor: Int, secondColor: Int): Int { + // Opaque background + if (Color.alpha(this) == 255) { + val contrastFirstColor = ColorUtils.calculateContrast(firstColor, this) + val contrastSecondColor = ColorUtils.calculateContrast(secondColor, this) - return if (contrastFirst >= contrastSecond) colorFirst else colorSecond + return if (contrastFirstColor >= contrastSecondColor) firstColor else secondColor + } + + // Translucent background: fallback heuristic + val luminanceBackground = ColorUtils.calculateLuminance(this) + val luminanceFirstColor = ColorUtils.calculateLuminance(firstColor) + val luminanceSecondColor = ColorUtils.calculateLuminance(secondColor) + + val lightColor: Int + val darkColor: Int + val luminanceLight: Double + val luminanceDark: Double + + if (luminanceFirstColor >= luminanceSecondColor) { + lightColor = firstColor + darkColor = secondColor + luminanceLight = luminanceFirstColor + luminanceDark = luminanceSecondColor + } else { + lightColor = secondColor + darkColor = firstColor + luminanceLight = luminanceSecondColor + luminanceDark = luminanceFirstColor + } + + // Compute crossover luminance where both candidates have equal WCAG contrast + // against a (hypothetical) opaque background of luminance L: + // (L + 0.05)^2 = (lLight + 0.05) * (lDark + 0.05) + val threshold = sqrt((luminanceLight + 0.05) * (luminanceDark + 0.05)) - 0.05 + + // If background is lighter than the threshold -> choose darker foreground, + // else choose lighter foreground. + return if (luminanceBackground >= threshold) darkColor else lightColor } fun Int.toHex() = String.format("#%06X", 0xFFFFFF and this).uppercase(Locale.getDefault()) From 7e5c92fcc76babd791bd3c87ed56d9392d2aed78 Mon Sep 17 00:00:00 2001 From: Robert Ros Date: Thu, 12 Feb 2026 21:43:07 +0100 Subject: [PATCH 2/3] Replace magic numbers with constants. --- .../src/main/kotlin/org/fossify/commons/extensions/Int.kt | 6 ++++-- .../main/kotlin/org/fossify/commons/helpers/Constants.kt | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt b/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt index 3b6e37d31..fa8eb7724 100644 --- a/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt +++ b/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt @@ -9,7 +9,9 @@ import android.os.Looper import androidx.core.graphics.ColorUtils import androidx.core.os.postDelayed import org.fossify.commons.helpers.DARK_GREY +import org.fossify.commons.helpers.MAX_ALPHA_INT import org.fossify.commons.helpers.WCAG_AA_NORMAL +import org.fossify.commons.helpers.WCAG_LUMINANCE_OFFSET import java.text.DecimalFormat import java.util.Locale import java.util.Random @@ -23,7 +25,7 @@ fun Int.getContrastColor(): Int { fun Int.getContrastColor(firstColor: Int, secondColor: Int): Int { // Opaque background - if (Color.alpha(this) == 255) { + if (Color.alpha(this) == MAX_ALPHA_INT) { val contrastFirstColor = ColorUtils.calculateContrast(firstColor, this) val contrastSecondColor = ColorUtils.calculateContrast(secondColor, this) @@ -55,7 +57,7 @@ fun Int.getContrastColor(firstColor: Int, secondColor: Int): Int { // Compute crossover luminance where both candidates have equal WCAG contrast // against a (hypothetical) opaque background of luminance L: // (L + 0.05)^2 = (lLight + 0.05) * (lDark + 0.05) - val threshold = sqrt((luminanceLight + 0.05) * (luminanceDark + 0.05)) - 0.05 + val threshold = sqrt((luminanceLight + WCAG_LUMINANCE_OFFSET) * (luminanceDark + WCAG_LUMINANCE_OFFSET)) - WCAG_LUMINANCE_OFFSET // If background is lighter than the threshold -> choose darker foreground, // else choose lighter foreground. diff --git a/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt b/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt index 0813dd571..19827c4a8 100644 --- a/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt +++ b/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt @@ -60,10 +60,13 @@ const val HIGHER_ALPHA = 0.75f // alpha values on a scale 0 - 255 const val LOWER_ALPHA_INT = 30 const val MEDIUM_ALPHA_INT = 90 +const val MAX_ALPHA_INT = 255 const val WCAG_AA_NORMAL = 4.5 const val WCAG_AA_LARGE = 3.0 +const val WCAG_LUMINANCE_OFFSET = 0.05 + const val HOUR_MINUTES = 60 const val DAY_MINUTES = 24 * HOUR_MINUTES const val WEEK_MINUTES = DAY_MINUTES * 7 From 94468b66a2b80a665d4534a504cccd1d2eaeff38 Mon Sep 17 00:00:00 2001 From: Robert Ros Date: Thu, 12 Feb 2026 21:51:58 +0100 Subject: [PATCH 3/3] Rename constant to make calculating the threshold more readable. --- commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt | 4 ++-- .../src/main/kotlin/org/fossify/commons/helpers/Constants.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt b/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt index fa8eb7724..9f5f383f6 100644 --- a/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt +++ b/commons/src/main/kotlin/org/fossify/commons/extensions/Int.kt @@ -11,7 +11,7 @@ import androidx.core.os.postDelayed import org.fossify.commons.helpers.DARK_GREY import org.fossify.commons.helpers.MAX_ALPHA_INT import org.fossify.commons.helpers.WCAG_AA_NORMAL -import org.fossify.commons.helpers.WCAG_LUMINANCE_OFFSET +import org.fossify.commons.helpers.LUMINANCE_OFFSET import java.text.DecimalFormat import java.util.Locale import java.util.Random @@ -57,7 +57,7 @@ fun Int.getContrastColor(firstColor: Int, secondColor: Int): Int { // Compute crossover luminance where both candidates have equal WCAG contrast // against a (hypothetical) opaque background of luminance L: // (L + 0.05)^2 = (lLight + 0.05) * (lDark + 0.05) - val threshold = sqrt((luminanceLight + WCAG_LUMINANCE_OFFSET) * (luminanceDark + WCAG_LUMINANCE_OFFSET)) - WCAG_LUMINANCE_OFFSET + val threshold = sqrt((luminanceLight + LUMINANCE_OFFSET) * (luminanceDark + LUMINANCE_OFFSET)) - LUMINANCE_OFFSET // If background is lighter than the threshold -> choose darker foreground, // else choose lighter foreground. diff --git a/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt b/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt index 19827c4a8..64ac18970 100644 --- a/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt +++ b/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt @@ -65,7 +65,7 @@ const val MAX_ALPHA_INT = 255 const val WCAG_AA_NORMAL = 4.5 const val WCAG_AA_LARGE = 3.0 -const val WCAG_LUMINANCE_OFFSET = 0.05 +const val LUMINANCE_OFFSET = 0.05 const val HOUR_MINUTES = 60 const val DAY_MINUTES = 24 * HOUR_MINUTES