18

I have a custom ViewGroup that has a child ViewPager. The ViewPager is fed by a PagerAdapter that provides a LinearLayout to the ViewPager which has LayoutParams of WRAP_CONTENT on both height and width.

The view displays correctly but when the child.measure() method is called on the ViewPager it does not return the actual dimensions of the LinearLayout but seems to fill all the remaining space.

Any ideas why this is happening and how to amend it?

Steve Konves
  • 2,648
  • 3
  • 25
  • 44
Saad Farooq
  • 13,172
  • 10
  • 68
  • 94
  • please star issue https://code.google.com/p/android/issues/detail?id=54604 – Christ Jul 15 '14 at 10:05
  • FYI : The issue 54604 has been bulk closed by Google yesterday. If you still have the problem, I suggest you re-open a new issue and put the link here. – Christ Dec 09 '14 at 13:10
  • possible duplicate of [Android: I am unable to have ViewPager WRAP\_CONTENT](http://stackoverflow.com/questions/8394681/android-i-am-unable-to-have-viewpager-wrap-content) – Raanan Jul 25 '15 at 21:35

6 Answers6

57

I wasn't very happy with the accepted answer (nor with the pre-inflate-all-views solution in the comments), so I put together a ViewPager that takes its height from the first available child. It does this by doing a second measurement pass, allowing you to steal the first child's height.

A better solution would be to make a new class inside the android.support.v4.view package that implements a better version of onMeasure (with access to package-visible methods like populate())

For the time being, though, the solution below suits me fine.

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

        boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) 
                == MeasureSpec.AT_MOST;

        if(wrapHeight) {
            /**
             * The first super.onMeasure call made the pager take up all the 
             * available height. Since we really wanted to wrap it, we need 
             * to remeasure it. Luckily, after that call the first child is 
             * now available. So, we take the height from it. 
             */

            int width = getMeasuredWidth(), height = getMeasuredHeight();

            // Use the previously measured width but simplify the calculations
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);

            /* If the pager actually has any children, take the first child's 
             * height and call that our own */ 
            if(getChildCount() > 0) {
                View firstChild = getChildAt(0);

                /* The child was previously measured with exactly the full height.
                 * Allow it to wrap this time around. */
                firstChild.measure(widthMeasureSpec, 
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));

                height = firstChild.getMeasuredHeight();
            }

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

            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
}
Delyan
  • 8,881
  • 4
  • 37
  • 42
  • Why yours HeightWrappingViewPager doesn't work inside ScrollView with android:fillViewport="true"? When I replace ViewPager with HeightWrappingViewPager I can't scroll content within ScrollView – Ziem Apr 05 '13 at 13:01
  • @Ziem, I've no idea. Does a normal ViewPager work? I'd be very surprised if a ScrollView changes its behaviour based on the inner child's dimensions. – Delyan Apr 06 '13 at 19:39
  • @Delian - yes, normal ViewPager works fine (with fixed height). In general HeightWrappingViewPager do its job (its wrap content), but on smaller screen devices I'm unable to scroll. I will investigate it more on monday and send you feedback. – Ziem Apr 06 '13 at 20:52
  • @Ziem, try to put the ViewPager a layer deeper into the hierarchy. For example, you can wrap it in a FrameLayout. – Delyan Apr 06 '13 at 20:55
  • @Delyen, HeightWrappingViewPager doesn't show any content on viewpager's page change while normal ViewPager does. What to do? – neeraj Oct 23 '13 at 09:46
  • 1
    No Content is showing still. If I hard code the height of the Pager then it is working good, but i dont want to fix the height. Can you please help me in this ? – Gaurav Arora Nov 22 '13 at 09:07
  • 5
    I don't know if anyone have trouble but this doesn't work and I change `MeasureSpec.AT_MOST` to `MeasureSpec.UNSPECIFIED` because `heightMeasureSpec` parameter is 0. And it works... – Zyoo May 07 '14 at 18:48
  • Thank you soooooooooo much! Saved me from throwing my phone out of the fourth floor window. – Eric Cochran Jun 06 '14 at 00:30
  • hmm, not working when rotating to landscape. It keeps the original height. – Eric Cochran Jun 06 '14 at 00:45
  • 1
    Is it possible to have it also wrap width? – JY2k Nov 12 '14 at 16:59
  • seriously a great answer..saved me lots of effort and time. – Ankit Bansal Feb 24 '15 at 09:58
  • I did an implementation based on this code, adapted for my problem and it worked fine. Thanks for the inspiration – voghDev Sep 02 '15 at 11:49
  • I follow codes above of @Delyan but it does not work. I followed [link](https://gist.github.com/egslava/589b82a6add9c816a007) and it works – RoShan Shan Jan 10 '17 at 10:19
  • I had to add a max height variable to the class and make sure heightMeasureSpec did not return something less than my max. This kept the views the right size when tabbing back and forth in the tablayout. – musterjunk Jan 13 '19 at 20:38
  • Saved my day. I've been trying to achieve this for hours. Thanks :) – Ivan Peshev Mar 23 '20 at 02:10
12

Looking at the internals of the ViewPager class in the compatibility jar:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
    // For simple implementation, or internal size is always 0.
    // We depend on the container to specify the layout size of
    // our view. We can't really know what it is since we will be
    // adding and removing different arbitrary views and do not
    // want the layout to change as this happens.
    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));

   ...
}

