32

There are a few posts on getting ViewPager to work with varying height items that center around extending ViewPager itself to modify its onMeasure to support this.

However, given that ViewPager2 is marked as a final class, extending it isn't something that we can do.

Does anyone know if there's a way to make this work out?


E.g. let's say I have two views:

View1 = 200dp

View2 = 300dp

When the ViewPager2 (layout_height="wrap_content") loads -- looking at View1, its height will be 200dp.

But when I scroll over to View2, the height is still 200dp; the last 100dp of View2 is cut off.

Community
  • 1
  • 1
Mephoros
  • 2,013
  • 1
  • 16
  • 31
  • 1
    Have you tried keeping both views in the hierarchy by calling [`setOffscreenPageLimit`](https://developer.android.com/reference/androidx/viewpager2/widget/ViewPager2.html#setOffscreenPageLimit(int))? – Sandi Oct 21 '19 at 22:46
  • 1
    Hi - while that would resolve the problem, the issue with using `setOffscreenPageLimit` is that I don't want the smaller view to have a bunch of whitespace / empty underneath it. – Mephoros Oct 30 '19 at 19:38
  • use this solution https://stackoverflow.com/a/64235840/16000346 – Tarique Anowar May 11 '22 at 02:52

14 Answers14

18

The solution is to register a PageChangeCallback and adjust the LayoutParams of the ViewPager2 after asking the child to re-measure itself.

pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        super.onPageSelected(position)
        val view = // ... get the view
        view.post {
            val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
            val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
            view.measure(wMeasureSpec, hMeasureSpec)

            if (pager.layoutParams.height != view.measuredHeight) {
                // ParentViewGroup is, for example, LinearLayout
                // ... or whatever the parent of the ViewPager2 is
                pager.layoutParams = (pager.layoutParams as ParentViewGroup.LayoutParams)
                    .also { lp -> lp.height = view.measuredHeight }
            }
        }
    }
})

Alternatively, if your view's height can change at some point due to e.g. asynchronous data load, then use a global layout listener instead:

pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    private val listener = ViewTreeObserver.OnGlobalLayoutListener {
        val view = // ... get the view
        updatePagerHeightForChild(view)
    }

    override fun onPageSelected(position: Int) {
        super.onPageSelected(position)
        val view = // ... get the view
        // ... IMPORTANT: remove the global layout listener from other views
        otherViews.forEach { it.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener) }
        view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
    }

    private fun updatePagerHeightForChild(view: View) {
        view.post {
            val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
            val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
            view.measure(wMeasureSpec, hMeasureSpec)

            if (pager.layoutParams.height != view.measuredHeight) {
                // ParentViewGroup is, for example, LinearLayout
                // ... or whatever the parent of the ViewPager2 is
                pager.layoutParams = (pager.layoutParams as ParentViewGroup.LayoutParams)
                    .also { lp -> lp.height = view.measuredHeight }
            }
        }
    }
}

See discussion here:

https://issuetracker.google.com/u/0/issues/143095219

rekire
  • 46,262
  • 29
  • 163
  • 256
Mephoros
  • 2,013
  • 1
  • 16
  • 31
  • 16
    How do you ```// ... get the view```? – xxfast Dec 14 '19 at 08:59
  • @xxfast You have to pass items into your adapter - so I just get it from the list of Views I pass in – Mephoros Dec 20 '19 at 19:08
  • @xxfast I did it super ugly on my end but it works. First I keep reference to the recyclerview taking it from onAttachToRecyclerView(recyclerView) and then I get the view like: ```(viewPager2.adapter as Adapter).recyclerView.layoutManager?.findViewByPosition(position)``` Of course first you have to assign Adapter instance to viewpager2 (setAdapter) – Borislav Kamenov Jan 07 '20 at 22:14
  • [This doesn't work when views are peeked.](https://github.com/android/views-widgets-samples/blob/ed02093413/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/PreviewPagesActivity.kt) [Refer this https://stackoverflow.com/a/60410925/1008278](https://stackoverflow.com/a/60410925/1008278) – VenomVendor Apr 30 '20 at 07:05
  • What is otherViews? I am not able to clear all the listener of the rest of the views. – AndroidRuntimeException Jul 08 '20 at 22:02
  • I am using fragments, so I use a getInstance method in my Adapter in order to get the Views, I don't pass the Views. How can I acces them in the listener then? – Funnycuni Jul 20 '20 at 12:06
  • 2
    How do I `// ... get the view` if I'm not passing any items to the adapter? I'm using `FragmentStateAdapter` and I am instantiating the fragments there. Because the size is always two, I simply use a static value (2 for the `getItemCount`). – Richard Jul 25 '20 at 11:05
  • @Richard Any solution? – praj Jul 26 '20 at 12:17
  • @praj was also struggling with this, but actually it's easy: in your FragmentStateAdapter derived class, just add a public property like List or List. In createFragment just update this list before returning your new fragment instance. In your onPageSelected callback then simply access this public property. – stefan.at.wpf Sep 05 '20 at 21:54
  • Since the inner `RecyclerView` in `ViewPager2` is an implementation detail and cannot be accessed, I use this to get ahold of the item view: `fun getViewAtPosition(position: Int): View = (viewPager.getChildAt(0) as RecyclerView).layoutManager?.findViewByPosition(position) ?: error("No layout manager set or no view found at position $position")` – Thomas Keller Sep 02 '21 at 08:27
  • When using TabLayout with ViewPager2,`// ... get the view` would be null on the tab item clicked. So it may be more precise to measure `the view` in the first call of `onPageScrolled` after every `onPageSelected` – luobo25 Nov 01 '21 at 10:32
11

In my case, adding adapter.notifyDataSetChanged() in onPageSelected helped.

colidyre
  • 3,401
  • 11
  • 33
  • 47
Almir Burnashev
  • 127
  • 1
  • 3
8

Just do this for the desired Fragment in ViewPager2:

override fun onResume() {
     super.onResume()
     layoutTaskMenu.requestLayout()
}

Jetpack: binding.root.requestLayout() (thanks @syed-zeeshan for the specifics)

  • 2
    This works very well.. I just called this in onResume 'binding.root.requestLayout()' – Syed Zeeshan Aug 11 '21 at 11:26
  • 1
    This is perfect solution for my case.... with FragmentStateAdapter. Needed to add onResume code in all fragments. Voila!!! – skafle Oct 09 '21 at 00:21
6

Stumbled across this case myself however with fragments. Instead of resizing the view as the accepted answer I decided to wrap the view in a ConstraintLayout. This requires you to specify a size of your ViewPager2 and not use wrap_content.

So Instead of changing size of our viewpager it will have to be minimum size of the largest view it handles.

A bit new to Android so don't know if this is a good solution or not, but it does the job for me.

In other words:

<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  >
    <!-- Adding transparency above your view due to wrap_content -->
    <androidx.constraintlayout.widget.ConstraintLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      >

      <!-- Your view here -->

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
rupinderjeet
  • 2,829
  • 26
  • 48
Miniskurken
  • 104
  • 1
  • 5
5

For me this worked perfectly:


    viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageScrolled(
                position: Int,
                positionOffset: Float,
                positionOffsetPixels: Int
            ) {
                super.onPageScrolled(position,positionOffset,positionOffsetPixels)
                if (position>0 && positionOffset==0.0f && positionOffsetPixels==0){
                    viewPager2.layoutParams.height =
                        viewPager2.getChildAt(0).height
                }
            }
        })
