3

I have a ViewPager2 that has different height fragments as children, the height of the children change after data is loaded

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:expandedTitleGravity="top"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <com.google.android.material.tabs.TabLayout
                    android:id="@+id/tabs_header"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <com.google.android.material.tabs.TabItem
                        android:layout_width="wrap_content"
                        android:layout_height="match_parent"
                        android:text="@string/prospect_detail_tab_prospect" />

                    <com.google.android.material.tabs.TabItem
                        android:layout_width="wrap_content"
                        android:layout_height="match_parent"
                        android:text="@string/prospect_detail_tab_influencers" />

                    <com.google.android.material.tabs.TabItem
                        android:layout_width="wrap_content"
                        android:layout_height="match_parent"
                        android:text="@string/prospect_detail_tab_sales" />

                </com.google.android.material.tabs.TabLayout>

                <androidx.viewpager2.widget.ViewPager2
                    android:id="@+id/pager_header"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" />

            </LinearLayout>
        </com.google.android.material.appbar.CollapsingToolbarLayout>

I've tried a few different solutions but nothing has worked for resizing the viewpager2 if it goes back to a smaller fragment, it just stays the same height as the tallest fragment

Derek Hannah
  • 537
  • 7
  • 23
  • What have you tried exactly? My first thought would be to call `requestLayout()` on the viewpager when a page changes, but I don't know how that'll work. – Nicolas Apr 21 '20 at 22:58
  • Please have a look at [here](https://stackoverflow.com/questions/8394681/android-i-am-unable-to-have-viewpager-wrap-content) – Zain Apr 21 '20 at 23:13
  • i did try requestLayout() on the viewpager when the page changes but that had no effect – Derek Hannah Apr 21 '20 at 23:51

2 Answers2

14

What happen is the ViewPager2 doesn't refresh its own height when your fragment's height changes (if you are using a RecyclerView inside for instance).

In order to solve this, we need to calculate the height when the user select the tab. This can be done using registerOnPageChangeCallback and onPageSelected

registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)

                    // Here we need to calculate height
                }
            })

Now, we need to calculate the height of the fragment. To do so, we need to access the fragment and its root view. This can be done using the fragmentManager provided to your ViewPagerAdapter, when creating it, e.g. here we are using childFragmentManager: PagerViewAdapter(childFragmentManager, lifecycle)


registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        super.onPageSelected(position)
    
        // Because the fragment might or might not be created yet, 
        // we need to check for the size of the fragmentManager 
        // before accessing it.
        if (childFragmentManager.fragments.size > position) {
            val fragment = childFragmentManager.fragments.get(position)
            fragment.view?.let {
                // Now we've got access to the fragment Root View
                // we will use it to calculate the height and 
                // apply it to the ViewPager2
                updatePagerHeightForChild(it, binding.viewPager)
            }
        }
    }
})
// This function can sit in an Helper file, so it can be shared across your project.
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)

        if (pager.layoutParams.height != view.measuredHeight) {
            pager.layoutParams = (pager.layoutParams)
                    .also { lp ->
                        // applying Fragment Root View Height to
                        // the pager LayoutParams, so they match
                        lp.height = view.measuredHeight
                    }
        }
    }
}

Notice: We are accessing the viewPager using binding, if you do not use binding, provide the viewPager your own way, maybe with findViewById()

Limitation: Async Loading Content

if you've got some dynamic loading content, you might need to couple with solution with ViewTreeObserver.OnGlobalLayoutListener, I haven't tested that yet.

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

sonique
  • 4,539
  • 2
  • 30
  • 39
  • Great answer, but what if I'm not using Fragments within my ViewPager2? I'm just using a RecyclerView.Adapter and a layout and ViewHolder... – ProjectDelta May 11 '23 at 10:14
  • 1
    similar principle applies. onPageSelected, you will need to calculate the new height. notice how the view (fragment.view?.let { ... }) is used for the calculation, you should be able to do the same with your layout. – sonique May 17 '23 at 21:04
  • 1
    Thanks for that, I had to do a bit extra to also call the updatePagerHeightForChild function inside my pagerAdapter when submitting the inner list to make the page size grow as items are added, thank you :D – ProjectDelta May 18 '23 at 08:23
0

You need to custom ViewPager a little bit:

public class WrapHeightViewPager extends ViewPager {
    private int currentPosition = 0;

    public WrapHeightViewPager(@NonNull Context context) {
        super(context);
    }

    public WrapHeightViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (currentPosition < getChildCount()) {
            View child = getChildAt(currentPosition);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            int height = child.getMeasuredHeight();
            if (height != 0) {
                heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
            }
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    public void measureViewPager(int currentPosition) {
        this.currentPosition = currentPosition;
        requestLayout();
    }
}

After that, when init tabLayout, just call measureViewPager(currentPosition) in onTabSelected function

tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        viewPager.measureViewPager(tab.getPosition());
    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {

    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {
    }
});
Man1s
  • 192
  • 1
  • 7
  • 1
    this doesn't works as your are using ViewPager (not ViewPager2) and ViewPager2 class is final (cannot be extended): https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-collection-release/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java – sonique Oct 07 '20 at 02:44