Blog Infos
Author
Published
Topics
, , , ,
Published
All good so far

 

Widgets can look great against a home screen wallpaper when they have a solid background (check out my article Widgets with Glance: Blending in 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?

Works on a light background, not on a dark.

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:

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
}
}
view raw WidgetUtil.kt hosted with ❤ by GitHub

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. 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
}
view raw WidgetUtil.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Building Modern Responsive Widgets with Jetpack Glance

In this lightning talk, we’ll explore how to get started with building responsive widgets using Jetpack Glance, a modern toolkit designed to simplify and enhance the widget development experience on Android.
Watch Video

Building Modern Responsive Widgets with Jetpack Glance

Jonathan Petit-Frere
Software Engineer,
Android at Netflix

Building Modern Responsive Widgets with Jetpack Glance

Jonathan Petit-Fre ...
Software Engineer,
Android at Netflix

Building Modern Responsive Widgets with Jetpack Glance

Jonathan Petit-F ...
Software Engineer,
Android at Netflix

Jobs

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!

Looking good in all situations!

 

To see a full example, see my sample widget app:

https://github.com/KatieBarnett/MotivateMe/tree/workshop/Activity-12?source=post_page—–33834eee2dee——————————–

Check out my article Widgets with Glance: Blending in to see how to pick a color that matches the app icons and device dynamic colours.

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu