Android: detectar cuando ScrollView deja de desplazarse

Estoy usando ScrollView en Android y la parte visible de ScrollView tiene el mismo tamaño que una de las celdas dentro de Scrollview . Cada “célula” tiene la misma altura. Entonces, lo que bash hacer es ajustarme a su posición después de que ScrollView se haya desplazado.

Actualmente estoy detectando cuándo el usuario ha tocado el ScrollView y cuándo han comenzado a desplazarse y trabajar desde allí, pero es bastante problemático. También debe funcionar cuando el usuario simplemente lo mueve y se desplaza y luego desacelera.

En iPhone hay una función que es algo así como didDecelerate y allí puedo hacer cualquier código que quiera cuando ScrollView haya terminado de desplazarse. ¿Hay algo así con Android? ¿O hay algún código que podría ver para encontrar una mejor manera de hacerlo?

Revisé los documentos de Android y no pude encontrar nada de eso.

Recientemente tuve que implementar la función que describiste. Lo que hice fue tener un Runnable revisando si el ScrollView había dejado de desplazarse al comparar el valor devuelto por getScrollY () cuando el evento onTouchEvent se desencadena por primera vez con el valor devuelto después de un tiempo definido por la variable newCheck .

Vea el código a continuación (solución de trabajo):

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

Luego tengo:

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

Por cierto, utilicé algunas ideas de otras respuestas para hacer esto en mi aplicación. Espero que esto ayude. Cualquier pregunta no dude en preguntar. Aclamaciones.

Aquí hay otra solución para el, en mi humilde opinión, error de evento OnEndScroll faltante en el ScrollView.

Está inspirado en la respuesta hambonious . Simplemente deje caer esta clase en su proyecto (cambie el paquete para que coincida con el suyo) y use el siguiente 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; } } 

cambiando nuevamente el nombre del paquete para que coincida con su proyecto

  ....  

Subclassed (Horizontal) ScrollView e hice algo como esto:

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

Usé un valor de 1 para el SlowDownThreshold, ya que siempre parece ser la diferencia del último evento onScrollChanged.

Para hacer que esto se comporte correctamente al arrastrar lentamente, tuve que hacer esto:

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

Mi enfoque es determinar el estado de desplazamiento por una marca de tiempo cambiada cada vez que se llama a onScrollChanged (). Es muy fácil determinar cuándo es el inicio y el final del desplazamiento. También puede cambiar el umbral (uso 100ms) para corregir la sensibilidad.

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

Mi enfoque para esta pregunta es utilizar un temporizador para verificar los siguientes 2 “eventos”.

1) onScrollChanged () dejó de llamar

2) El dedo del usuario se levanta de la vista de desplazamiento

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

Creo que esto ha surgido en el pasado. AFAIK, no puedes detectarlo fácilmente . Mi sugerencia es que ScrollView.java un vistazo a ScrollView.java (así es como hacemos las cosas en Android land 🙂 ) y averigua cómo puedes ampliar la clase para proporcionar la funcionalidad que estás buscando. Esto es lo que probaría primero:

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

Aquí hay otra solución, bastante simple y limpia en mi opinión, inspirada naturalmente por las respuestas anteriores. Básicamente, una vez que el usuario finalizó la comprobación de gesto, si getScrollY () todavía está cambiando, después de un breve retraso (aquí 50 ms).

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

Intenta echar un vistazo a esta pregunta aquí en StackOverflow: no es exactamente lo mismo que tu pregunta, pero te da una idea de cómo puedes administrar el evento scroll de un ScrollView .

Básicamente, necesita crear su propio CustomScrollView extendiendo ScrollView y sobrescribiendo onScrollChanged(int x, int y, int oldx, int oldy) . Luego debe hacer referencia a esto en su archivo de diseño en lugar del ScrollView estándar como com.mypackage.CustomScrollView .

Para un caso simple como el que describió, probablemente pueda salirse con la suya anulando el método fling en su vista de desplazamiento personalizada. Se llama al método Fling para realizar “deceleración” cada vez que el usuario levanta su dedo de la pantalla.

Entonces, ¿qué deberías hacer es algo como esto?

  1. Subclase 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. Método de lanzamiento de subclase y NO invocar implementación de superclase.

     @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. Fling no se activará si el usuario deja de desplazarse antes de levantar el dedo (velocidadY == 0). En caso de que desee interceptar este tipo de eventos también, anule el evento táctil.

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

NOTA Aunque esto funciona, el método de arrojar por la fuerza puede ser una mala idea. Es público, pero apenas está diseñado para subclases. En este momento hace 3 cosas: inicia fling para mScroller privado, maneja posibles cambios de enfoque y muestra barras de desplazamiento. Esto podría cambiar en futuras versiones de Android. Por ejemplo, la instancia privada de mScroller cambió su clase de Scroller a OvershootScroller entre 2.3 y 3.0. Tienes que tener en cuenta todas estas pequeñas diferencias. En cualquier caso, prepárate para las consecuencias imprevistas en el futuro.

Aquí está mi solución que incluye seguimiento de desplazamiento y final de desplazamiento:

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

Hice algunas mejoras a la respuesta de ZeroG. Principalmente la cancelación de llamadas de tareas en exceso y la implementación de todo como un OnTouchListener privado, por lo que todo el código de detección de desplazamiento estaría en un solo lugar.

Pegue el siguiente código en su propia implementación de 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; } } 

Y luego en su implementación de ScrollView:

 setOnTouchListener( new ScrollFinishHandler() ); 

Aquí hay algunas respuestas excelentes, pero mi código puede detectar cuándo se detiene el desplazamiento sin tener que extender la clase ScrollView. cada instancia de vista puede llamar a getViewTreeObserver (). Al mantener esta instancia de ViewTreeObserver, puede agregar OnScrollChangedListener con la función addOnScrollChangedListener ().

declara lo siguiente:

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

y en su onCreate (u otro lugar) agregue:

  scrollListener = (ScrollView) findViewById(R.id.scroll); scrollListener.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() { @Override public void onScrollChanged() { milesec = System.currentTimeMillis(); scrollStopDetector.postDelayed(scrollcalled, 200); } }); 

Es posible que desee tomar un tiempo más largo o más lento entre estas comprobaciones, pero cuando se desplaza este listador se llama muy rápido, por lo que funcionará muy rápido.

Mi solución es una variación de la gran solución de Lin Yu Cheng y también detecta cuándo se inició y se detuvo el desplazamiento.

Paso 1. Defina un HorizontalScrollView y 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); 

Paso 2. Agrega la clase 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(); } } } 
  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... } 

Espero que el fragmento ayude 🙂