如何在使用setNestedScrollingEnabled(false)时避免滚动本身被阻止?

21

背景

我们的布局相当复杂,其中包含CollapsingToolbarLayout和底部的RecyclerView。

在某些情况下,我们通过调用RecyclerView上的setNestedScrollingEnabled(boolean)方法来暂时禁用CollapsingToolbarLayout的展开/折叠功能。

问题

通常情况下这样做没问题。

然而,在一些(比较罕见的)情况下,RecyclerView缓慢滚动会出现卡顿,意味着在向下滚动时它会试图向后滚动。就像有两个滚动方向相互斗争:向上滚动和向下滚动。

enter image description here

触发此问题的代码如下:

res/layout/activity_scrolling.xml

<android.support.design.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"
    android:fitsSystemWindows="true"
    tools:context="com.example.user.myapplication.ScrollingActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/nestedView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end">

        <Button
            android:id="@+id/disableNestedScrollingButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="disable"/>

        <Button
            android:id="@+id/enableNestedScrollingButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="enable"
            />
    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>

ScrollingActivity.java

public class ScrollingActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scrolling);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        final RecyclerView nestedView = (RecyclerView) findViewById(R.id.nestedView);
        findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                nestedView.setNestedScrollingEnabled(false);
            }
        });
        findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                nestedView.setNestedScrollingEnabled(true);
            }
        });
        nestedView.setLayoutManager(new LinearLayoutManager(this));
        nestedView.setAdapter(new Adapter() {
            @Override
            public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
                return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(
                        android.R.layout.simple_list_item_1,
                        parent,
                        false)) {
                };
            }

            @Override
            public void onBindViewHolder(final ViewHolder holder, final int position) {
                ((TextView) holder.itemView.findViewById(android.R.id.text1)).setText("item " + position);
            }

            @Override
            public int getItemCount() {
                return 100;
            }
        });
    }

}

我尝试过的

一开始,我认为这是因为其他原因(我认为它与DrawerLayout的奇怪组合有关),但后来我找到了一个最小的示例来展示它,结果正如我所想:这完全是由于setNestedScrollingEnabled引起的。

我试图在Google网站上报告此问题(此处),希望如果这是真正的bug,就会得到修复。如果您想要尝试或观看该问题的视频,请前往上述网址,因为我无法在此处上传所有文件(太大且文件太多)。

我还尝试按其他帖子中的说明使用特殊标志(例如:此处此处此处此处此处),但没有帮助。实际上,每个标志都有问题,无论是保持扩展模式还是以与我不同的方式滚动。

问题

  1. 这是一个已知的问题吗?为什么会发生这种情况?

  2. 有没有办法克服这个问题?

  3. 是否有其他方法调用setNestedScrollingEnabled函数而不会有任何滚动问题或锁定CollapsingToolbarLayout状态的替代方案?


状态栏中有一个青色基因头。这是青色基因还是原生安卓? - Sagar V
@SagarV 这不是定制 ROM。这是来自 Google 的官方、原版的 Android O。该问题也会出现在其他 Android 版本上,包括 Android 6 和 Android 7。 - android developer
取出嵌套滚动视图,其中包含RecyclerView和下方的LinearLayout,并尝试此操作。 - Moinkhan
@Moinkhan,我不明白。你找到了一个可行的解决方案吗?能否请你展示一下?没有额外的嵌套滚动视图,只有一个单独的RecyclerView。 - android developer
8个回答

