1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > 再次尝试适配Android透明状态栏 导航栏

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

时间:2024-03-16 09:26:52

相关推荐

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

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

如有侵权请联系!

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

在导航栏后面绘图

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

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

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

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE orView.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}

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

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

<BottomNavigationViewandroid: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 后的累积填充

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

// Keep a record of the intended bottom padding of the view// 记录视图的预期底部填充val bottomNavBottomPadding = bottomNav.paddingBottombottomNav.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 stateval initialPadding = recordInitialPaddingForView(this)// Set an actual OnApplyWindowInsetsListener which proxies to the given// lambda, also passing in the original padding statesetOnApplyWindowInsetsListener { v, insets ->f(v, insets, initialPadding)// Always return the insets, so that children can also use theminsets}// request some insetsrequestApplyInsetsWhenAttached()}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 0view.updatePadding(bottom = padding.bottom + insets.systemWindowInsetBottom)}}

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

<BottomNavigationViewandroid: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 0val top = if (applyTop) insets.systemWindowInsetTop else 0val right = if (applyRight) insets.systemWindowInsetRight else 0val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0view.setPadding(padding.left + left,padding.top + top,padding.right + right,padding.bottom + bottom)}}

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

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

易用程度:💯

android:fitSystemWindows

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

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

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

我为什么要使用fitSystemWindows

作者的话

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

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

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。