1 of 41

It’s finally time to go

Edge to Edge

Hugo Visser

hugo@littlerobots.nl

2 of 41

What is edge to edge?

Content area of the app is drawn between the status bar and navigation bar

3 of 41

What is edge to edge?

Content area of the app is the entire screen, overlapping the status bar and navigation bar

4 of 41

Rainy Days

Lees Simpel

Soosee

5 of 41

Brief history lesson

Android 4.4 Kitkat (2013)

Status and navigation bar translucent

Android 5 Lollipop (2014)

Status and navigation bar colors

Android 9 Pie (2018)

Initial gesture navigation

Android 10 Pie (2019)

Gesture navigation, system bar protection

6 of 41

7 of 41

8 of 41

Why now?

  • Android 15 enforces Edge to Edge (when you target sdk 35)

Note:

  • APIs to set status & navigation bar colors are deprecated
  • Also affects handling of IME

9 of 41

10 of 41

Going edge to edge

  1. Enable edge to edge
  2. Handle visual overlaps
  3. Decide on scrims for system bars

11 of 41

Enable edge to edge

  • setSystemUiVisibility
  • WindowCompat.setDecorFitsSystemWindows(window, false)
  • WindowInsetsControllerCompat
  • enableEdgeToEdge() in onCreate() (androidx.activity)

12 of 41

enableEdgeToEdge()

  • Also on Android 15!
  • Enables edge to edge on API <35
  • Setup of status and navigation light/dark appearance & scrims

13 of 41

enableEdgeToEdge()

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

enableEdgeToEdge()

setContent {

// compose stuff

}

}

14 of 41

enableEdgeToEdge()

15 of 41

Specify SystemBarStyle

  • SystemBarStyle.auto() → switch light/dark based on ui night mode
  • SystemBarStyle.light() → always light
  • SystemBarStyle.dark() → always dark

fun ComponentActivity.enableEdgeToEdge(

statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),

navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim)

)

16 of 41

SystemBarStyle.auto()

  • API 29+: default platform scrim colors (only for 3 button nav)
  • API < 29: specified scrim colors (status bar and nav bar)
    • edgeToEdge() sets the API 29 scrim for nav bar, status bar always transparent
  • Light status bar or dark status bar based on ui dark mode flag
    • Custom condition can be used (for example: override in app)

17 of 41

SystemBarStyle.light/dark()

  • API 35: nav bar and status bar are always transparent
  • API < 35: specified scrim colors (status bar and nav bar)
    • Also for gesture navigation!
    • sets isNavigationBarContrastEnforced to false
    • light() takes 2 scrim values, dark scrim is only used on (very) old versions of Android

18 of 41

My recommendation

  • Use auto()
    • Transparent status bar, nav bar scrim for 3 button nav only
    • Set scrim colors to 0 for targetSdk >= 29
    • Adjust detectDarkMode as needed
  • When using light/dark set scrim transparent for consistency with API 35
    • Draw the system bar background(s) if needed

19 of 41

enableEdgeToEdge()

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

enableEdgeToEdge(

statusBarStyle = SystemBarStyle.auto(0, 0, { /* dark */ true }),

navigationBarStyle = SystemBarStyle.auto(0, 0, { /* dark */ true })

)

setContent {

// compose stuff

}

}

20 of 41

21 of 41

Handle visual overlap

22 of 41

Window insets

  • Describe the areas for system elements
    • System bars
    • Software keyboard
    • Display cutouts
    • Gesture areas
  • For views: WindowInsetsCompat.Type
  • For compose: WindowInsets.Companion

23 of 41

Common insets

  • statusBars
  • navigationBar
  • ime
  • displayCutout

24 of 41

Lesser used

  • captionBar
  • tappableElement
  • waterfall
  • systemGestures

25 of 41

Combined insets

  • systemBars
    • Union of statusBars, navigationBar and captionBar
  • safeDrawing
    • Area that the app can draw without overlapping any visual system element
  • safeContent
    • Safe drawing + all areas for system gestures (tapableElement, waterfall, systemGestures)

26 of 41

Applying insets (views)

27 of 41

Applying insets (views)

ViewCompat.setOnApplyWindowInsetsListener(fab) { v, windowInsets ->

val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())

// Apply the insets as a margin to the view. This solution sets

// only the bottom, left, and right dimensions, but you can apply whichever

// insets are appropriate to your layout. You can also update the view padding

// if that's more appropriate.

v.updateLayoutParams<ViewGroup.MarginLayoutParams> {

leftMargin = insets.left

bottomMargin = insets.bottom

rightMargin = insets.right

}

// Return CONSUMED if you don't want want the window insets to keep passing

// down to descendant views.

WindowInsetsCompat.CONSUMED

}

28 of 41

Applying insets (views)

  • Use ViewCompat.setOnApplyWindowInsetsListener()
  • Update padding or margin
  • Consume the applied amount or return CONSUMED
    • Determines which insets child views receive

29 of 41

Tips & tricks

  • Adding margin/padding to existing values
  • https://github.com/chrisbanes/insetter

30 of 41

Window insets in Compose

  • Apply padding through modifiers
    • Modifier.windowInsetsPadding(...)
  • Set the size based on insets
    • Modifier.windowInsets*Height(...)
    • Modifier.windowInsets*Width(...)
  • Convenience modifiers
    • Modifier.systemBarsPadding()
    • Modifier.navigationBarsPadding()
  • Consumes the window insets for ancestors

31 of 41

Window insets in Compose

  • Get the window inset sizes
    • WindowInsets.systemBars.asPaddingValues()
  • Does not consume the insets
  • Modifier.consumeWindowInsets(...)
    • Does :)
    • To explicitly consume when using window insets directly or for non-ancestors

32 of 41

Demo

33 of 41

Tips & tricks

  • If possible, use safeDrawing to prevent accidental overlaps
    • Use WindowInsetSides to select parts of the insets
    • More specific insets for more control
  • Don’t forget the IME & display cutout
  • For IME insets use android:windowSoftInputMode="adjustResize"

34 of 41

Default insets handling

  • Material 3 (views & compose):
    • App bars, containers, sheets etc take insets into account
    • Also allow for overrides
  • Material 2
    • Recent versions allow for specifying insets

35 of 41

Scaffold

Scaffold(topBar = {

// applies top + horizontal systembars padding

TopAppBar({ Text("My App") })

}) { innerPadding ->

// innerPadding contains inset information for you to use and apply

LazyColumn(

// consume insets as scaffold doesn't do it by default (optional)

modifier = Modifier.consumeWindowInsets(innerPadding),

contentPadding = innerPadding

) {

// ..

}

}

36 of 41

LazyColumn

  • Set contentPadding to insets
    • paddingValues from Scaffold
    • WindowInsets.safeDrawing.asPaddingValues()
  • Add spacer as the last item
    • Height of the navigationBar inset
    • When you have TextFields in the list

37 of 41

LazyColumn

LazyColumn(

Modifier.imePadding()

) {

// Other content

item {

Spacer(

Modifier.windowInsetsBottomHeight(

WindowInsets.systemBars

)

)

}

}

38 of 41

Testing

  • Emulator
    • Display cutout might not work (depends on image)
    • Issue with navigation bar appearance?
  • Compose previews
    • Always emulate edge-to-edge
    • Can emulate notch & display cutout
    • Integrate into screenshot testing
    • But: 3 button nav not always correct

39 of 41

Demo

40 of 41

Resources

41 of 41

Q&A