Search code examples
androidandroid-scrollviewandroid-nestedscrollviewoverscroll

How to get overScroll Height in NestedScrollView?


I want to get have over scroll listener in NestedScrollView in order to make my top ImageView get Zoomed when user over scrolls. Something like this

Above library uses ScrollView which in my case I need NestedScrollView. So i wanted to follow the same approach by the developer, but having some trouble solving few issues.

In View there is a protected method overScrollBy that is used in ScrollView which developer overrides in his CustomScrollView. Unfortunately, instead of overScrollBy NestedScrollView uses it's own overScrollByCombat which is private and I cannot override it. So, I am kind of stuck at how to get "overScrollListener" in my CustomNestedScrollView.

The only solution I could think of was actually making my PreCustomNestedScrollView in which I just copy paste the source code of NestedScrollView and setting the overScrollByCombat as public. It works but I don't thinks it's an elegant way.

If there are already any such libraries that gives the same effect with NestedScrollView, you are welcome to recommend.


Solution

  • Here are two ways to get this.

    Here is a demo Link, and Gif

    1. Implementing with a CoordiantorLayout Behavior
    
    import android.content.Context;
    import android.support.annotation.NonNull;
    import android.support.design.widget.CoordinatorLayout;
    import android.support.v4.view.ViewCompat;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.View;
    import android.view.animation.Animation;
    import android.view.animation.Transformation;
    
    
    public class OverScrollBounceBehavior extends CoordinatorLayout.Behavior<View> {
    
        private static final String TAG = "Behavior";
    
        private int mNormalHeight = 0;
        private int mMaxHeight = 0;
        private float mFactor = 1.8f;
        private int mOverScrollY;
        private View mTargetView;
        private OnScrollChangeListener mListener;
    
        public OverScrollBounceBehavior() {
        }
    
        public OverScrollBounceBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                           @NonNull View child,
                                           @NonNull View directTargetChild,
                                           @NonNull View target,
                                           int nestedScrollAxes, int type) {
            findTargetView();
            Log.d(TAG, "onStartNestedScroll " + "type = " + type);
            //TYPE_TOUCH handle over scroll
            if (checkTouchType(type) && checkTargetView()) {
                mOverScrollY = 0;
                mNormalHeight = mTargetView.getHeight();
                mMaxHeight = (int) (mNormalHeight * mFactor);
            }
            return true;
        }
    
        public void setFactor(float factor) {
            this.mFactor = factor;
        }
    
        public void setOnScrollChangeListener(OnScrollChangeListener listener) {
            this.mListener = listener;
        }
    
        public void setTargetView(View targetView) {
            //set a target view from outside, target view should be NestedScrollView child
            this.mTargetView = targetView;
        }
    
        private void findTargetView() {
            //implement a fixed find target view as you wish
        }
    
        @Override
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                   @NonNull View child,
                                   @NonNull View target,
                                   int dxConsumed, int dyConsumed,
                                   int dxUnconsumed, int dyUnconsumed,
                                   int type) {
            //unconsumed == 0 no overScroll
            //unconsumed > 0 overScroll up
            if (dyUnconsumed >= 0) {
                return;
            }
            Log.d(TAG, "onNestedScroll : dyUnconsumed = " + dyUnconsumed);
            mOverScrollY -= dyUnconsumed;
            Log.d(TAG, "onNestedScroll : mOverScrollY = " + mOverScrollY + "type = " + type);
            //TYPE_TOUCH handle over scroll
            if (checkTouchType(type) && checkTargetView()) {
                if (mOverScrollY > 0 && mTargetView.getLayoutParams().height + Math.abs(mOverScrollY) <= mMaxHeight) {
                    mTargetView.getLayoutParams().height += Math.abs(mOverScrollY);
                    mTargetView.requestLayout();
                    if (mListener != null) {
                        mListener.onScrollChanged(calculateRate(mTargetView, mMaxHeight, mNormalHeight));
                    }
                }
            }
        }
    
        @Override
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull View child,
                                       @NonNull View target,
                                       int type) {
            Log.d(TAG, "onStopNestedScroll" + "type = " + type);
            //TYPE_TOUCH handle over scroll
            if (checkTouchType(type)
                    && checkTargetView()
                    && mTargetView.getHeight() > mNormalHeight) {
                ResetAnimation animation = new ResetAnimation(mTargetView, mNormalHeight, mListener);
                animation.setDuration(300);
                mTargetView.startAnimation(animation);
            }
        }
    
        private boolean checkTouchType(int type) {
            return type == ViewCompat.TYPE_TOUCH;
        }
    
        private boolean checkTargetView() {
            return mTargetView != null;
        }
    
        public static class ResetAnimation extends Animation {
            int targetHeight;
            int originalHeight;
            int extraHeight;
            View view;
            OnScrollChangeListener listener;
    
            ResetAnimation(View view, int targetHeight, OnScrollChangeListener listener) {
                this.view = view;
                this.targetHeight = targetHeight;
                this.originalHeight = view.getHeight();
                this.extraHeight = this.targetHeight - originalHeight;
                this.listener = listener;
            }
    
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                int newHeight = (int) (targetHeight - extraHeight * (1 - interpolatedTime));
                view.getLayoutParams().height = newHeight;
                view.requestLayout();
                if (listener != null) {
                    listener.onScrollChanged(calculateRate(view, originalHeight, targetHeight));
                }
            }
        }
    
        public interface OnScrollChangeListener {
            void onScrollChanged(float rate);
        }
    
        private static float calculateRate(View targetView, int maxHeight, int targetHeight) {
            float rate = 0;
            if (targetView != null) {
                rate = (maxHeight - (float) targetView.getLayoutParams().height) / (maxHeight - targetHeight);
            }
            return rate;
        }
    }
    
    
    1. Implementing with a subclass of NestedScrollView

    (1). Create a delegate subclass in package android.support.v4.widget

     and override `overScrollByCompat()` to invoke customized `openedOverScrollByCompat()` method.
    

    (2). Create your owner StretchTopNestedScrollView override

    openedOverScrollByCompat() then you can do what you want.

    Delegate view

    package android.support.v4.widget;
    
    import android.content.Context;
    import android.support.annotation.NonNull;
    import android.support.annotation.Nullable;
    import android.util.AttributeSet;
    
    public class OpenedNestedScrollView extends NestedScrollView {
    
        public OpenedNestedScrollView(@NonNull Context context) {
            this(context, null);
        }
    
        public OpenedNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, -1);
        }
    
        public OpenedNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        boolean overScrollByCompat(int deltaX, int deltaY,
                                   int scrollX, int scrollY,
                                   int scrollRangeX, int scrollRangeY,
                                   int maxOverScrollX, int maxOverScrollY,
                                   boolean isTouchEvent) {
            return openedOverScrollByCompat(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
        }
    
        protected boolean openedOverScrollByCompat(int deltaX, int deltaY,
                                                   int scrollX, int scrollY,
                                                   int scrollRangeX, int scrollRangeY,
                                                   int maxOverScrollX, int maxOverScrollY,
                                                   boolean isTouchEvent) {
            return super.overScrollByCompat(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
        }
    }
    

    Your owner view

    ublic class StretchTopNestedScrollView extends OpenedNestedScrollView {
    
        private View mTopView, mBottomView;
        private int mNormalHeight, mMaxHeight;
        private onOverScrollChanged mChangeListener;
        private float mFactor = 1.6f;
    
        private interface OnTouchEventListener {
            void onTouchEvent(MotionEvent ev);
        }
    
        public StretchTopNestedScrollView(Context context) {
            this(context, null);
        }
    
        public StretchTopNestedScrollView(Context context, AttributeSet attrs) {
            this(context, attrs, -1);
        }
    
        public StretchTopNestedScrollView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        public void setFactor(float f) {
            mFactor = f;
    
            mTopView.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mNormalHeight = mTopView.getHeight();
                    mMaxHeight = (int) (mNormalHeight * mFactor);
                }
            }, 50);
        }
    
        public View getTopView() {
            return mTopView;
        }
    
        public View getBottomView() {
            return mBottomView;
        }
    
        @Override
        public void onFinishInflate() {
            super.onFinishInflate();
    
            if (getChildCount() > 1)
                throw new IllegalArgumentException("Root layout must be a LinearLayout, and only one child on this view!");
    
            if (getChildCount() == 0 || !(getChildAt(0) instanceof LinearLayout))
                throw new IllegalArgumentException("Root layout is not a LinearLayout!");
    
            if (getChildCount() == 1 && (getChildAt(0) instanceof LinearLayout)) {
                LinearLayout parent = (LinearLayout) getChildAt(0);
    
                if (parent.getChildCount() != 2) {
                    throw new IllegalArgumentException("Root LinearLayout's has not EXACTLY two Views!");
                } else {
                    mTopView = parent.getChildAt(0);
                    mBottomView = parent.getChildAt(1);
    
                    mTopView.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mNormalHeight = mTopView.getHeight();
                            mMaxHeight = (int) (mNormalHeight * mFactor);
                        }
                    }, 50);
                }
            }
    
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
        }
    
        @Override
        protected boolean openedOverScrollByCompat(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
    
            if (scrollY == 0) {
                //down, zoom in
                if (deltaY < 0 && mTopView.getLayoutParams().height + Math.abs(deltaY) > mMaxHeight) {
                    mTopView.getLayoutParams().height = mMaxHeight;
                } else if (deltaY < 0 && mTopView.getLayoutParams().height + Math.abs(deltaY) <= mMaxHeight) {
                    mTopView.getLayoutParams().height += Math.abs(deltaY);
                }
                //up, zoom out
                else if (deltaY > 0 && mTopView.getLayoutParams().height - Math.abs(deltaY) < mNormalHeight) {
                    mTopView.getLayoutParams().height = mNormalHeight;
                } else if (deltaY > 0 && mTopView.getLayoutParams().height - Math.abs(deltaY) >= mNormalHeight) {
                    mTopView.getLayoutParams().height -= Math.abs(deltaY);
                }
            }
    
            if (mChangeListener != null) mChangeListener.onChanged(
                    (mMaxHeight - (float) mTopView.getLayoutParams().height) / (mMaxHeight - mNormalHeight)
            );
    
            if (deltaY != 0 && scrollY == 0) {
                mTopView.requestLayout();
                mBottomView.requestLayout();
            }
    
            if (mTopView.getLayoutParams().height == mNormalHeight) {
                super.overScrollBy(deltaX, deltaY, scrollX,
                        scrollY, scrollRangeX, scrollRangeY,
                        maxOverScrollX, maxOverScrollY, isTouchEvent);
            }
    
            return true;
    
        }
    
        @Override
        protected void onScrollChanged(int l, int t, int oldl, int oldt) {
            super.onScrollChanged(l, t, oldl, oldt);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            touchListener.onTouchEvent(ev);
            return super.onTouchEvent(ev);
        }
    
        public interface onOverScrollChanged {
            void onChanged(float v);
        }
    
        public void setChangeListener(onOverScrollChanged changeListener) {
            mChangeListener = changeListener;
        }
    
        private OnTouchEventListener touchListener = new OnTouchEventListener() {
            @Override
            public void onTouchEvent(MotionEvent ev) {
                if (ev.getAction() == MotionEvent.ACTION_UP) {
                    if (mTopView != null && mTopView.getHeight() > mNormalHeight) {
                        ResetAnimation animation = new ResetAnimation(mTopView, mNormalHeight);
                        animation.setDuration(400);
                        mTopView.startAnimation(animation);
                    }
                }
            }
        };
    
        public class ResetAnimation extends Animation {
            int targetHeight;
            int originalHeight;
            int extraHeight;
            View mView;
    
            ResetAnimation(View view, int targetHeight) {
                this.mView = view;
                this.targetHeight = targetHeight;
                originalHeight = view.getHeight();
                extraHeight = this.targetHeight - originalHeight;
            }
    
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                int newHeight = (int) (targetHeight - extraHeight * (1 - interpolatedTime));
                mView.getLayoutParams().height = newHeight;
                mView.requestLayout();
    
                if (mChangeListener != null) mChangeListener.onChanged(
                        (mMaxHeight - (float) mTopView.getLayoutParams().height) / (mMaxHeight - mNormalHeight)
                );
    
            }
        }
    }