HorizontalScrollView in Gestione tocco di ScrollView


226

Ho un ScrollView che circonda il mio intero layout in modo che l'intero schermo sia scorrevole. Il primo elemento che ho in questo ScrollView è un blocco HorizontalScrollView che ha funzionalità che possono essere fatte scorrere orizzontalmente. Ho aggiunto un intouchlistener alla visualizzazione orizzontale per gestire gli eventi touch e forzare la vista a "scattare" sull'immagine più vicina all'evento ACTION_UP.

Quindi l'effetto che sto cercando è come la schermata iniziale di Android in cui è possibile scorrere da uno all'altro e scatta su uno schermo quando si solleva il dito.

Tutto funziona alla grande, tranne per un problema: ho bisogno di scorrere da sinistra a destra quasi perfettamente in orizzontale affinché un ACTION_UP possa registrarsi. Se scorro per lo meno verticalmente (cosa che penso che molte persone tendano a fare sui loro telefoni quando passano da una parte all'altra), riceverò un ACTION_CANCEL invece di un ACTION_UP. La mia teoria è che ciò avviene perché la vista orizzontale è all'interno di una vista di scorrimento e la vista di scorrimento dirotta il tocco verticale per consentire lo scorrimento verticale.

Come posso disabilitare gli eventi tattili per la vista di scorrimento appena all'interno della mia vista di scorrimento orizzontale, ma consentire comunque il normale scorrimento verticale altrove nella vista di scorrimento?

Ecco un esempio del mio codice:

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

Ho provato tutti i metodi in questo post, ma nessuno di loro funziona per me. Sto usando la MeetMe's HorizontalListViewbiblioteca.
The Nomad,

C'è un articolo con un codice simile ( HomeFeatureLayout extends HorizontalScrollView) qui velir.com/blog/index.php/2010/11/17/… Ci sono alcuni commenti aggiuntivi su ciò che sta accadendo mentre la classe di scorrimento personalizzata è composta.
CJBS,

Risposte:


280

Aggiornamento: l'ho capito. Sul mio ScrollView, avevo bisogno di sovrascrivere il metodo onInterceptTouchEvent per intercettare l'evento touch solo se il movimento Y è> movimento X. Sembra che il comportamento predefinito di ScrollView sia quello di intercettare l'evento touch ogni qualvolta si presenti QUALSIASI movimento Y. Pertanto, con la correzione, ScrollView intercetterà l'evento solo se l'utente sta deliberatamente scorrendo nella direzione Y e in tal caso trasmette ACTION_CANCEL ai bambini.

Ecco il codice per la mia classe Scroll View che 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);
        }
    }
}

sto usando lo stesso ma al momento dello scorrimento nella direzione x sto ottenendo un'eccezione ECCELLENTE PUNTATORE NULL su public boolean onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } Ho usato lo scorrimento orizzontale all'interno di scrollview
Vipin Sahu

@Tutto grazie funziona, dimentico di aver inizializzato il rilevatore di gesti nel costruttore scrollview
Vipin Sahu

Come dovrei usare questo codice per implementare un ViewPager in ScrollView
Harsha MV

Ho avuto alcuni problemi con questo codice quando avevo una vista griglia sempre estesa, a volte non scorreva. Ho dovuto ignorare il metodo onDown (...) in YScrollDetector per restituire sempre true come suggerito nella documentazione (come qui developer.android.com/training/custom-views/… ) Questo ha risolto il mio problema.
Nemanja Kovacevic,

3
Ho appena incontrato un piccolo bug che vale la pena menzionare. Credo che il codice in onInterceptTouchEvent dovrebbe dividere le due chiamate booleane, per garantire che venga mGestureDetector.onTouchEvent(ev)chiamato. Come è ora, non verrà chiamato se super.onInterceptTouchEvent(ev)è falso. Ho appena incontrato un caso in cui i bambini cliccabili nella vista di scorrimento possono catturare gli eventi touch e onScroll non verrà chiamato affatto. Altrimenti, grazie, ottima risposta!
Accidenti

176

Grazie Joel per avermi dato un indizio su come risolvere questo problema.

Ho semplificato il codice (senza bisogno di un GestureDetector ) per ottenere lo stesso effetto:

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

eccellente, grazie per questo refactor. Stavo riscontrando problemi con l'approccio sopra quando scorrevo in fondo all'elencoView poiché qualsiasi comportamento tattile sugli elementi figlio ha iniziato a non essere intercettato, indipendentemente dal rapporto di movimento Y / X. Strano!
Dori,

1
Grazie! Funzionava anche con un ViewPager all'interno di un ListView, con un ListView personalizzato.
Sharief Shaik,

2
Ho appena sostituito la risposta accettata con questa e ora funziona molto meglio per me. Grazie!
David Scott,

