动态高度的ViewPager

35
我试图在自定义滚动视图内创建一个自定义的viewpager,它可以动态地包裹当前子项的高度。
package com.example.vihaan.dynamicviewpager;

import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.widget.ScrollView;

    /**
     * Created by vihaan on 1/9/15.
     */
    public class CustomScrollView extends ScrollView {

        private GestureDetector mGestureDetector;

        public CustomScrollView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mGestureDetector = new GestureDetector(context, new YScrollDetector());
            setFadingEdgeLength(0);
        }

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return super.onInterceptTouchEvent(ev)
                    && mGestureDetector.onTouchEvent(ev);
        }

        // Return false if we're scrolling in the x direction
        class YScrollDetector extends GestureDetector.SimpleOnGestureListener {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2,
                                    float distanceX, float distanceY) {
                return (Math.abs(distanceY) > Math.abs(distanceX));
            }
        }
    }

自定义分页器

/**
 * Created by vihaan on 1/9/15.
 */
public class CustomPager extends ViewPager {

    public CustomPager (Context context) {
        super(context);
    }

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

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

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

        final View tab = getChildAt(0);
        int width = getMeasuredWidth();
        int tabHeight = tab.getMeasuredHeight();

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

        int fragmentHeight = measureFragment(((Fragment) getAdapter().instantiateItem(this, getCurrentItem())).getView());
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(tabHeight + fragmentHeight + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics()), MeasureSpec.AT_MOST);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

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

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

我的PagerAdapter

public class MyPagerAdapter extends FragmentPagerAdapter {

    private List<Fragment> fragments;

    public MyPagerAdapter(FragmentManager fm) {
        super(fm);
        this.fragments = new ArrayList<Fragment>();
        fragments.add(new FirstFragment());
        fragments.add(new SecondFragment());
        fragments.add(new ThirdFragment());
        fragments.add(new FourthFragment());
    }

    @Override
    public Fragment getItem(int position) {
        return fragments.get(position);
    }

    @Override
    public int getCount() {
        return fragments.size();
    }
}

我希望这个能够自动适应当前碎片的高度,但它只考虑了第一个子节点的高度。

示例 Github 项目: https://github.com/VihaanVerma89/DynamicViewPager

6个回答

77

我对你的代码进行了一些调整,现在它可以正常工作了。

1] onMeasure 函数不正确。请使用以下逻辑:

@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mCurrentView == null) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        return;
    }
    int height = 0;
    mCurrentView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    int h = mCurrentView.getMeasuredHeight();
    if (h > height) height = h;
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

2] ViewPager每次页面改变都需要重新测量。最好的做法是在PagerAdaptersetPrimaryItem函数中实现。

@Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, object);
        if (position != mCurrentPosition) {
            Fragment fragment = (Fragment) object;
            CustomPager pager = (CustomPager) container;
            if (fragment != null && fragment.getView() != null) {
                mCurrentPosition = position;
                pager.measureCurrentView(fragment.getView());
            }
        }
    }

以下是包含这些调整的 GitHub 项目链接:https://github.com/vabhishek/WrapContentViewPagerDemo


1
你好!我使用了你的解决方案将ViewPager嵌套在ScrollView中。ViewPager的第一个片段总是得到0的高度,即使我有内容。其他片段正常工作。当我滑动到第一个片段时,我可以看到里面的内容,但是一旦我完全在那个选项卡上,片段就会崩溃。我似乎无法弄清楚出了什么问题。 - Narayan Acharya
通过清理和重新构建项目解决了类转换异常。 - cammando
@AbhishekV 这个解决方案第一次不会填充视图页。 - cammando
@AbhishekV 我知道这是一个旧帖子,但你能告诉我如何修改它以始终适应最大的子视图高度吗? - Asalas77
@AbhishekV,你能帮我吗?我有一个问题,因为我在viewpager上膨胀的片段中有一个recyclerview...并且layoutmanager在onMeasure方法触发后触发...我需要怎么做? - Felipe A.
显示剩余9条评论