6
这是一种替代方法,可以实现与这个答案相同的目标。虽然那个答案使用了反射,但这个答案没有使用,但推理仍然是相同的。
为什么会发生这种情况?
问题在于RecyclerView有时会使用过期的值作为成员变量mScrollOffsetmScrollOffset仅在dispatchNestedPreScrolldispatchNestedScroll两个位置中设置。我们只关心dispatchNestedPreScroll。当RecyclerView#onTouchEvent处理MotionEvent.ACTION_MOVE事件时,此方法由该方法调用。
下面摘自dispatchNestedPreScroll的文档。
"dispatchNestedPreScroll" boolean dispatchNestedPreScroll(int dx, int dy, int [] consumed, int [] offsetInWindow) 在此视图消耗任何部分之前,将正在进行的嵌套滚动的一个步骤发送。
嵌套预滚动事件是嵌套滚动事件对于触摸拦截来说是什么。dispatchNestedPreScroll为嵌套滚动操作中的父视图提供了一个机会,在子视图消耗它之前,可以消耗一些或全部滚动操作。
......
"offsetInWindow"实际上是一个int[2],第二个索引表示由于嵌套滚动而应用于RecyclerView的y偏移量。 RecyclerView#DispatchNestedPrescroll解析为同名方法,位于NestedScrollingChildHelper中。
RecyclerView调用dispatchNestedPreScroll时,mScrollOffset被用作offsetInWindow参数。因此,对offsetInWindow所做的任何更改都会直接更新mScrollOffset。只要嵌套滚动有效,dispatchNestedPreScroll就会更新mScrollOffset如果嵌套滚动无效,则mScrollOffset不会更新,并且继续使用由dispatchNestedPreScroll设置的值。因此,当关闭嵌套滚动时,mScrollOffset的值立即变得过时,但RecyclerView仍然继续使用它。
dispatchNestedPreScroll返回时,mScrollOffset[1]的正确值是要调整input coordinate tracking的量(见上文)。在RecyclerView中,以下行代码调整了y触摸坐标:
mLastTouchY = y - mScrollOffset[1];

如果mScrollOffset[1]是-30(因为它已经过时,应该为零),那么mLastTouchY就会偏离+30像素(-30= +30)。这种错误计算的影响是它会看起来触摸发生在屏幕下方比实际更远。所以,缓慢向下滚动实际上会向上滚动,而向上滚动会滚动得更快。(如果向下滚动足够快地克服这个30px的障碍,那么向下滚动将发生,但速度比它应该慢。)向上滚动将过于快,因为应用程序认为覆盖了更多空间。
在启用嵌套滚动并且dispatchNestedPreScroll再次报告mScrollOffset的正确值之前,mScrollOffset将继续作为旧变量。
方法:
由于mScrollOffset[1]在某些情况下具有旧值,因此目标是在这些情况下将其设置为正确的值。当AppBar展开或折叠时,此值应为零,即当未进行嵌套滚动时。不幸的是,mScrollOffset是局部变量,没有setter。为了在不使用反射的情况下访问mScrollOffset,创建了一个自定义的RecyclerView,覆盖了dispatchNestedPreScroll。第四个参数是offsetInWindow,这是我们需要更改的变量。
当禁用RecyclerView的嵌套滚动时,会出现过时的mScrollOffset。我们将施加的额外条件是AppBar必须处于空闲状态,因此我们可以安全地说mScrollOffset[1]应该为零。这不是问题,因为CollapsingToolbarLayout在滚动标志中指定了snap
在示例应用程序中,已修改ScrollingActivity以记录AppBar何时展开和关闭。还创建了一个回调(clampPrescrollOffsetListener),当满足我们的两个条件时将返回true。我们重写的dispatchNestedPreScroll将调用此回调,并在true响应上将mScrollOffset[1]夹紧为零。
以下是更新后的ScrollingActivity源文件以及自定义RecyclerView-MyRecyclerView。XML布局文件必须更改以反映自定义的MyRecyclerView
public class ScrollingActivity extends AppCompatActivity
        implements MyRecyclerView.OnClampPrescrollOffsetListener {

    private CollapsingToolbarLayout mCollapsingToolbarLayout;
    private AppBarLayout mAppBarLayout;
    private MyRecyclerView mNestedView;
    // This variable will be true when the app bar is completely open or completely collapsed.
    private boolean mAppBarIdle = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scrolling);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mNestedView = (MyRecyclerView) findViewById(R.id.nestedView);
        mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar);
        mCollapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.toolbar_layout);

        // Set the listener for the patch code.
        mNestedView.setOnClampPrescrollOffsetListener(this);

        // Listener to determine when the app bar is collapsed or fully open (idle).
        mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mAppBarIdle = verticalOffset == 0
                        || verticalOffset <= appBarLayout.getTotalScrollRange();
            }
        });
        findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                // If the AppBar is fully expanded or fully collapsed (idle), then disable
                // expansion and apply the patch; otherwise, set a flag to disable the expansion
                // and apply the patch when the AppBar is idle.
                setExpandEnabled(false);

            }
        });
        findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                setExpandEnabled(true);
            }
        });
        mNestedView.setLayoutManager(new LinearLayoutManager(this));
        mNestedView.setAdapter(new Adapter() {
            @Override
            public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
                return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(
                        android.R.layout.simple_list_item_1,
                        parent,
                        false)) {
                };
            }

            @Override
            public void onBindViewHolder(final ViewHolder holder, final int position) {
                ((TextView) holder.itemView.findViewById(android.R.id.text1)).setText("item " + position);
            }

            @Override
            public int getItemCount() {
                return 100;
            }
        });
    }

    private void setExpandEnabled(boolean enabled) {
        mNestedView.setNestedScrollingEnabled(enabled);
    }

    // Return "true" when the app bar is idle and nested scrolling is disabled. This is a signal
    // to the custom RecyclerView to clamp the y prescroll offset to zero.
    @Override
    public boolean clampPrescrollOffsetListener() {
        return mAppBarIdle && !mNestedView.isNestedScrollingEnabled();
    }

    private static final String TAG = "ScrollingActivity";
}

