Measuring a ViewPager
Asked Answered
T

6

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?

Tabard answered 16/2, 2012 at 15:1 Comment(3)
please star issue code.google.com/p/android/issues/detail?id=54604Propel
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.Propel
possible duplicate of Android: I am unable to have ViewPager WRAP_CONTENTTerryl
Z
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);
        }
    }
}
Zaslow answered 20/2, 2013 at 15:34 Comment(15)
Why yours HeightWrappingViewPager doesn't work inside ScrollView with android:fillViewport="true"? When I replace ViewPager with HeightWrappingViewPager I can't scroll content within ScrollViewLithopone
@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.Zaslow
@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.Lithopone
@Ziem, try to put the ViewPager a layer deeper into the hierarchy. For example, you can wrap it in a FrameLayout.Zaslow
@Delyen, HeightWrappingViewPager doesn't show any content on viewpager's page change while normal ViewPager does. What to do?Eec
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 ?Rattray
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...Mccormick
Thank you soooooooooo much! Saved me from throwing my phone out of the fourth floor window.Corves
hmm, not working when rotating to landscape. It keeps the original height.Corves
Is it possible to have it also wrap width?Stet
seriously a great answer..saved me lots of effort and time.Pedicure
I did an implementation based on this code, adapted for my problem and it worked fine. Thanks for the inspirationStipple
I follow codes above of @Zaslow but it does not work. I followed link and it worksKarilynn
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.Christachristabel
Saved my day. I've been trying to achieve this for hours. Thanks :)Benefice
G
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

Gilberte answered 16/2, 2012 at 15:10 Comment(7)
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 ?Tabard
It looks like the existing onMeasure has a call to populate() before it gets the children. You'll have to do that too.Gilberte
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.Tabard
Could you share how you passed the EXACT MeasureSpec? Thanks in advance.Petty
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?Salbu
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 memoryTabard
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.Salbu
K
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);
    }
}
Kamala answered 1/3, 2014 at 18:41 Comment(0)
B
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!

Buckinghamshire answered 5/6, 2013 at 16:19 Comment(3)
java.lang.StackOverflowErrorApplewhite
This code is tested an still running on Android 4.2 and 4.3. Could you please elaborate on where you get the error?Buckinghamshire
i'm checking it in 5.0 you can also check itApplewhite
C
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);
}
Chaste answered 19/6, 2014 at 12:18 Comment(1)
The method measureFragment(View) is undefined for the type HeightWrappingViewPagerApplewhite
Z
0

better change

height = firstChild.getMeasuredHeight();

to

height = firstChild.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();
Ziagos answered 22/1, 2015 at 15:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.