26

@abhishek的答案能够实现所需功能,但下面的代码还可以在高度变化时添加动画效果。

public class WrappingViewPager extends ViewPager {

    private Boolean mAnimStarted = false;

    public WrappingViewPager(Context context) {
        super(context);
    }

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

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if(!mAnimStarted && null != getAdapter()) {
            int height = 0;
            View child = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem()).getView();
            if (child != null) {
                child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
                height = child.getMeasuredHeight();
                if (VersionUtils.isJellyBean() && height < getMinimumHeight()) {
                    height = getMinimumHeight();
                }
            }

            // Not the best place to put this animation, but it works pretty good.
            int newHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
            if (getLayoutParams().height != 0 && heightMeasureSpec != newHeight) {
                    final int targetHeight = height;
                    final int currentHeight = getLayoutParams().height;
                    final int heightChange = targetHeight - currentHeight;

                    Animation a = new Animation() {
                        @Override
                        protected void applyTransformation(float interpolatedTime, Transformation t) {
                            if (interpolatedTime >= 1) {
                                getLayoutParams().height = targetHeight;
                            } else {
                                int stepHeight = (int) (heightChange * interpolatedTime);
                                getLayoutParams().height = currentHeight + stepHeight;
                            }
                            requestLayout();
                        }

                        @Override
                        public boolean willChangeBounds() {
                            return true;
                        }
                    };

                    a.setAnimationListener(new Animation.AnimationListener() {
                        @Override
                        public void onAnimationStart(Animation animation) {
                            mAnimStarted = true;
                        }

                        @Override
                        public void onAnimationEnd(Animation animation) {
                            mAnimStarted = false;
                        }

                        @Override
                        public void onAnimationRepeat(Animation animation) {
                        }
                    });

                    a.setDuration(1000);
                    startAnimation(a);
                    mAnimStarted = true;
            } else {
                heightMeasureSpec = newHeight;
            }
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

这不是非常卡/不流畅吗?至少对我来说是这样(在模拟器上)。 - Timmiej93
@Timmiej93 你可以在我的应用程序上实时查看它 https://play.google.com/store/apps/details?id=com.mirraw.android 。进入产品详细页面。它运行得非常流畅 :) - Vihaan Verma
对不起,我可能很愚蠢,但我应该在哪里找到你的应用程序?我可以从GitHub上获取它,但那个版本没有所需的更改(就我所知)。编辑:你刚刚在我打字时添加了链接,谢谢! - Timmiej93
你所说的“产品详情页”是指[规格-运输-付款-退货]部分吗?如果是,那确实非常流畅。我猜我的很卡可能是因为我正在调整AlertDialog的大小。为什么我总是想要不可能的事情,哈哈。 - Timmiej93
2
版本工具是什么? - Faisal Naseer
显示剩余3条评论

21

如果有其他人像我一样发现这篇文章,请看下面的经过修正、没有初始高度为零的版本:

public class DynamicHeightViewPager extends ViewPager {
    private View mCurrentView;

    public DynamicHeightViewPager(Context context) {
        super(context);
    }

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

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (mCurrentView != null) {
            mCurrentView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

            int height = Math.max(0, mCurrentView.getMeasuredHeight());
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    public void measureCurrentView(View currentView) {
        mCurrentView = currentView;
        requestLayout();
    }
}

然后像这样在自定义的FragmentPagerAdapter中使用它

public abstract class AutoheightFragmentPagerAdapter extends FragmentPagerAdapter {
    private int mCurrentPosition = -1;

    public AutoheightFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, object);

        if (position != mCurrentPosition && container instanceof DynamicHeightViewPager) {
            Fragment fragment = (Fragment) object;
            DynamicHeightViewPager pager = (DynamicHeightViewPager) container;

            if (fragment != null && fragment.getView() != null) {
                mCurrentPosition = position;
                pager.measureCurrentView(fragment.getView());
            }
        }
    }
}

