Meetarp
Meetarp

Reputation: 2471

ViewPager2 with differing item heights and WRAP_CONTENT

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.

Upvotes: 40

Views: 37601

Answers (20)

Mahdi Zareei
Mahdi Zareei

Reputation: 2056

i handle the dynamic height of view pager2 by this

private fun updateViewPagerHeightForCurrentPage(position: Int) {
    val viewPager = itemView.findViewById<ViewPager2>(R.id.vp_orders)
    val recyclerView = viewPager.getChildAt(0) as? RecyclerView ?: return
    val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) ?: return
    val itemView = viewHolder.itemView

    itemView.measure(
        View.MeasureSpec.makeMeasureSpec(recyclerView.width, View.MeasureSpec.EXACTLY),
        View.MeasureSpec.UNSPECIFIED
    )
    val newHeight = itemView.measuredHeight

    val layoutParams = viewPager.layoutParams
    layoutParams.height = newHeight
    viewPager.layoutParams = layoutParams
}

how to use:

viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback(){
       override fun onPageSelected(position: Int) {
           super.onPageSelected(position)
           updateViewPagerHeightForCurrentPage(position)
       }
})

Upvotes: 0

Dmitry Mersiyanov
Dmitry Mersiyanov

Reputation: 88

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

Upvotes: 0

Rem
Rem

Reputation: 392

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());
   }

Upvotes: 0

Alireza Ghanbarinia
Alireza Ghanbarinia

Reputation: 944

just add this code to each fragments :

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

Upvotes: 2

Aditya Nandardhane
Aditya Nandardhane

Reputation: 1413

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)

Upvotes: 8

Nikola Djokic
Nikola Djokic

Reputation: 113

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

Upvotes: 1

StackOverflower
StackOverflower

Reputation: 437

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.

Upvotes: 0

francis
francis

Reputation: 4515

I'm using the ViewPager2ViewHeightAnimator from here

Upvotes: 1

Swapnil Kale
Swapnil Kale

Reputation: 3040

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()
            }
        })

Upvotes: 0

Wiatrak
Wiatrak

Reputation: 19

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.

Upvotes: 2

gnekki4
gnekki4

Reputation: 51

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.

Upvotes: 0

Victor
Victor

Reputation: 461

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)

Upvotes: 34

Syed Zeeshan
Syed Zeeshan

Reputation: 570

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

Upvotes: 4

Hun
Hun

Reputation: 3798

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

Upvotes: -1

Sahal Nazar
Sahal Nazar

Reputation: 1005

@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 

Upvotes: 1

Potass
Potass

Reputation: 267

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>

Upvotes: 2

Meetarp
Meetarp

Reputation: 2471

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

Upvotes: 26

Bikash Das
Bikash Das

Reputation: 51

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

Upvotes: 5

Miniskurken
Miniskurken

Reputation: 114

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>

Upvotes: 6

Almir Burnashev
Almir Burnashev

Reputation: 249

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

Upvotes: 14

Related Questions