注册

Android 系统 Bar 沉浸式完美兼容方案(上)

引言

自 Android 5.0 版本,Android 带来了沉浸式系统 bar(状态栏和导航栏),Android 的视觉效果进一步提高,各大 app 厂商也在大多数场景上使用沉浸式效果。但由于 Android 碎片化比较严重,每个版本的系统 bar 效果可能会有所差异,导致开发者往往需要进行兼容适配。为了简化系统 bar 沉浸式的使用,以及统一机型、版本差异所造成的效果差异,本文将介绍系统 bar 的组成以及沉浸式适配方案。

背景

问题一:沉浸式下无法设置背景色

对于大于等于 Android 5.0 版本的系统,在 Activity 的 onCreate 时,通过给 window 设置属性:

window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)

即可开启沉浸式系统 bar,效果如下:

0ec362e6fc584739a0d174e4ffe0a21d~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

Android 5.0 沉浸式状态栏

f880af31b2d74a33bc79758543274f78~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

Android 5.0 沉浸式导航栏

但是设置沉浸式之后,原来通过 window.statusBarColorwindow.statusBarColor 设置的颜色也不可用,也就是说不支持自定义半透明系统 bar 的颜色。

问题二:无法全透明导航栏

系统默认的状态栏和导航栏都有一个半透明的蒙层,虽然不支持设置颜色,但通过设置以下代码,可让状态栏变为全透明:

window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
      or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.statusBarColor = Color.TRANSPARENT

效果如下:

8f58da54071f474b9f9b3f3beba375a1~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

Android 10.0 沉浸式全透明状态栏

通过类似的方式尝试将导航栏设置为全透明:

window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
      or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.navigationBarColor = Color.TRANSPARENT

但发现导航栏半透明背景依然无法去掉:

bf22bee193784cdc9ff79861ae87a4c1~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

问题三:亮色系统 bar 版本差异

对于大于等于 Android 6.0 版本的系统,如果背景是浅色的,可通过设置状态栏和导航栏文字颜色为深色,也就是导航栏和状态栏为浅色(只有 Android 8.0 及以上才支持导航栏文字颜色修改):

window.decorView.systemUiVisibility =
  View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

window.decorView.systemUiVisibility =
  window.decorView.systemUiVisibility or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0

效果如下:

70e577e0839942d7bffd2f2f5cef3fe6~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

Android 8.0 亮色状态栏

ad9544fbfe954ac58571afcddc0f0deb~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

Android 8.0 亮色导航栏

但是在亮色系统 bar 基础上开启沉浸式后,在 8.0 至 9.0 系统中,导航栏深色导航 icon 不生效,而 10.0 以上版本能显示深色导航 icon:

ea2eac021a014440920b64fcd2d74ef1~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

Android 8.0 亮色沉浸式亮色导航栏

215fb8e52ffa4569ac6cdf704ff72957~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

Android 10.0 亮色沉浸式亮色导航栏

问题分析

问题一:沉浸式下无法设置背景色

查看源码发现设置状态栏和导航栏背景颜色时,是不能为沉浸式的:

aa19d11ba784410a8a4e87ab744bfdfc~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

问题二:无法全透明导航栏

当设置导航栏为透明色(Color.TRANSPARENT)时,导航栏会变成半透明,当设置其他颜色,则是正常的,例如设置颜色为 0x700F7FFF,显示效果如下:

1dbc256ace8e49aca797e5dc3617da36~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

Android 10.0 沉浸式导航栏

为什么会出现这个情况呢,通过调试进入源码,发现 activity 的 onApplyThemeResource 方法中有一个逻辑:

// Get the primary color and update the TaskDescription for this activity
TypedArray a = theme.obtainStyledAttributes(
      com.android.internal.R.styleable.ActivityTaskDescription);
if (mTaskDescription.getPrimaryColor() == 0) {
  int colorPrimary = a.getColor(
          com.android.internal.R.styleable.ActivityTaskDescription_colorPrimary, 0);
  if (colorPrimary != 0 && Color.alpha(colorPrimary) == 0xFF) {
      mTaskDescription.setPrimaryColor(colorPrimary);
  }
}

也就是说如果设置的导航栏颜色为 0(纯透明)时,将会为其修改为内置的颜色:ActivityTaskDescription_colorPrimary,因此就会出现灰色蒙层效果。

问题三:亮色系统 bar 版本差异

通过查看源码发现,与设置状态栏和导航栏背景颜色类似,设置导航栏 icon 颜色也是不能为沉浸式:

0bf1288173484a9a89e73920b00f2b72~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

解决沉浸式兼容性问题

对于问题二无法全透明导航栏,由上述问题分析中的代码可以看出,当且仅当设置的导航栏颜色为纯透明时(0),才会置换为半透明的蒙层。那么,我们可以将纯透明这种情况修改颜色为 0x01000000,这样也能达到接近纯透明的效果:

73445096de3849898fb183892035e1cf~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

对于问题一,难以通过常规方式进行沉浸式下的系统 bar 背景颜色设置。而对于问题三,通过常规方式需要分别对各个版本进行适配,对于国内手机来说,适配难度更大。

为了解决兼容性问题,以及更好的管理状态栏和导航栏,我们是否能自己实现状态栏和导航栏的背景 View 呢?

通过 Layout Inspector 可以看出,导航栏和状态栏本质上也是一个 view:

45979958c09f4ca78300909781e1702a~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

在 activity 创建的时候,会创建两个 view(navigationBarBackground 和 statusBarBackground),将其加到 decorView 中,从而可以控制状态栏的颜色。那么,是否能把系统的这两个 view 隐藏起来,替换成自定义的 view 呢?

