Android: I am unable to have ViewPager WRAP_CONTENT
Asked Answered
E

33

279

I have setup a simple ViewPager that has an ImageView with a height of 200dp on each page.

Here is my pager:

pager = new ViewPager(this);
pager.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
pager.setBackgroundColor(Color.WHITE);
pager.setOnPageChangeListener(listener);
layout.addView(pager);

Despite the height set as wrap_content, the pager always fills the screen even though the imageview is only 200dp. I tried to replace the height of the pager with "200" but that gives me different results with multiple resolutions. I am unable to add "dp" to that value. How do I add 200dp to the pager's layout?

Electromagnet answered 6/12, 2011 at 2:33 Comment(1)
please star issue code.google.com/p/android/issues/detail?id=54604Shuddering
F
435

Overriding onMeasure of your ViewPager as follows will make it get the height of the biggest child it currently has.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int height = 0;
    int childWidthSpec = MeasureSpec.makeMeasureSpec(
        Math.max(0, MeasureSpec.getSize(widthMeasureSpec) -
            getPaddingLeft() - getPaddingRight()),
        MeasureSpec.getMode(widthMeasureSpec)
    );
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        child.measure(childWidthSpec, MeasureSpec.UNSPECIFIED);
        int h = child.getMeasuredHeight();
        if (h > height) height = h;
    }
    
    if (height != 0) {
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
    }

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Fastigium answered 26/12, 2013 at 12:22 Comment(21)
Great for me, using a viewpager with only two views. Never have to worry about measuring views that aren't currently in the pager.Guyton
This is out standnig answer, Ifound. I wans getting blank pages in @cybergen answer but This is perfectChuchuah
This comes closest to what I need, but there are two things to add: 1. The ViewPager only resizes to the biggest of its actual children, that is, only the currently visible item and the directly adjacent ones. Calling setOffscreenPageLimit(total number of children) on the ViewPager solves this and results in a ViewPager whose size is set to the biggest of all its items and never resizes. 2. WebViews have some strange issues when trying to measure them. Calling requestLayout() on a WebView after loading something solves that.Midway
There is just a small issue that I'm going to fix: if the viewPager has its visibility to GONE and you set it to visible, onMeasure gets called before its fragment get created. So it will end up having a height of 0. If anyone has an idea, he's welcome. I think I will go with a callback for when the fragment is createdDodson
made it close to my problem. But how can i make my viewpager to acquire particular portion of the screen? let's say layout_weight = 60%.. This solution is measuring the height of child.Doody
This won't work if you have decor child views - this is because ViewPager.onMeasure() measures the decor views and allocates space to them first, then gives the remainder of the space to the non-decor children. Nonetheless, this by far the least incorrect solution here so I've upvoted ;)Balough
@BenjaminDobell any idea i m using viewpager at bottom with slideup while at position 0 viewpager contains edittext and listview (it may contain different list size somtimes 2 or 3 8 etc ) and other than at position 0 of view pager i have fixed height two textviews i.e 80dp and 40dp now my issue i m unable to set viewpager height dynamically ? how to resolve ?Behling
Is there any way to account for a PagerTitleStrip child view's height in this solution? Currently the wrap_content is too short by the height of a PagerTitleStrip I have included. If I remove the PagerTitleStrip the above code works like a charmPersonate
add plus paddings (top, bottom)Lardner
@0101100101, the way I got around your first point (the ViewPager only resizing to the visible children) is storing a private variable mBiggestHeight. It starts at 0, and if (height > mBiggestHeight) on any measure, then mBiggestHeight gets set to that height. The implication is that your ViewPager doesn't start off very tall if it doesn't have any tall neighbors, but once you've scrolled to them, the height grows to the biggest one and never gets smaller again.Latecomer
Is there a way to set the width too? I want to display several 'carousel' items at the same time, but the viewpager makes each 'item' width match the width of my activity/fragment.Lowelllowenstein
I keep coming back to this every time I use a ViewPagerCystocarp
getChildCount() might return 0 while you've already performed setAdapter() on ViewPager! The actual populate() call (that creates the views) happens inside the super.onMeasure(widthMeasureSpec, heightMeasureSpec); call. Putting the extra super.onMeasure() call at the start of this function did the trick. Also check #38492710Cordiform
getChildCount(); always returns 0 why?Hulsey
This helped me out a lot, but on older devices (4.4) it cuts off some of the bottom. Is it because I am using padding and margins on my view pager items?Florafloral
Cool :) I have two recyclerview's in viewpager. Say please, why after orientation changed last item of recyclerview not see completely and he don't scroll.Verrucose
Why would I want the height to be the biggest among its children? If I scroll to the adjacent children, it will make the view have a LOT of extra space and look funny. For example, I'm on page 3 out of 5 with a height of 32, and I move to page 2 with a height of 8. Now page 2 will have a height of 32 but the content doesn't even fit that.Blooming
Working really good but just one thing, I used this in fragment looking good when coming on the fragment at the first time but when I come from back stack, the view pager becomes invisible(height 0)Y
What if one is using ViewPager2 (which is a final class)? What to do then :/Mathieu
Please how to make it work with PagerAdapter ?Flooded
@Midway - Thumbs up to the tweak mentioned. I had a similar requirementRoyce
F
112

Another more generic solution is to get wrap_content to just work.

I've extended ViewPager to override onMeasure(). The height is wraped around the first child view. This could lead to unexpected results if the child views are not exactly the same height. For that the class can be easily extended to let's say animate to the size of the current view/page. But I didn't need that.

You can use this ViewPager in yout XML layouts just like the original ViewPager:

<view
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    class="de.cybergen.ui.layout.WrapContentHeightViewPager"
    android:id="@+id/wrapContentHeightViewPager"
    android:layout_alignParentBottom="true"
    android:layout_alignParentLeft="true"/>

Advantage: This approach allows using the ViewPager in any layout including RelativeLayout to overlay other ui elements.

One drawback remains: If you want to use margins, you have to create two nested layouts and give the inner one the desired margins.

Here's the code:

public class WrapContentHeightViewPager extends ViewPager {

    /**
     * Constructor
     *
     * @param context the context
     */
    public WrapContentHeightViewPager(Context context) {
        super(context);
    }

    /**
     * Constructor
     *
     * @param context the context
     * @param attrs the attribute set
     */
    public WrapContentHeightViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // find the first child view
        View view = getChildAt(0);
        if (view != null) {
            // measure the first child view with the specified measure spec
            view.measure(widthMeasureSpec, heightMeasureSpec);
        }

