Search code examples
androidandroid-layout

Right way to get insets


I have an Activity with a RecyclerView in a data binding layout. RecyclerView takes up the whole screen, and looking at making the UX go full screen, drawn under the status and nav bars.

I'm calling setSystemUiVisibility in activity's onCreate as below.

window.decorView.setSystemUiVisibility(
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
        )

Now the RecyclerView is drawn under the system bars, so I want to make sure it has enough padding so the items don't overlap with the system UI.

I found 2 ways of doing this, via a BindingAdapter.

Option 1

    var  statusBar = 0
    var resourceId = view.resources.getIdentifier("status_bar_height", "dimen", "android")
    if (resourceId > 0) {
        statusBar = view.resources.getDimensionPixelSize(resourceId)
    }
    var  navBar = 0
    resourceId = view.resources.getIdentifier("navigation_bar_height", "dimen", "android")
    if (resourceId > 0) {
        navBar = view.resources.getDimensionPixelSize(resourceId)
    }
    view.setPadding(0, statusBar, 0, navBar)

Option 2

var insets = view.rootWindowInsets.stableInsets
view.setPadding(0, insets.top, 0, insets.bottom)

I prefer the first, because it (with limited testing on emulators seems to) work on API 21, 28 and 29.

Option 2 only works on API 29, and also seems to get null on view.rootWindowInsets if/when the view is not attached. (So I guess I have to add a listener and wait for it to be attached before doing this)

So my question is, is there a down side to Option 1? Can I use it over the new API in 29? Is there any scenarios that Option 1 would not work?

(I think Option 1 might not work well on tablets where both nav and systems bars are on the bottom, so extra padding will be applied to the wrong side.)


Solution

  • Update 2: consider using enableEdgeToEdge API from androidx.activity which will likely do what you want.


    Update:

    stableInsetBottom .etc. are now deprecated with message

    Use {@link #getInsetsIgnoringVisibility(int)} with {@link Type#systemBars()} * instead.

    Unfortunately systemBars() was graylisted in API 29 and is blacklisted in API 30 plus using this seems to work on API 30 emulator, however (some) real devices even running API 29 throws.

    Below is logcat from Galaxy S20 FE

    Accessing hidden method Landroid/view/WindowInsets$Type;->systemBars()I (blacklist, linking, denied)
    2021-01-17 01:45:18.348 23013-23013/? E/AndroidRuntime: FATAL EXCEPTION: main
        Process: test.app.package, PID: 23013
        java.lang.NoSuchMethodError: No static method systemBars()I in class Landroid/view/WindowInsets$Type; or its super classes (declaration of 'android.view.WindowInsets$Type' appears in /system/framework/framework.jar!classes3.dex)
    
    

    No answer for this it seems. Please put an answer if you find anything not covered below.


    Using Option 1 I noticed on devices that do OEM specific gesture navigation atleast, when those gesture modes are active, above will still return full navigation bar height even though no visible navigation bar is present. So above will still pad the UI when it shouldn't.

    Option 2 keeps returning null for insets until the view is attached so if you're doing this on a BindingAdapter, it won't work. It needs to be called after the view is attached.

    My current solution is as below.

    if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        view.doOnAttach {
            var bottom = it.rootWindowInsets?.stableInsetBottom?: 0
            var top = it.rootWindowInsets?.stableInsetTop?: 0
    
            view.setPadding(0, top, 0, bottom)
        }
    }
    else {
      // use option1, old devices don't have custom OEM specific gesture navigation.
      // or.. just don't support versions below Android M ¯\_(ツ)_/¯
    }
    

    Caveats

    • Some OEMs (well atleast OnePlus) decided not to restart some activities, especially ones that are paused, when the navigation mode changed. So if the user decides to switch away from your app, change the navigation mode and return, your app may still overlap navigation bar until the activity is restarted.