Blog Infos
Author
Published
Topics
, , , ,
Published

Featured in Android Weekly Issue #614

Watching YouTube, one might notice a nice lighting effect going on around the edges of a video.

This is Ambient Mode. It’s a lighting effect that surrounds a YouTube video with a soft, glowing light that usually reflects the colors featured in the video itself.

The effect seems to be taking advantage of the whole screen. This includes the space right where the clock, notifications and other system related functionalities reside.

In other words, the system status bar.

While the video player component can be dragged down to a mini-player, the lighting effect itself stays where it is. It slowly loses its alpha, until it is not visible at all anymore when the video player is fully minimized.

Quite nice eh?

Housekeeping

The goal of this post is to figure out how to use Window Insets and display content edge-to-edge in Compose.

Imitation is the sincerest form of flattery, so let’s try to copy what YouTube does in 5 minutes — badly. 💀

TL;DR

Besides a stale meme, what do we have here?

  • Edge-to-edge effect
    – Background is fully covered by the app, system bars included
  • A space behind the system status bar where color/alpha can be manipulated
    – Bright red when the doggo is fully expanded, faint red when contracted
  • Control of the status bar icon tint
    – Light, in this case, to contrast the dark background
  • A resizable box (bonk included), playing the role of the video player
Setting up the theme

A normal theme will do.

<style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/md_theme_light_primary</item>
// ....
</style>
view raw theme.xml hosted with ❤ by GitHub

To avoid confusion, anything handling system bars in the XML theme is a no-no.

// don't do this 🙁
<item name="android:statusBarColor">....</item>
<item name="android:navigationBarColor">.....</item>
<item name="android:windowLightStatusBar">....</item>
<item name="android:windowLightNavigationBar">.....</item>
//....
view raw nono.xml hosted with ❤ by GitHub
Sidenote on theming

All compose theming below uses hard-coded colors for brevity.

Theming with respect to light/dark/dynamic color schemes should ideally be implemented depending on your needs.

Enable edge-to-edge

Since we want our app to display content behind the system UI and cover the whole screen, it would be nice to make the system bars completely transparent.

This can be accomplished with enableEdgeToEdge():

AppTheme {
var systemBarStyle by remember {
val defaultSystemBarColor = android.graphics.Color.TRANSPARENT
mutableStateOf(
SystemBarStyle.auto(
lightScrim = defaultSystemBarColor,
darkScrim = defaultSystemBarColor
)
)
}
LaunchedEffect(systemBarStyle) {
enableEdgeToEdge(
statusBarStyle = systemBarStyle,
navigationBarStyle = systemBarStyle
)
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

SystemBarStyle.auto handles the icon tint automatically. Meaning:

  • In light mode, icon tint will be dark
    – The system expects to have some sort of light color background behind the icons. That way they contrast nicely and are clearly visible
  • The opposite happens for dark mode — i.e. icon tint will be light
Time to test it
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.Blue
) { }
view raw Surface.kt hosted with ❤ by GitHub

Light mode + auto = dark icons. They are barely discernible. 😰

Let’s fix that.

Scaffolding

Usually, the first thing someone adds at the top level is a Scaffold. Let’s make it black and draw on top this ugly blue color.

While we are at it, fix the status bar icon tint with a LaunchedEffect.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

@Composable
private fun MainContent(
changeSystemBarStyle: (SystemBarStyle) -> Unit // pass function from top level to change the SystemBarStyle
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = Color.Black
) { paddingValues ->
LaunchedEffect(Unit) {
changeSystemBarStyle(SystemBarStyle.dark(android.graphics.Color.TRANSPARENT))
}
}
}
view raw MainContent.kt hosted with ❤ by GitHub

SystemBarStyle.dark is signalling to the system that we have a dark background occupying the status bar space.

To that end, light icon tint will be provided, for a nice contrast.

hey, it works
Handling insets manually

Scaffold provides paddingValues to help avoid the system bars. Normally, these paddings would be assigned as is, to the first child container.

Let’s use them, with a slight twist:

 

@Composable
fun MainContent() {
Scaffold { paddingValues ->
// ....
val layoutDirection = LocalLayoutDirection.current
Box(
modifier = Modifier
.fillMaxSize()
.padding(
start = paddingValues.calculateStartPadding(layoutDirection),
end = paddingValues.calculateEndPadding(layoutDirection),
bottom = paddingValues.calculateBottomPadding(),
)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Spacer(
modifier = Modifier
.windowInsetsTopHeight(WindowInsets.statusBars)
.fillMaxWidth()
.background(Color.Red)
)
}
}
}
}
view raw MainContent.kt hosted with ❤ by GitHub

What is happening here?

  • We ensure that important content and interactions are not obscured by the system UI, with paddingValues.calculateStartPadding, and equivalents
  • Since we do want to draw behind the status bar, the top padding is omitted
  • Finally, a red-colored Spacer is positioned right where the status bar is, matching its exact height with windowInsetsTopHeight(WindowInsets.statusBars)

Bonus round — Implementing a resizable and draggable Box

Now that the system bars are taken care of, time for the poor man’s video player.

This is the part where someone can get really clever with some compose magic.

Gesture/scroll detection, advanced math and graceful recalculation of dimensions/colors, in order to save CPU cycles and the UI thread from being overloaded.

Unfortunately, I am way too stupid for that. A simple caveman solution based on detectVerticalGestures will do.

@Composable
fun BoxWithConstraintsScope.draggableBox() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(boxHeight)
.background(Color.White)
.align(Alignment.BottomCenter)
.pointerInput(Unit) {
detectVerticalDragGestures { change, dragAmount ->
if (dragAmount < 0f) {
// dragging up
} else if (dragAmount > 0f) {
// dragging down
}
}
}
) {
Image( .... ) // doggo here
}
}
view raw DraggableBox.kt hosted with ❤ by GitHub

The complete solution is way too long-winded for this little blog. You can find it here if you are curious.

All that’s left is actually implementing the YouTube UI.

One clap = one prayer 🙏, and I’ll get right on it on part 2. (lie)

Anyways

Hope you found this somewhat useful.

markasduplicate

Later.

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