Graeme
Graeme

Reputation: 25864

Stop AppBarLayout scrolling off screen when NestedScrollView is empty

I have a fairly typical List functionality using a CoordinatorLayout, AppBarLayout, SwipeRefreshLayout and RecyclerView -

When the RecyclerView has enough content to scroll, the page seems fine. When the RecyclerView is empty or doesn't have enough content to scroll however, the behavior is that the AppBarLayout children with app:layout_scrollFlags="scroll|enterAlwaysCollapsed" will continue to scroll - which looks odd.

Is there a way to stop the AppBarLayout children scrolling when the NestedScrollView is empty?

enter image description here

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.CoordinatorLayout
        android:id="@+id/coordinatorLayout"
        android:background="@android:color/transparent"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/transparent"
            android:elevation="4dp">

            <LinearLayout
                android:id="@+id/eventHeader"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="16dp"
                android:background="@color/green"
                android:orientation="horizontal"
                app:layout_scrollFlags="scroll|enterAlwaysCollapsed">

                <View
                    android:layout_width="0dp"
                    android:layout_height="0dp"
                    android:layout_weight="1"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="scroll|enterAlwaysCollapsed"
                    android:textColor="@color/white"
                    android:textSize="15sp"/>

                <View
                    android:layout_width="0dp"
                    android:layout_height="0dp"
                    android:layout_weight="1"/>

            </LinearLayout>

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

        <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/swipeToRefresh"
            android:layout_width="match_parent"
            android:layout_gravity="fill_vertical"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@android:color/transparent"
                android:dividerHeight="0dp"
                android:layout_gravity="fill_vertical"
                android:drawSelectorOnTop="true"
                android:listSelector="@drawable/selector_ripple_grey_transparent"
                android:scrollbars="vertical"/>

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

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

    <TextView
        android:id="@+id/noData"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:padding="16dp"
        android:text="@string/no_data_available"
        android:textSize="17sp"/>

</FrameLayout>

Upvotes: 16

Views: 7637

Answers (5)

SerjantArbuz
SerjantArbuz

Reputation: 1244

Based on these answers: Graeme, Kostek. Thank you guys!

Mine solution combine them + it's Kotlin realization and fix deprecated code.

Create class AppBarLayoutNoNeedScrollBehavior:

/**
 * Behavior to skip scrolling (hide) [AppBarLayout] if [RecyclerView] not scrollable.
 */
class AppBarLayoutNoNeedScrollBehavior(
    private val appBarLayout: AppBarLayout,
    private val recyclerView: RecyclerView
) : AppBarLayout.Behavior() {

    init {
        setDragCallback(object : DragCallback() {
            override fun canDrag(appBarLayout: AppBarLayout): Boolean = isScrollable()
        })
    }

    fun isScrollable(): Boolean {
        /** Check [recyclerView] has some content. */
        recyclerView.adapter?.itemCount?.takeIf { it > 0 } ?: return false

        /** [recyclerView] height includes [appBarLayout], because we must exclude it. */
        var recyclerHeight = recyclerView.height - appBarLayout.height

        /** In case if we have some padding at the bottom - it should be excluded too. */
        recyclerHeight -= recyclerView.paddingBottom

        return recyclerView.computeVerticalScrollRange() > recyclerHeight
    }

    override fun onStartNestedScroll(
        parent: CoordinatorLayout,
        child: AppBarLayout,
        directTargetChild: View,
        target: View,
        nestedScrollAxes: Int,
        type: Int
    ): Boolean {
        /** If [recyclerView] not scrollable - don't allow to scroll. */
        if (!isScrollable()) return false

        return super.onStartNestedScroll(
            parent, child, directTargetChild, target, nestedScrollAxes, type
        )
    }
}

In your code:

val appBarLayout = ..
val recyclerView = ..


val layoutParams = appBarLayout.layoutParams as? CoordinatorLayout.LayoutParams
layoutParams?.behavior = AppBarLayoutNoNeedScrollBehavior(appBarLayout, recyclerView)

Upvotes: 0

Kostek
Kostek