MyRecyclerView

public class MyRecyclerView extends RecyclerView {
    private OnClampPrescrollOffsetListener mPatchListener;

    public MyRecyclerView(Context context) {
        super(context);
    }

    public MyRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    // Just a call to super plus code to force offsetInWindow[1] to zero if the patchlistener
    // instructs it.
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        boolean returnValue;
        int currentOffset;
        returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
        currentOffset = offsetInWindow[1];
        Log.d(TAG, "<<<<dispatchNestedPreScroll: " + currentOffset);
        if (mPatchListener.clampPrescrollOffsetListener() && offsetInWindow[1] != 0) {
            Log.d(TAG, "<<<<dispatchNestedPreScroll: " + currentOffset + " -> 0");
            offsetInWindow[1] = 0;
        }
        return returnValue;
    }

    public void setOnClampPrescrollOffsetListener(OnClampPrescrollOffsetListener patchListener) {
        mPatchListener = patchListener;
    }

    public interface OnClampPrescrollOffsetListener {
        boolean clampPrescrollOffsetListener();
    }

    private static final String TAG = "MyRecyclerView";
}

看起来运行良好,但为什么要使用Gist(在此处编写...),而且这个注释是什么意思:“如果AppBar完全展开或完全折叠(空闲),则禁用扩展并应用补丁;否则,设置一个标志以禁用扩展,并在AppBar空闲时应用补丁。”?它与代码无关。只有与原始代码相同的代码... - android developer
另外,y偏移量是什么?我正在尝试为patchListener函数和类名称寻找更好的命名,因为我不清楚它的工作原理。 - android developer
好的,我已经进一步测试了它,它运行得非常好。现在我想要理解它是如何工作的,如果你能在这里发布代码,我会尝试为patchListener找到更好的名称。目前,我将把这个答案标记为正确的。 - android developer
@androiddeveloper 很好的建议。今天晚些时候我会回去审查我所呈现的内容,并加以处理。 - Cheticamp
谢谢。你赢得了它。 - android developer

4

实际上,您可能正在错误的方式下考虑这个问题。

您需要做的唯一事情是相应地设置工具栏标志。您并不需要其他任何东西,因此我会说您的布局应该简化为:

<android.support.design.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"
    android:fitsSystemWindows="true"
    tools:context="com.example.user.myapplication.ScrollingActivity">

    <android.support.design.widget.AppBarLayout
         android:id="@+id/app_bar"
         android:layout_width="match_parent"
         android:layout_height="@dimen/app_bar_height"
         android:fitsSystemWindows="true"
         android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways"
            app:popupTheme="@style/AppTheme.PopupOverlay"
            app:title="Title" />

    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/nestedView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"            
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end">

        <Button
            android:id="@+id/disableNestedScrollingButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="disable"/>

        <Button
            android:id="@+id/enableNestedScrollingButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="enable"
            />
    </LinearLayout>
</android.support.design.widget.CoordinatorLayout>

如果您想禁用折叠功能,只需设置工具栏标志:

// To disable collapsing
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
toolbar.setLayoutParams(params);

为了启用

// To enable collapsing
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL|AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
toolbar.setLayoutParams(params);

如果你需要更改而不是一直获取布局参数,请保留对布局参数的引用。

如果需要让CollapsingToolbarLayoutView获取并设置LayoutParams,则以同样的方式更新标志,但现在添加appBarLayout.setExpanded(true/false)

注意:使用setScrollFlags会清除所有先前的标志,因此使用此方法时要小心,设置所有必需的标志。