1
measureCurrentView 没有被任何地方使用? - John61590
1
@John61590 感谢您的评论,我添加了自定义适配器示例。 - ITurchenko
如果在您的Fragment布局中使用ConstraintLayout,似乎它无法正常工作。 - Sarvesh Athawale

4

在 @vihaan 的解决方案基础上,如果你有 PagerTitleStrip 或者 PagerTabStrip,你可以添加如下代码:

// Account for pagerTitleStrip or pagerTabStrip
View tabStrip = getChildAt(0);
if (tabStrip instanceof PagerTitleStrip) {
    tabStrip.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.UNSPECIFIED));
    height += tabStrip.getMeasuredHeight();
}

在开始动画之前(在注释之前),将以下代码添加到该行的前面:

// 不是最好的放置动画的位置,但它效果非常好。

这样可以考虑到条带的高度。


1
非常有帮助,我的高度计算不正确,因为它将TabLayout视为子项0,因此取所有子项的最大高度不是一个好的解决方案。我需要最大高度+子项0的高度。谢谢,你救了我的一天。 - Vucko

0
这几行代码将解决问题。
为viewpager类创建自定义小部件,并在xml中使用它来查看页面。
public class DynamicHeightViewPager extends ViewPager {
private View mCurrentView;

public DynamicHeightViewPager(Context context) {
    super(context);
}

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

@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (mCurrentView != null) {
        mCurrentView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

        int height = Math.max(0, mCurrentView.getMeasuredHeight());
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

public void measureCurrentView(View currentView) {
    mCurrentView = currentView;
    requestLayout();
}
}

在视图页面适配器中,重写以下方法并添加代码。
  @Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    super.setPrimaryItem(container, position, object);
    if (container instanceof DynamicHeightViewPager) {
// instead of card view give your root view from your item.xml file.
        CardView cardView = (CardView) object;
        ((DynamicHeightViewPager) container).measureCurrentView(cardView);
    }
}

在XML文件中,将您的视图页面高度设置为“wrap_content”,以便您可以查看结果。

这看起来与ITurchenko的答案几乎完全相同,但是比其早两年。不清楚ITurchenko解决方案存在什么问题需要这个解决。 - arberg

0
public class WrapContentViewPager extends ViewPager {
private Boolean mAnimStarted = false;

public WrapContentViewPager(Context context) {
    super(context);
}

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

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (!mAnimStarted && null != getAdapter()) {
        int height = 0;
        View child = ((CommonViewPagerAdapter) getAdapter()).getItem(getCurrentItem()).getView();
        if (child != null) {
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            height = child.getMeasuredHeight();
            if (height < getMinimumHeight()) {
                height = getMinimumHeight();
            }
        }

        // Not the best place to put this animation, but it works pretty good.
        int newHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        if (getLayoutParams().height != 0 && heightMeasureSpec != newHeight) {
            final int targetHeight = height;
            final int currentHeight = getLayoutParams().height;
            final int heightChange = targetHeight - currentHeight;

            Animation a = new Animation() {
                @Override
                protected void applyTransformation(float interpolatedTime, Transformation t) {
                    if (interpolatedTime >= 1) {
                        getLayoutParams().height = targetHeight;
                    } else {
                        int stepHeight = (int) (heightChange * interpolatedTime);
                        getLayoutParams().height = currentHeight + stepHeight;
                    }
                    requestLayout();
                }

                @Override
                public boolean willChangeBounds() {
                    return true;
                }
            };

            a.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                    mAnimStarted = true;
                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    mAnimStarted = false;
                }

                @Override
                public void onAnimationRepeat(Animation animation) {
                }
            });

            a.setDuration(100);
            startAnimation(a);
            mAnimStarted = true;
        } else {
            heightMeasureSpec = newHeight;
        }
    }

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

==============================

wrapContentViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }

        @Override
        public void onPageSelected(int position) {
            wrapContentViewPager.measure(wrapContentViewPager.getMeasuredWidth(), wrapContentViewPager.getMeasuredHeight());
        }

        @Override
        public void onPageScrollStateChanged(int state) {

        }
    });

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