glance: RemoteColor helpers for widget views drawn as plain RemoteViews, dynamic roles as palette resources
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<Context, Context> {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The dynamic roles Glance's own theme maps, for widget views drawn as plain RemoteViews. -->
|
||||
<resources>
|
||||
<color name="floret_widget_on_surface">@android:color/system_neutral1_100</color>
|
||||
<color name="floret_widget_on_surface_variant">@android:color/system_neutral2_200</color>
|
||||
<color name="floret_widget_primary">@android:color/system_accent1_200</color>
|
||||
<color name="floret_widget_on_primary">@android:color/system_accent1_800</color>
|
||||
<color name="floret_widget_on_secondary_container">@android:color/system_accent2_100</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Fallbacks only: floret reads these roles from the system palette, on API 31+ (values-v31). -->
|
||||
<resources>
|
||||
<color name="floret_widget_on_surface">#FFE4E1E6</color>
|
||||
<color name="floret_widget_on_surface_variant">#FFC6C5D0</color>
|
||||
<color name="floret_widget_primary">#FFBAC3FF</color>
|
||||
<color name="floret_widget_on_primary">#FF08218A</color>
|
||||
<color name="floret_widget_on_secondary_container">#FFDEE1F9</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The dynamic roles Glance's own theme maps, for widget views drawn as plain RemoteViews. -->
|
||||
<resources>
|
||||
<color name="floret_widget_on_surface">@android:color/system_neutral1_900</color>
|
||||
<color name="floret_widget_on_surface_variant">@android:color/system_neutral2_700</color>
|
||||
<color name="floret_widget_primary">@android:color/system_accent1_600</color>
|
||||
<color name="floret_widget_on_primary">@android:color/system_accent1_0</color>
|
||||
<color name="floret_widget_on_secondary_container">@android:color/system_accent2_900</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Fallbacks only: floret reads these roles from the system palette, on API 31+ (values-v31). -->
|
||||
<resources>
|
||||
<color name="floret_widget_on_surface">#FF1B1B1F</color>
|
||||
<color name="floret_widget_on_surface_variant">#FF46464F</color>
|
||||
<color name="floret_widget_primary">#FF4355B9</color>
|
||||
<color name="floret_widget_on_primary">#FFFFFFFF</color>
|
||||
<color name="floret_widget_on_secondary_container">#FF171B2C</color>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user