HorizontalScrollView dentro de ScrollView Touch Handling

Tengo un ScrollView que rodea mi disposición entera de modo que la pantalla entera sea scrollable. 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 horizontal para manejar eventos táctiles y forzar la vista a "ajustar" a la imagen más cercana en el evento ACTION_UP.

Así que el efecto que voy a hacer es como la pantalla de inicio androide de acciones donde se puede desplazar de uno a otro y se encaja a una pantalla cuando levanta el dedo.

Todo esto funciona muy bien excepto por un problema: necesito deslizar de izquierda a derecha casi perfectamente horizontalmente para que un ACTION_UP se registre. Si me deslizo verticalmente por lo menos (lo que creo que mucha gente tiende a hacer en sus teléfonos al pasar 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 deshabilitar los eventos táctiles para la vista de desplazamiento desde mi vista de desplazamiento horizontal, pero todavía permiten el desplazamiento vertical normal en otro lugar de la vista de desplazamiento?

He aquí un ejemplo 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, necesitaba anular el método onInterceptTouchEvent para interceptar sólo 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 cuando hay cualquier movimiento Y. Así que con la corrección, el ScrollView sólo interceptará el evento si el usuario está deliberadamente desplazándose en la dirección Y y en ese caso pase 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); } } } 

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, sólo que esto utiliza una subclase de ViewPager en lugar de (su padre) ScrollView.

UPDATE 2013-07-16 : He añadido una anulación para onTouchEvent también. Podría ayudar con los temas mencionados en los comentarios, a pesar de 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 () . También se explica por la presentación de Google I / O 2013 Redacción de vistas personalizadas para Android .

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

He descubierto que somethimes un ScrollView recupera el foco y el otro pierde el foco. Puede evitar que, al conceder sólo uno de los scrollView enfoque:

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

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 dedo desplazarse verticalmente empieza a desplazar la vista de 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); } 

No funcionaba bien para mí. Lo he cambiado 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; } } 

}

Esto finalmente se convirtió en una parte de la biblioteca de apoyo v4, NestedScrollView . Por lo tanto, no más hacks locales es necesario para la mayoría de los casos, supongo.

La solución de Neevek funciona mejor que la de Joel en dispositivos con la versión 3.2 o superior. Hay un error en Android que causará java.lang.IllegalArgumentException: pointerIndex fuera de rango si se utiliza un detector de gestos dentro de un scollview. Para duplicar el problema, implemente un scollview personalizado como Joel sugirió y poner un paginador de vista dentro. Si arrastra (no levante la figura) a una dirección (izquierda / derecha) y luego al contrario, verá el choque. También en la solución de Joel, si arrastra el paginador de vista moviendo su dedo diagonalmente, una vez que su dedo salga del área de vista de contenido del buscapersonas, el buscapersonas volverá a su posición anterior. Todas estas cuestiones tienen que ver más con el diseño interno de Android o la falta de él que la implementación de Joel, que en sí es una pieza de código inteligente y conciso.

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

  • ¿Adaptador personalizado de HorizontalScrollView?
  • Vista personalizada no mostrada en la vista de desplazamiento horizontal No funciona
  • TabWidget envuelto en HorizontalScrollView no se desplaza con ViewPager
  • Java.lang.OutOfMemoryError BitmapFactory.nativeDecodeAsset ()
  • ListView con ScrollView horizontal
  • ¿Cómo desplazarse al centro de HorizontalScrollView sólo cuando se inicia la actividad?
  • Vista de desplazamiento horizontal que oculta a algunos niños
  • Android: Cómo implementar una galería de imágenes con RecyclerView
  • Menú horizontal con HorizontalScrollView-android
  • Resalte la imagen seleccionada en HorizantalListView en android y permanecerá seleccionada hasta que no se haga clic en otra imagen
  • Efecto marquesina para diseño lineal
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.