再次尝试适配Android透明状态栏、导航栏

终于翻译完了 由于我并不是很懂英语 并且里面的部分名词不知道如何翻译 就采用了机翻或者直接删掉、改为我理解中的意思 所以本文章内容可能和原文有出入

如有侵权请联系!

如果您观看了我 Becoming a Master Window Fitter 的演讲,你就会知道处理窗口插入视图(以下统一使用机翻:插图)可能很复杂。 最近,我一直在改善一些应用程序中的系统栏处理功能,使它们可以隐藏状态栏和导航栏。 我想出了一些方法,可以更轻松地处理插图。

在导航栏后面绘图

对于本文的其余部分,我们将使用 BottomNavigationView 演示一个简单的示例,该示例位于屏幕底部。 它非常简单地实现为:

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent" />

 

 

默认情况下,您活动的内容将布置在系统提供的UI(导航栏等)中,因此我们的视图与导航栏齐平。 我们的设计师已决定,他们希望该应用开始在导航栏后面绘制。 为此,我们将使用适当的标记调用 setSystemUiVisibility()

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

注:以上代码使用的是Kotlin 想看Java版的可以看看我之前转载的文章:关于Android 10的手势导航条适配问题

最后,我们将更新主题,以使我们拥有一个带有深色图标的半透明导航栏:

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <!-- Set the navigation bar to 50% translucent white -->
    <item name="android:navigationBarColor">#80FFFFFF</item>
    <!-- Since the nav bar is white, we will use dark icons -->
    <item name="android:windowLightNavigationBar">true</item>
</style>

 

 

视图显示在导航栏的后面
视图显示在导航栏的后面

如您所见,这只是我们要做的开始。 由于活动现在位于导航栏的后面,因此我们的 BottomNavigationView 也是如此。 这意味着用户实际上无法单击任何导航项。 要解决此问题,我们需要处理系统提供的所有 WindowInsets ,并使用这些值将适当的填充应用于视图。

通过填充处理插图

处理 WindowInsets 的常用方法之一是向视图中添加填充,以使它们的内容不显示在系统视图的后面。 为此,我们可以设置一个 OnApplyWindowInsetsListener 来向视图添加必要的底部填充,以确保其内容不会被遮盖。

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
    view.updatePadding(bottom = insets.systemWindowInsetBottom)
    insets
}

 

 

该视图现在具有与导航栏大小匹配的底部填充
该视图现在具有与导航栏大小匹配的底部填充

好的,我们现在已经正确处理了系统底部的插图。 但是后来,设计师决定在底部的布局中也添加一些填充:

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:paddingVertical="24dp" />

作者注:我不建议在 BottomNavigationView 上使用 24dp的垂直填充,此处使用较大的值只是为了使效果更加明显。

 

 

该视图具有正确的顶部填充,但是没有所需的底部填充
该视图具有正确的顶部填充,但是没有所需的底部填充

嗯,那是不对的。 你看到问题了吗? 现在,我们从 OnApplyWindowInsetsListener 中对 updatePadding() 的调用将清除布局中预期的底部填充:

fun View.doOnApplyWindowInsets(f: (View, WindowInsets, InitialPadding) -> Unit) {
     // Create a snapshot of the view's padding state
    //  创建视图填充状态的快照
    val initialPadding = recordInitialPaddingForView(this)
       // Set an actual OnApplyWindowInsetsListener which proxies to the given
      //  设置一个实际的 OnApplyWindowInsetsListener 代理
     //   lambda, also passing in the original padding state
    //    lambda,也以原始填充状态传递
    setOnApplyWindowInsetsListener { v, insets ->
        f(v, insets, initialPadding)
         // Always return the insets, so that children can also use them
        //  始终返回插图,以便子控件也可以使用它们
        insets
    }
     // request some insets
    //  请求插图
    requestApplyInsetsWhenAttached()
}

data class InitialPadding(val left: Int, val top: Int, 
    val right: Int, val bottom: Int)

