musooff
musooff

Reputation: 6852

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.

Upvotes: 1

Views: 2627

Answers (1)

Jeffery Ma
Jeffery Ma

Reputation: 3341

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

        }
    }
}

Upvotes: 2

Related Questions