1

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.

musooff
  • 6,412
  • 3
  • 36
  • 65
  • This concept is similar to collapsing toolbar layout @musooff – Brahma Datta May 17 '19 at 06:03
  • @BrahmaDatta yeah I know it is similar to collapsing toolbar, but I need to do it with nestedscrollview – musooff May 17 '19 at 06:29
  • maybe you can override the `onOverScrolled()` this method modifier is protected, according to the `NestedScrollView.java` source code in `android-28`, `onOverScrolled()` will be invoked in `overScrollByCompat()` – Jeffery Ma May 17 '19 at 06:45
  • @Magic I thought about it, but it will be called with `newScrollY` which will always be `0` if it's at the top. This method will not give amount of overScroll. – musooff May 17 '19 at 07:28
  • @musooff i have check the code which you past above [github link](https://github.com/klaszlo8207/StretchTopViewExample). `StretchTopViewExample` override the view's `overScrollBy()` and using the `deltaY` param to adjust the topView's height. You can try this. – Jeffery Ma May 17 '19 at 08:13
  • as I mentioned, I tried to do the same approach. But NestedScrollView will call `overScrollByCombat` instead of `overScrollBy`. `overScrollB` will not be executed – musooff May 17 '19 at 08:15

1 Answers1

2

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

        }
    }
}

Jeffery Ma
  • 3,051
  • 1
  • 23
  • 26
  • indeed this is the best work around. Could you give any reference for `delegate` sub classes? I am never knew there is such way of extending libraries. – musooff May 18 '19 at 13:49
  • @musooff I have past the github demo code link above, you can go and check it. – Jeffery Ma May 18 '19 at 14:09
  • yeah I checked it out. I implemented the way you did. What I am asking is if you could give resources related to `delegate subclass`. I mean how did you get to know you can create such class? – musooff May 18 '19 at 14:14
  • Have you played scrolling. There is a weird behavior while scrolling. Sometimes flingin does not work. Any idea what is causing this? – musooff May 18 '19 at 14:19
  • You mean fling to zoom the top view? – Jeffery Ma May 18 '19 at 14:21
  • Not the zoom effect. "When you scroll fast with a velocity, it should scroll fast according to your velocity. But it kinda "freezes"". So there is drag scrolling and there is fling scrolling. Like when you are at the top of NestedScrollView and you fling up and it scrolls to the end quickly, instead of dragging all the way to the bottom. So that affect freezes sometimes. I thinks it's dragging, instead of flinging – musooff May 19 '19 at 00:09
  • I have got a new way to get this. Using CoordinatorLayout and Behavior this way will not perform like what you said. i have upload the demo code to the github take a look. – Jeffery Ma May 19 '19 at 05:34
  • The new way that you have suggested is much better I suppose. There is a small bug that I am not able to fix with Behavior. when you fling quickly multiple times, the size of `mTargetView` get's increased. – musooff May 20 '19 at 01:06
  • 1
    This is a demo, you could know the idea how to deal with the over scroll effect with CoordinatorLayout behavior you can optimize it as you wish. – Jeffery Ma May 20 '19 at 02:47
  • Yeah, I actually solved that. Thats for your help. Could you give me any direction how to implement an opposite effect. When user scrolling up, reducing the size of `mTargetView.layoutParams.height` until `mMinHeight` and then when the height reaches scroll normally. I followed you approach and when `dyConsumed` is positive. I am doing `mTargetView.layoutparams -= dyConsumed`, which will decrease size of the `mTargetView` height. But the problem is I am not getting the shrinking effect. – musooff May 20 '19 at 02:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/193607/discussion-between-musooff-and-magic). – musooff May 20 '19 at 03:07