Custom NestedScrollview

Some of the developers might have come across a scenario when there is a need to send a callback when the scrolling stops in a Nested Scrollview. This is where the below Custom NestedScrollview comes to rescue.

Since we are aware of the fact, the nested Scroll view does not provide any state or callback when the scroll stops. Although it provides scrollviewonScrollChange(), this method will result in multiple callbacks to our parent container, think of a situation where we need to perform an Action only when the scrolling comes to halt. In order to overcome this issue we came across two more:

  1. How to handle use case, when user Scrolls and lifts his finger from the screen and scrolling comes to a halt.
  2. How to handle use case, when user scrolls but do not lift his finger from the screen when scrolling is halted(Dragging effect)

Scenario 1 can be handled by implementing a checkCoordinatesAndPerformAction() in scrollviewonScrollChange() of NestedScrollView

   private void checkCoordinatesAndPerformAction() {
        if (Math.abs(mOldY - mNewY) > 0) {
            if (mScrollingRunnable != null) {
                removeCallbacks(mScrollingRunnable);
            }
            mScrollingRunnable = new Runnable() {
                public void run() {
                    if (mIsTouching) {
                        mPerformActionLater = true;
                    }
                    if (mIsScrolling && !mIsTouching && mNestedScrollListener != null) {
                        mNestedScrollListener.onNestedScrollStopped();
                    }
                    mIsScrolling = false;
                    mScrollingRunnable = null;
                }
            };
            postDelayed(mScrollingRunnable, DELAY);
        }
    }

Scenario 2 can be handled by implementing performActionAfterTouch() in the OnTouchEvent() of NestedScrollView

private void performActionAfterTouch() {
        if (mScrollingRunnable == null && Math.abs(mOldY - mNewY) > 0) {
            mScrollingRunnable = new Runnable() {
                public void run() {
                    if (mPerformActionLater && mNestedScrollListener != null) {
                        mNestedScrollListener.onNestedScrollStopped();
                    }
                    mScrollingRunnable = null;
                }
            };
            post(mScrollingRunnable);
        }
    }

Now, Let’s take a look at the whole class :

public class CustomizedNestedScrollView extends NestedScrollView {

    private static final int DELAY = 200;
    private NestedScrollListener mNestedScrollListener;
    private boolean mIsScrolling;
    private boolean mIsTouching;
    private Runnable mScrollingRunnable;
    private int mOldY;
    private int mNewY;
    private boolean mPerformActionLater;

    public CustomizedNestedScrollView(Context context) {
        super(context);
    }

    public CustomizedNestedScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomizedNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if (action == MotionEvent.ACTION_MOVE) {
            mIsScrolling = true;
            mIsTouching = true;
        }
        if (action == MotionEvent.ACTION_UP) {
            mIsTouching = false;
            performActionAfterTouch();
        }
        performClick();
        return super.onTouchEvent(ev);
    }

    @Override
    public boolean performClick() {
        super.performClick();
        return true;
    }

    /**
     * Set Listener to send callback to the parent container
     *
     * @param nestedScrollListener
     */
    public void setNestedScrollListener(NestedScrollListener nestedScrollListener) {
        this.mNestedScrollListener = nestedScrollListener;
    }

    @Override
    protected void onScrollChanged(int newX, int newY, int oldX, int oldY) {
        super.onScrollChanged(newX, newY, oldX, oldY);
        mOldY = oldY;
        mNewY = newY;
        checkCoordinatesAndPerformAction();
    }

    /**
     * Determines & fire event if user has lifted the finger and scroll has stopped
     */
    private void checkCoordinatesAndPerformAction() {
        if (Math.abs(mOldY - mNewY) > 0) {
            if (mScrollingRunnable != null) {
                removeCallbacks(mScrollingRunnable);
            }
            mScrollingRunnable = new Runnable() {
                public void run() {
                    if (mIsTouching) {
                        mPerformActionLater = true;
                    }
                    if (mIsScrolling && !mIsTouching && mNestedScrollListener != null) {
                        mNestedScrollListener.onNestedScrollStopped();
                    }
                    mIsScrolling = false;
                    mScrollingRunnable = null;
                }
            };
            postDelayed(mScrollingRunnable, DELAY);
        }
    }

    /**
     * Fire Event when Scroll has stopped but user did not lifted finger from screen.
     */
    private void performActionAfterTouch() {
        if (mScrollingRunnable == null && Math.abs(mOldY - mNewY) > 0) {
            mScrollingRunnable = new Runnable() {
                public void run() {
                    if (mPerformActionLater && mNestedScrollListener != null) {
                        mNestedScrollListener.onNestedScrollStopped();
                    }
                    mScrollingRunnable = null;
                }
            };
            post(mScrollingRunnable);
        }
    }

    /**
     * Listener to send the event when the scrolling stops
     */
    public interface NestedScrollListener {
        /**
         * Callback when scroll is stopped.
         */
        void onNestedScrollStopped();
    }
}

We have created a NestedScrollListener interface which gives a callback when the scrolling is stopped. At this moment, I want to iterate that this Custom NestedScrollview is one among many other approaches to detect when the scrolling is stopped inside the nested scroll view. It might not be the most optimized but definitely a promising one!

A simple example demonstrating the above approach can be found here