Android:在ScrollView停止滚动时检测

时间:2011-11-18 11:39:42

标签: android scrollview

我在Android中使用ScrollView,其中ScrollView的可见部分与Scrollview中的一个单元格的大小相同。每个“细胞”都是相同的高度。因此,我想要做的是在滚动ScrollView之后抓住位置。

目前,我正在检测用户何时触及ScrollView以及何时开始滚动并从那里开始工作,但这是非常错误的。当用户只是轻弹它并滚动然后减速时,它也需要工作。

在iPhone上有一个类似didDecelerate的功能,当ScrollView完成滚动时,我可以执行任何我想要的代码。 Android有这样的东西吗?或者是否有一些我可以看到的代码来找出更好的方法呢?

我查看过Android文档但找不到类似内容。

14 个答案:

答案 0 :(得分:92)

我最近不得不实现你描述的功能。我做的是让一个Runnable检查出ScrollView是否已经停止滚动,当首次触发onTouchEvent时,getScrollY()返回的值与变量 newCheck 定义的时间后返回的值相比较。

见下面的代码(工作解决方案):

public class MyScrollView extends ScrollView{

private Runnable scrollerTask;
private int initialPosition;

private int newCheck = 100;
private static final String TAG = "MyScrollView";

public interface OnScrollStoppedListener{
    void onScrollStopped();
}

private OnScrollStoppedListener onScrollStoppedListener;

public MyScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);

    scrollerTask = new Runnable() {

        public void run() {

            int newPosition = getScrollY();
            if(initialPosition - newPosition == 0){//has stopped

                if(onScrollStoppedListener!=null){

                    onScrollStoppedListener.onScrollStopped();
                }
            }else{
                initialPosition = getScrollY();
                MyScrollView.this.postDelayed(scrollerTask, newCheck);
            }
        }
    };
}

public void setOnScrollStoppedListener(MyScrollView.OnScrollStoppedListener listener){
    onScrollStoppedListener = listener;
}

public void startScrollerTask(){

    initialPosition = getScrollY();
    MyScrollView.this.postDelayed(scrollerTask, newCheck);
}

}

然后我有:

scroll.setOnTouchListener(new OnTouchListener() {

        public boolean onTouch(View v, MotionEvent event) {

            if (event.getAction() == MotionEvent.ACTION_UP) {

                scroll.startScrollerTask();
            }

            return false;
        }
});
scroll.setOnScrollStoppedListener(new OnScrollStoppedListener() {

        public void onScrollStopped() {

            Log.i(TAG, "stopped");

        }
});

BTW我在我的应用程序中使用了其他回复中的一些想法来做到这一点。 希望这可以帮助。有任何问题请随时询问我(们)。 欢呼声。

答案 1 :(得分:22)

这是对IMHO的另一个修复,在ScrollView中缺少OnEndScroll事件错误。

hambonious回答的启发。 只需将此类放入您的项目中(更改包以匹配您自己的项目)并使用以下xml

package com.thecrag.components.ui;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ScrollView;

public class ResponsiveScrollView extends ScrollView {

    public interface OnEndScrollListener {
        public void onEndScroll();
    }

    private boolean mIsFling;
    private OnEndScrollListener mOnEndScrollListener;

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

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

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

    @Override
    public void fling(int velocityY) {
        super.fling(velocityY);
        mIsFling = true;
    }

    @Override
    protected void onScrollChanged(int x, int y, int oldX, int oldY) {
        super.onScrollChanged(x, y, oldX, oldY);
        if (mIsFling) {
            if (Math.abs(y - oldY) < 2 || y >= getMeasuredHeight() || y == 0) {
                if (mOnEndScrollListener != null) {
                    mOnEndScrollListener.onEndScroll();
                }
                mIsFling = false;
            }
        }
    }

    public OnEndScrollListener getOnEndScrollListener() {
        return mOnEndScrollListener;
    }

    public void setOnEndScrollListener(OnEndScrollListener mOnEndScrollListener) {
        this.mOnEndScrollListener = mOnEndScrollListener;
    }

}

再次更改包名称以匹配您的项目

<com.thecrag.components.ui.ResponsiveScrollView
        android:id="@+id/welcome_scroller"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/welcome_scroll_command_help_container"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/welcome_header_text_thecrag"
        android:layout_margin="6dp">
    ....
</com.thecrag.components.ui.ResponsiveScrollView>

答案 2 :(得分:9)

我将(水平)ScrollView子类化并做了类似的事情:

@Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
    if (Math.abs(x - oldX) > SlowDownThreshold) {  
        currentlyScrolling = true;
    } else {
        currentlyScrolling = false;
        if (!currentlyTouching) {
            //scrolling stopped...handle here
        }
    }
    super.onScrollChanged(x, y, oldX, oldY);
}

我对SlowDownThreshold使用了值1,因为它似乎总是与最后一次onScrollChanged事件的区别。

