【问题标题】:How to support full screen navigation UI for RecyclerView?RecyclerView如何支持全屏导航UI?
【发布时间】:2020-02-25 06:16:28
【问题描述】:

背景

我想支持如下所示的全屏导航 UI:

https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1 https://developer.android.com/guide/navigation/gesturenav

问题

虽然我的应用程序的所有活动都运行良好,但我突然遇到了一个有问题的活动,它有一个带有拇指的 RecyclerView。

在这里,我遇到了 2 个奇怪的问题:

  1. 滚动到底部时,最后一项不会完全显示。

  1. 在横向模式下,拇指会移到我可以触及的任何地方,因此它们也无法触摸。不仅如此,我还可以看到正常的滚动条,滚动到底部时两者都会消失:

我尝试过的

我尝试将插图应用于各种视图,包括填充和边距,但没有任何帮助。

另外,在某些网站中,我看到我应该使用View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,而在某些网站中,我还需要添加View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN。没有帮助。

这是我当前的代码(项目可用here,因为我认为这是一个错误):

MainActivity.kt

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    inline fun View.updateMargins(@Px left: Int = marginLeft, @Px top: Int = marginTop, @Px right: Int = marginRight, @Px bottom: Int = marginBottom) {
        updateLayoutParams<ViewGroup.MarginLayoutParams> {
            this.bottomMargin = bottom
            this.topMargin = top
            this.leftMargin = left
            this.rightMargin = right
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setSupportActionBar(toolbar)
        findViewById<View>(android.R.id.content).systemUiVisibility =
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
        ViewCompat.setOnApplyWindowInsetsListener(appBarLayout) { _, insets ->
            val systemWindowInsets = insets.systemWindowInsets
            appBarLayout.updateMargins(
                left = systemWindowInsets.left,
                top = systemWindowInsets.top,
                right = systemWindowInsets.right
            )
            insets
        }

        ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { _, insets ->
            val systemWindowInsets = insets.systemWindowInsets
            recyclerView.updatePadding(
                left = systemWindowInsets.left,
                bottom = systemWindowInsets.bottom,
                right = systemWindowInsets.right
            )
            insets
        }

        recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            init {
                setHasStableIds(true)
            }

            override fun onCreateViewHolder(
                parent: ViewGroup,
                viewType: Int
            ): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(
                    LayoutInflater.from(this@MainActivity).inflate(R.layout.list_item, parent, false)
                ) {}
            }

            override fun getItemId(position: Int): Long = position.toLong()

            override fun getItemCount(): Int = 100

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                holder.itemView.imageView.setColorFilter(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                holder.itemView.textView.text = "item $position"
            }

        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menu.add("test").setIcon(android.R.drawable.ic_dialog_email).setOnMenuItemClickListener {
            true
        }.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
        return super.onCreateOptionsMenu(menu)
    }
}

styles.xml(我在清单中使用 AppTheme 作为主题)

<resources xmlns:tools="http://schemas.android.com/tools">

    <style name="AppTheme" parent="Theme.MaterialComponents.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:navigationBarColor" tools:targetApi="lollipop">
            @android:color/transparent
        </item>
        <item name="android:statusBarColor" tools:targetApi="lollipop">@color/colorPrimaryDark
        </item>
    </style>

    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
</resources>

activity_main.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">


    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBarLayout" android:layout_width="match_parent" android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">
        <!--app:popupTheme="@style/AppTheme.PopupOverlay"-->
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary" />
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent"
        android:orientation="vertical" android:scrollbars="vertical" app:fastScrollEnabled="true"
        app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
        app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
        app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
        app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:itemCount="100"
        tools:listitem="@layout/list_item" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

line.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">

    <solid android:color="@android:color/darker_gray" />

    <padding
        android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
</shape>

line_drawable.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/line" android:state_pressed="true" />

    <item android:drawable="@drawable/line" />
</selector>

thumb.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
    <corners
        android:bottomLeftRadius="44dp" android:topLeftRadius="44dp" android:topRightRadius="44dp" />
    <padding
        android:paddingLeft="22dp" android:paddingRight="22dp" />
    <solid android:color="@color/colorPrimaryDark" />
</shape>