private fun recordInitialPaddingForView(view: View) = InitialPadding(
    view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

啊哈! 让我们将当前的填充和插图添加在一起:

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
  view.updatePadding(
    bottom = view.paddingBottom + insets.systemWindowInsetsBottom
  )
  insets
}

我们现在有一个新问题。 WindowInsets 可以在视图的生命周期中的 任何 时间分配,并且可以 多次 分配。 这意味着我们的新逻辑将在第一次的调用运行良好,但是对于每次调用,我们将添加越来越多的底部填充。 不是我们想要的。🤦

 

调用 3 次 WindowInset 后的累积填充
调用 3 次 WindowInset 后的累积填充

我想出的解决方案是在第一次调用后记录视图的填充值,然后再引用这些值。 例:

 // Keep a record of the intended bottom padding of the view
//  记录视图的预期底部填充
val bottomNavBottomPadding = bottomNav.paddingBottom

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
     // We've got some insets, set the bottom padding to be the
    //  我们有一些插图,将底部填充设置为
   //   original value + the inset value
  //    原始值 + 插入值
  view.updatePadding(
    bottom = bottomNavBottomPadding + insets.systemWindowInsetBottom
  )
  insets
}

 

 

最后,目标达成
最后,目标达成

这很好用,这意味着我们从布局中保留了填充的意图,并且我们仍然根据需要进行了插入视图。 但是,为每个填充值保留对象级属性非常混乱,我们还可以做得更好……🤔

doOnApplyWindowInsets

输入我的新 doOnApplyWindowInsets() 扩展方法。 这是对 setOnApplyWindowInsetsListener() 的包装,它概括了上面的模式。

fun View.doOnApplyWindowInsets(f: (View, WindowInsets, InitialPadding) -> Unit) {
    // Create a snapshot of the view's padding state
    val initialPadding = recordInitialPaddingForView(this)
    // Set an actual OnApplyWindowInsetsListener which proxies to the given
    // lambda, also passing in the original padding state
    setOnApplyWindowInsetsListener { v, insets ->
        f(v, insets, initialPadding)
        // Always return the insets, so that children can also use them
        insets
    }
    // request some insets
    requestApplyInsetsWhenAttached()
}

data class InitialPadding(val left: Int, val top: Int, 
    val right: Int, val bottom: Int)

private fun recordInitialPaddingForView(view: View) = InitialPadding(
    view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

现在,当我们需要一个视图来处理插图时,我们可以执行以下操作:

bottomNav.doOnApplyWindowInsets { view, insets, padding ->
     // padding contains the original padding values after inflation
    //  填充包含插图的原始填充值
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    )
}

好多了!😏

requestApplyInsetsWhenAttached()

您可能已经注意到上面的 requestApplyInsetsWhenAttached() 。 这不是必要的,但可以解决 WindowInsets 分配方式的限制。 如果视图未连接到视图层次结构时调用了 requestApplyInsets() ,则该调用将被忽略。

在 Fragment.onCreateView() 中创建视图时,这是常见的情况。 解决方法是确保仅在 onStart() 中调用该方法,或者使用附加的监听器请求插入。 以下扩展功能可处理两种情况:

fun View.requestApplyInsetsWhenAttached() {
    if (isAttachedToWindow) {
         // We're already attached, just request as normal
        //  我们已经附加,只需正常请求
        requestApplyInsets()
    } else {
           // We're not attached to the hierarchy, add a listener to
          //  我们没有附加到层次结构,而是添加了一个监听器
         //   request when we are
        //    当我们进行请求
        addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
            override fun onViewAttachedToWindow(v: View) {
                v.removeOnAttachStateChangeListener(this)
                v.requestApplyInsets()
            }

            override fun onViewDetachedFromWindow(v: View) = Unit
        })
    }
}

将其包装

至此,我们已经大大简化了处理窗口插图的方法。😉但它仍然有一些缺点:

  • 逻辑与我们的布局背道而驰,这意味着很容易忘记。
  • 我们可能需要在许多地方使用它,导致大量几乎完全相同的代码散布在整个应用程序中。

