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

后端 未结 8 1796
盖世英雄少女心
盖世英雄少女心 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 01:47

    I believe that this problem is related to the collapsing toolbar snapping into place (either closed or open) and leaving a vertical offset variable (mScrollOffset[1] in RecyclerView) with a non-zero value that subsequently biases the scroll - slowing or reversing the scroll in one direction and speeding it up in the other. This variable only seems to be set in NestedScrollingChildHelper if nested scrolling is enabled. So, whatever value mScrollOffset[1] has goes unchanged once nest scrolling is disabled.

    To reliably reproduce this issue, you can cause the toolbar to snap into place then immediately click disable. See this video for a demonstration. I believe, that the magnitude of the issue varies by how much "snapping" occurs.

    If I drag the toolbar to the fully open or closed position and don't let it "snap", then I have not been able to reproduce this problem and mScrollOffset[1] is set to zero which I think is the right value. I have also reproduced the problem by removing snap from the layout_scrollFlags of the collapsing toolbar in the layout and placing the toolbar in a partially open state.

    If you want to play around with this, you can put your demo app into debug mode and observe the value of mScrollOffset[1] in RecyclerView#onTouchEvent. Also take a look at NestedScrollingChildHelper's dispatchNestedScroll and dispatchNestedPreScroll methods to see how the offset is set only when nested scrolling is enabled.

    So, how to fix this? mScrollOffset is private toRecyclerView and it is not immediately obvious how to subclass anything to change the value of mScrollOffset[1]. That would leave Reflection, but that may not be desirable to you. Maybe another reader has an idea about how to approach this or knows of some secret sauce. I will repost if anything occurs to me.

    Edit: I have provided a new ScrollingActivity.java class that overcomes this issue. It does use reflection and applies a patch to set mScrollOffset[1] of RecyclerView to zero when the disable scroll button has been pressed and the AppBar is idle. I have done some preliminary testing and it is working. Here is the gist. (See updated gist below.)

    Second edit: I was able to get the toolbar to snap in funny ways and get stuck in the middle without the patch, so it doesn't look like the patch is causing that particular issue. I can get the toolbar to bounce from fully open to collapsed by scrolling down fast enough in the unpatched app.

    I also took another look at what the patch is doing and I think that it will behave itself: The variable is private and referred to only in one place after scrolling is turned off. With scrolling enabled, the variable is always reset before use. The real answer is for Google to fix this problem. Until they do, I think this may be the closest you can get to an acceptable work-around with this particular design. (I have posted an updated gist that addresses potential issues with a quick click-around leaving switches in a potential unsuitable state.)

    Regardless, the underlying issue has been identified and you have a reliable way to reproduce the problem, so you can more easily verify other proposed solutions.

    I hope this helps.

    0 讨论(0)
  • 2020-12-30 01:49

    As @Moinkhan points out, you could try wrapping the RecyclerView and next elements in a NestedScrollView like this, this should resolve your problem of scrolling alongside with your collapsing toolbar layout:

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

    In case the contents of the recyclerview are not displayed you can follow this thread to solve that issue How to use RecyclerView inside NestedScrollView?.

    Hope it helps.

    0 讨论(0)
  • 2020-12-30 01:50

    I had to solve a similar issue and did it using a custom behaviour on the AppBarLayout. Everything works great. By overriding onStartNestedScroll in the custom behaviour it is possible to block to collapsing toolbar layout from expanding or collapsing while keeping the scroll view (NestedScrollView) in my case, working as expected. I explained the details here, hope it helps.

    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
        }}
    
    0 讨论(0)
  • 2020-12-30 01:51

    Use following code, it works fine for me:

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

    and implement the following methods:

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

    I want to present a nice alternative, mainly based on the one here :

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

    Usage: besides using it in the layout XML file, you can disable/enable the expanding of it using:

    appBarLayout.enableExpandAndCollapseByDraggingToolbar = true/false
    
    appBarLayout.enableExpandAndCollapseByDraggingContent = true/false
    
    0 讨论(0)
  • 2020-12-30 02:06

    Actually, you might be looking at the problem in the wrong way.

    The only thing you need is to set the Toolbar flags accordingly. You don't really anything else so I would say that your layout should be simplified to:

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

    Then when you wish to disable the collapsing just set your toolbar flags:

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

    And to enable

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

    Hold a reference to the layout params if you are changing instead of getting it all the time.

    If you need to have the CollapsingToolbarLayout get from and set the LayoutParams to that View instead, update the flags the same way but now adding the appBarLayout.setExpanded(true/false)

    Note: Using the setScrollFlags clears all previous flags, so be careful and set all required flags when using this method.

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