Android:我无法使用ViewPager的WRAP_CONTENT选项

277

我设置了一个简单的ViewPager,每个页面上都有一个高度为200dp的ImageView。

这是我的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);

尽管高度设置为wrap_content,但翻页控件仍然充满整个屏幕,即使ImageView只有200dp大小。我试图将翻页控件的高度替换为“200”,但在多个分辨率下会得到不同的结果。我无法将“dp”添加到该值中。如何将200dp添加到翻页控件的布局中?


1
请给这个问题点个赞 https://code.google.com/p/android/issues/detail?id=54604 - Christ
33个回答

434

如果按照以下方式重写您的ViewPageronMeasure,它将获得当前拥有的最大子视图的高度。

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

29
这个最接近我需要的,但有两件事要补充:1. ViewPager 只会调整到其实际子项中最大的一个,也就是当前可见的项和直接相邻的项。在 ViewPager 上调用 setOffscreenPageLimit(总子项数)可以解决这个问题,并得到一个大小设置为所有项中最大的 ViewPager ,且不再重新调整大小。2. 当尝试测量 WebView 时,它们会出现一些奇怪的问题。在加载后对 WebView 调用 requestLayout() 可以解决这个问题。 - 0101100101
4
只有一个小问题需要解决:如果viewPager的可见性为GONE,而你将其设置为可见,则在其片段创建之前会调用onMeasure。因此,它最终会具有高度为0的情况。如果有人有想法,欢迎提出。我想我会使用回调来处理片段创建的情况。 - edoardotognoni
4
如果你有装饰子视图,这种方法就不可行了——因为ViewPager.onMeasure()会先测量装饰视图并为它们分配空间,然后再将剩余的空间分配给非装饰子视图。尽管如此,在这里这是最正确的解决方案,所以我已经点了赞 ;) - Benjamin Dobell
10
在ViewPager上执行setAdapter()时,getChildCount()可能返回0!实际创建视图的populate()调用发生在super.onMeasure(widthMeasureSpec, heightMeasureSpec)中。将额外的super.onMeasure()调用放在这个函数的开头就能解决问题。还可以参考http://stackoverflow.com/questions/38492210/viewpager-getchildcount-returns-zero。 - Kurovsky
3
为什么我想让父元素的高度比子元素都大?如果我滚动到相邻的子元素,它会使视图有很多额外的空间,并且看起来很奇怪。例如,我在高度为32的第3页,共5页,然后移动到高度为8的第2页。现在第2页的高度将变为32,但内容甚至都不适合这种高度。 - John61590
显示剩余16条评论

112

另一个更通用的解决方案是让wrap_content正常工作。

我扩展了ViewPager来覆盖onMeasure()。高度围绕第一个子视图包裹。如果子视图的高度不完全相同,这可能会导致意外结果。为此,该类可以很容易地扩展,例如动画到当前视图/页面的大小。但我不需要那个。

您可以像原始ViewPager一样在XML布局中使用此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"/>

优点:这种方法允许在任何布局中使用ViewPager,包括RelativeLayout来覆盖其他UI元素。

一个缺点仍然存在:如果您想使用边距,您必须创建两个嵌套布局并给内部布局所需的边距。

这是代码:

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

}

35
当ViewPager被销毁并重新打开时,是否有其他人除了当前项外也出现了空白页? - Zyoo
1
我也遇到了空白页面。 - aeren
10
你只需要按照我博客中的描述,合并这个问题的两个最佳答案:http://pristalovpavel.wordpress.com/2014/12/26/doing-it-right-vertical-scrollview-with-viewpager-and-listview/ - anil
4
请将'onMeasure'方法的代码替换为'Daniel López Lacalle'提供的答案。 - Yog Guru
使用'Daniel López Lacalle'在此答案中给出的答案仍然无法正确更改高度,请更改此行super.onMeasure(widthMeasureSpec, heightMeasureSpec*2);将其乘以2。 - Sohail Zahid
也许今天需要一些改进。在我实现它的那些日子里,它运行得很好。 - cybergen

63
我根据Daniel López Lacalle和这篇文章http://www.henning.ms/2013/09/09/viewpager-that-simply-dont-measure-up/来回答。但问题是,在某些情况下我的孩子的高度为零。解决办法是不幸地测量两次。
@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);
}

这也让您可以在ViewPager上设置高度,如果需要的话,或者只需使用wrap_content。


