37

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
Meetarp
  • 2,331
  • 1
  • 23
  • 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. – Meetarp Oct 30 '19 at 19:38
  • use this solution https://stackoverflow.com/a/64235840/16000346 – Tarique Anowar May 11 '22 at 02:52

19 Answers19

25

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
  • 47,260
  • 30
  • 167
  • 264
Meetarp
  • 2,331
  • 1
  • 23
  • 31
  • 17
    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 – Meetarp 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.kotlin 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
21

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)

Victor
  • 331
  • 2
  • 7
12

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

colidyre
  • 4,170
  • 12
  • 37
  • 53
Almir Burnashev
  • 179
  • 1
  • 6
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,984
  • 30
  • 54
Miniskurken
  • 114
  • 1
  • 5
6

Just Add this small code in your all fragments of ViewPager2

@Override
    public void onResume() {
        super.onResume();
        binding.getRoot().requestLayout();
    }

This is working for me perfectly (If you are not using binding then Just get a root layout instance in place of binding)

Aditya Nandardhane
  • 915
  • 1
  • 8
  • 22
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
4

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

Syed Zeeshan
  • 490
  • 1
  • 7
  • 13
  • 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
2

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
  • 21
  • 3
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
  • 237
  • 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 
Sahal Nazar
  • 625
  • 4
  • 9
1

I'm using the ViewPager2ViewHeightAnimator from here

francis
  • 3,852
  • 1
  • 28
  • 30
1

Answer by @Mephoros worked for me in the end. I had a Recyclerview with pagination(v3) in one of the fragments and it was behaving really strangely with page loads. Here is a working snippet based on the answer in case anyone has problems getting and cleaning views.

viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            var view : View? = null
            private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
                view?.let {
                    updatePagerHeightForChild(it)
                }
            }

            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                // ... IMPORTANT: remove the global layout listener from other view
                view?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
                view = (viewPager[0] as RecyclerView).layoutManager?.findViewByPosition(position)
                view?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
            }

            private fun updatePagerHeightForChild(view: View) {
                view.post {
                    val wMeasureSpec = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY)
                    val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
                    view.measure(wMeasureSpec, hMeasureSpec)
                    if (viewPager.layoutParams.height != view.measuredHeight) {
                        viewPager.layoutParams = (viewPager.layoutParams)
                            .also { lp -> lp.height = view.measuredHeight }
                    }
                }
            }
        })
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
  • 41
  • 2
  • I checked your answer but `ViewPager2` will apply highest fragment for other shorter fragments. So this is not really help in this case. – Harry T. Dec 07 '21 at 03:19
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,620
  • 2
  • 23
  • 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.

StackOverflower
  • 369
  • 2
  • 12
0

just add this code to each fragments :

override fun onResume() {
    super.onResume()
    binding.root.requestLayout()
}
0

Finally, I can fix this without requestLayout, notifyDataChanged, or the other solutions above!

It's really easy and simple!

You just need to save current height onPause, then load the saved height onResume.

Look at this example code:


public class MyTabbedFragment extends Fragment {

   public MyTabbedFragmentViewBinding binding;

   String TAG = "MyTabbedFragment";

   int heightBeforePause;

   // other code

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

      Log.d(TAG, "lifecycle | onResume | before set height | rec view height: " + binding.recycleView.getHeight() + " | height before pause: " + heightBeforePause);

      // load the saved height
      if(heightBeforePause > 0) {
         FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, heightBeforePause);
         binding.recycleView.setLayoutParams(layoutParams);
      }
   }

   @Override
   public void onPause() {
      super.onPause();

      // save the current height
      heightBeforePause = binding.recycleView.getHeight();
      
      Log.d(TAG, "lifecycle | onPause | rec view height: " + binding.recycleView.getHeight());
   }

Rem
  • 176
  • 3
  • 7
0

I case if you have dynamic content height on each page and it varies from page to page here is the code this handles page height base on actual page height and available space on a screen.

import android.graphics.Rect
import android.view.View
import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams
import androidx.viewpager2.widget.ViewPager2

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

    view.doOnLayout {
        val pageVisibleRect = Rect().also {
            view.getGlobalVisibleRect(it)
        }
        val pageVisibleHeight = pageVisibleRect.height()
        val pageActualHeight = view.measuredHeight

        val pagerVisibleRect = Rect().also {
            pager.getGlobalVisibleRect(it)
        }
        val pagerVisibleHeight = pagerVisibleRect.height()

        val rootVisibleRect = Rect().also {
            pager.rootView.getGlobalVisibleRect(it)
        }
        val rootVisibleHeight = rootVisibleRect.height()

        val isPageSmallerThanAvailableHeight =
            pageActualHeight <= pageVisibleHeight && pagerVisibleHeight <= rootVisibleHeight

        if (isPageSmallerThanAvailableHeight) {
            pager.updateLayoutParams {
                 height = pagerVisibleRect.bottom - pageVisibleRect.top

            }
        } else {
            pager.updateLayoutParams {
                height = pageActualHeight
            }
        }
        pager.invalidate()
    }
 }

Add this code in fragment or activity:

    pager.setPageTransformer { page, pos ->
        if (pos == 0.0F) {
            updatePagerHeightForChild(page, pager)
        }
    }
-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 }
      }
    }
  }
})
Hun
  • 3,652
  • 35
  • 72