android - HorizontalScrollView dentro de ScrollView Touch Handling
ontouchlistener android-scrollview (8)
Tengo un ScrollView que rodea todo mi diseño para que la pantalla completa se pueda desplazar. El primer elemento que tengo en este ScrollView es un bloque HorizontalScrollView que tiene características que se pueden desplazar horizontalmente. Agregué un guardabosque en la vista de desplazamiento horizontal para manejar eventos táctiles y forzar a la vista a "ajustarse" 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, donde puedes desplazarte de una a otra y se ajusta a una pantalla cuando levantas el dedo.
Todo esto funciona muy bien, excepto por un problema: tengo que deslizar de izquierda a derecha casi perfectamente horizontalmente para que un ACTION_UP se registre. Si deslizo verticalmente por lo menos (lo que creo que muchas personas tienden a hacer en sus teléfonos al deslizar de lado a lado), recibiré un ACTION_CANCEL en lugar de un ACTION_UP. Mi teoría es que esto se debe a que la vista horizontal de desplazamiento 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 deshabilitar los eventos táctiles para la vista de desplazamiento desde solo dentro de mi vista de desplazamiento horizontal, y aún así permitir el desplazamiento vertical normal en otra parte de la vista de desplazamiento?
Aquí hay una muestra de mi código:
public class HomeFeatureLayout extends HorizontalScrollView {
private ArrayList<ListItem> 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<ListItem> 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< items.size();i++){
LinearLayout featureLayout = (LinearLayout) View.inflate(this.getContext(),R.layout.homefeature,null);
TextView header = (TextView) featureLayout.findViewById(R.id.featureheader);
ImageView image = (ImageView) featureLayout.findViewById(R.id.featureimage);
TextView title = (TextView) featureLayout.findViewById(R.id.featuretitle);
title.setTag(items.get(i).GetLinkURL());
TextView date = (TextView) featureLayout.findViewById(R.id.featuredate);
header.setText("FEATURED");
Image cachedImage = new Image(this.getContext(), items.get(i).GetImageURL());
image.setImageDrawable(cachedImage.getImage());
title.setText(items.get(i).GetTitle());
date.setText(items.get(i).GetDate());
internalWrapper.addView(featureLayout);
}
gestureDetector = new GestureDetector(new MyGestureDetector());
setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
return true;
}
else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL ){
int scrollX = getScrollX();
int featureWidth = getMeasuredWidth();
activeFeature = ((scrollX + (featureWidth/2))/featureWidth);
int scrollTo = activeFeature*featureWidth;
smoothScrollTo(scrollTo, 0);
return true;
}
else{
return false;
}
}
});
}
class MyGestureDetector extends SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
try {
//right to left
if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
activeFeature = (activeFeature < (items.size() - 1))? activeFeature + 1:items.size() -1;
smoothScrollTo(activeFeature*getMeasuredWidth(), 0);
return true;
}
//left to right
else if (e2.getX() - e1.getX() > 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 un ScrollView es interceptar el evento táctil siempre que exista CUALQUIER movimiento Y. Así que con la corrección, ScrollView solo interceptará el evento si el usuario se desplaza deliberadamente en la dirección Y y, en ese caso, pasa el ACTION_CANCEL a los niños.
Aquí está el código para mi clase de Vista de desplazamiento 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);
}
}
}
Creo que encontré una solución más simple, solo que esta usa una subclase de ViewPager en lugar de ScrollView (su principal).
ACTUALIZACIÓN 2013-07-16 : También agregué una anulación para onTouchEvent
. 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''s onScroll () . Se explica con más detalle en la presentación de Google I / O 2013, Cómo escribir 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 de Android Market .
Descubrí que a veces un ScrollView recupera el enfoque y el otro pierde el enfoque. Puede evitar eso, solo otorgando 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;
}
});
Esto finalmente se convirtió en parte de la biblioteca de v4 de soporte, NestedScrollView . Por lo tanto, ya no se necesitan hacks locales para la mayoría de los casos, supongo.
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);
}
}
Gracias a Neevek, su respuesta funcionó para mí, pero no bloquea el desplazamiento vertical cuando el usuario ha comenzado a desplazar la vista horizontal (ViewPager) en dirección horizontal y luego, sin levantar el desplazamiento del dedo verticalmente, comienza a desplazarse por la vista del contenedor subyacente (ScrollView) . Lo arreglé haciendo un ligero 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);
}
La solución de Neevek funciona mejor que la de Joel en dispositivos que ejecutan 3.2 y superiores. Hay un error en Android que causará java.lang.IllegalArgumentException: pointerIndex fuera de rango si se usa un detector de gestos dentro de un scollview. Para duplicar el problema, implemente un scollview personalizado como Joel sugirió y coloque un localizador de vista en su interior. Si arrastra (no levanta la figura) a una dirección (izquierda / derecha) y luego a la opuesta, verá el choque. También en la solución de Joel, si arrastra el visor de la vista moviendo el dedo en diagonal, una vez que el dedo abandone el área de visualización del contenido del localizador de la vista, el localizador volverá a su posición anterior. Todos estos problemas tienen más que ver con el diseño interno de Android o la falta de él que con la implementación de Joel, que en sí misma es una pieza de código inteligente y conciso.
No estaba funcionando bien para mí. Lo cambié y ahora funciona sin problemas. Si alguien 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;
}
}
}