为什么在这个ViewPager中的图片实际上比使用相同的scaleTypelayout_width=match_parentlayout_height=wrap_content的ImageView中的图片要短,好像有20dp的差距。 - Shark
鲨鱼,我真的不确定。那可能与你的比例尺类型有关。也许可以尝试设置高度。 - MinceMan
3
我简直不敢相信!我花了两天时间黏合我的自定义ViewPager,遇到了一个问题,我的初始视图没有显示出来,我就是想不明白为什么! // super has to be called in the beginning so the child views can be initialized. <----- 这就是原因,必须在onMeasure函数的开头和结尾调用它。今天给我一个虚拟高五吧! - Starwave
对我没用。孩子们的高度仍然是零。 - Eduardo Reis
谢谢你。它工作得很好,解决了我的问题。 - jojemapa
显示剩余4条评论

38

我刚刚回答了一个非常类似的问题,并在寻找支持我的论点的链接时偶然发现了这个,所以你很幸运 :)

我的另一个答案:
ViewPager不支持wrap_content,因为它通常不会同时加载所有子视图,因此无法获得适当的大小(选项是拥有每次切换页面都更改大小的Pager)。

但是您可以设置精确的尺寸(例如150dp),match_parent也可以使用。
您还可以通过更改其LayoutParams中的height属性,在代码中动态修改尺寸。

对于您的需求,您可以在自己的xml文件中创建ViewPager,将layout_height设置为200dp,然后在代码中,而不是从头开始创建新的ViewPager,您可以填充该xml文件:

LayoutInflater inflater = context.getLayoutInflater();
inflater.inflate(R.layout.viewpagerxml, layout, true);

4
好答案,有点烦人的是默认行为是“做一些难以理解的事情”。感谢解释。 - Chris Vandevelde
10
@ChrisVandevelde 这似乎是一些 Android 库的共同特征。一旦你掌握了基础知识,你会意识到没有东西遵循它们。 - CQM
1
但是,@Java,为什么ViewPager不能在每次加载其子项时调整其高度呢? - Diffy
@CQM 确实如此!ViewPagerIndicator库在将layout_height设置为wrap_content时也存在同样的问题,但更糟糕的是,简单的解决方法——将其设置为固定值——并不起作用。 - Giulio Piancastelli

25

使用Daniel López Localle的答案,我在Kotlin中创建了这个类。希望它能为您节省更多时间。

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

在对话框中和包含回收视图时运行良好。 - Slion
1
如果您的分页器有一些垂直填充,它可能会出现问题。这意味着即使不需要滚动,它也会强制内容滚动。 - Slion
1
通过以下方式编辑答案以支持垂直填充:heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height + paddingBottom + paddingTop, View.MeasureSpec.EXACTLY) - Slion
1
嘿@Slion,感谢您的反馈。虽然我同意这样改进代码以更好地适应任何项目并避免未来更改的副作用,但我对更改答案表示怀疑,因为这将做比要求的更多的事情。因为我认为即使在此更改后,类的名称也不够清晰。也许我必须将其更改为DynamicVerticalViewpager?但你明白我的意思吧?但是,如果您在我的项目中提出了此更改请求,我会全心全意地去做。 - Felipe Castilhos

17

我刚刚遇到了同样的问题。我有一个ViewPager,我想在其底部显示广告。我找到的解决方案是将Pager放入RelativeLayout中,并将其layout_above设置为我想要在其下方看到的视图ID。这对我有用。

这是我的布局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>

4
仅供参考,你不需要在两个中都加上xmlns:android="http://schemas.android.com/apk/res/android",只需在第一个中添加即可。 - Martin Marconcini
2
你的问题完全不同。你的布局使用match_parent设置ViewPager可以正常工作 - OP遇到了一种情况,他希望ViewPager包裹其内容。 - k2col

16

我已经在几个项目中遇到了这个问题,但从未有过完整的解决方案。因此,我创建了一个名为WrapContentViewPager的Github项目,作为ViewPager的就地替代品。

https://github.com/rnevet/WCViewPager

该解决方案受到了这里一些答案的启发,并改进了以下内容:

  • 根据当前视图动态更改ViewPager的高度,包括滚动时。
  • 考虑“装饰”视图(如PagerTabStrip)的高度。
  • 考虑所有填充。