为了在慢慢拖动时使其表现正常,我必须这样做:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            currentlyTouching = true;
    }
    return super.onInterceptTouchEvent(event);
}

@Override
public boolean onTouch(View view, MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            currentlyTouching = false;
            if (!currentlyScrolling) {
                //I handle the release from a drag here
                return true;
            }
    }
    return false;
}

答案 3 :(得分:7)

我的方法是每次调用onScrollChanged()时更改时间戳来确定滚动状态。 可以很容易地确定滚动的开始和结束时间。 您还可以更改阈值(我使用100毫秒)来修复灵敏度。

public class CustomScrollView extends ScrollView {
    private long lastScrollUpdate = -1;

    private class ScrollStateHandler implements Runnable {

        @Override
        public void run() {
            long currentTime = System.currentTimeMillis();
            if ((currentTime - lastScrollUpdate) > 100) {
                lastScrollUpdate = -1;
                onScrollEnd();
            } else {
                postDelayed(this, 100);
            }
        }
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (lastScrollUpdate == -1) {
            onScrollStart();
            postDelayed(new ScrollStateHandler(), 100);
        }

        lastScrollUpdate = System.currentTimeMillis();
    }

    private void onScrollStart() {
        // do something
    }

    private void onScrollEnd() {
        // do something
    }

}

答案 4 :(得分:5)

这是另一种解决方案,在我看来非常简单和干净,自然受到上述答案的启发。基本上,一旦用户结束手势检查,getScrollY()是否仍在改变,经过短暂的延迟(此处为50ms)。

public class ScrollViewWithOnStopListener extends ScrollView {

    OnScrollStopListener listener;

    public interface OnScrollStopListener {
        void onScrollStopped(int y);
    }

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

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_UP:
                checkIfScrollStopped();
        }

        return super.onTouchEvent(ev);
    }

    int initialY = 0;

    private void checkIfScrollStopped() {
        initialY = getScrollY();
        this.postDelayed(new Runnable() {
            @Override
            public void run() {
                int updatedY = getScrollY();
                if (updatedY == initialY) {
                    //we've stopped
                    if (listener != null) {
                        listener.onScrollStopped(getScrollY());
                    }
                } else {
                    initialY = updatedY;
                    checkIfScrollStopped();
                }
            }
        }, 50);
    }

    public void setOnScrollStoppedListener(OnScrollStopListener yListener) {
        listener = yListener;
    }
}

答案 5 :(得分:4)

我对此问题的处理方法是使用计时器检查以下2个“事件”。

1)onScrollChanged()停止被调用

2)用户的手指从滚动视图中抬起

public class CustomScrollView extends HorizontalScrollView {

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

Timer ntimer = new Timer();
MotionEvent event;

@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) 
{
    checkAgain();
    super.onScrollChanged(l, t, oldl, oldt);
}

public void checkAgain(){
    try{
        ntimer.cancel();
        ntimer.purge();
    }
    catch(Exception e){}
    ntimer = new Timer();
    ntimer.schedule(new TimerTask() {

        @Override
        public void run() {

            if(event.getAction() == MotionEvent.ACTION_UP){
                // ScrollView Stopped Scrolling and Finger is not on the ScrollView
            }
            else{
                // ScrollView Stopped Scrolling But Finger is still on the ScrollView
                checkAgain();
            }
        }
    },100);

}

@Override
public boolean onTouchEvent(MotionEvent event) {
    this.event = event; 
    return super.onTouchEvent(event);
    }
}

答案 6 :(得分:2)

尝试在StackOverflow上查看this question - 它与您的问题不完全相同,但它可以让您了解如何管理ScrollView的滚动事件。

基本上,您需要通过展开CustomScrollView并覆盖ScrollView来创建自己的onScrollChanged(int x, int y, int oldx, int oldy)。然后,您需要在布局文件中引用它,而不是ScrollView之类的标准com.mypackage.CustomScrollView

答案 7 :(得分:2)

我认为这已经出现了。 AFAIK,你不能轻松地检测到它。我的建议是你看一下ScrollView.java(这就是我们在Android的工作方式:),并弄清楚如何扩展类以提供你正在寻找的功能。这是我首先要尝试的:

 @Override
 protected void onScrollChanged(int l, int t, int oldl, int oldt) {
     if (mScroller.isFinished()) {
         // do something, for example call a listener
     }
 }

答案 8 :(得分:2)

对于您所描述的简单案例,您可以在自定义滚动视图中使用重写fling方法。每次用户从屏幕上抬起手指时,都会调用Fling方法执行“减速”。

