HorizontalScrollView dentro de ScrollView Touch Handling

Tengo un ScrollView que rodea mi diseño completo para que toda la pantalla se pueda desplazar. El primer elemento que tengo en este ScrollView es un bloque HorizontalScrollView que tiene características que se pueden desplazar horizontalmente. He agregado un ontouchlistener a la vista de desplazamiento horizontal para manejar eventos táctiles y forzar la vista para “ajustar” a la imagen más cercana en el evento ACTION_UP.

Así que el efecto que estoy buscando es como la pantalla de inicio de android stock donde puedes desplazarte de una a la otra y se ajusta a una pantalla cuando levantas tu dedo.

Todo esto funciona muy bien, excepto por un problema: necesito deslizarme de izquierda a derecha casi perfectamente horizontalmente para que ACTION_UP se registre. Si deslizo verticalmente por lo menos (lo cual creo que muchas personas tienden a hacer en sus teléfonos al deslizar de un lado a otro), recibiré un ACTION_CANCEL en lugar de un ACTION_UP. Mi teoría es que esto se debe a que la vista de desplazamiento horizontal está dentro de una vista de desplazamiento, y la vista de desplazamiento está secuestrando el toque vertical para permitir el desplazamiento vertical.

¿Cómo puedo desactivar los eventos táctiles para la vista de desplazamiento desde mi vista de desplazamiento horizontal, pero todavía puedo permitir el desplazamiento vertical normal en cualquier lugar de la vista de desplazamiento?

Aquí hay una muestra de mi código:

public class HomeFeatureLayout extends HorizontalScrollView { private ArrayList items = null; private GestureDetector gestureDetector; View.OnTouchListener gestureListener; private static final int SWIPE_MIN_DISTANCE = 5; private static final int SWIPE_THRESHOLD_VELOCITY = 300; private int activeFeature = 0; public HomeFeatureLayout(Context context, ArrayList items){ super(context); setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT)); setFadingEdgeLength(0); this.setHorizontalScrollBarEnabled(false); this.setVerticalScrollBarEnabled(false); LinearLayout internalWrapper = new LinearLayout(context); internalWrapper.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); internalWrapper.setOrientation(LinearLayout.HORIZONTAL); addView(internalWrapper); this.items = items; for(int i = 0; i SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { activeFeature = (activeFeature  SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { activeFeature = (activeFeature > 0)? activeFeature - 1:0; smoothScrollTo(activeFeature*getMeasuredWidth(), 0); return true; } } catch (Exception e) { // nothing } return false; } } 

}

Actualización: me di cuenta de esto. En mi ScrollView, tuve que anular el método onInterceptTouchEvent para interceptar solo el evento táctil si el movimiento Y es> el movimiento X. Parece que el comportamiento predeterminado de ScrollView es interceptar el evento táctil siempre que exista CUALQUIER movimiento Y. Entonces, con la solución, ScrollView solo interceptará el evento si el usuario está deliberadamente desplazándose en la dirección Y y en ese caso pasará ACTION_CANCEL a los niños.

Aquí está el código para mi clase Scroll View que contiene el HorizontalScrollView:

 public class CustomScrollView extends ScrollView { private GestureDetector mGestureDetector; public CustomScrollView(Context context, AttributeSet attrs) { super(context, attrs); mGestureDetector = new GestureDetector(context, new YScrollDetector()); setFadingEdgeLength(0); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return super.onInterceptTouchEvent(ev) && mGestureDetector.onTouchEvent(ev); } // Return false if we're scrolling in the x direction class YScrollDetector extends SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return Math.abs(distanceY) > Math.abs(distanceX); } } } 

Gracias Joel por darme una pista sobre cómo resolver este problema.

He simplificado el código (sin necesidad de un GestureDetector ) para lograr el mismo efecto:

 public class VerticalScrollView extends ScrollView { private float xDistance, yDistance, lastX, lastY; public VerticalScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: xDistance = yDistance = 0f; lastX = ev.getX(); lastY = ev.getY(); break; case MotionEvent.ACTION_MOVE: final float curX = ev.getX(); final float curY = ev.getY(); xDistance += Math.abs(curX - lastX); yDistance += Math.abs(curY - lastY); lastX = curX; lastY = curY; if(xDistance > yDistance) return false; } return super.onInterceptTouchEvent(ev); } } 

Creo que encontré una solución más simple, solo que esto usa una subclase de ViewPager en lugar de (su principal) ScrollView.

ACTUALIZACIÓN 2013-07-16 : onTouchEvent una anulación para onTouchEvent también. Posiblemente podría ayudar con los problemas mencionados en los comentarios, aunque YMMV.

 public class UninterceptableViewPager extends ViewPager { public UninterceptableViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean ret = super.onInterceptTouchEvent(ev); if (ret) getParent().requestDisallowInterceptTouchEvent(true); return ret; } @Override public boolean onTouchEvent(MotionEvent ev) { boolean ret = super.onTouchEvent(ev); if (ret) getParent().requestDisallowInterceptTouchEvent(true); return ret; } } 

