简体   繁体   中英

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.)

A little bit late to the party, but here is the way that I've been doing, someone might need it.

For Android M and above, you can call View#rootWindowInsets directly, otherwise rely on Java's Reflection to access the private field mStableInsets

fun getStableInsets(view: View): Rect {
   return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      val windowInsets = view.rootWindowInsets
      if (windowInsets != null) {
         Rect(windowInsets.stableInsetLeft, windowInsets.stableInsetTop,
                            windowInsets.stableInsetRight, windowInsets.stableInsetBottom)
      } else {
         // TODO: Edge case, you might want to return a default value here
         Rect(defaultInsetLeft, defaultInsetTop, defaultInsetRight, defaultInsetBottom)
      }
   } else {
      val attachInfoField = View::class.java.getDeclaredField("mAttachInfo")
      attachInfoField.isAccessible = true
      val attachInfo = attachInfoField.get(view);
      if (attachInfo != null) {
         val stableInsetsField = attachInfo.javaClass.getDeclaredField("mStableInsets")
         stableInsetsField.isAccessible = true        
         Rect(stableInsetsField.get(attachInfo) as Rect)
      } else {
         // TODO: Edge case, you might want to return a default value here
         Rect(defaultInsetLeft, defaultInsetTop, defaultInsetRight, defaultInsetBottom)
      }
   }     
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM