Ankur Gupta
Ankur Gupta

Reputation: 410

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?

Upvotes: 3

Views: 1672

Answers (1)

Zain
Zain

Reputation: 40878

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
}

Upvotes: 5

Related Questions