Android: обнаружение, когда ScrollView прекращает прокрутку
Я использую ScrollView
в Android и где видимая часть ScrollView
такой же размер, как одна из клеток внутри Scrollview
, Каждая "клетка" имеет одинаковую высоту. Итак, что я пытаюсь сделать, это встать в положение после ScrollView
был прокручен
В настоящее время я определяю, когда пользователь коснулся ScrollView
и когда они начали прокручивать и обрабатывать это оттуда, но это довольно глючно. Это также должно работать, когда пользователь просто щелкает его, и он прокручивает, а затем замедляется.
На айфоне есть функция что то типа didDecelerate
и там я могу сделать любой код, который я хочу, когда ScrollView
закончил прокрутку. Есть ли такая вещь с Android? Или есть какой-то код, на который я мог бы посмотреть, чтобы выяснить лучший способ сделать это?
Я просмотрел документы по Android и не смог найти ничего подобного.
14 ответов
Недавно мне пришлось реализовать описанную вами функцию. Я сделал, чтобы Runnable проверял, прекратила ли прокрутка ScrollView, сравнивая значение, возвращаемое getScrollY(), когда onTouchEvent впервые запускается со значением, возвращаемым через время, определенное переменной 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");
}
});
Кстати, я использовал несколько идей из других ответов, чтобы сделать это в моем приложении. Надеюсь это поможет. Любые вопросы, не стесняйтесь спрашивать. Приветствия.
Вот еще одно исправление, IMHO, отсутствующей ошибки события OnEndScroll в ScrollView.
Его вдохновил Ross Hambrick ответ. Просто вставьте этот класс в свой проект (измените пакет, чтобы он соответствовал вашему собственному) и используйте приведенный ниже 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>
Я подкласс (горизонтальный)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);
}
Я использовал значение 1 для SlowDownThreshold, так как оно всегда похоже на разницу последнего события 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;
}
Вот еще одно решение, довольно простое и чистое, на мой взгляд, естественно вдохновленное ответами выше. Как правило, после того, как пользователь завершил жест, проверьте, изменяется ли 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;
}
}
Мой подход заключается в определении состояния прокрутки по метке времени, изменяемой каждый раз при вызове 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
}
}
Мой подход к этому вопросу заключается в использовании таймера для проверки следующих 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);
}
}
Это старый поток, но я хотел бы добавить более короткое решение, которое я придумал:
buttonsScrollView.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
handler.removeCallbacksAndMessages(null)
handler.postDelayed({
//YOUR CODE TO BE EXECUTED HERE
},1000)
}
Естественно есть задержка в 1000 миллисекунд. Отрегулируйте это, если вам нужно.
Для простого случая, как вы описали, вы, вероятно, можете обойтись без переопределения метода 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 } } }; //... } }
Подбрасывать метод подкласса и НЕ вызывать реализацию суперкласса.
@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. }
Fling не сработает, если пользователь прекратит прокрутку, прежде чем поднять палец вверх (скорость Y == 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; }
ПРИМЕЧАНИЕ Хотя это работает, переопределение метода fling может быть плохой идеей. Он общедоступен, но едва предназначен для создания подклассов. Сейчас он делает 3 вещи - он запускает сброс для частного mScroller, обрабатывает возможные изменения фокуса и показывает полосы прокрутки. Это может измениться в будущем выпуске Android. Например, частный экземпляр mScroller изменил свой класс с Scroller на OvershootScroller между 2.3 и 3.0. Вы должны иметь в виду все эти небольшие различия. В любом случае будьте готовы к непредвиденным последствиям в будущем.
Мое решение представляет собой вариант отличного решения Линь Ю Чена, а также определяет, когда прокрутка началась и остановилась.
Шаг 1. Определите 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. Добавьте класс CustomHor horizontalScrollView:
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();
}
}
}
Я думаю, что это произошло в прошлом. AFAIK, вы не можете легко обнаружить это. Я предлагаю вам взглянуть на ScrollView.java
(именно так мы и поступаем в Android Land :)) и выясните, как вы можете расширить класс, чтобы обеспечить нужную вам функциональность. Это то, что я бы попробовал сначала:
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
if (mScroller.isFinished()) {
// do something, for example call a listener
}
}
Попробуйте взглянуть на этот вопрос здесь, в Stackru - он не совсем совпадает с вашим вопросом, но дает представление о том, как вы можете управлять событием прокрутки ScrollView
,
В основном вам нужно создать свой собственный CustomScrollView
расширяя ScrollView
и переопределить onScrollChanged(int x, int y, int oldx, int oldy)
, Тогда вам нужно сослаться на это в вашем файле макета вместо стандартного ScrollView
лайк com.mypackage.CustomScrollView
,
Здесь есть несколько отличных ответов, но мой код может обнаружить, когда прокрутка останавливается без необходимости расширять класс ScrollView. каждый экземпляр представления может вызывать getViewTreeObserver(). удерживая этот экземпляр ViewTreeObserver, вы можете добавить OnScrollChangedListener, используя функцию addOnScrollChangedListener().
объявить следующее:
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);
}
});
вам может потребоваться больше или меньше времени между этими проверками, но при прокрутке этот список вызывается очень быстро, поэтому он будет работать очень быстро.
Вот мое решение, которое включает в себя отслеживание прокрутки и завершение прокрутки:
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;
}
}
Я внес некоторые улучшения в ответ 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() );
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...
}
Надеюсь, фрагмент поможет:)