ChatGPT解决这个技术问题 Extra ChatGPT

Can I have onScrollListener for a ScrollView?

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.

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.
accept anyone useful answer..if else post your own answer..
Why is detecting a scroll event with a ScrollView so difficult in Android? This is nuts imo.
fortunately ten years later this is now very easy

P
Pavneet_Singh

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

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?
L
Lavekush Agrawal

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

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
G
Gary Chen

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

}

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 :)
C
Cristan

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

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.
A
Akn

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

You can implement OnScrollChangedListener in your activity/fragment. MyFragment : ViewTreeObserver.OnScrollChangedListener Add it to scrollView when your view is ready. scrollView.viewTreeObserver.addOnScrollChangedListener(this) Remove listener when no longer need (ie. onPause()) scrollView.viewTreeObserver.removeOnScrollChangedListener(this)


4
476rick

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

N
Nikhil

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

}

L
LucidSoftworksLLC

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

F
Fattie

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


W
Wirote W.
    // --------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---------

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?