35

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

IntelliJ Amiya
  • 74,896
  • 15
  • 165
  • 198
Vihaan Verma
  • 12,815
  • 19
  • 97
  • 126

5 Answers5

77

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

Abhishek V
  • 12,488
  • 6
  • 51
  • 63
  • 1
    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. – Narayan Acharya Jul 12 '16 at 09:40
  • @NarayanAcharya Can you post your code? preferably as a new question & put the link to the question here. – Abhishek V Jul 13 '16 at 04:51
  • I added my code in this question http://stackoverflow.com/questions/38349809/android-viewpager-with-a-dynamic-height-within-a-scrollview . Please have a look. Thanks! – Narayan Acharya Jul 13 '16 at 11:05
  • @NarayanAcharya Hey, I know its a bit late, but use the mCurrentPosition like ... private int mCurrentPosition = -1; ... its important to set it to -1 – Bartando Jul 19 '16 at 13:50
  • Hi @Bartando Thanks for the reply. I have initialized the mCurrentPosition to -1. You can check out a separate question here http://stackoverflow.com/questions/38349809/android-viewpager-with-a-dynamic-height-within-a-scrollview . I am yet to find a solution to this :( – Narayan Acharya Jul 19 '16 at 14:00
  • @AbhishekV why did you created the custom ScrollView – cammando Feb 22 '17 at 10:34
  • It is giving me class cast exception i am using it without scroll view – cammando Feb 22 '17 at 10:42
  • resolved the class cast exception with cleaning and rebuilding the project – cammando Feb 22 '17 at 11:46
  • @AbhishekV this solution is not inflating the view Pager for the first time – cammando Feb 22 '17 at 13:02
  • @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? – Asalas77 Mar 27 '17 at 13:36
  • @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? – Felipe A. May 17 '17 at 14:22
  • Removing the return method inside if (mCurrentView == null) on the onMeasure, solved my issues of the first fragment not getting shown. – Sonu Sanjeev Oct 26 '18 at 12:16
  • great example, I've combined it with my customViewPager and works beautifully, thanks – ziniestro May 14 '19 at 07:25
  • This folds the longest view and does not stretch it after refocus – David Kariuki Jun 26 '21 at 13:18
26

@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);
    }
}
Vihaan Verma
  • 12,815
  • 19
  • 97
  • 126
  • Isn't this horribly laggy / not-smooth? It is for me at least (on emulator). – Timmiej93 Mar 25 '16 at 12:30
  • @Timmiej93 You can check it out live on my app https://play.google.com/store/apps/details?id=com.mirraw.android . Go onto product detail page. It works flawlessly :) – Vihaan Verma Mar 25 '16 at 21:12
  • 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 Mar 25 '16 at 21:17
  • 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. – Timmiej93 Mar 25 '16 at 21:20
  • 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); – Govind Nov 23 '16 at 09:26
  • 2
    whats the versionUtils – Faisal Naseer Feb 15 '17 at 14:48
  • I think it is not working on view pager inside nestedscrollview. – Noorul Apr 23 '20 at 07:03
  • Take care of which adapter you are using. this line `View child = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem()).getView();` might crash duo to cast exception. – Ali Rezaiyan May 18 '20 at 07:40
21

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());
            }
        }
    }
}
ITurchenko
  • 1,828
  • 2
  • 17
  • 24
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.

talosdev
  • 193
  • 1
  • 11
  • 1
    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. – Vucko Jun 20 '18 at 11:42
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) {

        }
    });
Zoe
  • 27,060
  • 21
  • 118
  • 148