简体   繁体   中英

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?


Eg 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.

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

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

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)

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>

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

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

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>

@sonique has a very detailed answer with explaination over here.

https://stackoverflow.com/a/64235840/2867351

Basically, what you should be trying to do is to calculate the height dynamically when onPageSelected() .

You can easily just access your views inside your ViewPager2 by using childFragmentManager .

Make sure that your child fragments layout_height is wrap_content . Else, you're going to see weird behaviours when the view is trying to measure its height.

I've learn it the hard way.

viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        super.onPageSelected(position)
        if (childFragmentManager.fragments.size > position) {
            val fragment = childFragmentManager.fragments.get(position)
            fragment.view?.let {
                updatePagerHeightForChild(it, viewPager)
            }
        }
    }

    fun updatePagerHeightForChild(view: View, pager: ViewPager2) {
        view.post {
            val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
                view.width, View.MeasureSpec.EXACTLY
            )
            val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
                0, View.MeasureSpec.UNSPECIFIED
            )
            view.measure(widthMeasureSpec, heightMeasureSpec)
            if (pager.layoutParams.height != view.measuredHeight) {
                pager.layoutParams = (pager.layoutParams).also {
                    it.height = view.measuredHeight
                }
            }
        }
    }
})

@ 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 

I'm using the ViewPager2ViewHeightAnimator from here

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:

在此处输入图像描述

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!

Ps I'm sorry for my bad english skills.

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.

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

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.

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

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)

Viewpager2 by default forces the pages to be MATCH_PARENT, or an exception will be thrown

A solution for that is to do the following before you do anything to the viewpager. This will prevent the viewpager from throwing an exception when the page size is not MATCH_PARENT . Of course you must make your layout wrap_content in order for this to work.

((RecyclerView) yourViewPager2Object.getChildAt(0)).clearOnChildAttachStateChangeListeners();

Further Explanation.

if you go to the viewpager2 code you will find the following, and what we actually do is disabling this behavior.


    /**
     * A lot of places in code rely on an assumption that the page fills the whole ViewPager2.
     *
     * TODO(b/70666617) Allow page width different than width/height 100%/100%
     */
    private RecyclerView.OnChildAttachStateChangeListener enforceChildFillListener() {
        return new RecyclerView.OnChildAttachStateChangeListener() {
            @Override
            public void onChildViewAttachedToWindow(@NonNull View view) {
                RecyclerView.LayoutParams layoutParams =
                        (RecyclerView.LayoutParams) view.getLayoutParams();
                if (layoutParams.width != LayoutParams.MATCH_PARENT
                        || layoutParams.height != LayoutParams.MATCH_PARENT) {
                    throw new IllegalStateException(
                            "Pages must fill the whole ViewPager2 (use match_parent)");
                }
            }

            @Override
            public void onChildViewDetachedFromWindow(@NonNull View view) {
                // nothing
            }
        };
    }

Resources:

https://issuetracker.google.com/issues/70666617 https://gist.github.com/safaorhan/1a541af729c7657426138d18b87d5bd4

just add this code to each fragments:

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

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM