diff --git a/glance/src/main/kotlin/de/jeanlucmakiola/floret/glance/FloretGlanceTheme.kt b/glance/src/main/kotlin/de/jeanlucmakiola/floret/glance/FloretGlanceTheme.kt index 8eb19f8..c494afb 100644 --- a/glance/src/main/kotlin/de/jeanlucmakiola/floret/glance/FloretGlanceTheme.kt +++ b/glance/src/main/kotlin/de/jeanlucmakiola/floret/glance/FloretGlanceTheme.kt @@ -46,7 +46,7 @@ fun FloretGlanceTheme( WidgetThemeMode.LIGHT -> ColorProviders(if (dynamic) dynamicLightColorScheme(context) else lightScheme) WidgetThemeMode.DARK -> ColorProviders(if (dynamic) dynamicDarkColorScheme(context) else darkScheme) } - CompositionLocalProvider(LocalWidgetThemeMode provides mode) { + CompositionLocalProvider(LocalWidgetThemeMode provides mode, LocalWidgetDynamicColor provides dynamic) { GlanceTheme(colors = colors, content = content) } } @@ -54,6 +54,9 @@ fun FloretGlanceTheme( /** The [WidgetThemeMode] the nearest [FloretGlanceTheme] was given. */ val LocalWidgetThemeMode = compositionLocalOf { WidgetThemeMode.SYSTEM } +/** Whether the nearest [FloretGlanceTheme] draws Material You colours. */ +val LocalWidgetDynamicColor = compositionLocalOf { false } + /** * A colour an app computes per theme (an event colour, say), resolved like the * theme around it: a day/night pair on [WidgetThemeMode.SYSTEM], only [day] or diff --git a/glance/src/main/kotlin/de/jeanlucmakiola/floret/glance/RemoteViewsColors.kt b/glance/src/main/kotlin/de/jeanlucmakiola/floret/glance/RemoteViewsColors.kt new file mode 100644 index 0000000..7656ae4 --- /dev/null +++ b/glance/src/main/kotlin/de/jeanlucmakiola/floret/glance/RemoteViewsColors.kt @@ -0,0 +1,131 @@ +package de.jeanlucmakiola.floret.glance + +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.os.Build +import android.widget.RemoteViews +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.glance.GlanceTheme +import androidx.glance.LocalContext +import androidx.glance.color.ColorProviders +import androidx.glance.unit.ColorProvider + +/** + * A colour for a view a widget draws as plain [RemoteViews] (embedded through + * `AndroidRemoteViews`), resolved like the Glance theme around it. + */ +sealed interface RemoteColor { + /** A colour resource with its own night variant: the host resolves it, so it follows palette and light/dark changes. */ + data class Resource(@ColorRes val resId: Int) : RemoteColor + + /** A light/dark pair; equal when the widget pins one theme. */ + data class DayNight(@ColorInt val day: Int, @ColorInt val night: Int) : RemoteColor +} + +/** The theme roles [remoteColor] hands to plain RemoteViews. */ +enum class WidgetColorRole(@ColorRes internal val resId: Int) { + ON_SURFACE(R.color.floret_widget_on_surface), + ON_SURFACE_VARIANT(R.color.floret_widget_on_surface_variant), + PRIMARY(R.color.floret_widget_primary), + ON_PRIMARY(R.color.floret_widget_on_primary), + ON_SECONDARY_CONTAINER(R.color.floret_widget_on_secondary_container), +} + +/** + * [role] as the nearest [FloretGlanceTheme] and [WidgetSurface] draw it: the + * system palette's resource when the theme follows Material You and the device, + * else the theme's own light/dark pair. Text roles on a [WidgetBackground.TINTED] + * surface move to `onSecondaryContainer`, as `GlanceTheme.colors` does there. + */ +@Composable +fun remoteColor(role: WidgetColorRole): RemoteColor { + val tinted = LocalWidgetBackground.current == WidgetBackground.TINTED + val resolved = if (tinted && (role == WidgetColorRole.ON_SURFACE || role == WidgetColorRole.ON_SURFACE_VARIANT)) { + WidgetColorRole.ON_SECONDARY_CONTAINER + } else { + role + } + val mode = LocalWidgetThemeMode.current + if (mode == WidgetThemeMode.SYSTEM && LocalWidgetDynamicColor.current && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return RemoteColor.Resource(resolved.resId) + } + val provider = GlanceTheme.colors.provider(resolved) + val contexts = dayNightContexts() + return when (mode) { + WidgetThemeMode.SYSTEM -> RemoteColor.DayNight( + day = provider.getColor(contexts.first).toArgb(), + night = provider.getColor(contexts.second).toArgb(), + ) + WidgetThemeMode.LIGHT -> provider.getColor(contexts.first).toArgb().let { RemoteColor.DayNight(it, it) } + WidgetThemeMode.DARK -> provider.getColor(contexts.second).toArgb().let { RemoteColor.DayNight(it, it) } + } +} + +/** An app-computed [day]/[night] colour (an event colour, say) for plain RemoteViews, pinned like [themedColor]. */ +fun WidgetThemeMode.remoteColor(day: Color, night: Color): RemoteColor.DayNight = when (this) { + WidgetThemeMode.SYSTEM -> RemoteColor.DayNight(day.toArgb(), night.toArgb()) + WidgetThemeMode.LIGHT -> RemoteColor.DayNight(day.toArgb(), day.toArgb()) + WidgetThemeMode.DARK -> RemoteColor.DayNight(night.toArgb(), night.toArgb()) +} + +/** Sets [viewId]'s text colour to [color]. */ +fun RemoteViews.setTextColor(context: Context, viewId: Int, color: RemoteColor) { + when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> setTextColor(viewId, color.current(context)) + color is RemoteColor.Resource -> setColorStateList(viewId, "setTextColor", color.resId) + color is RemoteColor.DayNight -> setColorInt(viewId, "setTextColor", color.day, color.night) + } +} + +/** + * Tints [viewId]'s image with [color]. Always the tint list from API 31 on, so a + * host reapplying a new rendering over the last never stacks a filter on a tint. + */ +fun RemoteViews.setImageTint(context: Context, viewId: Int, color: RemoteColor) { + when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> setInt(viewId, "setColorFilter", color.current(context)) + color is RemoteColor.Resource -> setColorStateList(viewId, "setImageTintList", color.resId) + color is RemoteColor.DayNight -> setColorStateList( + viewId, + "setImageTintList", + ColorStateList.valueOf(color.day), + ColorStateList.valueOf(color.night), + ) + } +} + +private fun RemoteColor.current(context: Context): Int = when (this) { + is RemoteColor.Resource -> context.getColor(resId) + is RemoteColor.DayNight -> if (context.isNight()) night else day +} + +private fun Context.isNight(): Boolean = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + +@Composable +private fun dayNightContexts(): Pair { + val context = LocalContext.current + return remember(context) { context.withNight(false) to context.withNight(true) } +} + +private fun Context.withNight(night: Boolean): Context { + val config = Configuration(resources.configuration).apply { + uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or + if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO + } + return createConfigurationContext(config) +} + +private fun ColorProviders.provider(role: WidgetColorRole): ColorProvider = when (role) { + WidgetColorRole.ON_SURFACE -> onSurface + WidgetColorRole.ON_SURFACE_VARIANT -> onSurfaceVariant + WidgetColorRole.PRIMARY -> primary + WidgetColorRole.ON_PRIMARY -> onPrimary + WidgetColorRole.ON_SECONDARY_CONTAINER -> onSecondaryContainer +} diff --git a/glance/src/main/res/values-night-v31/floret_widget_colors.xml b/glance/src/main/res/values-night-v31/floret_widget_colors.xml new file mode 100644 index 0000000..d6143ed --- /dev/null +++ b/glance/src/main/res/values-night-v31/floret_widget_colors.xml @@ -0,0 +1,9 @@ + + + + @android:color/system_neutral1_100 + @android:color/system_neutral2_200 + @android:color/system_accent1_200 + @android:color/system_accent1_800 + @android:color/system_accent2_100 + diff --git a/glance/src/main/res/values-night/floret_widget_colors.xml b/glance/src/main/res/values-night/floret_widget_colors.xml new file mode 100644 index 0000000..1868de7 --- /dev/null +++ b/glance/src/main/res/values-night/floret_widget_colors.xml @@ -0,0 +1,9 @@ + + + + #FFE4E1E6 + #FFC6C5D0 + #FFBAC3FF + #FF08218A + #FFDEE1F9 + diff --git a/glance/src/main/res/values-v31/floret_widget_colors.xml b/glance/src/main/res/values-v31/floret_widget_colors.xml new file mode 100644 index 0000000..82d4432 --- /dev/null +++ b/glance/src/main/res/values-v31/floret_widget_colors.xml @@ -0,0 +1,9 @@ + + + + @android:color/system_neutral1_900 + @android:color/system_neutral2_700 + @android:color/system_accent1_600 + @android:color/system_accent1_0 + @android:color/system_accent2_900 + diff --git a/glance/src/main/res/values/floret_widget_colors.xml b/glance/src/main/res/values/floret_widget_colors.xml new file mode 100644 index 0000000..a84fe7b --- /dev/null +++ b/glance/src/main/res/values/floret_widget_colors.xml @@ -0,0 +1,9 @@ + + + + #FF1B1B1F + #FF46464F + #FF4355B9 + #FFFFFFFF + #FF171B2C +