Bikash Das
  • 51
  • 1
  • 1
2

Just call .requestLayout() to the root view of layout in the onResume() of your Fragment class which is being used in ViewPager2

Syed Zeeshan
  • 402
  • 6
  • 11
  • 1
    this actually works, in each of my Fragment used by ViewPager, I add many children (Fragment). also, my app had some kind of Load More functionality, where you click to load more data, and it works. nice one – bobby I. Nov 25 '21 at 12:29
1

No posted answer was entirely applicable for my case - not knowing the height of each page in advance - so I solved different ViewPager2 pages heights using ConstraintLayout in the following way:

<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"
    >

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        >

        <!-- ... -->

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

    <!-- Wrapping view pager into constraint layout to make it use maximum height for each page. -->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/viewPagerContainer"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/bottomNavigationView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/appBarLayout"
        >

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigationView"
        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:menu="@menu/bottom_navigation_menu"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
Potass
  • 229
  • 3
  • 8
1

@Mephoros code works perfectly when swiped between views but won't work when views are peeked for first time. It works as intended after swiping it.

So, swipe viewpager programmatically:

binding.viewpager.setCurrentItem(1)
binding.viewpager.setCurrentItem(0) //back to initial page 
1

I'm using the ViewPager2ViewHeightAnimator from here

francis
  • 2,165
  • 20
  • 23
0

I got stuck with this problem too. I was implementing TabLayout and ViewPager2 for several tabs with account information. Those tabs had to be with different heights, for example: View1 - 300dp, View2 - 200dp, Tab3 - 500dp. The height was locked within first view's height and the others were cut or extended to (example) 300dp. Like so:

enter image description here

So after two days of searches nothing helped me (or i had to try better) but i gave up and used NestedScrollView for all my views. For sure, now i don't have effect, that the header of profile scrolls with info in 3 views, but at least it now works somehow. Hope this one helps someone! If you have some advices, feel free to reply!

P.s. I'm sorry for my bad english skills.

gnekki4
  • 21
  • 2
  • I checked your answer but `ViewPager2` will apply highest fragment for other shorter fragments. So this is not really help in this case. – Truong Hieu Dec 07 '21 at 03:19
0

I had a similar problem and solved it as below. In my case I had ViewPager2 working with TabLayout with fragments with different heights. In each fragment in the onResume() method, I added the following code:

@Override
public void onResume() {
    super.onResume();
    setProperHeightOfView();
}

private void setProperHeightOfView() {
    View layoutView = getView().findViewById( R.id.layout );
    if (layoutView!=null) {
        ViewGroup.LayoutParams layoutParams = layoutView.getLayoutParams();
        if (layoutParams!=null) {
            layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
            layoutView.requestLayout();
        }
    }
}

R.id.layout is layout of particular fragment. I hope I helped. Best regards, T.

Wiatrak
  • 1
  • 2
0

Only adapter.notifyDataSetChanged() worked for me in ViewPager2. Used below code in Kotlin.

 viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                adapter.notifyDataSetChanged()
            }
        })
Swapnil Kale
  • 2,090
  • 1
  • 21
  • 21
0

why don't you do it by replacing not using ViewPager2. like code in below:

private void fragmentController(Fragment newFragment){
    FragmentTransaction ft;
    ft = mainAct.getSupportFragmentManager().beginTransaction();
    ft.replace(R.id.relMaster, newFragment);
    ft.addToBackStack(null);
    ft.commitAllowingStateLoss();
}

Where relMaster is RelativeLayout.

-1
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
  override fun onPageSelected(position: Int) {
    super.onPageSelected(position)
    val view = (viewPager[0] as RecyclerView).layoutManager?.findViewByPosition(position)

    view?.post {
      val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
      val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
      view.measure(wMeasureSpec, hMeasureSpec)

      if (viewPager.layoutParams.height != view.measuredHeight) {
        viewPager.layoutParams = (viewPager.layoutParams).also { lp -> lp.height = view.measuredHeight }
      }
    }
  }
})
Changhoon
  • 3,316
  • 34
  • 67