How to avoid blocking of scrolling itself when using setNestedScrollingEnabled(false)?

后端 未结 8 1797
盖世英雄少女心
盖世英雄少女心 2020-12-30 01:07

Background

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

In certain cases, we tempora

相关标签:
8条回答
  • 2020-12-30 02:09

    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.

    Why is this happening?

    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.

    The following is from the documentation for dispatchNestedPreScroll.

    dispatchNestedPreScroll

    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.

    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: 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 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 resolves to a method with the same name in NestedScrollingChildHelper.

    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.

    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];
    

    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 will continue as a stale variable until nested scrolling is turned on and dispatchNestedPreScroll once again reports the correct value in mScrollOffset.

    Approach

    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.

    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.

    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.

    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";
    }
    
    0 讨论(0)
  • 2020-12-30 02:14

    inside the recycler view, to scrolling smooth

    android:nestedScrollingEnabled="false" 
    

    to overlap the cardView in the toolbar

     app:behavior_overlapTop = "24dp" 
    

    Try this code for 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

    0 讨论(0)
提交回复
热议问题