1
@VipinSahu, per dire la direzione dello spostamento del tocco, puoi prendere il delta della coordinata X corrente e lastX, se è maggiore di 0, il tocco si sposta da sinistra a destra, altrimenti da destra a sinistra. E poi salvi la X corrente come lastX per il prossimo calcolo.
Neevek,

1
che dire di scrollview orizzontale?
Zin Win Htet

60

Penso di aver trovato una soluzione più semplice, solo che utilizza una sottoclasse di ViewPager anziché (suo padre) ScrollView.

AGGIORNAMENTO 2013-07-16 : Ho aggiunto una sostituzione peronTouchEvent . Potrebbe eventualmente aiutare con i problemi menzionati nei commenti, anche se 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;
    }
}

Questo è simile alla tecnica usata in onScroll () di android.widget.Gallery . È ulteriormente spiegato dalla presentazione di Google I / O 2013 Scrittura di visualizzazioni personalizzate per Android .

Aggiornamento 2013-12-10 : un approccio simile è anche descritto in un post di Kirill Grouchnikov sull'app (allora) Android Market .


boolean ret = super.onInterceptTouchEvent (ev); sempre e solo restituito falso per me. Usando su UninterceptableViewPager in una vista di scorrimento
scottyab il

Questo non funziona per me, anche se mi piace la sua semplicità. Uso a ScrollViewcon a LinearLayoutin cui UninterceptableViewPagerè posizionato. Anzi, retè sempre falso ... Qualche idea su come risolvere questo problema?
Peterdk,

@scottyab & @Peterdk: Beh, il mio è in un TableRowche è dentro un TableLayoutche è dentro un ScrollView(sì, lo so ...), e funziona come previsto. Forse potresti provare a ignorare onScrollinvece di onInterceptTouchEvent, come fa Google (linea 1010)
Giorgos Kylafas

Ho appena codificato ret come true e funziona benissimo. Stava tornando falso prima, ma credo che sia perché lo scrollview ha un layout lineare che contiene tutti i bambini
Amanni

11

Ho scoperto che a volte uno ScrollView riacquista il focus e l'altro perde il focus. Puoi impedirlo, concedendo solo uno dei focus 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;
        }
    });

quale adattatore stai passando?
Harsha MV,

Ho ricontrollato e mi sono reso conto che in realtà non sto usando un ScrollView, ma un ViewPager e gli sto passando un FragmentStatePagerAdapter che include tutte le immagini per la galleria.
Marius Esilarante

8

Non funzionava bene per me. L'ho cambiato e ora funziona senza problemi. Se qualcuno fosse interessato.

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

Questa è la risposta per il mio progetto. Ho un cercapersone di visualizzazione che funge da galleria può essere cliccato in una vista di scorrimento. Uso la soluzione fornita sopra, funziona su scorrimento orizzontale, ma dopo aver fatto clic sull'immagine del cercapersone che avvia una nuova attività e torna indietro, il cercapersone non può scorrere. Funziona bene, tks!
Longkai,

Funziona perfettamente per me. Avevo una vista "scorri per sbloccare" personalizzata all'interno di una vista di scorrimento che mi dava lo stesso problema. Questa soluzione ha risolto il problema.
ibrido

6

Grazie a Neevek la sua risposta ha funzionato per me ma non blocca lo scorrimento verticale quando l'utente ha iniziato a scorrere la vista orizzontale (ViewPager) in direzione orizzontale e quindi senza sollevare il dito scorrere verticalmente inizia a scorrere la vista contenitore sottostante (ScrollView) . L'ho corretto apportando una leggera modifica al codice di 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);

}

5

Questo alla fine è diventato parte della libreria di supporto v4, NestedScrollView . Quindi, non sono più necessari hack locali per la maggior parte dei casi, immagino.


1

La soluzione di Neevek funziona meglio di quella di Joel su dispositivi con 3.2 e versioni successive. C'è un bug in Android che causerà java.lang.IllegalArgumentException: pointerIndex fuori portata se un rilevatore di gesti viene utilizzato all'interno di uno scollview. Per duplicare il problema, implementa uno scollview personalizzato come suggerito da Joel e inserisci un cercapersone all'interno. Se trascini (non sollevare la tua figura) in una direzione (sinistra / destra) e poi nell'opposto, vedrai l'incidente. Anche nella soluzione di Joel, se trascini il cercapersone della vista spostando il dito in diagonale, una volta che il dito lascia l'area di visualizzazione del contenuto del cercapersone della vista, il cercapersone tornerà alla sua posizione precedente. Tutti questi problemi hanno più a che fare con il design interno di Android o la sua mancanza rispetto all'implementazione di Joel, che a sua volta è un pezzo di codice intelligente e conciso.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.