因此,为了提高兼容性,以及更好的管理状态栏和导航栏,我们可以将系统的 navigationBarBackground 和 statusBarBackground 隐藏起来,替换成自定义的 view,而不再通过 FLAG_TRANSLUCENT_STATUSFLAG_TRANSLUCENT_NAVIGATION 来设置。

实现沉浸式状态栏

  1. 添加自定义的状态栏。通过创建一个 view ,让其高度等于状态栏的高度,并将其添加到 decorView 中:

View(window.context).apply {
id = R.id.status_bar_view
  val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, statusHeight)
  params.gravity = Gravity.TOP
  layoutParams = params
  (window.decorView as ViewGroup).addView(this)
}
  1. 隐藏系统的状态栏。由于 activity 在 onCreate 时,并没有创建状态栏的 view(statusBarBackground),因此无法直接将其隐藏。这里可以通过对 decorView 添加 OnHierarchyChangeListener 监听来捕获到 statusBarBackground:

(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
   override fun onChildViewAdded(parentView?childView?) {
       if (child?.id == android.R.id.statusBarBackground) {
           child.scaleX = 0f
      }
  }

   override fun onChildViewRemoved(parentView?childView?) {
  }
})

注意:这里将 child 的 scaleX 设为 0 即可将其隐藏起来,那么为什么不能设置 visibilityGONE 呢?这是因为后续在应用主题时(onApplyThemeResource),系统会将 visibility 又重新设置为 VISIBLE

隐藏之后,半透明的状态栏不显示,但是顶部会出现空白:

26a1b9990e774ca1ac42730bb7d24d65~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

通过 Layout Inspector 发现,decorView 的第一个元素(内容 view )会存在一个 padding:

c5831c99ac2042329e8f9304c033f9c9~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

因此,可以通过设置 paddingTop 为 0 将其去除:

val view = (window.decorView as ViewGroup).getChildAt(0)
view.addOnLayoutChangeListener { v________ ->
   if (view.paddingTop > 0) {
       view.setPadding(000view.paddingBottom)
       val content = findViewById<View>(android.R.id.content)
       content.requestLayout()
  }
}

注意:这里需要监听 view 的 layout 变化,否则只有一开始设置则后面又被修改了。

实现沉浸式导航栏

导航栏的自定义与状态栏类似,不过会存在一些差异。先创建一个自定义 view 将其添加到 decorView 中,然后把原来系统的 navigationBarBackground 隐藏:

window.decorView.findViewById(R.id.navigation_bar_view?View(window.context).apply {
id = R.id.navigation_bar_view
   val resourceId = resources.getIdentifiernavigation_bar_height ,  dimen ,  android )
   val navigationBarHeight = if (resourceId > 0resources.getDimensionPixelSize(resourceIdelse 0
   val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENTnavigationBarHeight)
   params.gravity = Gravity.BOTTOM
   layoutParams = params
  (window.decorView as ViewGroup).addView(this)

  (window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
       override fun onChildViewAdded(parentView?childView?) {
           if (child?.id == android.R.id.navigationBarBackground) {
               child.scaleX = 0f
          } else if (child?.id == android.R.id.statusBarBackground) {
               child.scaleX = 0f
          }
      }

       override fun onChildViewRemoved(parentView?childView?) {
      }
  })
}

注意:这里 onChildViewAdded 方法中,因为只能设置一次 OnHierarchyChangeListener ,需要同时考虑状态栏和导航栏。

通过这个方式,能将导航栏替换为自定义的 view ,但是存在一个问题,由于 navigationBarHeight 是固定的,如果用户切换了导航栏的样式,再回到 app 时,导航栏的高度不会重新调整。为了让导航栏看的清楚,设置其颜色为 0x7F00FF7F:

30eb2744ddcf4098b55303f829fda1ac~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

从图中可以看出,导航栏切换之后高度没有发生变化。为了解决这个问题,需要通过对 navigationBarBackground 设置 OnLayoutChangeListener 来监听导航栏高度的变化,并通过 liveData 关联到 view 中,代码实现如下:

val heightLiveData = MutableLiveData<Int>()
heightLiveData.value = 0
window.decorView.setTag(R.id.navigation_height_live_dataheightLiveData)

val navigationBarView = window.decorView.findViewById(R.id.navigation_bar_view?View(window.context).apply {
   id = R.id.navigation_bar_view
   val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENTheightLiveData.value ?0)
   params.gravity = Gravity.BOTTOM
   layoutParams = params
  (window.decorView as ViewGroup).addView(this)

   if (this@immersiveNavigationBar is FragmentActivity) {
       heightLiveData.observe(this@immersiveNavigationBar) {
           val lp = layoutParams
           lp.height = heightLiveData.value ?0
           layoutParams = lp
      }
  }

  (window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
       override fun onChildViewAdded(parentView?childView?) {
           if (child?.id == android.R.id.navigationBarBackground) {
               child.scaleX = 0f

               child.addOnLayoutChangeListener { __top_bottom____ ->
                   heightLiveData.value = bottom - top
              }
          } else if (child?.id == android.R.id.statusBarBackground) {
               child.scaleX = 0f
          }
      }

       override fun onChildViewRemoved(parentView?childView?) {
      }
  })
}

通过上面方式,可以解决切换导航栏样式后自定义的导航栏高度问题:

04a4cf914faf42c4815f9c066a2a6f7b~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image

作者:字节跳动技术团队
来源:juejin.cn/post/7075578574362640421

接 Android系统Bar沉浸式完美兼容方案(下)

0 个评论

要回复文章请先登录注册