153
votes

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.

9
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.JohnnyLambada
accept anyone useful answer..if else post your own answer..Ranjith Kumar
Why is detecting a scroll event with a ScrollView so difficult in Android? This is nuts imo.worked

9 Answers

406
votes

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

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

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

}
11
votes

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

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

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)
    
1
votes

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

}
1
votes

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