Widgets with Glance: Standing out
Widgets with Glance: Standing out ź“ė Ø
Widgets can look great against a home screen wallpaper when they have a solid background (check out my articleĀ Widgets with Glance: Blending in (proandroiddev
)Ā to see how to pick a color that matches the app icons) but what if instead the background is transparent? It looks fine if the text or graphics are a good contrast from the wallpaper:
But what about if the wallpaper is not a good contrast? How do you choose a suitable color?
Even if you are using dynamic colors in yourĀ GlanceTheme
Ā (as I am in the image above), the theme system wonāt automatically check for contrast against the background. So we must do this ourselves.
First thing, we need to detect the device wallpaper. This can be done using theĀ WallpaperManager API.
First, get theĀ WallpaperManager
Ā instance, then fetch the dominant colors. A list is available, arranged in order of priority (note: a minimum color occurrence percentageĀ MIN_COLOR_OCCURRENCE
Ā ā 5% by default ā is applied for the color to appear in this list), from here we need to get the primary color and decide whether dark or light text should be used.
This can be added to theĀ GlanceTheme
Ā and initialised in a boolean state variable that can be then passed into the composableĀ content
.
@Composable
fun MotivateMeGlanceTheme(
context: Context,
content: @Composable (Boolean) -> Unit,
) {
val wallpaperManager = WallpaperManager.getInstance(context)
val colors = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)
var useDarkColorOnWallpaper by remember {
mutableStateOf(
getUseDarkColorOnWallPaper(colors, FLAG_SYSTEM) ?: false
)
}
GlanceTheme(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
GlanceTheme.colors
} else {
MotivateMeGlanceColorScheme.colors
}
) {
content.invoke(useDarkColorOnWallpaper)
}
}
In the above code we can get the wallpaper colors using
wallpaperManager.getWallpaperColors(FLAG_SYSTEM)
FLAG_SYSTEM
Ā indicates we want the colors for the home screen ā passing inĀ FLAG_LOCK
Ā would give the colors of the lock screen.
An important thing to note is thatĀ getWallpaperColors
Ā is limited toĀ API 27
Ā and above so you can either update theĀ minimumSdk
Ā of the app toĀ 27
Ā or surround this with an version check if statement.
To detect whether to use dark or light text, we can use a utility functionĀ getUseDarkColorOnWallPaper
. In this we can use the wallpaper colorsĀ colorHints
Ā to check if we should use dark text with theĀ WallpaperColors.HINT_SUPPORTS_DARK_TEXT
Ā flag. As per the API documentation,Ā HINT_SUPPORTS_DARK_TEXT
:
Info
Specifies that dark text is preferred over the current wallpaper for best presentation.
eg. A launcher may set its text color to black if this flag is specified.
There is alsoĀ HINT_SUPPORTS_DARK_THEME
Ā which could also be useful for a widget with a solid background to detect whether a dark or light background would be preferable.
UsingĀ HINT_SUPPORTS_DARK_TEXT
Ā andĀ colorHints
:
fun getUseDarkColorOnWallpaper(colors: WallpaperColors?, type: Int): Boolean? {
return if (type and FLAG_SYSTEM != 0 && colors != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
(colors.colorHints) and WallpaperColors.HINT_SUPPORTS_DARK_TEXT != 0
} else {
val hsv = FloatArray(3)
val primaryColor = colors.primaryColor.toArgb()
RGBToHSV(
primaryColor.red,
primaryColor.green,
primaryColor.blue,
hsv
)
!colorIsDarkAdvanced(primaryColor)
}
} else {
null
}
}
colorHints
Ā is only available inĀ Android 12 and above, if we are using a lower version a more manual approach is required. For this, we get the primary color as a HSV value and then evaluate the intensity and contrast in another utility function.
Note
I did not originally write this code, I found it on this StackOverflow answer from SudoPlz (sudoplz
). You could replace this with whichever algorithm you prefer.
fun colorIsDarkAdvanced(bgColor: Int): Boolean {
// hexToB
val uicolors = doubleArrayOf(
bgColor.red.toDouble() / 255.0,
bgColor.green.toDouble() / 255.0,
bgColor.blue.toDouble() / 255.0
)
val c = uicolors.map { col ->
if (col <= 0.03928) {
col / 12.92
} else {
Math.pow((col + 0.055) / 1.055, 2.4)
}
}
val L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
return L <= 0.179
}
Now that we can tell if we should use dark or light text on widget creation, we need to ensure that whenever the wallpaper is changed the color is checked and the widget theme is updated.
To do this we can create aĀ WallpaperManager.OnColorsChangedListener
Ā in aĀ DisposableEffect
:
@Composable
fun MotivateMeGlanceTheme(
context: Context,
content: @Composable (Boolean) -> Unit,
) {
val wallpaperManager = WallpaperManager.getInstance(context)
val colors = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)
var useDarkColorOnWallpaper by remember {
mutableStateOf(
getUseDarkColorOnWallpaper(colors, FLAG_SYSTEM) ?: false
)
}
DisposableEffect(wallpaperManager) {
val listener = WallpaperManager.OnColorsChangedListener { colors, type ->
getUseDarkColorOnWallpaper(colors, type)?.let {
useDarkColorOnWallpaper = it
}
}
wallpaperManager.addOnColorsChangedListener(
listener,
Handler(Looper.getMainLooper())
)
onDispose {
wallpaperManager.removeOnColorsChangedListener(listener)
}
}
// ...
}
Now, every time the wallpaper is changed the widget will update!
To see a full example, see myĀ sample widget app (KatieBarnett/MotivateMe
):
Check out my articleĀ Widgets with Glance: Blending in (proandroiddev
)Ā to see how to pick a color that matches the app icons and device dynamic colours.
Info
This article is previously published on proandroiddev.com (proandroiddev
).