Reputation: 2471
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?
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
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
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
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
Reputation: 944
just add this code to each fragments :
override fun onResume() {
super.onResume()
binding.root.requestLayout()
}
Upvotes: 2
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
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
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
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
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
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:
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
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
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
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
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
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
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
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
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
Reputation: 249
In my case, adding adapter.notifyDataSetChanged()
in onPageSelected
helped.
Upvotes: 14