        setMeasuredDimension(getMeasuredWidth(), measureHeight(heightMeasureSpec, view));
    }

    /**
     * Determines the height of this view
     *
     * @param measureSpec A measureSpec packed into an int
     * @param view the base view with already measured height
     *
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec, View view) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            // set the height from the base view if available
            if (view != null) {
                result = view.getMeasuredHeight();
            }
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

}
Famed answered 10/8, 2013 at 23:26 Comment(6)
anybody else got blank page beside the current item when the viewpager destroyed and opened again?Heidi
I got blank pages too.Evidence
You just need to merge two top answers of this question as described in my blog: pristalovpavel.wordpress.com/2014/12/26/…Ihs
Just replace the code of 'onMeasure' method with the answer given by 'Daniel López Lacalle'.Bulletproof
use answer given by 'Daniel López Lacalle' in this answer still not getting correct height change this line super.onMeasure(widthMeasureSpec, heightMeasureSpec*2); multiply by 2Skippet
Maybe it needs some refinement today. It worked fine back in the days I implemented it.Famed
D
63

I based my answer on Daniel López Lacalle and this post http://www.henning.ms/2013/09/09/viewpager-that-simply-dont-measure-up/. The problem with Daniel's answer is that in some cases my children had a height of zero. The solution was to unfortunately measure twice.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    // Unspecified means that the ViewPager is in a ScrollView WRAP_CONTENT.
    // At Most means that the ViewPager is not in a ScrollView WRAP_CONTENT.
    if (mode == MeasureSpec.UNSPECIFIED || mode == MeasureSpec.AT_MOST) {
        // super has to be called in the beginning so the child views can be initialized.
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            int h = child.getMeasuredHeight();
            if (h > height) height = h;
        }
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
    }
    // super has to be called again so the new specs are treated as exact measurements
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

This also lets you set a height on the ViewPager if you so want to or just wrap_content.

Dannydannye answered 10/7, 2014 at 3:24 Comment(9)
I had the same problem and solved it with your answer, thanks. But any explanation as to why?Chemise
I think they didn't intend for wrap content to be supported as I don't think they thought it was a normal use case. To support it we have to re measure our selves after our children are measured so we can wrap content.Dannydannye
Why are images in this ViewPager actualy shorter than those in an ImageView that uses the same scaleType and similarly, layout_width=match_parent as well as layout_height=wrap_content ? there's like 20dp missing there.Pragmatism
Shark, I'm really not sure. That might have something to do with what your scale type is actually doing. Might want to try setting a height.Dannydannye
Out of other highly upvoted answers here, this one worked best!Altigraph
I CAN'T FRIKIN BELIEVE IT! I spent 2 days glueing together my custom viewpager and got stuck on a problem, when my initial view would not show up and I just couldnt figure out why! // super has to be called in the beginning so the child views can be initialized. <----- THAT was the reason, had to call it at start and at the end of the onMeasure function. Yippiii, virtual high fives on me today!Peregrination
Didn't work for me. The height of the children are still zero.Perdita
Thanks you. It's work fine and it's solved my problem.Detumescence
@Dannydannye It's working well except for the first position. In my case first item height is smaller than other items. Hence white space is showing below first item.Herzberg
N
38

I was just answering a very similar question about this, and happened to find this when looking for a link to back up my claims, so lucky you :)

My other answer:
The ViewPager does not support wrap_content as it (usually) never have all its children loaded at the same time, and can therefore not get an appropriate size (the option would be to have a pager that changes size every time you have switched page).

You can however set a precise dimension (e.g. 150dp) and match_parent works as well.
You can also modify the dimensions dynamically from your code by changing the height-attribute in its LayoutParams.

For your needs you can create the ViewPager in its own xml-file, with the layout_height set to 200dp, and then in your code, rather than creating a new ViewPager from scratch, you can inflate that xml-file:

LayoutInflater inflater = context.getLayoutInflater();
inflater.inflate(R.layout.viewpagerxml, layout, true);
Noctiluca answered 16/12, 2011 at 10:3 Comment(4)
Good answer, kind of annoying that the default behaviour is "do something somewhat incomprehensible." Thanks for the explanation.Vernation
@ChrisVandevelde this seems to be a common tenant of some android libraries. As soon as you learn the fundamentals, you realize nothing follows themFrancophile
But @Jave, why cant the viewpager adjust its height everytime its children are loaded?Yapon
@Francophile indeed! The ViewPagerIndicator library has the same problem with layout_height set to wrap_content, but it's even worse as the simple workaround to set it up to a fixed amount does not work.Jahdal
E
25

Using Daniel López Localle answer, I created this class in Kotlin. Hope it save you more time

class DynamicHeightViewPager @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ViewPager(context, attrs) {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    var heightMeasureSpec = heightMeasureSpec

    var height = 0
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        child.measure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
        val h = child.measuredHeight
        if (h > height) height = h
    }

    if (height != 0) {
        heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
    }

    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}}
Expunction answered 21/6, 2018 at 18:12 Comment(4)
Worked great in a dialog and when containing a recycler view.Gaylegayleen
Though it breaks if you have some vertical padding on your pager. Meaning it will force the content to scroll when not needed.Gaylegayleen
Edited that answer to support vertical padding by doing: heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height + paddingBottom + paddingTop, View.MeasureSpec.EXACTLY)Gaylegayleen
Hey @Slion, thanks for the feedback. although I agree this improve the code to fit better to use in any project to avoid side affects in future changes, I have my doubts about changing the answer because this will do more things than was asked. Because I don't think even the name of the class is clear enough after this change. Maybe I have to change it to DynamicVerticalViewpager? But you get the point what I saying? But if you asked this change request in my project i would do with all my heart.Expunction
C
17

I just bumped into the same issue. I had a ViewPager and I wanted to display an ad at the button of it. The solution I found was to get the pager into a RelativeView and set it's layout_above to the view id i want to see below it. that worked for me.

here is my layout XML:

  <RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:id="@+id/AdLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:orientation="vertical" >
    </LinearLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/mainpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/AdLayout" >
    </android.support.v4.view.ViewPager>
</RelativeLayout>
Chrysolite answered 24/1, 2012 at 20:35 Comment(2)
just for reference, you don't need xmlns:android="schemas.android.com/apk/res/android" in both, only in the first one.Sardanapalus
Your issue was not the same at all. Your layout works fine with ViewPager set to match_parent - the OP had a situation where he wanted the ViewPager to wrap to it's contents.Optimism
H
16

I have faced this issue in several project already and never had a complete solution. So I created a WrapContentViewPager github project as a in-place replacement for ViewPager.

https://github.com/rnevet/WCViewPager

The solution was inspired by some of the answers here, but improves on:

  • Dynamically changes the ViewPager height according to current View including while scrolling.
  • Takes into consideration the height of "decor" views like PagerTabStrip.
  • Takes into consideration all Padding.

Updated for support library version 24 which broke previous implementation.

Hosier answered 25/7, 2015 at 18:1 Comment(10)
@mvai can you open an issue, or fork it and modify the sample app?Hosier
I found out RecyclerView has some wrap_content issues too; it works if you use a custom LinearLayoutManager, like this. So nothing wrong with your library.Burlie
What still has to be fixed is its use with FragmentStatePagerAdapter. Looks like it is measuring childs before the fragments are laid out, thus giving smaller height. What worked for me was @logan 's answer, though I'm still working on it. You might want to try merging that approach into your library. I'm not familiar with github, sorry.Burlie
Thanks, I'll take a look into that.Hosier
@mvai I can't seem to find any issue with the current implementation and FragmentStatePageAdapter, you can add: requestLayout(); invalidate(); to the finishUpdate in the adapter wrapper to see if that solves your issue.Hosier
I cannot thank you enough man. I tried 15-20 solutions and this was only one to work. Remember to popultae all pages at once using "setOffscreenPageLimit(pages);". Thanks.Ungovernable
Happy to hear! Regarding populate all the pages "at once" actually you don't need to, in the sample app I even use dynamically loaded images.Hosier
There was a breaking change in support library v24, implementation in github is updated.Hosier
For anyone wondering how to make this work with a FragmentPagerAdapter, make your Adapter implement the ObjectAtPositionInterface by keeping a list of Fragments internally, so that it can return the corresponding Fragment from the getObjectAtPosition method.Sublingual
@Sublingual correct, but be careful if you use FragmentStatePagerAdapter as it maintains it's own Fragments internally and you shouldn't manage your own just call instantiateItem to get the Fragment.Hosier
S
9

I also ran into this problem, but in my case I had a FragmentPagerAdapter that was supplying the ViewPager with its pages. The problem I had was that onMeasure() of the ViewPager was called before any of the Fragments had been created (and therefore could not size itself correctly).

After a bit of trial and error, I found that the finishUpdate() method of the FragmentPagerAdapter is called after the Fragments have been initialized (from instantiateItem() in the FragmentPagerAdapter), and also after/during the page scrolling. I made a small interface:

public interface AdapterFinishUpdateCallbacks
{
    void onFinishUpdate();
}

which I pass into my FragmentPagerAdapter and call:

@Override
public void finishUpdate(ViewGroup container)
{
    super.finishUpdate(container);

    if (this.listener != null)
    {
        this.listener.onFinishUpdate();
    }
}

which in turn allows me to call setVariableHeight() on my CustomViewPager implementation:

public void setVariableHeight()
{
    // super.measure() calls finishUpdate() in adapter, so need this to stop infinite loop
    if (!this.isSettingHeight)
    {
        this.isSettingHeight = true;

        int maxChildHeight = 0;
        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
        for (int i = 0; i < getChildCount(); i++)
        {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED));
            maxChildHeight = child.getMeasuredHeight() > maxChildHeight ? child.getMeasuredHeight() : maxChildHeight;
        }

        int height = maxChildHeight + getPaddingTop() + getPaddingBottom();
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

        super.measure(widthMeasureSpec, heightMeasureSpec);
        requestLayout();

        this.isSettingHeight = false;
    }
}

I am not sure it is the best approach, would love comments if you think it is good/bad/evil, but it seems to be working pretty well in my implementation :)

Hope this helps someone out there!

EDIT: I forgot to add a requestLayout() after calling super.measure() (otherwise it doesn't redraw the view).

I also forgot to add the parent's padding to the final height.

I also dropped keeping the original width/height MeasureSpecs in favor of creating a new one as required. Have updated the code accordingly.

Another problem I had was that it wouldn't size itself correctly in a ScrollView and found the culprit was measuring the child with MeasureSpec.EXACTLY instead of MeasureSpec.UNSPECIFIED. Updated to reflect this.

These changes have all been added to the code. You can check the history to see the old (incorrect) versions if you want.

Synchronism answered 10/5, 2014 at 18:15 Comment(2)
Why you don't add those you forgot to the code please.Corroborant
@Corroborant I did already, sorry for any confusion! Will update the answer to say that as wellSynchronism
A
8

Another solution is to update ViewPager height according to the current page height in its PagerAdapter. Assuming that your are creating your ViewPager pages this way:

@Override
public Object instantiateItem(ViewGroup container, int position) {
  PageInfo item = mPages.get(position);
  item.mImageView = new CustomImageView(container.getContext());
  item.mImageView.setImageDrawable(item.mDrawable);
  container.addView(item.mImageView, 0);
  return item;
}

Where mPages is internal list of PageInfo structures dynamically added to the PagerAdapter and CustomImageView is just regular ImageView with overriden onMeasure() method that sets its height according to specified width and keeps image aspect ratio.

You can force ViewPager height in setPrimaryItem() method:

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
  super.setPrimaryItem(container, position, object);

  PageInfo item = (PageInfo) object;
  ViewPager pager = (ViewPager) container;
  int width = item.mImageView.getMeasuredWidth();
  int height = item.mImageView.getMeasuredHeight();
  pager.setLayoutParams(new FrameLayout.LayoutParams(width, Math.max(height, 1)));
}

Note the Math.max(height, 1). That fixes annoying bug that ViewPager does not update displayed page (shows it blank), when previous page has zero height (i. e. null drawable in the CustomImageView), each odd swipe back and forth between two pages.

Amadus answered 8/2, 2013 at 17:35 Comment(1)
it seems to me the right path to follow but i needed to ad a item.mImageView.measure(..) to get the correct dimensions in getMeasuredXXX() methods.Endometriosis
B
7

When using static content inside the viewpager and you wish no fancy animation you may use the following view pager

public class HeightWrappingViewPager extends ViewPager {

  public HeightWrappingViewPager(Context context) {
    super(context);
  }

  public HeightWrappingViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)   {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      View firstChild = getChildAt(0);
      firstChild.measure(widthMeasureSpec, heightMeasureSpec);
      super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(firstChild.getMeasuredHeight(), MeasureSpec.EXACTLY));
  }
}
Balthasar answered 13/4, 2016 at 13:6 Comment(4)
This works fine. I extended it by looping through the children and taking the one with max height.Lenrow
Works fine even under recycler viewDismissal
I am getting this exception- java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.measure(int, int)' on a null object referenceRisibility
But taking the first element might be the wrong one.Hilel
L
5

Improved Daniel López Lacalle answer, rewritten in Kotlin:

class MyViewPager(context: Context, attrs: AttributeSet): ViewPager(context, attrs) {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val zeroHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)

        val maxHeight = children
            .map { it.measure(widthMeasureSpec, zeroHeight); it.measuredHeight }
            .max() ?: 0

        if (maxHeight > 0) {
            val maxHeightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY)
            super.onMeasure(widthMeasureSpec, maxHeightSpec)
            return
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }
}
Libyan answered 7/10, 2019 at 13:3 Comment(0)
L
4

Nothing of suggested above worked for me. My use case is having 4 custom ViewPagers in ScrollView. Top of them is measured based on aspect ratio and the rest just has layout_height=wrap_content. I've tried cybergen , Daniel López Lacalle solutions. None of them work fully for me.

My guess why cybergen doesn't work on page > 1 is because it calculates height of pager based on page 1, that is hidden if you scroll further.

Both cybergen and Daniel López Lacalle suggestions have weird behavior in my case: 2 of 3 are loaded ok and 1 randomly height is 0. Appears that onMeasure was called before children were populated. So I came up with a mixture of these 2 answers + my own fixes:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        // find the first child view
        View view = getChildAt(0);
        if (view != null) {
            // measure the first child view with the specified measure spec
            view.measure(widthMeasureSpec, heightMeasureSpec);
            int h = view.getMeasuredHeight();
            setMeasuredDimension(getMeasuredWidth(), h);
            //do not recalculate height anymore
            getLayoutParams().height = h;
        }
    }
}

Idea is to let ViewPager calculate children's dimensions and save calculated height of first page in layout params of the ViewPager. Don't forget to set fragment's layout height to wrap_content otherwise you can get height=0. I've used this one:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="wrap_content">
        <!-- Childs are populated in fragment -->
</LinearLayout>

Please note that this solution works great if all of your pages have same height. Otherwise you need to recalculate ViewPager height based on current child active. I don't need it, but if you suggest the solution I would be happy to update the answer.

Lots answered 19/8, 2014 at 10:50 Comment(2)
Could you still update your answer after all those years? Would help me a tonDownatheel
@Downatheel as I said - only if you suggest the solution ;)Lots
D
4
public CustomPager (Context context) {
    super(context);
}

public CustomPager (Context context, AttributeSet attrs) {
    super(context, attrs);
}

int getMeasureExactly(View child, int widthMeasureSpec) {
    child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    int height = child.getMeasuredHeight();
    return MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
}

@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST;

    final View tab = getChildAt(0);
    if (tab == null) {
        return;
    }

    int width = getMeasuredWidth();
    if (wrapHeight) {
        // Keep the current measured width.
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
    }
    Fragment fragment = ((Fragment) getAdapter().instantiateItem(this, getCurrentItem()));
    heightMeasureSpec = getMeasureExactly(fragment.getView(), widthMeasureSpec);

    //Log.i(Constants.TAG, "item :" + getCurrentItem() + "|height" + heightMeasureSpec);
    // super has to be called again so the new specs are treated as
    // exact measurements.
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Dhruv answered 27/2, 2015 at 4:39 Comment(0)
Q
4

From Popcorn time android app's source code I found this solution which dynamically adjusts size of viewpager with nice animation depending on the size of current child.

https://git.popcorntime.io/popcorntime/android/blob/5934f8d0c8fed39af213af4512272d12d2efb6a6/mobile/src/main/java/pct/droid/widget/WrappingViewPager.java

public class WrappingViewPager extends ViewPager {

    private Boolean mAnimStarted = false;

    public WrappingViewPager(Context context) {
        super(context);
    }

    public WrappingViewPager(Context context, AttributeSet attrs){
        super(context, attrs);
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if(!mAnimStarted && null != getAdapter()) {
            int height = 0;
            View child = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem()).getView();
            if (child != null) {
                child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
                height = child.getMeasuredHeight();
                if (VersionUtils.isJellyBean() && height < getMinimumHeight()) {
                    height = getMinimumHeight();
                }
            }

            // Not the best place to put this animation, but it works pretty good.
            int newHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
            if (getLayoutParams().height != 0 && heightMeasureSpec != newHeight) {
                    final int targetHeight = height;
                    final int currentHeight = getLayoutParams().height;
                    final int heightChange = targetHeight - currentHeight;

                    Animation a = new Animation() {
                        @Override
                        protected void applyTransformation(float interpolatedTime, Transformation t) {
                            if (interpolatedTime >= 1) {
                                getLayoutParams().height = targetHeight;
                            } else {
                                int stepHeight = (int) (heightChange * interpolatedTime);
                                getLayoutParams().height = currentHeight + stepHeight;
                            }
                            requestLayout();
                        }

                        @Override
                        public boolean willChangeBounds() {
                            return true;
                        }
                    };

                    a.setAnimationListener(new Animation.AnimationListener() {
                        @Override
                        public void onAnimationStart(Animation animation) {
                            mAnimStarted = true;
                        }

                        @Override
                        public void onAnimationEnd(Animation animation) {
                            mAnimStarted = false;
                        }

                        @Override
                        public void onAnimationRepeat(Animation animation) {
                        }
                    });

                    a.setDuration(1000);
                    startAnimation(a);
                    mAnimStarted = true;
            } else {
                heightMeasureSpec = newHeight;
            }
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}
Quiff answered 9/9, 2015 at 20:25 Comment(0)
P
4

In case you need ViewPager that adjust its size to every child, not just to biggest one, I have wrote a piece of code that does it. Note that there is no animation upon that change (not neccessary in my case)

android:minHeight flag is also supported.

public class ChildWrappingAdjustableViewPager extends ViewPager {
    List<Integer> childHeights = new ArrayList<>(getChildCount());
    int minHeight = 0;
    int currentPos = 0;

    public ChildWrappingAdjustableViewPager(@NonNull Context context) {
        super(context);
        setOnPageChangeListener();
    }

    public ChildWrappingAdjustableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        obtainMinHeightAttribute(context, attrs);
        setOnPageChangeListener();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {            
        childHeights.clear();

        //calculate child views
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            int h = child.getMeasuredHeight();
            if (h < minHeight) {
                h = minHeight;
            }
            childHeights.add(i, h);
        }

        if (childHeights.size() - 1 >= currentPos) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeights.get(currentPos), MeasureSpec.EXACTLY);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    private void obtainMinHeightAttribute(@NonNull Context context, @Nullable AttributeSet attrs) {
        int[] heightAttr = new int[]{android.R.attr.minHeight};
        TypedArray typedArray = context.obtainStyledAttributes(attrs, heightAttr);
        minHeight = typedArray.getDimensionPixelOffset(0, -666);
        typedArray.recycle();
    }

    private void setOnPageChangeListener() {
        this.addOnPageChangeListener(new SimpleOnPageChangeListener() {
            @Override
            public void onPageSelected(int position) {
                currentPos = position;

                ViewGroup.LayoutParams layoutParams = ChildWrappingAdjustableViewPager.this.getLayoutParams();
                layoutParams.height = childHeights.get(position);
                ChildWrappingAdjustableViewPager.this.setLayoutParams(layoutParams);
                ChildWrappingAdjustableViewPager.this.invalidate();
            }
        });
    }
}
Pirogue answered 2/7, 2018 at 12:29 Comment(3)
So this adapter has a huge problem when the amout of items in the adapter changesWhatever
can you clarify your statement?Pirogue
This code can cause nullpointers as not every child is calculated at start. Try a tab layout and scroll from 1 to 5 or code wise and you'll see it.Whatever
M
3

I bumped into the same issue, and I also had to make the ViewPager wrap around its contents when the user scrolled between the pages. Using cybergen's above answer, I defined the onMeasure method as following:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (getCurrentItem() < getChildCount()) {
        View child = getChildAt(getCurrentItem());
        if (child.getVisibility() != GONE) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
                    MeasureSpec.UNSPECIFIED);
            child.measure(widthMeasureSpec, heightMeasureSpec);
        }

        setMeasuredDimension(getMeasuredWidth(), measureHeight(heightMeasureSpec, getChildAt(getCurrentItem())));            
    }
}

This way, the onMeasure method sets the height of the current page displayed by the ViewPager.

Middlebrow answered 14/10, 2013 at 10:31 Comment(1)
Only the most height content is appeared with your answer, the other content is disappeared...Clouet
Z
2

For people having this problem and coding for Xamarin Android in C#, this might also be a quick solution:

pager.ChildViewAdded += (sender, e) => {
    e.Child.Measure ((int)MeasureSpecMode.Unspecified, (int)MeasureSpecMode.Unspecified);
    e.Parent.LayoutParameters.Height = e.Child.MeasuredHeight;
};

This is mainly useful if your child views are of the same height. Otherwise, you would be required to store some kind of "minimumHeight" value over all children that you check against, and even then you might not want to have empty spaces visible beneath your smaller child views.

The solution itself is not sufficient for me though, but that is because my child items are listViews and their MeasuredHeight is not calculated correctly, it seems.

Zed answered 18/6, 2015 at 16:16 Comment(1)
This worked for me. All of my child views in the viewpager are of the same height.Adlee
N
2

I have an version of WrapContentHeightViewPager that was working correctly before API 23 that will resize the parent view's height base on the current child view selected.

After upgrading to API 23, it stopped working. It turns out the old solution was using getChildAt(getCurrentItem()) to get the current child view to measure which is not working. See solution here: https://mcmap.net/q/110253/-how-do-i-get-the-child-view-of-a-viewpager-at-a-given-item

Below works with API 23:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int height = 0;
    ViewPagerAdapter adapter = (ViewPagerAdapter)getAdapter();
    View child = adapter.getItem(getCurrentItem()).getView();
    if(child != null) {
        child.measure(widthMeasureSpec,  MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        height = child.getMeasuredHeight();
    }
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Nall answered 24/6, 2016 at 17:34 Comment(2)
Thank you!! Have been trying answers for hours and this is the only one that fully works for me. It needs to be combined with a custom adapter where 'setPrimaryItem()` calls a function in the pager that calls requestLayout() so height is adjusted as we go from one tab to the next. Do you remember why super needs to be called twice? I noticed that it won't work otherwise.Libelant
Works with API 28.Ceiba
N
2

The below code is the only thing worked for me

1. Use this class to declare a HeightWrappingViewPager:

 public class HeightWrappingViewPager extends ViewPager {

        public HeightWrappingViewPager(Context context) {
            super(context);
        }

        public HeightWrappingViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int mode = MeasureSpec.getMode(heightMeasureSpec);
            // Unspecified means that the ViewPager is in a ScrollView WRAP_CONTENT.
            // At Most means that the ViewPager is not in a ScrollView WRAP_CONTENT.
            if (mode == MeasureSpec.UNSPECIFIED || mode == MeasureSpec.AT_MOST) {
                // super has to be called in the beginning so the child views can be initialized.
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                int height = 0;
                for (int i = 0; i < getChildCount(); i++) {
                    View child = getChildAt(i);
                    child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
                    int h = child.getMeasuredHeight();
                    if (h > height) height = h;
                }
                heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
            }
            // super has to be called again so the new specs are treated as exact measurements
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

2. Insert the height wrapping view pager to your xml file:

<com.project.test.HeightWrappingViewPager
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</com.project.test.HeightWrappingViewPager>

3. Declare your view pager:

HeightWrappingViewPager mViewPager;
mViewPager = (HeightWrappingViewPager) itemView.findViewById(R.id.pager);
CustomAdapter adapter = new CustomAdapter(context);
mViewPager.setAdapter(adapter);
mViewPager.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
Nelsen answered 3/9, 2019 at 14:28 Comment(2)
Thanks. This worked. But why can't android team have this in their code base?Raul
This is one of the things you have to customise yourself depends on your need, also google introduced viewPager2 in this year 2019 Google I/O and is a replacement of the old ViewPager, which was created in 2011, implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha04'Nelsen
W
2

This ViewPager only resizes to the current visible children (not the biggest of its actual children)

The idea from https://mcmap.net/q/110254/-android-viewpager-get-the-current-view

public class DynamicHeightViewPager extends ViewPager {

public DynamicHeightViewPager (Context context) {
    super(context);
    initPageChangeListener();
}

public DynamicHeightViewPager (Context context, AttributeSet attrs) {
    super(context, attrs);
    initPageChangeListener();
}



private void initPageChangeListener() {
    addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            requestLayout();
        }
    });
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //View child = getChildAt(getCurrentItem());
    View child = getCurrentView(this);
    if (child != null) {
        child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, 
         MeasureSpec.UNSPECIFIED));
        int h = child.getMeasuredHeight();

        heightMeasureSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}


View getCurrentView(ViewPager viewPager) {
    try {
        final int currentItem = viewPager.getCurrentItem();
        for (int i = 0; i < viewPager.getChildCount(); i++) {
            final View child = viewPager.getChildAt(i);
            final ViewPager.LayoutParams layoutParams = (ViewPager.LayoutParams) 
             child.getLayoutParams();

            Field f = layoutParams.getClass().getDeclaredField("position"); 
            //NoSuchFieldException
            f.setAccessible(true);
            int position = (Integer) f.get(layoutParams); //IllegalAccessException

            if (!layoutParams.isDecor && currentItem == position) {
                return child;
            }
        }
    } catch (NoSuchFieldException e) {
        e.fillInStackTrace();
    } catch (IllegalArgumentException e) {
        e.fillInStackTrace();
    } catch (IllegalAccessException e) {
        e.fillInStackTrace();
    }
    return null;
}

}

Winegar answered 4/9, 2019 at 11:14 Comment(1)
This is the only solution that worked for me when using ViewPager with NestedScrollView in a fragment. So, thank you!Franzoni
C
2

I edit cybergen answer for make viewpager to change height depending on selected item The class is the same of cybergen's, but I added a Vector of integers that is all viewpager's child views heights and we can access it when page change to update height

This is the class:

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;

import java.util.Vector;

public class WrapContentHeightViewPager extends ViewPager {
    private Vector<Integer> heights = new Vector<>();

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        for(int i=0;i<getChildCount();i++) {
            View view = getChildAt(i);
            if (view != null) {
                view.measure(widthMeasureSpec, heightMeasureSpec);
                heights.add(measureHeight(heightMeasureSpec, view));
            }
        }
        setMeasuredDimension(getMeasuredWidth(), measureHeight(heightMeasureSpec, getChildAt(0)));
    }

    public int getHeightAt(int position){
        return heights.get(position);
    }

    private int measureHeight(int measureSpec, View view) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            if (view != null) {
                result = view.getMeasuredHeight();
            }
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
}

Then in your activity add an OnPageChangeListener

WrapContentHeightViewPager viewPager = findViewById(R.id.my_viewpager);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
     @Override
     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
     @Override
     public void onPageSelected(int position) {
         LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) viewPager.getLayoutParams();
         params.height = viewPager.getHeightAt(position);
         viewPager.setLayoutParams(params);
     }
     @Override
     public void onPageScrollStateChanged(int state) {}
});

And here is xml:

<com.example.example.WrapContentHeightViewPager
    android:id="@+id/my_viewpager"
    android:fillViewport="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

Please correct my English if necessary

Carboniferous answered 27/2, 2020 at 14:56 Comment(3)
This has some issues. The heights list may increase infinity.Rauscher
@Rauscher When did you encountered the issue? I used this only in TabLayout with ViewPager, so I'm not sure if it works well everywhereCarboniferous
@Carboniferous The problem is some pages would be recycled. And recreated when user swipe to them therefore the measure would be call in multi times. It may increase heights list. Another situation is user may call requestLayout(or setLayoutParams method, just like what you did) for this viewPager manually, also will measure multi times.Rauscher
T
2

Another Kotlin code

class DynamicViewPager @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : ViewPager(context, attrs) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var height = 0
        (0 until childCount).forEach {
            val child = getChildAt(it)
            child.measure(
                widthMeasureSpec,
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
            )
            height = max(height, child.measuredHeight)
        }
        if (height > 0) {
            super.onMeasure(
                widthMeasureSpec,
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
            )
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        }
    }
}
Trahan answered 28/4, 2020 at 15:34 Comment(1)
What about ViewPager2?Mcbrayer
P
1

If the ViewPager you're using is a child of a ScrollView AND has a PagerTitleStrip child you'll need to use a slight modification of the great answers already provided. For reference my XML looks like this:

<ScrollView
    android:id="@+id/match_scroll_view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/white">

    <LinearLayout
        android:id="@+id/match_and_graphs_wrapper"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <view
            android:id="@+id/pager"
            class="com.printandpixel.lolhistory.util.WrapContentHeightViewPager"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <android.support.v4.view.PagerTitleStrip
                android:id="@+id/pager_title_strip"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="top"
                android:background="#33b5e5"
                android:paddingBottom="4dp"
                android:paddingTop="4dp"
                android:textColor="#fff" />
        </view>
    </LinearLayout>
</ScrollView>

In your onMeasure you have to ADD the measuredHeight of the PagerTitleStrip if one is found. Otherwise its height won't be considered into the largest height of all the children even though it takes up additional space.

Hope this helps someone else. Sorry that it's a bit of a hack...

public class WrapContentHeightViewPager extends ViewPager {

    public WrapContentHeightViewPager(Context context) {
        super(context);
    }

    public WrapContentHeightViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int pagerTitleStripHeight = 0;
        int height = 0;
        for(int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            int h = child.getMeasuredHeight();
            if (h > height) {
                // get the measuredHeight of the tallest fragment
                height = h;
            }
            if (child.getClass() == PagerTitleStrip.class) {
                // store the measured height of the pagerTitleStrip if one is found. This will only
                // happen if you have a android.support.v4.view.PagerTitleStrip as a direct child
                // of this class in your XML.
                pagerTitleStripHeight = h;
            }
        }

        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height+pagerTitleStripHeight, MeasureSpec.EXACTLY);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}
Personate answered 1/9, 2015 at 0:41 Comment(0)
T
1

Most of the solutions I see here seem to be doing a double measurement: first measuring the child views, and then calling the super.onMeasure()

I have come up with a custom WrapContentViewPager that is more efficient, works well with RecyclerView and Fragment

You can check the demo here:

github/ssynhtn/WrapContentViewPager

and the code of the class here: WrapContentViewPager.java

Tabethatabib answered 20/6, 2019 at 3:35 Comment(0)
I
1

In my case, I needed a viewpager with a wrap_content for the currently selected element and animation when applying the size. Below you can see my implementation. Can someone come in handy.

package one.xcorp.widget

import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import one.xcorp.widget.R
import kotlin.properties.Delegates.observable

class ViewPager : android.support.v4.view.ViewPager {

    var enableAnimation by observable(false) { _, _, enable ->
        if (enable) {
            addOnPageChangeListener(onPageChangeListener)
        } else {
            removeOnPageChangeListener(onPageChangeListener)
        }
    }

    private var animationDuration = 0L
    private var animator: ValueAnimator? = null

    constructor (context: Context) : super(context) {
        init(context, null)
    }

    constructor (context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(context, attrs)
    }

    private fun init(context: Context, attrs: AttributeSet?) {
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.ViewPager,
            0,
            0
        ).apply {
            try {
                enableAnimation = getBoolean(
                    R.styleable.ViewPager_enableAnimation,
                    enableAnimation
                )
                animationDuration = getInteger(
                    R.styleable.ViewPager_animationDuration,
                    resources.getInteger(android.R.integer.config_shortAnimTime)
                ).toLong()
            } finally {
                recycle()
            }
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        val measuredHeight = if (heightMode == MeasureSpec.EXACTLY) {
            MeasureSpec.getSize(heightMeasureSpec)
        } else {
            val currentViewHeight = findViewByPosition(currentItem)?.also {
                measureView(it)
            }?.measuredHeight ?: 0

            if (heightMode != MeasureSpec.AT_MOST) {
                currentViewHeight
            } else {
                Math.min(
                    currentViewHeight,
                    MeasureSpec.getSize(heightMeasureSpec)
                )
            }
        }

        super.onMeasure(
            widthMeasureSpec,
            MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
        )
    }

    private fun measureView(view: View) = with(view) {
        val horizontalMode: Int
        val horizontalSize: Int
        when (layoutParams.width) {
            MATCH_PARENT -> {
                horizontalMode = MeasureSpec.EXACTLY
                horizontalSize = [email protected]
            }
            WRAP_CONTENT -> {
                horizontalMode = MeasureSpec.UNSPECIFIED
                horizontalSize = 0
            }
            else -> {
                horizontalMode = MeasureSpec.EXACTLY
                horizontalSize = layoutParams.width
            }
        }

        val verticalMode: Int
        val verticalSize: Int
        when (layoutParams.height) {
            MATCH_PARENT -> {
                verticalMode = MeasureSpec.EXACTLY
                verticalSize = [email protected]
            }
            WRAP_CONTENT -> {
                verticalMode = MeasureSpec.UNSPECIFIED
                verticalSize = 0
            }
            else -> {
                verticalMode = MeasureSpec.EXACTLY
                verticalSize = layoutParams.height
            }
        }

        val horizontalMeasureSpec = MeasureSpec.makeMeasureSpec(horizontalSize, horizontalMode)
        val verticalMeasureSpec = MeasureSpec.makeMeasureSpec(verticalSize, verticalMode)

        measure(horizontalMeasureSpec, verticalMeasureSpec)
    }

    private fun findViewByPosition(position: Int): View? {
        for (i in 0 until childCount) {
            val childView = getChildAt(i)
            val childLayoutParams = childView.layoutParams as LayoutParams

            val childPosition by lazy {
                val field = childLayoutParams.javaClass.getDeclaredField("position")
                field.isAccessible = true
                field.get(childLayoutParams) as Int
            }

            if (!childLayoutParams.isDecor && position == childPosition) {
                return childView
            }
        }

        return null
    }

    private fun animateContentHeight(childView: View, fromHeight: Int, toHeight: Int) {
        animator?.cancel()

        if (fromHeight == toHeight) {
            return
        }

        animator = ValueAnimator.ofInt(fromHeight, toHeight).apply {
            addUpdateListener {
                measureView(childView)
                if (childView.measuredHeight != toHeight) {
                    animateContentHeight(childView, height, childView.measuredHeight)
                } else {
                    layoutParams.height = animatedValue as Int
                    requestLayout()
                }
            }
            duration = animationDuration
            start()
        }
    }

    private val onPageChangeListener = object : OnPageChangeListener {

        override fun onPageScrollStateChanged(state: Int) {
            /* do nothing */
        }

        override fun onPageScrolled(
            position: Int,
            positionOffset: Float,
            positionOffsetPixels: Int
        ) {
            /* do nothing */
        }

        override fun onPageSelected(position: Int) {
            if (!isAttachedToWindow) {
                return
            }

            findViewByPosition(position)?.let { childView ->
                measureView(childView)
                animateContentHeight(childView, height, childView.measuredHeight)
            }
        }
    }
}

