androidandroid-layoutandroid-coordinatorlayoutandroid-appbarlayoutandroid-viewpager2

ViewPager within a CoordinatorLayout (for sticky header purposes) leads to broken scrolling


Test app demonstrating the problem here: https://github.com/jstubing/ViewPagerTest

I'm using a CoordinatorLayout/AppBarLayout combo to achieve a sticky header within a scrollable page. At the very bottom of the page is a ViewPager2. When I swipe to a new ViewPager page, content loads asynchronously and populates a RecyclerView within the ViewPager. This all works well.

The problem occurs when I swipe to a new ViewPager page. At this point, the content loads and renders fine but I am no longer able to initiate a scroll outside of the ViewPager. In other words, swiping up and down in the "UPPER PAGE CONTENT" section (see XML) does nothing.

However, scrolling up and down does work within the ViewPager. If I scroll up or down in the ViewPager, or even just tap the ViewPager once, it fixes everything and I am again able to initiate a scroll from outside of the ViewPager.

I tried switching to the original ViewPager instead of ViewPager2 and it has definitely been more reliable, but the problem still happens every so often.

Any ideas how I can fix this? Ultimately I just want to be able to swipe between ViewPager pages and have the whole entire activity remain scrollable. I'm so confused as to why I lose scrolling ability in the first place, and why tapping on the ViewPager fixes it.

<androidx.constraintlayout.widget.ConstraintLayout 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">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:elevation="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/background"
            app:elevation="0dp">

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="32dp"
                app:layout_scrollFlags="scroll">

                <!-- UPPER PAGE CONTENT -->

            </androidx.constraintlayout.widget.ConstraintLayout>

            <!-- Sticky header -->
            <include
                android:id="@+id/sticky_header"
                layout="@layout/sticky_header"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

        </com.google.android.material.appbar.AppBarLayout>

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/events_pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Solution

  • The problem occurs when I swipe to a new ViewPager page. At this point, the content loads and renders fine but I am no longer able to initiate a scroll outside of the ViewPager.

    I am again able to initiate a scroll from outside of the ViewPager.

    So, the problem is now limited to the ViewPager. Most probably because the ViewPager2 supports nested scrolling through its inner RecyclerView (I don't mean the RecyclerView of the page, but the one that makes the ViewPager functions internally.

    So, first thing we need to disable this nested-scrolling of the ViewPager2:

    Kotlin:

    viewPager.children.find { it is RecyclerView }?.let {
            (it as RecyclerView).isNestedScrollingEnabled = false
    }
    

    Java:

    for (int i = 0; i < viewPager.getChildCount(); i++) {
        View child = viewPager.getChildAt(i);
        if (child instanceof RecyclerView) 
            child.setNestedScrollingEnabled(false);
    }
    

    We can't completely disable the nested scrolling because this will badly affect the scrolling outside, so, we can manipulate this by a NestedScrollView that wraps the ViewPager:

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
    
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/events_pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent />
    
    </androidx.core.widget.NestedScrollView>