测量ViewPager

18

我有一个自定义ViewGroup,它带有一个子ViewPager。ViewPager由提供一个LinearLayout给ViewPager的PagerAdapter来填充,并且该LinearLayoutLayoutParams在高度和宽度上均为WRAP_CONTENT

视图显示正确,但当在ViewPager上调用child.measure()方法时,它并不返回LinearLayout的实际尺寸,而似乎填充了所有剩余的空间。

有任何想法是因为什么原因导致这种情况以及如何修正它?


请给这个问题点个赞 https://code.google.com/p/android/issues/detail?id=54604 - Christ
FYI:问题54604已在昨天被Google批量关闭。如果您仍然有这个问题,我建议您重新打开一个新问题并把链接放在这里。 - Christ
可能是重复的问题:Android:我无法让ViewPager WRAP_CONTENT - Raanan
6个回答

57

对于被接受的答案(以及评论中提到的预充气所有视图的解决方案),我觉得并不是很满意,因此我组合了一个 ViewPager,它从第一个可用子项获取其高度。它通过进行第二次测量传递来实现这一点,允许您获取第一个子项的高度。

更好的解决方案是在 android.support.v4.view 包内创建一个新类,该类实现了一个更好版本的 onMeasure(具有访问包可见方法,如 populate()

不过,暂时来说,下面的解决方案已经完全适合我了。

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

@Delyen,HeightWrappingViewPager在viewpager页面切换时不显示任何内容,而普通的ViewPager则可以。该怎么办? - neeraj
1
仍未显示任何内容。如果我硬编码Pager的高度,则它可以正常工作,但我不想固定高度。你能帮我吗? - Gaurav Arora
5
我不知道是否有人遇到了问题,但是这个方法不起作用,因为heightMeasureSpec参数为0,所以我将MeasureSpec.AT_MOST更改为MeasureSpec.UNSPECIFIED。然后它就起作用了... - Zyoo
1
它是否可以自动换行? - JY2k
我根据这段代码进行了实现,针对我的问题进行了适应,并且它运行得很好。感谢您的启发。 - voghDev
显示剩余11条评论

12

查看兼容性jar中ViewPager类的内部:

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

   ...
}

看起来ViewPager的实现并没有测量子视图的大小,而只是根据父级传入的内容设置ViewPager为一个标准视图。当您传递wrap_content时,由于视图页面实际上没有测量其内容,它会占用整个可用区域。
我的建议是根据子视图的大小在ViewPager上设置静态大小。如果这不可能(例如,子视图可能会发生变化),则需要选择最大大小并处理一些视图中的额外空间,或者扩展ViewPager并提供一个onMeasure来测量子视图。您将遇到的一个问题是,视图页面被设计为在显示不同视图时不会变化宽度,因此您可能会被迫选择一个大小并保持不变。

我尝试扩展ViewPager类并覆盖onMeasure方法,但似乎ViewPager没有子项,getChildCount()返回0。你有任何想法为什么会这样吗? - Saad Farooq
看起来现有的onMeasure在获取子项之前调用了populate()。你也需要这样做。 - Nick Campion
1
再次感谢... populate() 是私有的,所以无法工作,但我找到了一种方法将 EXACT MeasureSpec 传递给 ViewPager,并使其工作。 - Saad Farooq
2
你能分享一下你是如何通过 EXACT MeasureSpec 的吗?先谢谢了。 - user640688
1
嗯...我忘记了我是怎么做到的...但可能在我的Github PaginatedGallery库中可以找到。如果有人需要详细信息,可以发送给我确切的应用程序,也许这会帮助我恢复记忆。 - Saad Farooq
显示剩余2条评论

3
如果您在PageAdapter的instantiateItem中使用setTag(position):
@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);

然后可以通过OnPageChangeListener检索视图(适配器的页面),对其进行测量,并调整您的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);
    }
}

0

最好改变

height = firstChild.getMeasuredHeight();

height = firstChild.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();

0

参考以上解决方案,加入一些语句以获得视图页面子元素的最大高度。

请参考以下代码。

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

方法measureFragment(View)在类型HeightWrappingViewPager中未定义。 - Smit Patel

0
根据上述示例,我发现测量子视图的高度并不总是返回准确的结果。解决方案是测量任何静态视图(在xml中定义的视图)的高度,然后再添加动态创建在底部的片段的高度。
在我的情况下,静态元素是PagerTitleStrip,在横向模式下,我还必须重写它以允许使用match_parent设置宽度。
所以这里是我对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();
}}

还有自定义的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);
}}

干杯!


java.lang.StackOverflowError - Smit Patel
这段代码已经在Android 4.2和4.3上测试并且仍然运行正常。您能否详细说明您遇到错误的地方? - Vladislav
我正在5.0中进行检查,你也可以检查它。 - Smit Patel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接