DEV Community

Lejdi
Lejdi

Posted on

Handling Android toolbars in Android 15+

In this article I'll present how to handle activities with toolbars on Android 15 and later. I'll cover three scenarios: activity with system toolbar, activity with custom toolbar and compose layout.

Since Android 15, the previously optional possibility to enable Edge2Edge became mandatory, and now it's the default behavior of Android activities. However some people not have time or budget in their project to rewrite all screens to match this requirement. I'll present you three simple workarounds.

Add flag in activity's style

The simplest solution is adding following flag to the style of your activity:
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
This solution must be considered as a temporary one, because as Google recently announced, it won't work on Android 16 and later. I recommend using it only as a quick fix and work on other long term solution.

General mechanism for old-school activities with XML layouts

Google provided other way of handling edge to edge enforcement.

ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowInsets ->
  val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
  v.updateLayoutParams<MarginLayoutParams> {
      leftMargin = insets.left
      bottomMargin = insets.bottom
      rightMargin = insets.right
  }
  WindowInsetsCompat.CONSUMED
}
Enter fullscreen mode Exit fullscreen mode

Above code with description is available here.
This listener is designed to add margins for specific views (like floating action button) so they are not overlapped by system bars.
We can reuse it to handle margins for the entire activity.
Let's write some extension function:

fun Activity.addMarginsForEdgeToEdge() {
    val view = this.window.decorView.findViewById<View>(android.R.id.content)
    ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowInsets ->
        val insets = windowInsets.getInsets(
            WindowInsetsCompat.Type.systemBars()
        )
        v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
            leftMargin = insets.left
            bottomMargin = insets.bottom
            rightMargin = insets.right
            topMargin = insets.top
        }
        WindowInsetsCompat.CONSUMED
    }
}
Enter fullscreen mode Exit fullscreen mode

We have used the same funcion, but as a view we have provided activity's decorView.
For activities with system's default toolbar you can simply use this function in onCreate() of your activity.

Handling custom toolbar

If your activity have some custom toolbar written specifically for your application, the code has to be a little bit more complicated. Since a toolbar is a part of your layout it is not considered as part of system bars. If you simply add margins as presented above, the blank, semi-transparent space will be left above activity with clock, battery etc. It would definitely look better if this space remains the same color as your toolbar. To achieve this let's modify the solution:

fun Activity.addMarginsForEdgeToEdge(
    getToolbar: () -> View?,
) {
    var toolbarHeight: Int? = null
    val view = this.window.decorView.findViewById<View>(android.R.id.content)
    ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowInsets ->
        val insets = windowInsets.getInsets(
            WindowInsetsCompat.Type.systemBars()
        )
        val toolbarView = getToolbar()
        if(toolbarView != null) {
            val layoutParams = toolbarView.layoutParams
            if(toolbarHeight == null)
                toolbarHeight = layoutParams.height
            layoutParams.height = toolbarHeight?.plus(insets.top) ?: insets.top
            toolbarView.layoutParams = layoutParams
            toolbarView.updatePadding(
                top = insets.top
            )
        }
        else {
            v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
                topMargin = insets.top
            }
        }
        v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
            leftMargin = insets.left
            bottomMargin = insets.bottom
            rightMargin = insets.right
        }
        WindowInsetsCompat.CONSUMED
    }
}
Enter fullscreen mode Exit fullscreen mode

Our function now can get your custom toolbar's view as a parameter. If you won't provide it, the behavior won't change and the code will handle system toolbar.
If the toolbar provided is not-null, then we are not setting the top insets of activity. Instead we preserve the toolbar's height, and then we increase it by the top insets (which is now height of this semi transparent bar above our toolbar). Additionally we update top padding to make the toolbar's content centered as before.

Handling toolbars in compose

The workaround for compose is the simplest. We simply need to wrap our compose view in some function like that:

protected fun setContent(content: @Composable () -> Unit) {
  Column(
    modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars)
  ) {
      content()
    }
}
Enter fullscreen mode Exit fullscreen mode

And that's all. Enjoy!

Top comments (0)