Add attrs.xml in project:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ViewPager">
        <attr name="enableAnimation" format="boolean" />
        <attr name="animationDuration" format="integer" />
    </declare-styleable>
</resources>

And use:

<one.xcorp.widget.ViewPager
    android:id="@+id/wt_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:enableAnimation="true" />
Improbability answered 7/7, 2019 at 20:44 Comment(0)
A
1

Measure the height of ViewPager:

public class WrapViewPager extends ViewPager {
    View primaryView;

    public WrapViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (primaryView != null) {
            int height = 0;
            for (int i = 0; i < getChildCount(); i++) {
                if (primaryView == getChildAt(i)) {
                    int childHeightSpec = MeasureSpec.makeMeasureSpec(0x1 << 30 - 1, MeasureSpec.AT_MOST);
                    getChildAt(i).measure(widthMeasureSpec, childHeightSpec);
                    height = getChildAt(i).getMeasuredHeight();
                }

            }

            setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        }
    }

    public void setPrimaryView(View view) {
        primaryView = view;
    }

}

call setPrimaryView(View) :

public class ZGAdapter extends PagerAdapter {

    @Override
    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        super.setPrimaryItem(container, position, object);
        ((WrapViewPager)container).setPrimaryView((View)object);
    }

}

Aa answered 24/9, 2019 at 7:54 Comment(0)
S
0