所以你应该做的是这样的事情:

  1. 子类ScrollView。

    public class MyScrollView extends ScrollView { 
    
        private Scroller scroller;  
        private Runnable scrollerTask; 
    
        //...
    
        public MyScrollView(Context context, AttributeSet attrs) {
            super(context, attrs);
    
            scroller = new Scroller(getContext()); //or OverScroller for 3.0+
            scrollerTask = new Runnable() {
                @Override
                public void run() {
                    scroller.computeScrollOffset();
                    scrollTo(0, scroller.getCurrY());
    
                    if (!scroller.isFinished()) {
                        MyScrollView.this.post(this);
                    } else {
                        //deceleration ends here, do your code
                    }
                }
            };
            //...
        }
    }
    
  2. 子类fling方法,不要调用超类实现。

    @Override  
    public void fling(int velocityY) {  
        scroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, container.getHeight());
        post(scrollerTask);  
    
        //add any extra functions you need from android source code:
        //show scroll bars
        //change focus
        //etc.
    }
    
  3. 如果用户在抬起手指之前停止滚动(velocityY == 0),则不会触发投掷。如果您想拦截此类事件,请覆盖onTouchEvent。

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean eventConsumed = super.onTouchEvent(ev);
        if (eventConsumed && ev.getAction() == MotionEvent.ACTION_UP) {
            if (scroller.isFinished()) {
                //do your code
            }
        }
        return eventConsumed;
    }
    
  4. 注意虽然这有效,但重写fling方法可能不是一个好主意。它是公开的,但它几乎没有为子类化而设计。现在它做了3件事 - 它启动私人mScroller的投掷,处理可能的焦点变化并显示滚动条。这可能会在未来的Android版本中发生变化例如,私有mScroller实例将其类从Scroller更改为OvershootScroller,介于2.3和3.0之间。你必须记住所有这些微小的差异。无论如何,要为未来不可预见的后果做好准备。

答案 9 :(得分:2)

这是我的解决方案,其中包括滚动跟踪和滚动结尾:

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

}

答案 10 :(得分:2)

这是一个旧线程,但是我想添加一个较短的解决方案:

 buttonsScrollView.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->

            handler.removeCallbacksAndMessages(null)

            handler.postDelayed({
                  //YOUR CODE TO BE EXECUTED HERE
                },1000)
        }

自然会有1000毫秒的延迟。如有需要,请进行调整。

答案 11 :(得分:1)

我对ZeroG的回答做了一些改进。主要取消多余的任务调用并将整个事件作为私有OnTouchListener实现,因此所有滚动检测代码都在一个地方。

将以下代码粘贴到您自己的ScrollView实现中:

private class ScrollFinishHandler implements OnTouchListener
{
        private static final int SCROLL_TASK_INTERVAL = 100;

        private Runnable    mScrollerTask;
        private int         mInitialPosition = 0;

        public ScrollFinishHandler()
        {
            mScrollerTask = new Runnable() {

                public void run() {

                    int newPosition = getScrollY();
                    if(mInitialPosition - newPosition == 0)
                    {//has stopped

                       onScrollStopped(); // Implement this on your main ScrollView class
                    }else{
                        mInitialPosition = getScrollY();
                        ExpandingLinearLayout.this.postDelayed(mScrollerTask, SCROLL_TASK_INTERVAL);
                    }
                }
            };
        }

        @Override
        public boolean onTouch(View v, MotionEvent event) 
        {
            if (event.getAction() == MotionEvent.ACTION_UP) 
            {

                startScrollerTask();
            }
            else
            {
                stopScrollerTask();
            }

            return false;
        }

}

然后在ScrollView实现中:

setOnTouchListener( new ScrollFinishHandler() );

答案 12 :(得分:1)

这里有一些很好的答案,但我的代码可以检测滚动何时停止而无需扩展ScrollView类。 每个视图实例都可以调用getViewTreeObserver()。当持有ViewTreeObserver的这个实例时,你可以使用addOnScrollChangedListener()函数添加一个OnScrollChangedListener。

声明以下内容:

private ScrollView scrollListener;
private volatile long milesec;

private Handler scrollStopDetector;
private Thread scrollcalled = new Thread() {

    @Override
    public void run() { 
        if (System.currentTimeMillis() - milesec > 200) {
            //scroll stopped - put your code here
        }
    }
}; 

并在你的onCreate(或其他地方)添加:

    scrollListener = (ScrollView) findViewById(R.id.scroll);

    scrollListener.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() {

        @Override
        public void onScrollChanged() {
            milesec = System.currentTimeMillis();
            scrollStopDetector.postDelayed(scrollcalled, 200);
        }
    });

你可能想要在这些检查之间花费更长或更慢的时间,但是当滚动时,这个列表器被快速调用,因此它将非常快速地工作。

答案 13 :(得分:-1)

        this.getListView().setOnScrollListener(new OnScrollListener(){
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {}

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount) {
                     if( firstVisibleItem + visibleItemCount >= totalItemCount ) 
                              // Last item is shown...

            }

希望片段有用:)