Search code examples
androidandroid-layoutandroid-coordinatorlayoutandroid-viewpager2

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?


Solution

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