AppBar not scrolling with nested ViewPager2

I have a view hierarchy as shown in the image below.

View hierarchy

I’m getting strange scroll behaviors like,

  1. If I scroll (drag slowly or fling) from Area 1 the AppBar collapses along with it. This is fine.
  2. But if I drag slowly from Area 2 the AppBar does not collapse. It stays there and RecyclerView goes beneath it. However, it works fine with a fling.

activity_challenge_detail.xml

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".challengedetail.ChallengeDetailActivity">

        <androidx.coordinatorlayout.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.google.android.material.appbar.AppBarLayout
                android:id="@+id/app_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/black">

                <com.google.android.material.appbar.CollapsingToolbarLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:contentScrim="@color/black"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_scrollFlags="scroll|exitUntilCollapsed">

                    <androidx.constraintlayout.widget.ConstraintLayout
                        android:id="@+id/header"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        app:layout_collapseMode="parallax"
                        app:layout_collapseParallaxMultiplier="0.2">

                        <FrameLayout
                            android:id="@+id/challengeBannerFrame"
                            android:layout_width="match_parent"
                            android:layout_height="0dp"
                            android:foreground="@drawable/banner_gradient"
                            app:layout_constraintDimensionRatio="H,1:1"
                            app:layout_constraintEnd_toEndOf="parent"
                            app:layout_constraintStart_toStartOf="parent"
                            app:layout_constraintTop_toTopOf="parent">

                            <ImageView
                                android:id="@+id/challengeBanner"
                                android:layout_width="match_parent"
                                android:layout_height="match_parent"
                                android:contentDescription="@string/challenge_banner"
                                android:scaleType="centerCrop"
                                tools:src="@tools:sample/avatars" />
                        </FrameLayout>
                    </androidx.constraintlayout.widget.ConstraintLayout>

                    <androidx.appcompat.widget.Toolbar
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:minHeight="@dimen/dp16"
                        app:layout_collapseMode="pin">

                        <com.company.widget.StatusBarSpacer
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content" />
                    </androidx.appcompat.widget.Toolbar>
                </com.google.android.material.appbar.CollapsingToolbarLayout>

                <androidx.appcompat.widget.Toolbar
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@android:color/transparent"
                    app:contentInsetEnd="0dp"
                    app:contentInsetStart="0dp"
                    app:layout_collapseMode="pin">

                    <androidx.constraintlayout.widget.ConstraintLayout
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:background="@android:color/transparent">

                        <com.google.android.material.tabs.TabLayout
                            android:id="@+id/switchingTabsBar"
                            android:layout_width="match_parent"
                            android:layout_height="@dimen/dp0"
                            android:background="@drawable/switching_tab_bg"
                            app:layout_constraintBottom_toBottomOf="parent"
                            app:layout_constraintDimensionRatio="4"
                            app:layout_constraintEnd_toEndOf="parent"
                            app:layout_constraintStart_toStartOf="parent"
                            app:layout_constraintTop_toTopOf="parent"
                            app:tabBackground="@drawable/active_tab_selector"
                            app:tabIconTint="@color/black"
                            app:tabIndicator="@drawable/active_tab_indicator"
                            app:tabIndicatorColor="@color/yellow_500"
                            app:tabMode="fixed"
                            app:tabRippleColor="@null" />

                    </androidx.constraintlayout.widget.ConstraintLayout>

                </androidx.appcompat.widget.Toolbar>

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

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

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

fragment_challenge_post.xml

<androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/gradient_challenge_post"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".challengedetail.fragment.ChallengePostFragment">
    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/challengePostRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0"
            tools:itemCount="1"
            tools:listitem="@layout/list_item_post" />
</androidx.constraintlayout.widget.ConstraintLayout>

list_item_post.xml

<com.google.android.material.card.MaterialCardView
        android:id="@+id/cardView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardBackgroundColor="@color/white"
        app:cardCornerRadius="@dimen/dp16"
        app:cardElevation="@dimen/dp0"
        app:strokeColor="@color/gray_f5"
        app:strokeWidth="@dimen/dp1">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="@dimen/dp16">

            <com.google.android.material.imageview.ShapeableImageView
                android:id="@+id/userImageView"
                android:layout_width="@dimen/dp48"
                android:layout_height="@dimen/dp48"
                android:layout_marginStart="16dp"
                android:layout_marginTop="16dp"
                android:scaleType="centerCrop"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:shapeAppearanceOverlay="@style/ShapeAppearance.userProfileImage"
                tools:src="@tools:sample/avatars" />

            <TextView
                android:id="@+id/userNameText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginTop="16dp"
                android:lineSpacingExtra="5sp"
                android:textAppearance="@style/Inter.Semi.16"
                app:layout_constraintStart_toEndOf="@+id/userImageView"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="@tools:sample/full_names" />

            <TextView
                android:id="@+id/timestampText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:lineSpacingExtra="7sp"
                android:textAppearance="@style/Inter.Regular.14"
                app:layout_constraintStart_toEndOf="@+id/userImageView"
                app:layout_constraintTop_toBottomOf="@+id/userNameText"
                tools:text="2 hrs ago" />

            <com.company.widget.NestedScrollableHost
                android:id="@+id/viewPagerHost"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/userImageView"
                tools:layout_constraintDimensionRatio="1:1">

                <androidx.viewpager2.widget.ViewPager2
                    android:id="@+id/postImagesViewPager"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" />
            </com.company.widget.NestedScrollableHost>
        </androidx.constraintlayout.widget.ConstraintLayout>
    </com.google.android.material.card.MaterialCardView>

I have tried solutions to other questions as well like wrapping the nested ViewPager2 by NestedScrollableHost class. But it did not seem to work. Any ideas?

Answer

To fix this you need a couple of steps:

  1. Wrap the outer ViewPager2 in a NestedScrollView, and of course transfer the scrolling behavior to it:

    So in activity_challenge_detail.xml:

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
    
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/challengeDetailsViewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent />
    
    </androidx.core.widget.NestedScrollView>
    
  2. Disable the nested scrolling of the internal RecyclerView of both ViewPagers: and as it’s not accessible, you can use java reflections to make that RecyclerView accessible through its field definition in the ViewPager2 class:

    Kotlin:

    fun ViewPager2.getRecyclerView(): RecyclerView? {
        try {
            val field = ViewPager2::class.java.getDeclaredField("mRecyclerView")
            field.isAccessible = true
            return field.get(this) as RecyclerView
        } catch (e: NoSuchFieldException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        }
        return null
    }

    val recyclerView = viewPager.getRecyclerView()
    recyclerView?.isNestedScrollingEnabled = false

Java

    public static RecyclerView getRecyclerView(ViewPager2 viewPager) {
        try {
            Field field = ViewPager2.class.getDeclaredField("mRecyclerView");
            field.setAccessible(true);
            return (RecyclerView) field.get(viewPager);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    RecyclerView recyclerView = getRecyclerView(viewPager);
    if (recyclerView != null)
        recyclerView.setNestedScrollingEnabled(false);

Preview:

  • The black area is the AppBarLayout
  • The grey area is the ViewPager2
  • The purple area is ViewPager pages

UPDATE:

Thanks @Ankur Gupta & @SimpleAndroid:

There is a nice trick instead of reflections to get the RecyclerView of the ViewPager2, and disable the nested scrolling accordingly:

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