更新以支持支持库版本24,该版本破坏了先前的实现。


@mvai,你可以打开一个问题(issue),或者fork它并修改样例应用吗? - Raanan
1
我发现RecyclerView也有一些wrap_content的问题;如果您使用自定义LinearLayoutManager,例如这个,它就可以正常工作。所以你的库没有任何问题。 - natario
1
还需要修复的是它与FragmentStatePagerAdapter的使用。看起来它在片段布局之前测量子项,因此给出较小的高度。对我有用的是@logan的答案,尽管我仍在努力解决它。您可能希望尝试将该方法合并到您的库中。我不熟悉github,抱歉。 - natario
@mvai,我似乎找不到当前实现和FragmentStatePageAdapter的任何问题,你可以在适配器包装器的finishUpdate中添加: requestLayout(); invalidate(); 看看是否解决了你的问题。 - Raanan
1
对于任何想知道如何在 FragmentPagerAdapter 中实现此功能的人,您需要让您的适配器通过在内部保持片段列表来实现 ObjectAtPositionInterface 接口,以便它可以从 getObjectAtPosition 方法返回相应的片段。 - Pablo
显示剩余5条评论

9
我也遇到过这个问题,但我的情况是一个FragmentPagerAdapterViewPager提供页面。我的问题是,在任何Fragment被创建之前(因此无法正确定位其大小),ViewPageronMeasure()被调用。
经过一些尝试和实验,我发现FragmentPagerAdapterfinishUpdate()方法在Fragment被初始化后(从FragmentPagerAdapterinstantiateItem()中)并且在/期间进行页面滚动。我制作了一个小接口:
public interface AdapterFinishUpdateCallbacks
{
    void onFinishUpdate();
}

我将其传递给我的FragmentPagerAdapter并调用:

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

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

这反过来使我能够在我的CustomViewPager实现上调用setVariableHeight()

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

我不确定这是否是最佳方法,如果您认为它好/坏/邪恶,请留下评论,但在我的实现中似乎效果不错 :)

希望这能帮助到某些人!

编辑:我忘记在调用super.measure()后添加requestLayout()(否则它不会重新绘制视图)。

我还忘记了将父级的填充添加到最终高度中。

我还放弃了保留原始的width/height MeasureSpecs,转而根据需要创建一个新的。已相应更新代码。

我还遇到的另一个问题是它在ScrollView中无法正确调整大小,并发现罪魁祸首是使用MeasureSpec.EXACTLY来测量子项而不是MeasureSpec.UNSPECIFIED。已更新以反映此更改。

所有这些更改都已添加到代码中。如果您想要查看旧的(不正确的)版本,则可以查看历史记录。


为什么您不把忘记添加的内容加入到代码中呢? - hasan
@hasan,我已经做了,抱歉如果有任何混淆!我会更新答案并说明的。 - logan
太棒了!很高兴能帮助到您 :) - logan

8
另一个解决方案是根据当前页面高度在 PagerAdapter 中更新 ViewPager 的高度。假设您是按照以下方式创建ViewPager 页面的:
@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;
}

mPages 是一个内部列表,其中包含动态添加到 PagerAdapter 中的 PageInfo 结构,而 CustomImageView 只是具有重写 onMeasure() 方法的常规 ImageView,该方法根据指定的宽度设置其高度并保持图像纵横比。

您可以在 setPrimaryItem() 方法中强制设置 ViewPager 的高度:

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

请注意Math.max(height, 1)。这可以解决一个很烦人的问题,即当前一页高度为零(即在CustomImageView中没有可绘制内容),每隔一次来回划动两个页面时,ViewPager不能更新显示的页面(显示为空白)。

在我看来,这似乎是正确的路径,但我需要添加一个 item.mImageView.measure(..) 来获取 getMeasuredXXX() 方法中的正确尺寸。 - Gianluca P.

7

当在viewpager内使用静态内容,而且你不需要复杂的动画效果时,可以使用以下的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));
  }
}

这个很好用。我通过循环遍历子元素并选择最大高度的一个来扩展它。 - Javier Mendonça
即使在回收视图下也能正常工作。 - kanudo
我遇到了这个异常 - java.lang.NullPointerException:尝试在空对象引用上调用虚拟方法'void android.view.View.measure(int,int)' - PJ2104
但是取第一个元素可能是错误的。 - Tobias Reich

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