使用setNestedScrollingEnabled(false)时如何避免滚动自身阻塞? [英] How to avoid blocking of scrolling itself when using setNestedScrollingEnabled(false)?

查看:4443
本文介绍了使用setNestedScrollingEnabled(false)时如何避免滚动自身阻塞?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

We have quite a complex layout that has CollapsingToolbarLayout in it, together with a RecyclerView at the bottom.

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

In certain cases, we temporarily disable the expanding/collapsing of the CollapsingToolbarLayout, by calling setNestedScrollingEnabled(boolean) on the RecyclerView.

这通常可以正常工作.

但是,在某些情况下(在极少数情况下),RecyclerView上的缓慢滚动会被半阻止,这意味着它在向下滚动时会尝试向后滚动.好像它有2个互相抗衡的滚动(向上滚动和向下滚动):

However, on some (bit rare) cases, slow scrolling on the RecyclerView gets semi-blocked, meaning it tries to scroll back when scrolling down. It's as if it has 2 scrolling that fight each other (scroll up and scroll down):

触发该代码的代码如下:

The code to trigger this is as such:

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.

What I've tried

At first I thought it's because of something else (I thought it's a weird combination with DrawerLayout), but then I've found a minimal sample to show it, and it's just as I thought: it's all because of the setNestedScrollingEnabled.

我试图在Google网站上进行举报( 此处 ),希望如果它是一个真正的错误,它将得到解决.如果您想尝试一下,或者观看问题的视频,请去那里,因为我不能在这里全部上传(文件太大和太多).

I tried to report about this on Google's website (here), hoping it will get fixed if it's a real bug. If you wish to try it out, or watch the videos of the issue, go there, as I can't upload them all here (too large and too many files).

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

I've also tried to use special flags as instructed on other posts (examples: here, here, here, here and here) , but none helped. In fact each of them had an issue, whether it's staying in expanded mode, or scrolling in a different way than what I do.

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

  1. Is this a known issue? Why does it happen?

有办法克服吗?

也许还有替代此setNestedScrollingEnabled函数的方法吗?一个没有任何滚动或锁定CollapsingToolbarLayout状态的问题?

Is there perhaps an alternative to calling this function of setNestedScrollingEnabled ? One without any issues of scrolling or locking the state of the CollapsingToolbarLayout ?

推荐答案

这是实现与此答案相同的目标的另一种方法.尽管该答案使用了反射,但该答案没有,但是推理保持不变.

This is an alternate approach to achieving the same goal as this answer. While that answer used Reflection, this answer does not, but the reasoning remains the same.

为什么会这样?

问题在于,RecyclerView有时会对成员变量mScrollOffset使用陈旧的值. mScrollOffset仅在RecyclerView中的两个位置设置:dispatchNestedPreScrolldispatchNestedScroll.我们只关心dispatchNestedPreScroll.当处理MotionEvent.ACTION_MOVE事件时,此方法由RecyclerView#onTouchEvent调用.

The problem is that RecyclerView sometimes uses a stale value for the member variable mScrollOffset. mScrollOffset is set in only two places in RecyclerView: dispatchNestedPreScroll and dispatchNestedScroll. We are only concerned with dispatchNestedPreScroll. This method is invoked by RecyclerView#onTouchEvent when it handles MotionEvent.ACTION_MOVE events.

以下内容来自

dispatchNestedPreScroll

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

boolean dispatchNestedPreScroll (int dx, int dy, int[] consumed, int[] offsetInWindow)

在此视图消耗其一部分之前,先调度正在进行的嵌套滚动的一个步骤.

Dispatch one step of a nested scroll in progress before this view consumes any portion of it.

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

Nested pre-scroll events are to nested scroll events what touch intercept is to touch. dispatchNestedPreScroll offers an opportunity for the parent view in a nested scrolling operation to consume some or all of the scroll operation before the child view consumes it.

...

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

offsetInWindow int: Optional. If not null, on return this will contain the offset in local view coordinates of this view from before this operation to after it completes. View implementations may use this to adjust expected input coordinate tracking.

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

offsetInWindow is actually an int[2] with the second index representing the y shift to be applied to the RecyclerView due to nested scrolling.

RecyclerView#DispatchNestedPrescroll解析为 NestedScrollingChildHelper .

RecyclerView调用dispatchNestedPreScroll时, mScrollOffset用作offsetInWindow参数.因此,对offsetInWindow所做的任何更改都会直接更新mScrollOffset.只要嵌套滚动有效,dispatchNestedPreScroll就会更新mScrollOffset .如果嵌套滚动无效,则mScrollOffset不会更新,并且 会使用上一次由 dispatchNestedPreScroll设置的值进行操作.因此,当嵌套滚动关闭时,mScrollOffset的值立即失效,而RecyclerView继续使用它.

