
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 theWallpaperManagerinstance, 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 theGlanceThemeand 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_SYSTEMindicates we want the colors for the home screen — passing inFLAG_LOCKwould give the colors of the lock screen.
An important thing to note is thatgetWallpaperColorsis limited toAPI 27and above so you can either update theminimumSdkof the app to27or 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 colorscolorHintsto check if we should use dark text with theWallpaperColors.HINT_SUPPORTS_DARK_TEXTflag. 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_THEMEwhich 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_TEXTandcolorHints:
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
}
}
colorHintsis 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.OnColorsChangedListenerin 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).
