
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 articleWidgets 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 yourGlanceTheme
(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 theWallpaperManager API.
First, get theWallpaperManager
instance, then fetch the dominant colors. A list is available, arranged in order of priority (note: a minimum color occurrence percentageMIN_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 theGlanceTheme
and initialised in a boolean state variable that can be then passed into the composablecontent
.
@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 inFLAG_LOCK
would give the colors of the lock screen.
An important thing to note is thatgetWallpaperColors
is limited toAPI 27
and above so you can either update theminimumSdk
of the app to27
or surround this with an version check if statement.
To detect whether to use dark or light text, we can use a utility functiongetUseDarkColorOnWallPaper
. In this we can use the wallpaper colorscolorHints
to check if we should use dark text with theWallpaperColors.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 alsoHINT_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.
UsingHINT_SUPPORTS_DARK_TEXT
andcolorHints
:
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 inAndroid 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 aWallpaperManager.OnColorsChangedListener
in aDisposableEffect
:
@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 mysample widget app (KatieBarnett/MotivateMe
):
Check out my articleWidgets 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
).