I have a similar (but more complex scenario). I have a dialog, which contains a ViewPager.
One of the child pages is short, with a static height.
Another child page should always be as tall as possible.
Another child page contains a ScrollView, and the page (and thus the entire dialog) should WRAP_CONTENT if the ScrollView contents don't need the full height available to the dialog.

None of the existing answers worked completely for this specific scenario. Hold on- it's a bumpy ride.

void setupView() {
    final ViewPager.SimpleOnPageChangeListener pageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            currentPagePosition = position;

            // Update the viewPager height for the current view

            /*
            Borrowed from https://github.com/rnevet/WCViewPager/blob/master/wcviewpager/src/main/java/nevet/me/wcviewpager/WrapContentViewPager.java
            Gather the height of the "decor" views, since this height isn't included
            when measuring each page's view height.
             */
            int decorHeight = 0;
            for (int i = 0; i < viewPager.getChildCount(); i++) {
                View child = viewPager.getChildAt(i);
                ViewPager.LayoutParams lp = (ViewPager.LayoutParams) child.getLayoutParams();
                if (lp != null && lp.isDecor) {
                    int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                    boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
                    if (consumeVertical) {
                        decorHeight += child.getMeasuredHeight();
                    }
                }
            }

            int newHeight = decorHeight;

            switch (position) {
                case PAGE_WITH_SHORT_AND_STATIC_CONTENT:
                    newHeight += measureViewHeight(thePageView1);
                    break;
                case PAGE_TO_FILL_PARENT:
                    newHeight = ViewGroup.LayoutParams.MATCH_PARENT;
                    break;
                case PAGE_TO_WRAP_CONTENT:
//                  newHeight = ViewGroup.LayoutParams.WRAP_CONTENT; // Works same as MATCH_PARENT because...reasons...
//                  newHeight += measureViewHeight(thePageView2); // Doesn't allow scrolling when sideways and height is clipped

                    /*
                    Only option that allows the ScrollView content to scroll fully.
                    Just doing this might be way too tall, especially on tablets.
                    (Will shrink it down below)
                     */
                    newHeight = ViewGroup.LayoutParams.MATCH_PARENT;
                    break;
            }

            // Update the height
            ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams();
            layoutParams.height = newHeight;
            viewPager.setLayoutParams(layoutParams);

            if (position == PAGE_TO_WRAP_CONTENT) {
                // This page should wrap content

                // Measure height of the scrollview child
                View scrollViewChild = ...; // (generally this is a LinearLayout)
                int scrollViewChildHeight = scrollViewChild.getHeight(); // full height (even portion which can't be shown)
                // ^ doesn't need measureViewHeight() because... reasons...

                if (viewPager.getHeight() > scrollViewChildHeight) { // View pager too tall?
                    // Wrap view pager height down to child height
                    newHeight = scrollViewChildHeight + decorHeight;

                    ViewGroup.LayoutParams layoutParams2 = viewPager.getLayoutParams();
                    layoutParams2.height = newHeight;
                    viewPager.setLayoutParams(layoutParams2);
                }
            }

            // Bonus goodies :)
            // Show or hide the keyboard as appropriate. (Some pages have EditTexts, some don't)
            switch (position) {
                // This case takes a little bit more aggressive code than usual

                if (position needs keyboard shown){
                    showKeyboardForEditText();
                } else if {
                    hideKeyboard();
                }
            }
        }
    };

    viewPager.addOnPageChangeListener(pageChangeListener);

    viewPager.getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    // https://mcmap.net/q/110255/-how-to-retrieve-the-dimensions-of-a-view
                    // Do things which require the views to have their height populated here
                    pageChangeListener.onPageSelected(currentPagePosition); // fix the height of the first page

                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                        viewPager.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    } else {
                        viewPager.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    }

                }
            }
    );
}


