diff --git a/app/src/main/java/org/tasks/compose/ShareInvite.kt b/app/src/main/java/org/tasks/compose/ShareInvite.kt index 6d238edd7..d4ac3c84c 100644 --- a/app/src/main/java/org/tasks/compose/ShareInvite.kt +++ b/app/src/main/java/org/tasks/compose/ShareInvite.kt @@ -36,7 +36,7 @@ private fun Invite() = TasksTheme { @Preview(showBackground = true, backgroundColor = 0x202124) @Composable -private fun InviteDark() = TasksTheme(useDarkTheme = true) { +private fun InviteDark() = TasksTheme(theme = 2) { ShareInvite(false, remember { mutableStateOf("") }) } @@ -48,7 +48,7 @@ private fun InviteFilled() = TasksTheme { @Preview(showBackground = true, backgroundColor = 0x202124) @Composable -private fun InviteDarkFilled() = TasksTheme(useDarkTheme = true) { +private fun InviteDarkFilled() = TasksTheme(theme = 2) { ShareInvite(false, remember { mutableStateOf("user@example.com") }) } diff --git a/app/src/main/java/org/tasks/themes/ThemeColor.java b/app/src/main/java/org/tasks/themes/ThemeColor.java index b47b73b0e..1b2c30452 100644 --- a/app/src/main/java/org/tasks/themes/ThemeColor.java +++ b/app/src/main/java/org/tasks/themes/ThemeColor.java @@ -1,6 +1,7 @@ package org.tasks.themes; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo; +import static org.tasks.themes.ColorUtilsKt.calculateContrast; import android.app.Activity; import android.app.ActivityManager; @@ -14,7 +15,6 @@ import android.view.View; import androidx.annotation.ColorInt; import androidx.annotation.RequiresApi; -import androidx.core.graphics.ColorUtils; import androidx.core.os.ParcelCompat; import com.google.android.material.bottomappbar.BottomAppBar; @@ -155,9 +155,6 @@ public class ThemeColor implements Pickable { } }; - private static final int BLUE = -14575885; - private static final int WHITE = -1; - private final int original; private final int colorOnPrimary; private final int colorPrimary; @@ -170,18 +167,18 @@ public class ThemeColor implements Pickable { public ThemeColor(Context context, int original, int color) { this.original = original; if (color == 0) { - color = BLUE; + color = TasksThemeKt.BLUE; } else { color |= 0xFF000000; // remove alpha } colorPrimary = color; - double contrast = ColorUtils.calculateContrast(WHITE, colorPrimary); + double contrast = calculateContrast(TasksThemeKt.WHITE, colorPrimary); isDark = contrast < 3; if (isDark) { colorOnPrimary = context.getColor(R.color.black_87); } else { - colorOnPrimary = WHITE; + colorOnPrimary = TasksThemeKt.WHITE; } } diff --git a/kmp/src/commonMain/kotlin/org/tasks/themes/ColorUtils.kt b/kmp/src/commonMain/kotlin/org/tasks/themes/ColorUtils.kt new file mode 100644 index 000000000..afa78d2b1 --- /dev/null +++ b/kmp/src/commonMain/kotlin/org/tasks/themes/ColorUtils.kt @@ -0,0 +1,90 @@ +package org.tasks.themes + +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow + +fun calculateContrast(foreground: Int, background: Int): Double { + var foreground = foreground + require(alpha(background) == 255) { + ("background can not be translucent: #" + Integer.toHexString(background)) + } + if (alpha(foreground) < 255) { + // If the foreground is translucent, composite the foreground over the background + foreground = compositeColors(foreground, background) + } + + val luminance1: Double = calculateLuminance(foreground) + 0.05 + val luminance2: Double = calculateLuminance(background) + 0.05 + + // Now return the lighter luminance divided by the darker luminance + return max(luminance1, luminance2) / min(luminance1, luminance2) +} + +private fun compositeAlpha(foregroundAlpha: Int, backgroundAlpha: Int): Int = + 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF) + +private fun compositeComponent(fgC: Int, fgA: Int, bgC: Int, bgA: Int, a: Int): Int { + if (a == 0) return 0 + return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF) +} + +private fun compositeColors(foreground: Int, background: Int): Int { + val bgAlpha = alpha(background) + val fgAlpha = alpha(foreground) + val a = compositeAlpha(fgAlpha, bgAlpha) + return argb( + alpha = a, + red = compositeComponent(red(foreground), fgAlpha, red(background), bgAlpha, a), + green = compositeComponent(green(foreground), fgAlpha, green(background), bgAlpha, a), + blue = compositeComponent(blue(foreground), fgAlpha, blue(background), bgAlpha, a) + ) +} + +private fun alpha(color: Int): Int = color ushr 24 + +private fun red(color: Int): Int = (color shr 16) and 0xFF + +private fun green(color: Int): Int = (color shr 8) and 0xFF + +private fun blue(color: Int): Int = color and 0xFF + +private fun argb(alpha: Int, red: Int, green: Int, blue: Int) = + (alpha shl 24) or (red shl 16) or (green shl 8) or blue + +private fun calculateLuminance(color: Int): Double { + val result: DoubleArray = getTempDouble3Array() + colorToXYZ(color, result) + // Luminance is the Y component + return result[1] / 100 +} + +private fun getTempDouble3Array(): DoubleArray { + var result: DoubleArray? = TEMP_ARRAY.get() + if (result == null) { + result = DoubleArray(3) + TEMP_ARRAY.set(result) + } + return result +} + +private val TEMP_ARRAY = ThreadLocal() + +private fun colorToXYZ(color: Int, outXyz: DoubleArray) { + RGBToXYZ(red(color), green(color), blue(color), outXyz) +} + +private fun RGBToXYZ(r: Int, g: Int, b: Int, outXyz: DoubleArray) { + require(outXyz.size == 3) { "outXyz must have a length of 3." } + + var sr = r / 255.0 + sr = if (sr < 0.04045) sr / 12.92 else ((sr + 0.055) / 1.055).pow(2.4) + var sg = g / 255.0 + sg = if (sg < 0.04045) sg / 12.92 else ((sg + 0.055) / 1.055).pow(2.4) + var sb = b / 255.0 + sb = if (sb < 0.04045) sb / 12.92 else ((sb + 0.055) / 1.055).pow(2.4) + + outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805) + outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722) + outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505) +} diff --git a/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt b/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt index af4e9ddfc..c3897e93b 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt @@ -1,18 +1,50 @@ package org.tasks.themes import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance + +const val BLUE = -14575885 +const val WHITE = -1 + + +@Composable +fun ColorScheme.isDark() = this.background.luminance() <= 0.5 @Composable fun TasksTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), + theme: Int = 5, + primary: Int = BLUE, content: @Composable () -> Unit, ) { + val colorScheme = when (theme) { + 0 -> lightColorScheme() + 1 -> darkColorScheme( + surface = Color.Black, + background = Color.Black, + ) + + 2, 3 -> darkColorScheme() + else -> if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + } + val colorOnPrimary = remember(primary) { + if (calculateContrast(WHITE, primary) < 3) { + Color.Black + } else { + Color.White + } + } MaterialTheme( - colorScheme = if (useDarkTheme) darkColorScheme() else lightColorScheme(), + colorScheme = colorScheme.copy( + primary = Color(primary), + onPrimary = colorOnPrimary, + ), ) { content() }