thumb_drawable.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/thumb" android:state_pressed="true" />

    <item android:drawable="@drawable/thumb" />
</selector>

问题

  1. 为什么会发生? 它适用于其他各种地方...
  2. 如何让 RecyclerView 避免这两种情况,同时允许底部的导航栏显示 RecyclerView 的内容,因为它是透明的?
  3. 在哪些情况下我应该添加标志View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN?在这些情况下它有什么帮助?

编辑:一种可能的解决方法是避免使用 CoordinatorLayout。它运作良好,但我想以“官方方式”做事。这是解决方法:

我使用的不是 CoordinatorLayout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"
    xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">

...

在代码中,我为 RecyclerView 设置了边距和内边距:

    ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { _, insets ->
        val systemWindowInsets = insets.systemWindowInsets
        recyclerView.updatePadding(
            bottom = systemWindowInsets.bottom
        )
        recyclerView.updateMargins(
            left = systemWindowInsets.left,
            right = systemWindowInsets.right
        )
        insets
    }

【问题讨论】:

    标签: android android-recyclerview


    【解决方案1】:

    由于对我之前的答案有评论讨论,我不会编辑那个(它也可能使某人受益)。

    最简单的做法是让AppBarLayout 处理顶部和底部插入。我们可以通过在AppBarLayout 上使用android:fitsSystemWindows="true" 来做到这一点。对于要通过导航栏查看的项目,请在RecyclerView 上使用android:clipToPadding="false"。在RecyclerView 上使用android:scrollbars="none" 禁用正常滚动条。那么布局 XML 是:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appBarLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            android:theme="@style/AppTheme.AppBarOverlay">
    
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary" />
    
        </com.google.android.material.appbar.AppBarLayout>
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:orientation="vertical"
            android:scrollbars="none"
            app:fastScrollEnabled="true"
            app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
            app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
            app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
            app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />
    
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    

    在活动/片段中,我们仍然需要处理RecyclerView 的底部系统插图和RecyclerViewAppBarLayout. We use margins for theRecyclerView 的左右系统插图,以使快速滚动条进入内容区域。

    ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { _, insets ->
                val systemWindowInsets = insets.systemWindowInsets
                appBarLayout.updatePadding(left = systemWindowInsets.left, right = systemWindowInsets.right)
                recyclerView.updatePadding(bottom = systemWindowInsets.bottom)
                recyclerView.updateMargins(left = systemWindowInsets.left, right = systemWindowInsets.right)
                insets
            }
    

    视频结果here, comment #27

    【讨论】:

    • 不错。谢谢!这不像其他人提供的那样是一种解决方法:)
    • @40 我实际上在您的解决方案中发现了一个问题。它也显示在视频中(这里:issuetracker.google.com/issues/143530492#comment27)。当您滚动到底部时,拇指将位于导航栏的后面。那里不能碰。我不接受这个答案,但为此 +1。
    • 是的,因为它忽略了我们讨论的填充。您可以等待官方更好的实施,使用我在此处的其他答案中的 FastScroller 或实施您自己的。这取决于您需要多少此功能。
    • 所以你同意这是一个错误?我应该在问题跟踪器上发布这个,对吧?
    • 我决定也为其他解决方案给你 +1,因为它似乎可以很好地解决这个问题。
    【解决方案2】:

    需要注意的是,您可能已经在 RecyclerView 上设置了android:clipToPadding=false",尽管它在上方不可见(否则这些项目在导航栏后面不会完全可见)。

    第一部分

    第一部分(纵向模式下的底部填充)很容易解决。似乎 CoordinatorLayout 以某种方式将 RecyclerView 向下移动了顶部插图(很可能是由 ScrollingViewBehavior 引起的,没有进一步研究)。所以解决办法是把底部和顶部的插入都添加到回收器的底部填充中:

    ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { _, insets -> 
        val systemWindowInsets = insets.systemWindowInsets
        recyclerView.updatePadding(
            left = systemWindowInsets.left,
            // Fix CoordinatorLayout behavior
            bottom = systemWindowInsets.bottom + systemWindowInsets.top
            right = systemWindowInsets.right
        )
        insets
    }
    

    第二部分说明

    第二部分有点棘手。更简单的部分是双滚动条,但随后是导航栏后面的快速滚动。 padding 是视图的part(检查 Android Studio 中的布局检查器,您会看到 RecyclerView 一直延伸到导航栏)。当 RecyclerView 快速滚动实现(基于 ItemDecoration)显示线条和拇指时,它确实考虑填充。

    RecyclerView 检查app:fastScrollEnabled="true" 属性并调用initFastScroller(...) 方法,该方法创建一个FastScroller 对象(请参阅here)。但是,你不能扩展这个类,因为它有一个@VisibleForTesting 注释(注释信息here,源代码FastScrollerhere)。

    第二部分的解决方案

    双滚动条修复

    最容易解决的是双滚动条。您必须禁用普通滚动条并仅显示快速滚动条。为此,请在 RecyclerView 上使用 android:scrollbars="none" 而不是 android:scrollbars="vertical"

    快速滚动修复

    对于快速滚动条,您可以做的是从FastScroller 复制代码并更改一些内容以考虑填充。 注意,这也会改变纵向模式 - 快速滚动条只会延伸到导航栏。所谓的FastScroller2 的最终代码是here on pastebin(有点长,这里就不贴了)。只需创建一个名为 FastScroller2 的新 Java 类并粘贴代码。

    然后您可以像这样使用FastScroller2(从onCreate 调用)

    private fun setupRecyclerView() {
        recyclerView.adapter = ... // Your adapter here
        val thumbDrawable = ContextCompat.getDrawable(this, R.drawable.thumb_drawable) as StateListDrawable?
        val lineDrawable = ContextCompat.getDrawable(this, R.drawable.line_drawable)
        val thickness = resources.getDimensionPixelSize(R.dimen.fastScrollThickness)
        val minRange = resources.getDimensionPixelSize(R.dimen.fastScrollMinimumRange)
        val margin = resources.getDimensionPixelSize(R.dimen.fastScrollMargin)
        if (thumbDrawable != null && lineDrawable != null) {
            // No need to do anything else, the fast scroller will take care of 
            // "connecting" itself to the recycler view
            FastScroller2(recyclerView, thumbDrawable, lineDrawable,
                thumbDrawable, lineDrawable, thickness, minRange, margin, true)
        }
    }
    

    厚度、minRange 和边距尺寸是从 recycler 视图库中复制的,参见 here,添加到 dimens.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <dimen name="fastScrollThickness">8dp</dimen>
        <dimen name="fastScrollMargin">0dp</dimen>
        <dimen name="fastScrollMinimumRange">50dp</dimen>
    </resources>
    

    这也意味着您不需要布局 xml 中的任何快速滚动代码:

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:orientation="vertical"
        android:scrollbars="none"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
        tools:itemCount="100"
        tools:listitem="@layout/list_item" />
    

    希望对你有所帮助。

    【讨论】:

    • 抱歉,这不是解决方案。这是一组可能并不总是有效的解决方法,只能根据问题自行调整。您只需添加更多空间。如果这是一个错误,他们应该修复它
    • 这不是错误,因为这是预期行为。填充是视图的一部分,所以他们把它画在那里。您需要的是 CUSTOM 行为,框架没有为您提供。
    • 如果您设置边距,一切都会正常,因为边距不是视图的一部分。但是您想要导航栏后面的项目,因此您必须使用将 clipToPadding 设置为 false 的填充。
    • 但是我已经为 RecyclerView 设置了填充,并且正如我所写的那样它不能很好地工作。我添加了底部插图,因为这在这里很重要。 RecyclerView 已经在工具栏下方,所以这就是应该缺少的。此外,问题也在侧面,因为 RecyclerView 也在导航栏上绘制内容,并忽略了我在那里设置的填充。为什么我需要一个特殊的行为?为什么原版不能用?为什么在有关这种特殊情况的文档中没有提到它?到处都有RecyclerView……我不明白你的意思。
    • 我不认为只有 XML 会起作用。我认为更好的 ReyclerView 解决方案是使用自定义 ItemDecoration,它只为最后一项添加预定义的底部偏移。可以在这里看到一个示例(称为填充,尽管这更像是边距):gist.github.com/vincetreur/4623110107c07c44b390(这也增加了一些顶部边距)。这可能是您需要的,因为保证金不会影响 considerPadding = true 的计算
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多