...

private void showKeyboardForEditText() {
    // Make the keyboard appear.
    getDialog().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
    getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);

    inputViewToFocus.requestFocus();

    // https://mcmap.net/q/110256/-open-soft-keyboard-programmatically
    InputMethodManager inputMethodManager =
            (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
    inputMethodManager.toggleSoftInputFromWindow(
            inputViewToFocus.getApplicationWindowToken(),
            InputMethodManager.SHOW_IMPLICIT, 0);
}

...

/**
 * Hide the keyboard - http://stackoverflow.com/a/8785471
 */
private void hideKeyboard() {
    InputMethodManager inputManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);

    inputManager.hideSoftInputFromWindow(inputBibleBookStart.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}

...

//https://github.com/rnevet/WCViewPager/blob/master/wcviewpager/src/main/java/nevet/me/wcviewpager/WrapContentViewPager.java
private int measureViewHeight(View view) {
    view.measure(ViewGroup.getChildMeasureSpec(-1, -1, view.getLayoutParams().width), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
    return view.getMeasuredHeight();
}

Much thanks to @Raanan for the code to measure views and measure the decor height. I ran into problems with his library- the animation stuttered, and I think my ScrollView wouldn't scroll when the height of the dialog was short enough to require it.

Stadia answered 23/4, 2017 at 5:5 Comment(0)
R
0

All answers do not work perfectly. So I created one. Below class would request layout when a new page was selected to make viewPager 's height is the current child view's height.

class WrapContentViewPager : ViewPager {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    private var curPos = 0

    init {
        addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
            override fun onPageScrollStateChanged(state: Int) {}

            override fun onPageScrolled(
                position: Int,
                positionOffset: Float,
                positionOffsetPixels: Int
            ) {}

            override fun onPageSelected(position: Int) {
                curPos = position
                requestLayout()
            }
        })
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        if (childCount == 0) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            return
        }
        measureChildren(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(measuredWidth, getChildAt(curPos).measuredHeight)
    }
}
Rauscher answered 21/4, 2020 at 13:16 Comment(0)
S
0

