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

and*_*per 20 android android-recyclerview android-collapsingtoolbarlayout

背景

我们有一个非常复杂的布局,其中包含CollapsingToolbarLayout,以及底部的RecyclerView.

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

问题

这通常很好.

但是,在某些(有点罕见的)情况下,RecyclerView上的慢速滚动会被半阻止,这意味着它会在向下滚动时尝试向后滚动.就像它有2个滚动相互争斗(向上滚动并向下滚动):

在此输入图像描述

触发此操作的代码如下:

RES /布局/ 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>
Run Code Online (Sandbox Code Playgroud)

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;
            }
        });
    }

}
Run Code Online (Sandbox Code Playgroud)

我试过的

起初我认为这是因为其他东西(我认为这是与DrawerLayout的奇怪组合),但后来我找到了一个最小的样本来展示它,它就像我想的那样:这都是因为setNestedScrollingEnabled.

我试图在谷歌的网站(这里)报告这个问题,希望如果这是一个真正的错误它将得到修复.如果你想尝试一下,或观看问题的视频,那就去那里,因为我不能在这里全部上传(太大和太多的文件).

我也试过按照其他帖子的指示使用特殊标志(例如:这里,这里,这里,这里这里),但没有一个帮助.事实上,他们每个人都有一个问题,无论是停留在扩展模式,还是以与我不同的方式滚动.

问题

  1. 这是一个已知的问题?为什么会这样?

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

  3. 是否有一个替代方法可以调用setNestedScrollingEnabled的这个函数?一个没有任何滚动或锁定CollapsingToolbarLayout状态的问题?

Che*_*amp 6

这是实现与此答案相同的目标的另一种方法。虽然那个答案使用了反射,但这个答案没有,但推理保持不变。

为什么会这样?

问题是RecyclerView有时对成员变量使用过时的值mScrollOffsetmScrollOffset仅设置在RecyclerView:dispatchNestedPreScroll和 中的两个位置dispatchNestedScroll。我们只关心dispatchNestedPreScroll. 该方法RecyclerView#onTouchEvent在处理MotionEvent.ACTION_MOVE事件时调用。

以下来自dispatchNestedPreScroll的文档。

dispatchNestedPreScroll

boolean dispatchNestedPreScroll (int dx, int dy, int[] 消耗, int[] offsetInWindow)

在此视图消耗它的任何部分之前,调度正在进行的嵌套滚动的一个步骤。

嵌套的预滚动事件是嵌套滚动事件,触摸拦截是触摸。dispatchNestedPreScroll 为嵌套滚动操作中的父视图提供了在子视图使用之前使用部分或全部滚动操作的机会。

...

offsetInWindow int:可选。如果不为空,则返回时将包含此视图的本地视图坐标中从此操作之前到它完成之后的偏移量。视图实现可以使用它来调整预期的输入坐标跟踪。

offsetInWindow实际上是一个int[2],第二个索引表示RecyclerView由于嵌套滚动而应用于 的 y 偏移。

RecyclerView#DispatchNestedPrescroll解析为NestedScrollingChildHelper.

RecyclerView调用dispatchNestedPreScroll, mScrollOffset用作offsetInWindow参数。因此,对offsetInWindow直接更新所做的任何更改mScrollOffset只要嵌套滚动有效,就会dispatchNestedPreScroll更新。如果嵌套滚动无效,则不会更新并继续使用上次设置的值。因此,当嵌套滚动被关闭时, 的值立即变得陈旧但继续使用它。mScrollOffset mScrollOffset dispatchNestedPreScrollmScrollOffsetRecyclerView

mScrollOffset[1]返回时的正确值dispatchNestedPreScroll是要调整的金额input coordinate tracking(见上文)。在RecyclerView以下几行中调整 y 触摸坐标:

mLastTouchY = y - mScrollOffset[1];
Run Code Online (Sandbox Code Playgroud)

如果mScrollOffset[1]是,比方说,-30(因为它是陈旧的并且应该为零)然后mLastTouchY将关闭 +30 像素(--30=+30)。这种计算错误的结果是,触摸似乎发生在屏幕下方的位置比实际发生的更远。因此,缓慢向下滚动实际上会向上滚动,向上滚动会更快滚动。(如果向下滚动的速度足够快以克服此30px障碍,则会发生向下滚动,但速度会比应有的慢。)向上滚动会过快,因为应用程序认为已经覆盖了更多空间。

mScrollOffset将继续作为陈旧变量,直到启用嵌套滚动并dispatchNestedPreScroll再次报告mScrollOffset.

方法

由于mScrollOffset[1]在某些情况下具有过时的值,因此目标是在这些情况下将其设置为正确的值。当未发生嵌套滚动时,即当 AppBar 展开或折叠时,此值应为零。不幸的是,它mScrollOffset是本地的,RecyclerView没有设置器。为了在mScrollOffset不诉诸反射的情况下获得访问权限,RecyclerView创建了一个覆盖dispatchNestedPreScroll. 第四个参数是offsetInWindow哪个是我们需要改变的变量。

mScrollOffset每当为 禁用嵌套滚动时,就会发生陈旧RecyclerView。我们将施加的一个附加条件是 AppBar 必须是空闲的,因此我们可以放心地说它mScrollOffset[1]应该为零。这不是问题,因为在滚动标志中CollapsingToolbarLayout指定snap

在示例应用中,ScrollingActivity已修改为记录 AppBar 何时展开和关闭。还创建了一个回调 ( clampPrescrollOffsetListener),true当我们的两个条件满足时,该回调将返回。我们的重写dispatchNestedPreScroll将调用此回调并mScrollOffset[1]true响应时钳位为零。

更新后的源文件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";
}
Run Code Online (Sandbox Code Playgroud)

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";
}
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

5269 次

最近记录:

7 年 前