对于 .gif 中展示的用例,您不需要折叠布局(我想说这是唯一的更改)。我会检查并修复任何样式更改。 - Joaquim Ley
GIF文件因过大而无法更长,因此我只展示了问题。这也是为什么我将GIF文件放在“问题”部分的原因。使用案例在示例项目和我在Google页面上发布的视频文件中展示。我还尝试在这里的帖子中进行描述。 - android developer
简而言之,我们需要的是一个可折叠和展开的CollapsingToolbarLayout(拥有内容),并且在滚动时像往常一样进行,但我们可以在任何时候禁用它的展开/折叠(但滚动将正常工作)。请尝试示例项目。通常情况下,它能够正常工作。 - android developer
你是否尝试过像这样实现?或者你需要可折叠的工具栏吗?你是否展开了比 gif 中显示的更多?如果是这样,你就需要更明确地说明。 - Joaquim Ley
请分享整个项目,因为当我将此代码添加到我放置的示例项目中时,它无法正常工作。此外,请填写此帖子中缺少的任何信息,以防它超出了您所写的内容。 - android developer
显示剩余10条评论

3

我曾经遇到过类似的问题,并使用自定义行为在 AppBarLayout 上解决了它。一切都很顺利。 通过在自定义行为中覆盖 onStartNestedScroll,可以阻止折叠工具栏布局的展开或收缩,同时保持滚动视图 (NestedScrollView) 的正常工作。在我的情况下,我在这里详细说明了细节,希望对您有所帮助。

private class AppBarLayoutBehavior : AppBarLayout.Behavior() {
    var canDrag = true
    var acceptsNestedScroll = true

    init {
        setDragCallback(object : AppBarLayout.Behavior.DragCallback() {
            override fun canDrag(appBarLayout: AppBarLayout): Boolean {
                // Allow/Do not allow dragging down/up to expand/collapse the layout
                return canDrag
            }
        })
    }

    override fun onStartNestedScroll(parent: CoordinatorLayout,
                                     child: AppBarLayout,
                                     directTargetChild: View,
                                     target: View,
                                     nestedScrollAxes: Int,
                                     type: Int): Boolean {
        // Refuse/Accept any nested scroll event
        return acceptsNestedScroll
    }}

添加了我的行为实现 ;) - Francesco Rigoni
谢谢,我还看到你有一个Github示例,链接在这里:https://github.com/FrancescoRigoni/CollapsingToolbarController……。你的努力得到了+1的认可。 - android developer
我认为你应该将Github存储库制作成一个库和示例,这样每个人都可以通过gradle轻松使用它。我认为这非常有用。 - android developer
好的,我正在考虑这件事,现在我有额外的确认,我会去做的 :) - Francesco Rigoni

3
如@Moinkhan所指出的,您可以尝试像这样将RecyclerView和下一个元素包装在NestedScrollView中,这应该解决您与折叠工具栏布局一起滚动的问题:
<android.support.design.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"
    android:fitsSystemWindows="true"
    tools:context="com.example.user.myapplication.ScrollingActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="fill_vertical"
        android:fillViewport="true"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/nestedView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

        </RelativeLayout>

    </android.support.v4.widget.NestedScrollView>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end">

        <Button
            android:id="@+id/disableNestedScrollingButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="disable"/>

        <Button
            android:id="@+id/enableNestedScrollingButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="enable"
            />
    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>

如果recyclerview的内容没有显示出来,你可以按照这篇文章解决这个问题:如何在NestedScrollView中使用RecyclerView?
希望对你有所帮助。

1
您提供的代码并没有像我所写的那样锁定滚动。按下“禁用”按钮(触发setNestedScrollingEnabled)没有任何作用。CollapsingToolbarLayout根据滚动事件自由地折叠/展开。就好像我没有调用setNestedScrollingEnabled一样。请在我放置的项目上尝试您的解决方案,链接在此处:https://issuetracker.google.com/issues/62513149 - android developer

3
在RecyclerView中,为了实现平滑滚动。
android:nestedScrollingEnabled="false" 
要在工具栏中重叠cardView。
 app:behavior_overlapTop = "24dp" 

