Dynamic height viewpager

31,563

Solution 1

@abhishek's ans does what is required but the code below also adds animation during height change

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

Solution 2

Made a few tweaks in your code and it is working fine now.

1] onMeasure function wasn't proper. Use below logic

@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 needs to be re-measured each time a page is changed. Good place to do this is setPrimaryItem function of PagerAdapter

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

Here is the link to GitHub project with these tweaks: https://github.com/vabhishek/WrapContentViewPagerDemo

Solution 3

Just in case someone else find this post like me. Worked version without bug of initially zero height:

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

And used it in custom FragmentPagerAdapter, like this

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

Solution 4

Adding to @vihaan's solution, if you have a PagerTitleStrip or PagetTabStrip, you can add this

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

just before starting the animation (before the comment

 // Not the best place to put this animation, but it works pretty good.

so that the height of the strip is taken into account.

Share:
31,563
Vihaan Verma
Author by

Vihaan Verma

Updated on July 21, 2022

Comments

  • Vihaan Verma
    Vihaan Verma almost 2 years

    I'm trying to create a custom viewpager inside custom scroll viewthat dynamically wraps the current child's height.

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

    CustomPager

    /**
     * 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();
        }
    }
    

    MyPagerAdapter

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

    I was hoping that this would wrap around current fragments height but it is only taking the height of first child into consideration.

    Sample github project : https://github.com/VihaanVerma89/DynamicViewPager

  • Timmiej93
    Timmiej93 about 8 years
    Isn't this horribly laggy / not-smooth? It is for me at least (on emulator).
  • Vihaan Verma
    Vihaan Verma about 8 years
    @Timmiej93 You can check it out live on my app play.google.com/store/apps/details?id=com.mirraw.android . Go onto product detail page. It works flawlessly :)
  • Timmiej93
    Timmiej93 about 8 years
    I'm sorry, I'm probably (most likely) being very stupid, but where would I find your app? I could pull it from GitHub, but that version doesn't have the changes in it (as far as I can see). EDIT: You just added the link as I typed this, thanks!
  • Timmiej93
    Timmiej93 about 8 years
    With 'product detail page' do you mean the section with [Specifications - Shipping - Payment - Returns]? If so, that's indeed very smooth. I guess mine is incredibly laggy because I'm resizing an AlertDialog. Why do I always want the impossible, lol.
  • Narayan Acharya
    Narayan Acharya almost 8 years
    Hi! I used your solution to nest my ViewPager within a ScrollView. The first fragment of the ViewPager always gets a height of 0 even though I have content there. Other fragments work properly. When I slide to the first fragment, I can see the content in there but as soon as I am completely on that tab the fragment collapses. I can't seem to figure out what is going wrong.
  • Abhishek V
    Abhishek V almost 8 years
    @NarayanAcharya Can you post your code? preferably as a new question & put the link to the question here.
  • Narayan Acharya
    Narayan Acharya almost 8 years
    I added my code in this question stackoverflow.com/questions/38349809/… . Please have a look. Thanks!
  • Bartando
    Bartando almost 8 years
    @NarayanAcharya Hey, I know its a bit late, but use the mCurrentPosition like ... private int mCurrentPosition = -1; ... its important to set it to -1
  • Narayan Acharya
    Narayan Acharya almost 8 years
    Hi @Bartando Thanks for the reply. I have initialized the mCurrentPosition to -1. You can check out a separate question here stackoverflow.com/questions/38349809/… . I am yet to find a solution to this :(
  • Govind
    Govind over 7 years
    This code will lead to blank first page in few cases. "heightMeasureSpec" in the last "super.onMeasure(widthMeasureSpec, heightMeasureSpec)" statement should have the new height.. basically use "super.onMeasure(widthMeasureSpec, newHeight);
  • Faisal Naseer
    Faisal Naseer about 7 years
    whats the versionUtils
  • cammando
    cammando about 7 years
    @AbhishekV why did you created the custom ScrollView
  • cammando
    cammando about 7 years
    It is giving me class cast exception i am using it without scroll view
  • cammando
    cammando about 7 years
    resolved the class cast exception with cleaning and rebuilding the project
  • cammando
    cammando about 7 years
    @AbhishekV this solution is not inflating the view Pager for the first time
  • Asalas77
    Asalas77 about 7 years
    @AbhishekV i know this is an old post, but could you tell me how to modify this to always fit height to the biggest child view?
  • Felipe A.
    Felipe A. almost 7 years
    @AbhishekV, can you help me man? I'm having a problem because I have a recyclerview inside the fragment I inflate on viewpager... And the layoutmanager trigger after onMeasure method have triggered... What i need to do with this?
  • Vucko
    Vucko almost 6 years
    Very helpful, my height was NOT being properly calculated cause it's counting the TabLayout as the child at 0, so taking max height of all the children was not a good solution. I needed max height + height of child at 0. Thanks man, you saved my day.
  • John61590
    John61590 almost 6 years
    measureCurrentView isn't used anywhere?
  • ITurchenko
    ITurchenko almost 6 years
    @John61590 thanks for comment, I added custom adapter example
  • Sonu Sanjeev
    Sonu Sanjeev over 5 years
    Removing the return method inside if (mCurrentView == null) on the onMeasure, solved my issues of the first fragment not getting shown.
  • ziniestro
    ziniestro almost 5 years
    great example, I've combined it with my customViewPager and works beautifully, thanks
  • arberg
    arberg about 4 years
    This looks nearly identical to ITurchenko answer, but is 2 years older. It is unclear what is problem of ITurchenko solution that this should solve.
  • Noorul
    Noorul about 4 years
    I think it is not working on view pager inside nestedscrollview.
  • Ali Rezaiyan
    Ali Rezaiyan almost 4 years
    Take care of which adapter you are using. this line View child = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem()).getView(); might crash duo to cast exception.
  • Sarvesh Athawale
    Sarvesh Athawale about 3 years
    It seems like it doesn't work if ConstraintLayout is used in one of your Fragment's layout.
  • David Kariuki
    David Kariuki almost 3 years
    This folds the longest view and does not stretch it after refocus