HorizontalScrollView dans ScrollView Touch Handling

226

J'ai un ScrollView qui entoure toute ma mise en page afin que tout l'écran puisse défiler. Le premier élément que j'ai dans ce ScrollView est un bloc HorizontalScrollView qui a des fonctionnalités qui peuvent être parcourues horizontalement. J'ai ajouté un écouteur d'écoute à la vue de défilement horizontale pour gérer les événements tactiles et forcer la vue à "s'accrochage" à l'image la plus proche sur l'événement ACTION_UP.

L'effet que je recherche est donc comme l'écran d'accueil Android d'origine, où vous pouvez faire défiler de l'un à l'autre et il s'enclenche sur un écran lorsque vous soulevez votre doigt.

Tout cela fonctionne très bien, sauf pour un problème: je dois balayer de gauche à droite presque parfaitement horizontalement pour qu'un ACTION_UP s'enregistre. Si je glisse verticalement à tout le moins (ce que je pense que beaucoup de gens ont tendance à faire sur leur téléphone en balayant d'un côté à l'autre), je recevrai un ACTION_CANCEL au lieu d'un ACTION_UP. Ma théorie est que cela est dû au fait que la vue de défilement horizontal est dans une vue de défilement, et que la vue de défilement détourne le toucher vertical pour permettre le défilement vertical.

Comment puis-je désactiver les événements tactiles pour la vue de défilement à partir de ma vue de défilement horizontale tout en permettant un défilement vertical normal ailleurs dans la vue de défilement?

Voici un exemple de mon code:

   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
la source
J'ai essayé toutes les méthodes de ce post, mais aucune ne fonctionne pour moi. J'utilise la MeetMe's HorizontalListViewbibliothèque.
The Nomad
Il y a un article avec un code similaire ( HomeFeatureLayout extends HorizontalScrollView) ici velir.com/blog/index.php/2010/11/17/… Il y a quelques commentaires supplémentaires sur ce qui se passe pendant que la classe de défilement personnalisée est composée.
CJBS le

Réponses:

280

Mise à jour: j'ai compris cela. Sur mon ScrollView, j'avais besoin de remplacer la méthode onInterceptTouchEvent pour intercepter l'événement tactile uniquement si le mouvement Y est> le mouvement X. Il semble que le comportement par défaut d'un ScrollView consiste à intercepter l'événement tactile chaque fois qu'il y a un mouvement Y. Ainsi, avec le correctif, ScrollView n'interceptera l'événement que si l'utilisateur fait délibérément défiler dans la direction Y et dans ce cas, transmettra ACTION_CANCEL aux enfants.

Voici le code de ma classe Scroll View qui contient 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
la source
j'utilise ce même mais au moment du défilement dans la direction x, je reçois une exception NULL POINTER EXCEPTION au public booléen onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } J'ai utilisé le défilement horizontal dans scrollview
Vipin Sahu
@Tous merci ça marche, j'oublie d'initialiser le détecteur de gestes dans scrollview constructeur
Vipin Sahu
Comment dois-je utiliser ce code pour implémenter un ViewPager dans ScrollView
Harsha MV
J'ai eu quelques problèmes avec ce code lorsque j'avais une vue de grille toujours étendue, parfois il ne défilait pas. J'ai dû remplacer la méthode onDown (...) dans YScrollDetector pour toujours retourner true comme suggéré dans la documentation (comme ici developer.android.com/training/custom-views/… ) Cela a résolu mon problème.
Nemanja Kovacevic
3
Je viens de rencontrer un petit bug qui mérite d'être mentionné. Je crois que le code dans onInterceptTouchEvent devrait diviser les deux appels booléens, pour garantir que ce mGestureDetector.onTouchEvent(ev)sera appelé. Comme c'est le cas maintenant, il ne sera pas appelé si super.onInterceptTouchEvent(ev)est faux. Je viens de tomber sur un cas où les enfants cliquables dans la vue de défilement peuvent saisir les événements tactiles et onScroll ne sera pas appelé du tout. Sinon, merci, bonne réponse!
GLee
176

Merci Joel de m'avoir donné un indice sur la façon de résoudre ce problème.

J'ai simplifié le code (sans besoin d'un GestureDetector ) pour obtenir le même effet:

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
la source
excellent, merci pour ce refactor. J'obtenais des problèmes avec l'approche ci-dessus lors du défilement vers le bas de la liste. Bizarre!
Dori
1
Merci! Fonctionne également avec un ViewPager dans un ListView, avec un ListView personnalisé.
Sharief Shaik
2
Je viens de remplacer la réponse acceptée par ceci et cela fonctionne beaucoup mieux pour moi maintenant. Merci!
David Scott
1
@VipinSahu, pour indiquer la direction du mouvement tactile, vous pouvez prendre le delta des coordonnées X actuelles et lastX, s'il est supérieur à 0, le toucher se déplace de gauche à droite, sinon de droite à gauche. Et puis vous enregistrez le X actuel comme lastX pour le prochain calcul.
neevek
1
qu'en est-il du scrollview horizontal?
Zin Win Htet
60

