Can I have onScrollListener for a ScrollView?

AndroidListenerScrollview

Android Problem Overview


I am using a HorizontalScrollView in a layout and I need to identify the user have reached the start and end point of the scroll.

For ListView I have tried a the onScrollListener and it is possible to find the start and end point of scroll.

I tried to do the same in my Scrollview but it seems not possible. Is there any other possible ways to achieve what I need.

Android Solutions


Solution 1 - Android

Every instance of View calls getViewTreeObserver(). Now when holding an instance of ViewTreeObserver, you can add an OnScrollChangedListener() to it using the method addOnScrollChangedListener().

You can see more information about this class here.

It lets you be aware of every scrolling event - but without the coordinates. You can get them by using getScrollY() or getScrollX() from within the listener though.

scrollView.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() {
	@Override
	public void onScrollChanged() {
		int scrollY = rootScrollView.getScrollY(); // For ScrollView
		int scrollX = rootScrollView.getScrollX(); // For HorizontalScrollView
		// DO SOMETHING WITH THE SCROLL COORDINATES
	}
});

Solution 2 - Android

This might be very useful. Use NestedScrollView instead of ScrollView. Support Library 23.1 introduced an OnScrollChangeListener to NestedScrollView. So you can do something like this.

 myScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
        @Override
        public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
            Log.d("ScrollView","scrollX_"+scrollX+"_scrollY_"+scrollY+"_oldScrollX_"+oldScrollX+"_oldScrollY_"+oldScrollY);
            //Do something
        }
    });

Solution 3 - Android

Here's a derived HorizontalScrollView I wrote to handle notifications about scrolling and scroll ending. It properly handles when a user has stopped actively scrolling and when it fully decelerates after a user lets go:

public class ObservableHorizontalScrollView extends HorizontalScrollView {
    public interface OnScrollListener {
        public void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldX, int oldY);
        public void onEndScroll(ObservableHorizontalScrollView scrollView);
    }

    private boolean mIsScrolling;
    private boolean mIsTouching;
    private Runnable mScrollingRunnable;
    private OnScrollListener mOnScrollListener;

    public ObservableHorizontalScrollView(Context context) {
        this(context, null, 0);
    }

    public ObservableHorizontalScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ObservableHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();

        if (action == MotionEvent.ACTION_MOVE) {
            mIsTouching = true;
            mIsScrolling = true;
        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (mIsTouching && !mIsScrolling) {
                if (mOnScrollListener != null) {
                    mOnScrollListener.onEndScroll(this);
                }
            }

            mIsTouching = false;
        }

        return super.onTouchEvent(ev);
    }

    @Override
    protected void onScrollChanged(int x, int y, int oldX, int oldY) {
        super.onScrollChanged(x, y, oldX, oldY);

        if (Math.abs(oldX - x) > 0) {
            if (mScrollingRunnable != null) {
                removeCallbacks(mScrollingRunnable);
            }

            mScrollingRunnable = new Runnable() {
                public void run() {
                    if (mIsScrolling && !mIsTouching) {
                        if (mOnScrollListener != null) {
                            mOnScrollListener.onEndScroll(ObservableHorizontalScrollView.this);
                        }
                    }

                    mIsScrolling = false;
                    mScrollingRunnable = null;
                }
            };

            postDelayed(mScrollingRunnable, 200);
        }

        if (mOnScrollListener != null) {
            mOnScrollListener.onScrollChanged(this, x, y, oldX, oldY);
        }
    }

    public OnScrollListener getOnScrollListener() {
        return mOnScrollListener;
    }

    public void setOnScrollListener(OnScrollListener mOnEndScrollListener) {
        this.mOnScrollListener = mOnEndScrollListener;
    }

}

Solution 4 - Android

You can use NestedScrollView instead of ScrollView. However, when using a Kotlin Lambda, it won't know you want NestedScrollView's setOnScrollChangeListener instead of the one at View (which is API level 23). You can fix this by specifying the first parameter as a NestedScrollView.

nestedScrollView.setOnScrollChangeListener { _: NestedScrollView, scrollX: Int, scrollY: Int, _: Int, _: Int ->
    Log.d("ScrollView", "Scrolled to $scrollX, $scrollY")
}

Solution 5 - Android

Beside accepted answer, you need to hold a reference of listener and remove when you don't need it. Otherwise you will get a null pointer exception for your ScrollView and memory leak (mentioned in comments of accepted answer).

  1. You can implement OnScrollChangedListener in your activity/fragment.

    MyFragment : ViewTreeObserver.OnScrollChangedListener
    
  2. Add it to scrollView when your view is ready.

    scrollView.viewTreeObserver.addOnScrollChangedListener(this)
    
  3. Remove listener when no longer need (ie. onPause())

    scrollView.viewTreeObserver.removeOnScrollChangedListener(this)
    

Solution 6 - Android

If you want to know the scroll position of a view, then you can use the following extension function on View class:

fun View?.onScroll(callback: (x: Int, y: Int) -> Unit) {
    var oldX = 0
    var oldY = 0
    this?.viewTreeObserver?.addOnScrollChangedListener {
        if (oldX != scrollX || oldY != scrollY) {
            callback(scrollX, scrollY)
            oldX = scrollX
            oldY = scrollY
        }
    }
}

Solution 7 - Android

you can define a custom ScrollView class, & add an interface be called when scrolling like this:

public class ScrollChangeListenerScrollView extends HorizontalScrollView {


private MyScrollListener mMyScrollListener;

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

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

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


public void setOnMyScrollListener(MyScrollListener myScrollListener){
    this.mMyScrollListener = myScrollListener;
}


@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    super.onScrollChanged(l, t, oldl, oldt);
    if(mMyScrollListener!=null){
        mMyScrollListener.onScrollChange(this,l,t,oldl,oldt);
    }

}

public interface MyScrollListener {
    void onScrollChange(View view,int scrollX,int scrollY,int oldScrollX, int oldScrollY);
}

}

Solution 8 - Android

Kotlin users looking for a solution for a normal ScrollView implementation:

As an extension to this answer, I created a custom view that solved my problems very well.

The view (create a new Kotlin file, maintain your package reference on line 1):

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.ScrollView
import kotlin.math.abs


class ScrollViewWithEndFunc (
    context: Context?,
    attrs: AttributeSet?,
    defStyle: Int
) : ScrollView(context, attrs, defStyle) {

    constructor(context: Context?) : this(context, null, 0) {}
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) {}

    interface OnScrollListener {
        fun onScrollChanged(scrollView: ScrollViewWithEndFunc?, x: Int, y: Int, oldX: Int, oldY: Int)
        fun onEndScroll(scrollView: ScrollViewWithEndFunc?)
    }

    private var isScrolling = false
    private var isTouching = false
    private var scrollingRunnable: Runnable? = null
    private var onScrollListener: OnScrollListener? = null

    fun setOnScrollListener(onScrollListener: OnScrollListener) {
        this.onScrollListener = onScrollListener
    }

    fun removeOnScrollListener() {
        this.onScrollListener = null
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent): Boolean {
        val action = ev.action
        if (action == MotionEvent.ACTION_MOVE) {
            isTouching = true; isScrolling = true
        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (isTouching && !isScrolling) {
                onScrollListener?.onEndScroll(this)
            }
            isTouching = false
        }
        return super.onTouchEvent(ev)
    }

    override fun onScrollChanged(x: Int, y: Int, oldX: Int, oldY: Int) {
        super.onScrollChanged(x, y, oldX, oldY)
        if (abs(oldY - y) > 0) {
            scrollingRunnable?.let { removeCallbacks(it) }
            scrollingRunnable = Runnable {
                if (isScrolling && !isTouching) {
                    onScrollListener?.onEndScroll(this@ScrollViewWithEndFunc)
                }
                isScrolling = false
                scrollingRunnable = null
            }
            postDelayed(scrollingRunnable, 200)
        }
        onScrollListener?.onScrollChanged(this, x, y, oldX, oldY)
     }
    }

XML view implementation:

<your.package.here.ScrollViewWithEndFunc
        android:id="@+id/scrollview_main_dashboard"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true">

Activity/Fragment implementation:

scrollviewMainDashboard.setOnScrollListener(object : ScrollViewWithEndFunc.OnScrollListener {
            override fun onScrollChanged(scrollView: ScrollViewWithEndFunc?, x: Int, y: Int, oldX: Int, oldY: Int) { }
            override fun onEndScroll(scrollView: ScrollViewWithEndFunc?) {
                /* Scroll ended, handle here */
        })

Solution 9 - Android

    // --------Start Scroll Bar Slide--------
    final HorizontalScrollView xHorizontalScrollViewHeader = (HorizontalScrollView) findViewById(R.id.HorizontalScrollViewHeader);
    final HorizontalScrollView xHorizontalScrollViewData = (HorizontalScrollView) findViewById(R.id.HorizontalScrollViewData);
    xHorizontalScrollViewData.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            int scrollX; int scrollY;
            scrollX=xHorizontalScrollViewData.getScrollX();
            scrollY=xHorizontalScrollViewData.getScrollY();
            xHorizontalScrollViewHeader.scrollTo(scrollX, scrollY);
        }
    });
    // ---------End Scroll Bar Slide---------

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionarnpView Question on Stackoverflow
Solution 1 - AndroidZbunView Answer on Stackoverflow
Solution 2 - AndroidMidhun VijayakumarView Answer on Stackoverflow
Solution 3 - AndroidZaBlancView Answer on Stackoverflow
Solution 4 - AndroidCristanView Answer on Stackoverflow
Solution 5 - AndroidAknView Answer on Stackoverflow
Solution 6 - AndroidmcspayneView Answer on Stackoverflow
Solution 7 - AndroidChuanxing ZhaoView Answer on Stackoverflow
Solution 8 - AndroidLucidSoftworksLLCView Answer on Stackoverflow
Solution 9 - AndroidWirote W.View Answer on Stackoverflow