Reputation: 177

Graeme answer is ok but I also added in constructor

public AppBarLayoutOnEmptyRecyclerViewScrollBehavior(@NonNull AppBarLayout appBarLayout, @NonNull RecyclerView recyclerView) {
    this.appBarLayout = checkNotNull(appBarLayout);
    this.recyclerView = checkNotNull(recyclerView);

    setDragCallback(new DragCallback() {
        @Override
        public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
            return isRecylerViewScrollable(recyclerView);
        }
    });
}

so when RecyclerView is empty I also disable drag from AppBarLayout

Upvotes: 2

YunhaoLIU
YunhaoLIU

Reputation: 815

After loading data to your RecyclerView, if it's empty, just disalbe the scroll flag manually:

AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mEventHeader.getLayoutParams();
toolbarLayoutParams.setScrollFlags(0);
mEventHeader.setLayoutParams(toolbarLayoutParams);

And if not empty, set back the scroll flag:

AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mEventHeader.getLayoutParams();
toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
mEventHeader.setLayoutParams(toolbarLayoutParams);

Upvotes: 0

OneEyeQuestion
OneEyeQuestion

Reputation: 742

(Based on : Reference)

(1) Create this class.

public class AppBarLayoutBehaviorForEmptyRecyclerView extends AppBarLayout.Behavior
{
    private boolean canRecyclerViewBeScrolled = false;

    public AppBarLayoutBehaviorForEmptyRecyclerView()
    {
    }

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

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev)
    {
        return canRecyclerViewBeScrolled && super.onInterceptTouchEvent(parent, child, ev);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes)
    {
        updateScrollable(target);
        return canRecyclerViewBeScrolled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed)
    {
        return canRecyclerViewBeScrolled && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    private void updateScrollable(View targetChild)
    {
        if(targetChild instanceof RecyclerView)
        {
            RecyclerView.Adapter adapter = ((RecyclerView) targetChild).getAdapter();

            canRecyclerViewBeScrolled = adapter != null && adapter.getItemCount() > 0;
        }
        else
        {
            canRecyclerViewBeScrolled = true;
        }
    }
}

(2) Add to your AppBarLayout XML element the following attribute:

app:layout_behavior="com.xxxx.xxxxxx.AppBarLayoutBehaviorForEmptyRecyclerView"

Upvotes: 3

Graeme
Graeme

Reputation: 25864

Not sure how elegant a solution this is but, I overrode the onStartNestedScroll() event to only fire if the NestedScrollView is scrollable (In this case a RecyclerView)

in onCreate():

CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) mAppBarLayout.getLayoutParams();
layoutParams.setBehavior(new AppBarLayoutNoEmptyScrollBehavior(mAppBarLayout, mRecyclerView));

Behavior:

public class AppBarLayoutNoEmptyScrollBehavior extends AppBarLayout.Behavior {

    AppBarLayout mAppBarLayout;
    RecyclerView mRecyclerView;
    public AppBarLayoutNoEmptyScrollBehavior(AppBarLayout appBarLayout, RecyclerView recyclerView) {
        mAppBarLayout = appBarLayout;
        mRecyclerView = recyclerView;
    }

    public boolean isRecylerViewScrollable(RecyclerView recyclerView) {
        int recyclerViewHeight = recyclerView.getHeight(); // Height includes RecyclerView plus AppBarLayout at same level
        int appCompatHeight    = mAppBarLayout != null ? mAppBarLayout.getHeight() : 0;
        recyclerViewHeight -= appCompatHeight;

        return recyclerView.computeVerticalScrollRange() > recyclerViewHeight;
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
        if (isRecylerViewScrollable(mRecyclerView)) {
            return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
        }
        return false;
    }

}

EDIT

Edited solution as RecyclerView gives height as visible RecyclerView height and AppBarLayout height (which is the CoordinatorLayout height).

However, if your scroll gesture starts on the visible AppBarLayout area, a scroll will still take place, even if you add this Behavior to the AppBarLayout as well. This answer therefore is not a fix for the problem.

Upvotes: 6

Related Questions