Je pense que j'ai trouvé une solution plus simple, seulement cela utilise une sous-classe de ViewPager au lieu de (son parent) ScrollView.

MISE À JOUR 2013-07-16 : J'ai également ajouté un remplacement pour onTouchEvent. Cela pourrait éventuellement aider avec les problèmes mentionnés dans les commentaires, bien que 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;
    }
}

Ceci est similaire à la technique utilisée dans onScroll () de android.widget.Gallery . Cela est expliqué plus en détail dans la présentation Google I / O 2013 Writing Custom Views for Android .

Mise à jour 2013-12-10 : Une approche similaire est également décrite dans un article de Kirill Grouchnikov sur l'application (alors) Android Market .

Giorgos Kylafas
la source
boolean ret = super.onInterceptTouchEvent (ev); n'a jamais retourné faux pour moi. Utilisation sur UninterceptableViewPager dans une vue de défilement
scottyab
Cela ne fonctionne pas pour moi, même si j'aime sa simplicité. J'utilise un ScrollViewavec un LinearLayoutdans lequel le UninterceptableViewPagerest placé. En effet, c'est rettoujours faux ... Une idée de comment résoudre ce problème?
Peterdk
@scottyab & @Peterdk: Eh bien, le mien est dans un TableRowqui est à l'intérieur d'un TableLayoutqui est à l'intérieur d'un ScrollView(ouais, je sais ...), et il fonctionne comme prévu. Vous pourriez peut-être essayer de onScrollremplacer au lieu de onInterceptTouchEvent, comme Google le fait (ligne 1010)
Giorgos Kylafas
Je viens de coder en dur comme vrai et cela fonctionne bien. Il retournait faux plus tôt, mais je pense que c'est parce que la vue de défilement a une disposition linéaire qui contient tous les enfants
Amanni
11

J'ai découvert que parfois un ScrollView retrouve le focus et l'autre perd le focus. Vous pouvez éviter cela, en accordant uniquement l'un des 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;
        }
    });
Marius Hilarious
la source
quel adaptateur passez-vous?
Harsha MV
J'ai vérifié à nouveau et j'ai réalisé que ce n'est pas réellement un ScrollView que j'utilise, mais un ViewPager et je lui passe un FragmentStatePagerAdapter qui inclut toutes les images de la galerie.
Marius Hilarious
8

Ça ne fonctionnait pas bien pour moi. Je l'ai changé et maintenant ça marche bien. Si quelqu'un est intéressé.

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
la source
Telle est la réponse à mon projet. J'ai un pager de vue qui agit comme une galerie qui peut être cliquée dans une vue de défilement J'utilise la solution fournie ci-dessus, cela fonctionne sur le défilement horizontal, mais après avoir cliqué sur l'image du pager qui démarre une nouvelle activité et revient en arrière, le pager ne peut pas défiler. Cela fonctionne bien, tks!
longkai
Fonctionne parfaitement pour moi. J'avais une vue personnalisée "glisser pour déverrouiller" à l'intérieur d'une vue de défilement qui me causait le même problème. Cette solution a résolu le problème.
hybride
6

Grâce à Neevek, sa réponse a fonctionné pour moi, mais elle ne bloque pas le défilement vertical lorsque l'utilisateur a commencé à faire défiler la vue horizontale (ViewPager) dans le sens horizontal, puis sans lever le défilement vertical, il commence à faire défiler la vue du conteneur sous-jacent (ScrollView) . Je l'ai corrigé en faisant un léger changement dans le code 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
la source
5

Cela est finalement devenu une partie de la bibliothèque de support v4, NestedScrollView . Donc, plus de piratages locaux ne sont nécessaires dans la plupart des cas, je suppose.

Ebrahim Byagowi
la source
1

La solution de Neevek fonctionne mieux que celle de Joel sur les appareils exécutant la version 3.2 et supérieure. Il y a un bogue dans Android qui provoquera java.lang.IllegalArgumentException: pointerIndex hors de portée si un détecteur de gestes est utilisé à l'intérieur d'une scollview. Pour dupliquer le problème, implémentez un scollview personnalisé comme l'a suggéré Joel et placez un pager de vue à l'intérieur. Si vous faites glisser (ne soulevez pas votre silhouette) dans une direction (gauche / droite) puis dans l'autre sens, vous verrez le crash. Toujours dans la solution de Joel, si vous faites glisser le téléavertisseur de vue en déplaçant votre doigt en diagonale, une fois que votre doigt quitte la zone d'affichage du contenu du téléavertisseur, le téléavertisseur revient à sa position précédente. Tous ces problèmes sont davantage liés à la conception interne d'Android ou à son absence qu'à la mise en œuvre de Joel, qui est elle-même un morceau de code intelligent et concis.

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

Don
la source