For those who want a solution of ViewPager2 that will have the ViewPager2 have the same height as the max height of all of its pages, sadly I've found only this workaround:

viewPager.doOnPreDraw {
    //workaround to set the viewPagerheight the same as its children
    var height = 0
    for (i in 0 until featuresViewPager.adapter!!.itemCount) {
        val viewHolder = viewPager.adapter!!.createViewHolder(viewPager, 0)
        viewPager.adapter!!.bindViewHolder(viewHolder, i)
        val child: View = viewHolder.itemView
        child.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
        val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(viewPager.width, View.MeasureSpec.EXACTLY)
        val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
        child.measure(widthMeasureSpec, heightMeasureSpec)
        val childHeight = child.measuredHeight
        child.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
        if (childHeight > height)
            height = childHeight
    }
    viewPager.layoutParams.height = height
}

I say "sadly" because it goes over all of the pages, creates their views, measures them, and on the way it calls functions that are meant for other purposes.

Should work fine in most cases.

Please let me know if you know of a nicer solution.

Sibyl answered 5/7, 2020 at 7:27 Comment(0)
S
-1

in my case adding clipToPadding solved the problem.

<android.support.v4.view.ViewPager
    ...
    android:clipToPadding="false"
    ...
    />

Cheers!

Supersonic answered 3/1, 2018 at 17:44 Comment(0)
V
-1

