문제

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?

도움이 되었습니까?

해결책

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

다른 팁

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

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

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!

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

better change

height = firstChild.getMeasuredHeight();

to

height = firstChild.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top