163

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.

4
  • 3
    It is possible. See user2695685's answer. In short the following in onStart will do the trick: hsv.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {@Override public void onScrollChanged() {Log.i(TAG,"scroll:"+hsv.getScrollX());}}); in onStart() where hsv is a HorizontalScrollView works. Commented Jun 19, 2014 at 22:02
  • accept anyone useful answer..if else post your own answer.. Commented Jun 1, 2015 at 17:36
  • 16
    Why is detecting a scroll event with a ScrollView so difficult in Android? This is nuts imo. Commented Sep 28, 2015 at 1:26
  • fortunately ten years later this is now very easy Commented Jun 20, 2022 at 12:10

10 Answers 10

423

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 } }); 
Sign up to request clarification or add additional context in comments.

11 Comments

This answer should be marked correct. hsv.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {@Override public void onScrollChanged() {Log.i(TAG,"scroll:"+hsv.getScrollX());}}); in onStart() where hsv is a HorizontalScrollView works. I suspect the same will work for a ScrollView as well.
exactly. this is the best answer. no need to extend HorizontalScrollView. Just tested with a ScrollView, works great: final ScrollView sv = (ScrollView) findViewById(R.id.scrollViewArticle); sv.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() { @Override public void onScrollChanged() { Log.d("onScrollChanged Y", String.valueOf(sv.getScrollY())); } });
You should first check ViewTreeObserver.isAlive()
The sample code in the answer introduces a memory leak. Since the method is add and not set, all listeners will be retained until explicitly removed. So the anonymous class used as a listener implementation in the sample will leak (along with anything it is referencing, i.e. the outer class).
Probably a dumb question, but what is rootScrollView?
|
65

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

6 Comments

Definitely easier than keeping track of whether you've added the listener before with getViewTreeObserver(). Thanks!
But how use NestedScrollView as horizontal scroll view? Cannt find any resource
What if I want to use horizontal scroll?
@dazed'n'confused NestedScrollView is part of the support library, its setOnScrollChangeListener method has no minimum version requirement.
Not to be confused with View's setOnScrollChangeListener which requires API level 23
|
21

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

3 Comments

It seems like this is better than using ViewTreeObserver method. Just because when you have an activity where you have multiple fragments loaded (for example 3 fragments with sliding tabs) with ListViews and ScrollViews and you need to register events for specific view. Whereas if the ViewTreeObserver is registered it will fire events even if it isn't active view.
@ZaBlanc - Can you please tel me how to initialise this class and the listeners from my Activity, which holds the ScrollView? I have implemented it just fine, but the listeners won't return any values yet.
This actually works! and no need for sdk > 23 for it :)
15

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") } 

2 Comments

This should be the accepted answer because the method using a viewTreeObserver causes memory leaks and must be manually removed or else multiple scroll observers will be added.
I absolutely agree. Switching to NestedScrollView and setting its OnScrollChangeListener solved my problem. When I set OnScrollChangedListener on ScrollView's ViewTreeObserver than the listener was called multiple times for no reason.
8

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) 

Comments

6

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 } } } 

Comments

3

2022 answer - it's now trivial:

ScrollView anyScrollView; ... anyScrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() { @Override public void onScrollChange(View view, int x, int y, int oldX, int oldY) { Log.d("yo", "it works " + y + " " + x); } }); 

Don't forget in practice in Android you want to convert everything back and fore to dp:

public void onScrollChange(View view, int x, int y, int oldX, int oldY) { float ydp = y / getBaseContext().getResources().getDisplayMetrics().density; Log.d("yo", "it works " + ydp); } 

So to copy paste:

anyScrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() { float dsy = getBaseContext().getResources().getDisplayMetrics().density; @Override public void onScrollChange(View view, int x, int y, int oldX, int oldY) { float ydp = y / dsy; Log.d("yo", "offset " + ydp); } }); 

Doco:

https://developer.android.com/reference/android/view/View.OnScrollChangeListener

Comments

1

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

Comments

0
 // --------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--------- 

2 Comments

please try to add some description to support you code which will make everyone understand batter
This code is very similar to another answer, isn't it?
0

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 */ }) 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.