Android:检测 ScrollView 何时停止滚动
Posted
技术标签:
【中文标题】Android:检测 ScrollView 何时停止滚动【英文标题】:Android: Detect when ScrollView stops scrolling 【发布时间】:2012-01-01 04:22:11 【问题描述】:我在 android 中使用ScrollView
,其中ScrollView
的可见部分与Scrollview
中的一个单元格大小相同。每个“单元格”的高度相同。所以我想要做的是在ScrollView
滚动后卡入位置。
目前我正在检测用户何时触摸ScrollView
以及他们何时开始滚动并从那里开始工作,但它有很多错误。当用户只是轻弹它并滚动然后减速时,它也需要工作。
在 iPhone 上,有一个类似于 didDecelerate
的函数,当 ScrollView
完成滚动时,我可以在那里执行任何我想要的代码。 Android有这样的事情吗?或者是否有一些我可以查看的代码来找出更好的方法?
我查看了 Android 文档,但找不到类似的内容。
【问题讨论】:
【参考方案1】:这是一个旧线程,但我想添加一个我想出的更短的解决方案:
buttonsScrollView.setOnScrollChangeListener v, scrollX, scrollY, oldScrollX, oldScrollY ->
handler.removeCallbacksAndMessages(null)
handler.postDelayed(
//YOUR CODE TO BE EXECUTED HERE
,1000)
自然会有 1000 毫秒的延迟。如果需要,请进行调整。
【讨论】:
【参考方案2】:我的解决方案是 Lin Yu Cheng 伟大解决方案的一个变体,并且还检测滚动何时开始和停止。
第一步:定义一个 HorizontalScrollView 和 OnScrollChangedListener:
CustomHorizontalScrollView scrollView = (CustomHorizontalScrollView) findViewById(R.id.horizontalScrollView);
horizontalScrollListener = new CustomHorizontalScrollView.OnScrollChangedListener()
@Override
public void onScrollStart()
// Scrolling has started. Insert your code here...
@Override
public void onScrollEnd()
// Scrolling has stopped. Insert your code here...
;
scrollView.setOnScrollChangedListener(horizontalScrollListener);
步骤 2. 添加 CustomHorizontalScrollView 类:
public class CustomHorizontalScrollView extends HorizontalScrollView
public interface OnScrollChangedListener
// Developer must implement these methods.
void onScrollStart();
void onScrollEnd();
private long lastScrollUpdate = -1;
private int scrollTaskInterval = 100;
private Runnable mScrollingRunnable;
public OnScrollChangedListener mOnScrollListener;
public CustomHorizontalScrollView(Context context)
this(context, null, 0);
init(context);
public CustomHorizontalScrollView(Context context, AttributeSet attrs)
this(context, attrs, 0);
init(context);
public CustomHorizontalScrollView(Context context, AttributeSet attrs, int defStyle)
super(context, attrs, defStyle);
init(context);
private void init(Context context)
// Check for scrolling every scrollTaskInterval milliseconds
mScrollingRunnable = new Runnable()
public void run()
if ((System.currentTimeMillis() - lastScrollUpdate) > scrollTaskInterval)
// Scrolling has stopped.
lastScrollUpdate = -1;
//CustomHorizontalScrollView.this.onScrollEnd();
mOnScrollListener.onScrollEnd();
else
// Still scrolling - Check again in scrollTaskInterval milliseconds...
postDelayed(this, scrollTaskInterval);
;
public void setOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener)
this.mOnScrollListener = onScrollChangedListener;
public void setScrollTaskInterval(int scrollTaskInterval)
this.scrollTaskInterval = scrollTaskInterval;
//void onScrollStart()
// System.out.println("Scroll started...");
//
//void onScrollEnd()
// System.out.println("Scroll ended...");
//
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
super.onScrollChanged(l, t, oldl, oldt);
if (mOnScrollListener != null)
if (lastScrollUpdate == -1)
//CustomHorizontalScrollView.this.onScrollStart();
mOnScrollListener.onScrollStart();
postDelayed(mScrollingRunnable, scrollTaskInterval);
lastScrollUpdate = System.currentTimeMillis();
【讨论】:
【参考方案3】:这是另一个解决方案,在我看来非常简单和干净,自然受到上述答案的启发。基本上一旦用户结束手势检查 getScrollY() 是否仍在变化,经过短暂的延迟(这里是 50 毫秒)。
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;
【讨论】:
【参考方案4】:我认为这在过去已经出现。 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
【讨论】:
很好,我没有注意到 isFinished 方法! 谢谢。如何访问这个 isFinished() 方法?我查看了 ScrollView.java 文件,并且 mScroller 是私有的,我无法确定可以访问它。 嗯,确实。我看的不够近。这就是为什么我说我会尝试first :)。您可以执行其他操作,例如跟踪对onScrollChanged
、scrollTo
或任何其他滚动相关方法的调用。或者,您可以复制整个 ScrollView
代码并将该字段设为 protected
。不过,我不建议这样做。
通过反射访问了 mScroller。也不起作用,每次滚动视图到达您的手指位置时都会触发 isFinished(),这几乎是连续的。当用户松开手指并且滚动条停止滚动时,应该触发滚动停止事件。
这个答案看起来很有希望,但由于它有点死胡同,也许应该删除它?【参考方案5】:
这是我的解决方案,其中包括滚动跟踪和滚动结束:
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;
【讨论】:
【参考方案6】:我的方法是通过每次调用 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
【讨论】:
很好的解决方案。按预期工作。 感谢您的代码。如何从您的代码中检测滚动启动侦听器方法。请看我的问题***.com/questions/27864700/…【参考方案7】:这里有一些很好的答案,但是我的代码可以检测到滚动停止的时间,而无需扩展 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);
);
您可能希望在两次检查之间花费更长或更慢的时间,但是当滚动此侦听器时,调用速度非常快,因此它的工作速度非常快。
【讨论】:
我没有尝试过这种情况,但根据我的经验,我猜想从 onScrollChanged 对 postDelayed 的重复和相互调用会导致内存泄漏。【参考方案8】:我对 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() );
【讨论】:
【参考方案9】:这是另一个修复,恕我直言,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_
android:layout_
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>
【讨论】:
+1 不错,简单的答案。我将差值2
更改为 0.5f
以反映原始 ScrollView
的 MAX_SCROLL_FACTOR
值。
这个实现很好,但是当滚动视图在开始时没有填充时它失败了。这是一种解决方法(对于水平滚动视图,因此将所有 x 替换为 y 以获得垂直滚动视图):@Override protected void onScrollChanged(int x, int y, int oldX, int oldY) super.onScrollChanged(x, y , 旧X, 旧Y); if (Math.abs(x - oldX) = getMeasuredWidth() || x == 0) lastStopX = x; if (mOnEndScrollListener != null) mOnEndScrollListener.onScrollEnded();
这并不总是有效。与模拟器一样,并不总是调用 public void fling(int velocityY) 。滚动结束阈值有时超过 20 像素。【参考方案10】:
我对这个问题的方法是使用计时器来检查以下 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);
【讨论】:
【参考方案11】:我最近不得不实现您描述的功能。我所做的是通过比较第一次触发 onTouchEvent 时 getScrollY() 返回的值与变量 newCheck 定义的时间后返回的值来检查 ScrollView 是否停止滚动.
参见下面的代码(工作解决方案):
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");
);
顺便说一句,我在我的应用程序中使用了其他回复中的一些想法来执行此操作。 希望这可以帮助。有任何问题请随时询问我(们)。 干杯。
【讨论】:
谢谢,一直在寻找类似的东西。用于 HorizontalScrollView 代替,它(显然)也有效。只有 getScrollY() 必须替换为 getScrollX() 在这里都试过了。这是唯一有效的。 即使单击滚动视图,它也会显示“已停止”。它应该只适用于用户滚动滚动视图。那我们需要做什么呢? 当你触摸屏幕时它会调用 onScrollStopped()。如果您向下滚动,则立即向上滚动 onScrollStopped() 方法将调用.. 如果您连续向上滚动和向下滚动,它将一直调用 onScrollStopped()。【参考方案12】:我将 (Horizontal)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;
【讨论】:
不错的答案。唯一的一个补充。在 onInterceptTouchEvent() 中,您还处理 MotionEvent.ACTION_UP 和 MotionEvent.ACTION_CANCEL 否则 currentTouching 将保持为真,以防您在点击时打开另一个活动。 另一个修复:在 onScrollChanged() 中,即使减速尚未达到 1,您也必须检查滚动结束。像这样: boolean end = (oldx > x) ? (x = getMaxScrollAmount()); if (Math.abs(x - oldx) > 1 && !end) 滚动开始; else 滚动结束 这在某些设备上不起作用,例如 GS3。在我的 GS3 上,y 和旧 y 之间的差异可以在滚动中间为 1。它在我的 Nexus 4 上完美运行。我不知道为什么。 在三星 Galaxy Note 2 和华硕 Memo Pad 7 上也无法使用。我还在这些设备上的滚动中间看到了 1 的增量。【参考方案13】:对于您描述的简单案例,您可能可以在自定义滚动视图中使用覆盖 fling 方法。每次用户从屏幕上抬起手指时,都会调用 Fling 方法来执行“减速”。
所以你应该做的是这样的:
子类 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
;
//...
子类 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.
如果用户在抬起手指之前停止滚动(velocityY == 0),则不会触发 Fling。如果您还想拦截此类事件,请覆盖 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;
注意虽然这可行,但覆盖 fling 方法可能不是一个好主意。它是公开的,但几乎没有为子类化而设计。现在它做了 3 件事——它为私有 mScroller 启动投掷,处理可能的焦点变化并显示滚动条。这可能会在未来的 android 版本中改变。例如,私有 mScroller 实例在 2.3 和 3.0 之间将其类从 Scroller 更改为 OvershootScroller。你必须记住所有这些小差异。无论如何,为未来不可预见的后果做好准备。
【讨论】:
第二步 (2.) 中的 container.getHeight() 是什么? container 是 scrollView 内部的视图。由于 scrollView 只能包含 1 个孩子,您可以将容器替换为 getChildAt(0)。【参考方案14】: 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...
希望sn-p帮助:)
【讨论】:
不幸的是,我使用的是 ScrollView,而不是 ListView。不过还是谢谢。 喂? ScrollView 不是 ListView。【参考方案15】:尝试在 *** 上查看 this question - 它与您的问题不完全相同,但它提供了一个关于如何管理 ScrollView
的滚动事件的想法。
基本上,您需要通过扩展ScrollView
并覆盖onScrollChanged(int x, int y, int oldx, int oldy)
来创建自己的CustomScrollView
。然后你需要在你的布局文件中引用它而不是像com.mypackage.CustomScrollView
这样的标准ScrollView
。
【讨论】:
这个 onScrollChanged() 实际上是我现在正在使用的。不过还是谢谢。以上是关于Android:检测 ScrollView 何时停止滚动的主要内容,如果未能解决你的问题,请参考以下文章