尝试使用以下代码实现CollapsingToolbar效果:
  <android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay"
                app:title="Title" />

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>


    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:background="@android:color/transparent"
        app:behavior_overlapTop="@dimen/behavior_overlap_top"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/recycler_view
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_min_padding"
                android:nestedScrollingEnabled="false"
                android:scrollbarSize="2dp"
                android:scrollbarStyle="outsideInset"
                android:scrollbarThumbVertical="@color/colorAccent"
                android:scrollbars="vertical" />

        </LinearLayout>

    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

Screenshot


请详细说明。如何以编程方式禁用展开/折叠? - android developer
虽然这段代码可能回答了问题,但提供有关它如何以及/或为什么解决问题的附加上下文将改善答案的长期价值。请记住,您正在为未来的读者回答问题,而不仅仅是现在提问的人!请编辑您的答案以添加说明,并指出适用的限制和假设。此外,提到为什么此答案比其他答案更合适也没有坏处。 - Dev-iL
这里没有可调用的函数。只有XML和属性。请解释您所更改的内容,以及我应该如何处理它。 - android developer

2

使用以下代码,我测试过可以正常工作:

lockAppBarClosed();
ViewCompat.setNestedScrollingEnabled(recyclerView, false);   // to lock the CollapsingToolbarLayout

实现以下方法:
private void setAppBarDragging(final boolean isEnabled) {
        CoordinatorLayout.LayoutParams params =
                (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
        AppBarLayout.Behavior behavior = new AppBarLayout.Behavior();
        behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
            @Override
            public boolean canDrag(AppBarLayout appBarLayout) {
                return isEnabled;
            }
        });
        params.setBehavior(behavior);
    }

    public void unlockAppBarOpen() {
        appBarLayout.setExpanded(true, false);
        appBarLayout.setActivated(true);
        setAppBarDragging(false);
    }

    public void lockAppBarClosed() {
        appBarLayout.setExpanded(false, false);
        appBarLayout.setActivated(false);
        setAppBarDragging(false);

    }

我不明白我应该在这里使用什么。是setAppBarDragging吗?还是unlockAppBarOpen,lockAppBarClosed?你能否在我放置的项目上检查一下? - android developer
在初始化视图后,在onCreate方法中放置上述提到的顶部方法,并在您的活动/片段中实现AppbarRequestListener并将给定的方法放置在同一活动中。 - Usman Rana
仍然不理解。如果没有任何东西以这种方式使用它,为什么需要“AppbarRequestListener”接口?另外,我应该怎么称呼?我应该调用lockAppBarClosed,然后ViewCompat.setNestedScrollingEnabled(recyclerView,false)吗?要重新启用:unlockAppBarOpen,然后ViewCompat.setNestedScrollingEnabled(recyclerView,true)?如果是这样,这也不起作用。它只会展开CollapsingToolbarLayout(即使我需要它保持折叠状态,当它被折叠时),并且它仍然无法禁用折叠/展开。 - android developer

1
我认为这个问题与折叠工具栏捕捉到位(关闭或打开)有关,并留下一个垂直偏移变量(RecyclerView中的mScrollOffset [1]),其值非零,随后会影响滚动 - 在一个方向上减慢或反向滚动,在另一个方向上加速滚动。只有在启用嵌套滚动时,此变量似乎才在NestedScrollingChildHelper中设置。因此,一旦禁用嵌套滚动,mScrollOffset [1]的任何值都不会改变。
要可靠地复制此问题,您可以使工具栏捕捉到位,然后立即单击禁用。请参见this video以获得演示。我相信,问题的严重程度取决于发生了多少“捕捉”。
如果我将工具栏拖到完全打开或关闭的位置并且不让它“捕捉”,那么我就无法复制此问题,并且mScrollOffset [1]被设置为零,我认为这是正确的值。我还通过从布局中的折叠工具栏的layout_scrollFlags中删除snap并将工具栏放置在部分打开状态来复制了该问题。
如果您想尝试一下,可以将演示应用程序放入调试模式,并观察RecyclerView#onTouchEventmScrollOffset[1]的值。此外,查看NestedScrollingChildHelperdispatchNestedScrolldispatchNestedPreScroll方法,以了解仅在启用嵌套滚动时设置偏移量的方式。
那么,如何解决这个问题?mScrollOffsetRecyclerView私有的,不明显如何子类化才能更改mScrollOffset[1]的值。这可能需要使用反射,但这可能不是您想要的方式。也许其他读者有一些关于如何解决此问题或了解一些秘密技巧的想法。如果我想到任何东西,我会再次发布。