When RecyclerView calls dispatchNestedPreScroll, mScrollOffset is used as the offsetInWindow argument. So any changes made to offsetInWindow directly updates mScrollOffset. dispatchNestedPreScroll updates mScrollOffset as long as nested scrolling is in effect. If nested scrolling is not in effect, then mScrollOffset is not updated and proceeds with the value that was last set by dispatchNestedPreScroll. Thus, when nested scrolling is turned off, the value of mScrollOffset becomes immediately stale but RecyclerView continues to use it.

dispatchNestedPreScroll返回时,mScrollOffset[1]的正确值是为input coordinate tracking进行调整的量(请参见上文).在RecyclerView中,以下几行调整y触摸坐标:

The correct value of mScrollOffset[1] upon return from dispatchNestedPreScroll is the amount to adjust for input coordinate tracking (see above). In RecyclerView the following lines adjusts the y touch coordinate:

mLastTouchY = y - mScrollOffset[1];

如果mScrollOffset[1]为-30(因为它过时且应为零),则mLastTouchY将偏离+30像素(--30 = + 30).这种计算错误的结果是,触摸发生在屏幕的下方似乎比实际发生的要远.因此,缓慢的向下滚动实际上会向上滚动,而向上滚动则会更快地滚动. (如果向下滚动的速度足以克服此30px障碍,则向下滚动将发生,但比应有的慢.)由于该应用程序认为覆盖了更多空间,因此向上滚动将过快.

If mScrollOffset[1] is, let's say, -30 (because it is stale and should be zero) then mLastTouchY will be off by +30 pixels (--30=+30). The effect of this miscalculation is that it will appear that the touch occurred further down the screen than it really did. So, a slow downward scroll will actually scrolls up and an upward scroll will scroll faster. (If a downward scroll is fast enough to overcome this 30px barrier, then downward scrolling will occur but more slowly than it should.) Upward scrolling will be overly quick since the app thinks more space has been covered.

mScrollOffset将作为陈旧变量继续运行,直到打开嵌套滚动,并且dispatchNestedPreScroll再次在mScrollOffset中报告正确的值.

mScrollOffset will continue as a stale variable until nested scrolling is turned on and dispatchNestedPreScroll once again reports the correct value in mScrollOffset.

方法

由于mScrollOffset[1]在某些情况下具有陈旧值,因此目标是在这些情况下将其设置为正确的值.当不进行嵌套滚动时,即当AppBar展开或折叠时,该值应为零.不幸的是,mScrollOffset对于RecyclerView是本地的,并且没有设置器.为了获得对mScrollOffset的访问而无需求助于Reflection,将创建一个覆盖dispatchNestedPreScroll的自定义RecyclerView.第四个参数是offsetInWindow,这是我们需要更改的变量.

Since mScrollOffset[1] has a stale value under certain circumstances, the goal is to set it to the correct value under those circumstances. This value should be zero when nested scrolling is not taking place, i.e., When the AppBar is expanded or collapsed. Unfortunately, mScrollOffset is local to RecyclerView and there is no setter for it. To gain access to mScrollOffset without resorting to Reflection, a custom RecyclerView is created that overrides dispatchNestedPreScroll. The fourth agument is offsetInWindow which is the variable we need to change.

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

A stale mScrollOffset occurs whenever nested scrolling is disabled for the RecyclerView. An additional condition we will impose is that the AppBar must be idle so we can safely say that mScrollOffset[1] should be zero. This is not an issue since the CollapsingToolbarLayout specifies snap in the scroll flags.

在示例应用程序中,已将ScrollingActivity修改为记录AppBar展开和关闭的时间.还创建了一个回调(clampPrescrollOffsetListener),当满足我们两个条件时,该回调将返回true.我们覆盖的dispatchNestedPreScroll将调用此回调,并在true响应上将mScrollOffset[1]限制为零.

In the sample app, ScrollingActivity has been modified to record when the AppBar is expanded and closed. A callback has also been created (clampPrescrollOffsetListener) that will return true when our two conditions are met. Our overridden dispatchNestedPreScroll will invoke this callback and clamp mScrollOffset[1] to zero on a true response.

ScrollingActivity的更新的源文件和自定义的RecyclerView - MyRecyclerView如下所示. 必须更改XML布局文件以反映自定义MyRecyclerView.

The updated source file for ScrollingActivity is presented below as is the custom RecyclerView - MyRecyclerView. The XML layout file must be changed to reflect the custom MyRecyclerView.

ScrollingActivity

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

这篇关于使用setNestedScrollingEnabled(false)时如何避免滚动自身阻塞?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