Esto es similar a la técnica utilizada en android.widget.Gallery onScroll () . Se explica mejor mediante la presentación de Google I / O 2013 Escritura de vistas personalizadas para Android .

Actualización 2013-12-10 : un enfoque similar también se describe en una publicación de Kirill Grouchnikov sobre la (entonces) aplicación Android Market .

Descubrí que algunas veces ScrollView recupera el foco y el otro pierde el foco. Puede evitar eso, al otorgar solo uno de los enfoques scrollView:

  scrollView1= (ScrollView) findViewById(R.id.scrollscroll); scrollView1.setAdapter(adapter); scrollView1.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { scrollView1.getParent().requestDisallowInterceptTouchEvent(true); return false; } }); 

No estaba funcionando bien para mí. Lo cambié y ahora funciona sin problemas. Si alguien está interesado

 public class ScrollViewForNesting extends ScrollView { private final int DIRECTION_VERTICAL = 0; private final int DIRECTION_HORIZONTAL = 1; private final int DIRECTION_NO_VALUE = -1; private final int mTouchSlop; private int mGestureDirection; private float mDistanceX; private float mDistanceY; private float mLastX; private float mLastY; public ScrollViewForNesting(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); } public ScrollViewForNesting(Context context, AttributeSet attrs) { this(context, attrs,0); } public ScrollViewForNesting(Context context) { this(context,null); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDistanceY = mDistanceX = 0f; mLastX = ev.getX(); mLastY = ev.getY(); mGestureDirection = DIRECTION_NO_VALUE; break; case MotionEvent.ACTION_MOVE: final float curX = ev.getX(); final float curY = ev.getY(); mDistanceX += Math.abs(curX - mLastX); mDistanceY += Math.abs(curY - mLastY); mLastX = curX; mLastY = curY; break; } return super.onInterceptTouchEvent(ev) && shouldIntercept(); } private boolean shouldIntercept(){ if((mDistanceY > mTouchSlop || mDistanceX > mTouchSlop) && mGestureDirection == DIRECTION_NO_VALUE){ if(Math.abs(mDistanceY) > Math.abs(mDistanceX)){ mGestureDirection = DIRECTION_VERTICAL; } else{ mGestureDirection = DIRECTION_HORIZONTAL; } } if(mGestureDirection == DIRECTION_VERTICAL){ return true; } else{ return false; } } } 

Gracias a Neevek su respuesta funcionó, pero no bloquea el desplazamiento vertical cuando el usuario comenzó a desplazarse por la vista horizontal (ViewPager) en dirección horizontal y luego, sin levantar el dedo verticalmente, comienza a desplazarse por la vista del contenedor subyacente (ScrollView) . Lo arreglé haciendo un pequeño cambio en el código de Neevak:

 private float xDistance, yDistance, lastX, lastY; int lastEvent=-1; boolean isLastEventIntercepted=false; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: xDistance = yDistance = 0f; lastX = ev.getX(); lastY = ev.getY(); break; case MotionEvent.ACTION_MOVE: final float curX = ev.getX(); final float curY = ev.getY(); xDistance += Math.abs(curX - lastX); yDistance += Math.abs(curY - lastY); lastX = curX; lastY = curY; if(isLastEventIntercepted && lastEvent== MotionEvent.ACTION_MOVE){ return false; } if(xDistance > yDistance ) { isLastEventIntercepted=true; lastEvent = MotionEvent.ACTION_MOVE; return false; } } lastEvent=ev.getAction(); isLastEventIntercepted=false; return super.onInterceptTouchEvent(ev); } 

Esto finalmente se convirtió en una parte de la biblioteca de soporte v4, NestedScrollView . Por lo tanto, ya no se necesitan hacks locales para la mayoría de los casos, supongo.

La solución de Neevek funciona mejor que la de Joel en dispositivos con 3.2 o más. Existe un error en Android que provocará que java.lang.IllegalArgumentException: punteroIndex fuera de rango si se usa un detector de gestos dentro de una vista scoll. Para duplicar el problema, implemente una vista personalizada como lo sugirió Joel y coloque un buscapersonas dentro. Si arrastras (no levantas figura) a una dirección (izquierda / derecha) y luego a la opuesta, verás el locking. También en la solución de Joel, si arrastra el buscapersonas moviendo el dedo diagonalmente, una vez que su dedo salga del área de visualización de contenido del buscapersonas, el buscapersonas regresará a su posición anterior. Todos estos problemas tienen que ver más con el diseño interno de Android o con la falta de él que con la implementación de Joel, que a su vez es un código inteligente y conciso.

http://code.google.com/p/android/issues/detail?id=18990