编辑:我提供了一个新的ScrollingActivity.java类,解决了这个问题。它使用反射并应用补丁将RecyclerViewmScrollOffset [1]设置为零,当禁用滚动按钮被按下并且AppBar处于空闲状态时。我做了一些初步测试,它正在工作。这里是gist(请参见下面的更新要点。)

第二次编辑:我能够以有趣的方式让工具栏捕捉并在中间卡住而不需要补丁,因此看起来补丁并没有引起特定的问题。我可以通过在未修补的应用程序中快速向下滚动来使工具栏从完全打开到折叠弹跳。

我重新查看了修补程序的功能,我认为它会自己处理:该变量是私有的,并且在关闭滚动后仅在一个地方引用。启用滚动时,变量始终在使用之前重置。真正的答案是谷歌解决这个问题。在他们这样做之前,我认为这可能是您可以通过特定设计获得的最接近可接受的解决方法。(我发布了一个更新的gist,解决了快速单击周围留下开关处于潜在不适当状态的潜在问题。)
无论如何,已经确定了潜在问题,并且您有一种可靠的方法来重现问题,因此您可以更轻松地验证其他建议的解决方案。
希望这可以帮到您。

我主要是询问如何实现这种行为,而不是为什么会发生这种情况。您可以使用任何其他方法代替我所做的方法。您可以完全避免使用setNestedScrollingEnabled。 - android developer
1
@androiddeveloper 你是指如何实现这种行为还是如何避免它?我所提到的行为是后退/慢滚动。我很快会发布一些关于如何避免它的内容(如果这是你想要的),并且它将使用你的示例代码。 - Cheticamp
这个...几乎工作得很好。有时候它不会自动吸附,所以工具栏可能会在我没有触摸任何东西的情况下滚动到中间。我认为它有时候在吸附时会跳跃,意味着它试图先吸附到顶部,然后再吸附到底部(反之亦然)。另外,不幸的是,正如你所提到的,这是一种反射解决方案,因此可能会产生后果... - android developer
关于糟糕的捕捉,你说得对。我也决定在这里报告:https://issuetracker.google.com/issues/63745189 。我看到你发布了一个新的答案,没有反思。两个答案似乎都很有效。我会尽快进一步尝试。目前,我会点赞那个新答案。 - android developer

0

我想提供一个不错的替代方案,主要基于这里

AppBarLayoutEx.kt

class AppBarLayoutEx : AppBarLayout {
    private var isAppBarExpanded = true
    private val behavior = AppBarLayoutBehavior()
    private var onStateChangedListener: (Boolean) -> Unit = {}
    var enableExpandAndCollapseByDraggingToolbar: Boolean
        get() = behavior.canDrag
        set(value) {
            behavior.canDrag = value
        }

    var enableExpandAndCollapseByDraggingContent: Boolean
        get() = behavior.acceptsNestedScroll
        set(value) {
            behavior.acceptsNestedScroll = value
        }

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    init {
        addOnOffsetChangedListener(
                AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
                    isAppBarExpanded = verticalOffset == 0
                    onStateChangedListener(isAppBarExpanded)
                })
    }

    override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
        super.setLayoutParams(params)
        (params as CoordinatorLayout.LayoutParams).behavior = behavior
    }

    fun toggleExpandedState() {
        setExpanded(!isAppBarExpanded, true)
    }

    fun setOnExpandAndCollapseListener(onStateChangedListener: (Boolean) -> Unit) {
        this.onStateChangedListener = onStateChangedListener
    }

    private class AppBarLayoutBehavior : AppBarLayout.Behavior() {
        var canDrag = true
        var acceptsNestedScroll = true

        init {
            setDragCallback(object : AppBarLayout.Behavior.DragCallback() {
                override fun canDrag(appBarLayout: AppBarLayout) = canDrag
            })
        }

        override fun onStartNestedScroll(parent: CoordinatorLayout, child: AppBarLayout, directTargetChild: View,
                                         target: View, nestedScrollAxes: Int, type: Int) = acceptsNestedScroll
    }
}

用法:除了在布局XML文件中使用它外,您还可以使用以下方法禁用/启用其扩展:

appBarLayout.enableExpandAndCollapseByDraggingToolbar = true/false

appBarLayout.enableExpandAndCollapseByDraggingContent = true/false

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接