我知道我们可以做得更好。

到目前为止,整个文章都只专注于代码,并通过设置监听器来处理插图。 不过,我们这里只是在讨论视图,因此在理想情况下,我们将声明要处理布局文件中的插图的方法。

使用 data binding adapters ! 如果您以前从未使用过它们,则可以让我们将代码映射到布局属性(使用数据绑定时)。 您可以在这里阅读有关它们的更多信息:

Make Data Binding Do What You Want(至少10个人需要就立刻翻译)

因此,让我们创建一个属性来为我们执行此操作:

@BindingAdapter("paddingBottomSystemWindowInsets")
fun applySystemWindowBottomInset(view: View, applyBottomInset: Boolean) {
    view.doOnApplyWindowInsets { view, insets, padding ->
        val bottom = if (applyBottomInset) insets.systemWindowInsetBottom else 0
        view.updatePadding(bottom = padding.bottom + insets.systemWindowInsetBottom)
    }
}

然后,在我们的布局中,我们可以简单地使用新的 paddingBottomSystemWindowInsets 属性,该属性将自动更新任何插图。

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:paddingVertical="24dp"
    app:paddingBottomSystemWindowInsets="@{ true }" />

希望您可以看到与单独使用 OnApplyWindowListener 相比,它在人性化和易用性方面有何优势。🌠

但是,等等,该绑定适配器被硬编码为仅设置底部尺寸。 如果我们也需要处理顶部插图怎么办? 还是左边? 右边? 幸运的是,绑定适配器使我们可以很好地概括所有维度上的模式:

@BindingAdapter(
    "paddingLeftSystemWindowInsets",
    "paddingTopSystemWindowInsets",
    "paddingRightSystemWindowInsets",
    "paddingBottomSystemWindowInsets",
    requireAll = false
)
fun applySystemWindows(
    view: View,
    applyLeft: Boolean,
    applyTop: Boolean,
    applyRight: Boolean,
    applyBottom: Boolean
) {
    view.doOnApplyWindowInsets { view, insets, padding ->
        val left = if (applyLeft) insets.systemWindowInsetLeft else 0
        val top = if (applyTop) insets.systemWindowInsetTop else 0
        val right = if (applyRight) insets.systemWindowInsetRight else 0
        val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0

        view.setPadding(
            padding.left + left,
            padding.top + top,
            padding.right + right,
            padding.bottom + bottom
        )
    }
}

在这里,我们声明了具有多个属性的适配器,每个属性都映射到相关的方法参数。 需要注意的是 requireAll = false 的用法,这意味着适配器可以处理所设置属性的任何组合。 这意味着我们可以执行以下操作,例如设置左和下:

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:paddingVertical="24dp"
    app:paddingBottomSystemWindowInsets="@{ true }"
    app:paddingLeftSystemWindowInsets="@{ true }" />

易用程度:💯

android:fitSystemWindows

您可能已经阅读了这篇文章,并且问“他为什么没有提到fitSystemWindows属性?”这样做的原因是因为属性带来的功能通常不是我们想要的。

如果您使用的是 AppBarLayoutCoordinatorLayoutDrawerLayout 等,那么可以使用它(注:在我使用后同样达不到效果 不知道是什么原因)。 这些视图已构建为可以识别属性,并以与这些视图相关的自觉方式应用窗口插图。

android:fitSystemWindows 的默认 View 实现意味着可以使用插图填充每个尺寸,并且不适用于上面的示例。 有关更多信息,请参阅此博客文章,该文章仍然非常相关:

我为什么要使用fitSystemWindows

作者的话

啊,这是一篇超长的文章! 除了让我们更轻松地处理 WindowInsets 之外,还希望它展示了扩展功能,lambda和绑定适配器等功能如何使任何API易于使用。

感谢尼克·切尔(Nick Butcher)和扎拉·多明格斯(Zarah Dominguez)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值