I my case adding android:fillViewport="true" solved the issue

Viridian answered 20/9, 2018 at 11:11 Comment(0)
W
-1

Give parent layout of ViewPager as NestedScrollView

   <androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="5dp"
    android:paddingRight="5dp"
    android:fillViewport="true">
        <androidx.viewpager.widget.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
        </androidx.viewpager.widget.ViewPager>
    </androidx.core.widget.NestedScrollView>

Don't forget to set android:fillViewport="true"

This will stretch scrollview and its child's content to fill the viewport.

https://developer.android.com/reference/android/widget/ScrollView.html#attr_android:fillViewport

Wyant answered 9/10, 2019 at 10:45 Comment(0)
S
-2

I've found a solution that is a bit like merging of some of the solutions mentioned here.

The idea is to measure the current view of the ViewPager.

Here's the full code:

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewPager.adapter = WrapHeightViewPager.CustomPagerAdapter(this)
    }
}

activity_main.xml

<FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <com.android.myapplication.WrapHeightViewPager
            android:layout_width="match_parent" android:id="@+id/viewPager"
            android:background="#33ff0000"
            android:layout_height="wrap_content"/>

</FrameLayout>

view_pager_page.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical" android:gravity="center"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">

    <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content"
               android:src="@android:drawable/sym_def_app_icon"/>


    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/textView"/>
