在 CoordinatorLayout 中禁用 AppBarLayout 时 RecyclerView 滚动出现故障?

RecyclerView scrolling glitches when disabling AppBarLayout in a CoordinatorLayout?

背景

我有一个基于 CoordinatorLayout 的视图,它有一个 AppbarLayoutRecyclerView 作为直接子项。 AppBarLayout 包含一个搜索栏,当您向上滚动 RecyclerView 时该搜索栏会折叠。此视图的 XML 是:

<androidx.coordinatorlayout.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/search_coordinator"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/search_app_bar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/transparent"
            app:elevation="0dp">

        <!-- Search bar -->
        <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/search_constraint"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:focusableInTouchMode="true"
                android:focusable="true"
                app:layout_scrollFlags="scroll|enterAlwaysCollapsed|snap">

            <!-- Abbreviated -->
            <EditText .../>

            <!-- Abbreviated -->
            <ImageView .../>

        </androidx.constraintlayout.widget.ConstraintLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <!-- List -->
    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/scroll_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

当您点击搜索栏中的 EditText 视图时,将启用我所说的“搜索模式”。搜索模式所做的只是在滚动 RecyclerView 时禁用 AppBarLayout 折叠。然后用户可以在搜索栏中输入内容——它会过滤 RecyclerView 中的项目——然后他们可以在搜索栏不折叠的情况下滚动列表。我连接到 EditText onFocus 事件来执行此操作:

searchField.setOnFocusChangeListener { _, hasFocus ->
    if (hasFocus) {
        // When user taps the search bar, enable search mode
        enableSearchMode()
    }
}

enableSearchMode 代码是:

private fun enableSearchMode() {
    ...

    itemRecyclerView.isNestedScrollingEnabled = false

    ...
}

问题

这个设置似乎工作得很好......大多数时候。随机 - 可能是 1% 的时间 - 当您触摸 EditText 以启用搜索模式时,出现问题并且我无法再有效地滚动 RecyclerView。就好像它被卡在了列表的顶部。如果您尝试向列表底部滚动,滚动会四处晃动,并且通常会跳回到列表顶部。一旦禁用搜索模式,问题就会消失。

// Disable search mode
itemRecyclerView.isNestedScrollingEnabled = true

尽管进行了大量测试,但我无法始终如一地重现该问题或确定导致该问题的操作进展。它只是随机发生的,就好像 CoordinatorLayout.

中发生了某种竞争条件

我在我的应用程序中删除了太多代码来隔离问题,我确信当我将 isNestedScrollingEnabled 设置为 false 时问题恰好发生。也就是说,我还尝试了一种替代方法来在 RecyclerView 滚动时禁用 AppBarLayout 移动,即覆盖 AppBarLayout 的行为,如 here 所述。奇怪的是,这会导致同样的问题。如果我不通过这种方式或通过设置 isNestedScrollingEnabled false 禁用 AppBarLayout,则问题永远不会出现。

这里发生了什么?!?

What is happening here?!?

isNestedScrollingEnabled 设置为 false,实际上会中断 itemRecyclerView 之间的通信,因为 滚动 childAppBarLayout因为 它是 parent。正常行为是 itemRecyclerView 通知它 parent AppBarLayout 它正在滚动进度,为此 parent 应该通过 计算它的折叠高度给定任何滚动进度和所有其他内容.
我在某处发现将 isNestedScrollingEnabled 设置为 false 会导致 RecyclerView 不回收 其视图。我不能确切地说这是不是真的,但如果是的话,我认为这是导致故障的原因。

我想提出的解决方案是以编程方式将 scroll_flags 更改为 NO_SCROLL,这样 AppBarLayout 就不会 react/scroll 滚动其 child滚动视图。
虽然它在 java 中,但以下代码片段应该对您有所帮助。

            // get a reference for your constraint layout
            ConstraintLayout constraintLayout = findViewById(R.id.search_constraint);
            // get the layout params object to change the scroll flags programmatically
            final AppBarLayout.LayoutParams layoutParams = (AppBarLayout.LayoutParams) constraintLayout.getLayoutParams();
    
            // flags variable to switch between
            final int noScrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL;
            final int defaultScrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 
                | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED
                | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP;

        // now we will set appropriate scroll flags according to the focus
        searchField.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                layoutParams.setScrollFlags(hasFocus ? noScrollFlags : defaultScrollFlags);
            }
        });