Dynamic height viewpager
Asked Answered
O

5

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

Olin answered 1/9, 2015 at 11:38 Comment(0)
O
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);
    }
}
Olin answered 9/9, 2015 at 20:27 Comment(8)
Isn't this horribly laggy / not-smooth? It is for me at least (on emulator).Palladous
@Palladous 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 :)Olin
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!Palladous
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.Palladous
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);Hildehildebrand
whats the versionUtilsDarter
I think it is not working on view pager inside nestedscrollview.Correlate
Take care of which adapter you are using. this line View child = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem()).getView(); might crash duo to cast exception.Torch
F
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

Folio answered 5/9, 2015 at 6:38 Comment(14)
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.Peggiepeggir
@NarayanAcharya Can you post your code? preferably as a new question & put the link to the question here.Folio
I added my code in this question #38350309 . Please have a look. Thanks!Peggiepeggir
@NarayanAcharya Hey, I know its a bit late, but use the mCurrentPosition like ... private int mCurrentPosition = -1; ... its important to set it to -1Flagellum
Hi @Flagellum Thanks for the reply. I have initialized the mCurrentPosition to -1. You can check out a separate question here #38350309 . I am yet to find a solution to this :(Peggiepeggir
@AbhishekV why did you created the custom ScrollViewImperfective
It is giving me class cast exception i am using it without scroll viewImperfective
resolved the class cast exception with cleaning and rebuilding the projectImperfective
@AbhishekV this solution is not inflating the view Pager for the first timeImperfective
@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?Joleen
@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?Cnidus
Removing the return method inside if (mCurrentView == null) on the onMeasure, solved my issues of the first fragment not getting shown.Dance
great example, I've combined it with my customViewPager and works beautifully, thanksKeenakeenan
This folds the longest view and does not stretch it after refocusGunnar
O
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);
    }
}
Olin answered 9/9, 2015 at 20:27 Comment(8)
Isn't this horribly laggy / not-smooth? It is for me at least (on emulator).Palladous
@Palladous 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 :)Olin
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!Palladous
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.Palladous
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);Hildehildebrand
whats the versionUtilsDarter
I think it is not working on view pager inside nestedscrollview.Correlate
Take care of which adapter you are using. this line View child = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem()).getView(); might crash duo to cast exception.Torch
Y
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());
            }
        }
    }
}
Yak answered 12/12, 2017 at 14:10 Comment(3)
measureCurrentView isn't used anywhere?Bushy
@Bushy thanks for comment, I added custom adapter exampleYak
It seems like it doesn't work if ConstraintLayout is used in one of your Fragment's layout.Zygote
T
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.

Tyr answered 1/3, 2016 at 12:7 Comment(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.Supersensitive
S
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) {

        }
    });
Sane answered 25/1, 2019 at 8:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.