It would appear that the ViewPager implementation does not measure the children views but just sets the ViewPager to be one standard view based on what the parent is passing in. When you pass wrap_content, since the view pager doesn't actually measure its content it takes up the full available area.

My recommendation would be to set a static size on your ViewPager based on the size of your child views. If this is impossible (for instance, the child views can vary) you'll either need to pick a maximum size and deal with the extra space in some views OR extend ViewPager and provide a onMeasure that measure the children. One issue you will run into is that the view pager was designed not to vary in width as different views are shown, so you'll probably be forced to pick a size and stay with it

Nick Campion
  • 10,479
  • 3
  • 44
  • 58
  • I tried extending the ViewPager class and overriding onMeasure but it appears that the ViewPager has not children, getChildCount() returns 0. Do you have any idea why that could be ? – Saad Farooq Feb 16 '12 at 15:51
  • It looks like the existing onMeasure has a call to populate() before it gets the children. You'll have to do that too. – Nick Campion Feb 16 '12 at 15:55
  • 1
    Thanks again... populate() was private so couldn't work but I found a way to pass EXACT MeasureSpec to the ViewPager and got it to work. – Saad Farooq Feb 16 '12 at 17:52
  • 2
    Could you share how you passed the EXACT MeasureSpec? Thanks in advance. – user640688 Jun 07 '12 at 14:13
  • did anyone find out how to do this? I am running into this issue and it is ANNOYING. My list of ViewPagers are all extremely small (seems like 0 pixels) when the children are set to wrap_content for height. When I pass a static value and then try to measure, you can see the view change size on the screen. It is annoying for the user. Anyone figure this out? How do you get the EXACT MeasureSpec? – Issa Fram Oct 15 '12 at 05:15
  • 1
    Hmm.. I forgot exactly how I did it... but it might be in my PaginatedGallery library at Github. I think if any one needs details you can find send me the exact application and maybe that will help refresh my memory – Saad Farooq Oct 15 '12 at 06:11
  • Stayed away from this problem for a couple of days and thought of the solution on the way home. All the views need to be inflated at the same time. The ViewPager and all its children. After the first child is inflated, measure it, then set the layout params for the ViewPager to be identical. It will work. The inflated children can then be passed on to a PagerAdapter in a custom constructor if need be. The PagerAdapter will add them to the ViewPager automatically or use .addView or whatever. Tough problem, solution does exist though. – Issa Fram Oct 19 '12 at 05:09
3

If you setTag(position) in the instantiateItem of your PageAdapter:

@Override
public Object instantiateItem(ViewGroup collection, int page) {
    LayoutInflater inflater = (LayoutInflater) context
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View view = (View) inflater.inflate(R.layout.page_item , null);
    view.setTag(page);

then can retrieve the view (page of the adapter) with an OnPageChangeListener, measure it, and resize your ViewPager:

private ViewPager pager;
@Override
protected void onCreate(Bundle savedInstanceState) {
    pager = findViewById(R.id.viewpager);
    pager.setOnPageChangeListener(new SimpleOnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            resizePager(position);
        }
    });

    public void resizePager(int position) {
        View view = pager.findViewWithTag(position);
        if (view == null) 
            return;
        view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
            //The layout params must match the parent of the ViewPager 
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width , height); 
        pager.setLayoutParams(params);
    }
}
Will Poitras
  • 404
  • 3
  • 7
0

Following the above example I discovered that measuring the height of the child views does not always return accurate results. The solution is to measure the height of any static views (defined in the xml) and then add the height of the fragment that is dynamically created at the bottom. In my case the static element was the PagerTitleStrip, which I also had to Override in order to enable the use of match_parent for the width in landscape mode.

So here is my take on the code from Delyan:

public class WrappingViewPager extends ViewPager {

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

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // super has to be called in the beginning so the child views can be
    // initialized.
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (getChildCount() <= 0)
        return;

    // Check if the selected layout_height mode is set to wrap_content
    // (represented by the AT_MOST constraint).
    boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec)
            == MeasureSpec.AT_MOST;

    int width = getMeasuredWidth();

    View firstChild = getChildAt(0);

    // Initially set the height to that of the first child - the
    // PagerTitleStrip (since we always know that it won't be 0).
    int height = firstChild.getMeasuredHeight();

    if (wrapHeight) {

        // Keep the current measured width.
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);

    }

    int fragmentHeight = 0;
    fragmentHeight = measureFragment(((Fragment) getAdapter().instantiateItem(this, getCurrentItem())).getView());

    // Just add the height of the fragment:
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + fragmentHeight,
            MeasureSpec.EXACTLY);

    // super has to be called again so the new specs are treated as
    // exact measurements.
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

public int measureFragment(View view) {
    if (view == null)
        return 0;

    view.measure(0, 0);
    return view.getMeasuredHeight();
}}

And the custom PagerTitleStrip:

public class MatchingPagerTitleStrip extends android.support.v4.view.PagerTitleStrip {

public MatchingPagerTitleStrip(Context arg0, AttributeSet arg1) {
    super(arg0, arg1);

}

@Override
protected void onMeasure(int arg0, int arg1) {

    int size = MeasureSpec.getSize(arg0);

    int newWidthSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);

    super.onMeasure(newWidthSpec, arg1);
}}

Cheers!

Vladislav
  • 13
  • 3
0

With Reference of above solutions, added some more statement to get maximum height of view pager child.

Refer the below code.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // super has to be called in the beginning so the child views can be
    // initialized.
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (getChildCount() <= 0)
        return;

    // Check if the selected layout_height mode is set to wrap_content
    // (represented by the AT_MOST constraint).
    boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST;

    int width = getMeasuredWidth();

    int childCount = getChildCount();

    int height = getChildAt(0).getMeasuredHeight();
    int fragmentHeight = 0;

    for (int index = 0; index < childCount; index++) {
        View firstChild = getChildAt(index);

        // Initially set the height to that of the first child - the
        // PagerTitleStrip (since we always know that it won't be 0).
        height = firstChild.getMeasuredHeight() > height ? firstChild.getMeasuredHeight() : height;

        int fHeight = measureFragment(((Fragment) getAdapter().instantiateItem(this, index)).getView());

        fragmentHeight = fHeight > fragmentHeight ? fHeight : fragmentHeight;

    }

    if (wrapHeight) {

        // Keep the current measured width.
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);

    }

    // Just add the height of the fragment:
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + fragmentHeight, MeasureSpec.EXACTLY);

    // super has to be called again so the new specs are treated as
    // exact measurements.
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Sachin Shelke
  • 449
  • 4
  • 12
0

better change

height = firstChild.getMeasuredHeight();

to

height = firstChild.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();
Alex.Li
  • 540
  • 1
  • 4
  • 12