</LinearLayout>

WrapHeightViewPager.kt

class WrapHeightViewPager : ViewPager {
    constructor(context: Context) : super(context) {}

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val adapter = adapter as CustomPagerAdapter?
        val currentView = adapter?.currentView
        if (currentView != null) {
            currentView.measure(widthMeasureSpec, heightMeasureSpec)
            super.onMeasure(
                widthMeasureSpec,
                View.MeasureSpec.makeMeasureSpec(currentView.measuredHeight, View.MeasureSpec.EXACTLY)
            )
            return
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    class CustomPagerAdapter(private val context: Context) : PagerAdapter() {
        var currentView: View? = null

        override fun instantiateItem(parent: ViewGroup, position: Int): Any {
            val inflater = LayoutInflater.from(context)
            val view = inflater.inflate(R.layout.view_pager_page, parent, false)
            view.textView.text = "item$position"
            parent.addView(view)
            return view
        }

        override fun setPrimaryItem(container: ViewGroup, position: Int, obj: Any) {
            super.setPrimaryItem(container, position, obj)
            currentView = obj as View
        }

        override fun destroyItem(collection: ViewGroup, position: Int, view: Any) = collection.removeView(view as View)

        override fun getCount(): Int = 3

        override fun isViewFromObject(view: View, obj: Any) = view === obj

        override fun getPageTitle(position: Int): CharSequence? = "item $position"

    }
}

If you use RecyclerPagerAdapter library, the way to get the "currentView" is by getting it from the pager-view-holder that you've set:

    val item = obj as PagerViewHolder
    currentView = item.itemView
Sibyl answered 6/11, 2017 at 10:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.