HorizontalScrollView dentro de ScrollView Touch Handling

226

Tengo un ScrollView que rodea todo mi diseño 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. Agregué un ontouchlistener a la vista de desplazamiento horizontal para manejar eventos táctiles y forzar la vista a "ajustar" a la imagen más cercana en el evento ACTION_UP.

Entonces, el efecto que busco es como la pantalla de inicio de Android en la que 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: necesito deslizar de izquierda a derecha casi perfectamente horizontalmente para que un ACTION_UP se registre. Si al menos deslizo verticalmente (lo que creo que muchas personas tienden a hacer en sus teléfonos cuando deslizan 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 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 aún 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;
        }
    }
}
Joel
fuente
He probado todos los métodos en esta publicación, pero ninguno de ellos funciona para mí. Estoy usando la MeetMe's HorizontalListViewbiblioteca
The Nomad
Hay un artículo con un código similar ( HomeFeatureLayout extends HorizontalScrollView) aquí velir.com/blog/index.php/2010/11/17/… Hay algunos comentarios adicionales sobre lo que está sucediendo a medida que se compone la clase de desplazamiento personalizada.
CJBS

Respuestas:

280

Actualización: descubrí esto. En mi ScrollView, necesitaba 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 haya CUALQUIER movimiento Y. Entonces, 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 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);
        }
    }
}
Joel
fuente
estoy usando esto mismo pero al momento de desplazarme en dirección x obtengo la excepción NULL POINTER EXCEPTION en public boolean onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } He usado el desplazamiento horizontal dentro de la vista de desplazamiento
Vipin Sahu
@Todos gracias, está funcionando, me olvidé de inicializar el detector de gestos en el constructor scrollview
Vipin Sahu
¿Cómo debo usar este código para implementar un ViewPager dentro de ScrollView
Harsha MV
Tuve algunos problemas con este código cuando tenía una vista de cuadrícula siempre expandida, a veces no se desplazaba. Tuve que anular el método onDown (...) en YScrollDetector para devolver siempre verdadero como se sugiere en toda la documentación (como aquí developer.android.com/training/custom-views/… ) Esto resolvió mi problema.
Nemanja Kovacevic
3
Me encontré con un pequeño error que vale la pena mencionar. Creo que el código en onInterceptTouchEvent debería dividir las dos llamadas booleanas, para garantizar que mGestureDetector.onTouchEvent(ev)se llame. Tal como está ahora, no se llamará si super.onInterceptTouchEvent(ev)es falso. Me encontré con un caso en el que los niños que pueden hacer clic en la vista de desplazamiento pueden tomar los eventos táctiles y onScroll no será llamado en absoluto. De lo contrario, gracias, gran respuesta!
GLee
176

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);
    }
}
neevek
fuente
excelente, gracias por este refactor. Estaba teniendo problemas con el enfoque anterior al desplazarme hacia el final de la vista de lista, ya que cualquier comportamiento táctil sobre los elementos secundarios comenzó a no ser interceptado sin importar la relación de movimiento Y / X. ¡Extraño!
Dori
1
¡Gracias! También trabajé con un ViewPager dentro de un ListView, con un ListView personalizado.
Sharief Shaik
2
Acabo de reemplazar la respuesta aceptada con esto y ahora funciona mucho mejor para mí. ¡Gracias!
David Scott
1
@VipinSahu, para indicar la dirección del movimiento táctil, puede tomar el delta de la coordenada X actual y lastX, si es mayor que 0, el tacto se mueve de izquierda a derecha, de lo contrario de derecha a izquierda. Y luego guarda la X actual como lastX para el próximo cálculo.
neevek
1
¿Qué pasa con scrollview horizontal?
Zin Win Htet
60

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

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 onScroll () de android.widget.Gallery . Se explica con más detalle en la presentación de Google I / O 2013 Escritura de vistas personalizadas para Android .

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

Giorgos Kylafas
fuente
ret booleano = super.onInterceptTouchEvent (ev); Solo alguna vez me devolvió falso. Uso en UninterceptableViewPager en una vista de desplazamiento
scottyab
Esto no funciona para mí, aunque me gusta su simplicidad. Yo uso un ScrollViewcon un LinearLayouten el que UninterceptableViewPagerse coloca el. De hecho, retsiempre es falso ... ¿Alguna idea de cómo solucionar esto?
Peterdk
@scottyab & @Peterdk: Bueno, el mío está en un TableRowque está dentro de un TableLayoutque está dentro de un ScrollView(sí, lo sé ...), y está funcionando según lo previsto. Tal vez podría intentar anular en onScrolllugar de onInterceptTouchEvent, como lo hace Google (línea 1010)
Giorgos Kylafas
Acabo de codificar ret como verdadero y funciona bien. Antes volvía a ser falso, pero creo que es porque la vista de desplazamiento tiene un diseño lineal que contiene a todos los niños
Amanni
11

Descubrí que a veces un ScrollView recupera el foco y el otro pierde el foco. Puede evitar eso, otorgando 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;
        }
    });
Marius Hilarante
fuente
¿Qué adaptador estás pasando?
Harsha MV
Verifiqué nuevamente y me di cuenta de que en realidad no estoy usando un ScrollView, sino un ViewPager y le estoy pasando un FragmentStatePagerAdapter que incluye todas las imágenes de la galería.
Marius Hilarante
8

No me estaba funcionando bien. Lo cambié y ahora funciona sin problemas. Si alguien esta 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;
        }
    }
}
snapix
fuente
Esta es la respuesta para mi proyecto. Tengo un localizador de vistas que actúa como una galería se puede hacer clic en una vista de desplazamiento. Utilizo la solución proporcionada anteriormente, funciona en desplazamiento horizontal, pero después de hacer clic en la imagen del localizador que inicia una nueva actividad y volver, el localizador no puede desplazarse. Esto funciona bien, tks!
longkai
Funciona perfectamente para mi. Tenía una vista personalizada de "deslizar para desbloquear" dentro de una vista de desplazamiento que me daba el mismo problema. Esta solución resolvió el problema.
híbrido
6

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 vertical del dedo, comienza a desplazar 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);

}
Saqib
fuente
5

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

Ebrahim Byagowi
fuente
1

La solución de Neevek funciona mejor que la de Joel en dispositivos con 3.2 y superior. Hay un error en Android que causará java.lang.IllegalArgumentException: pointerIndex fuera de rango si se usa un detector de gestos dentro de una vista de desplazamiento. Para duplicar el problema, implemente una vista de exploración personalizada como sugirió Joel y coloque un buscapersonas dentro. Si arrastra (no levante su figura) en una dirección (izquierda / derecha) y luego a la opuesta, verá el choque. También en la solución de Joel, si arrastra el localizador de vista moviendo el dedo en diagonal, una vez que su dedo abandone el área de visualización del contenido del localizador de visualización, 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 un código inteligente y conciso.

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

Don
fuente