web-dev-qa-db-fra.com

HorizontalScrollView dans ScrollView Touch Handling

J'ai un ScrollView qui entoure toute ma mise en page de sorte que tout l'écran est défilable. Le premier élément que j'ai dans cette ScrollView est un bloc HorizontalScrollView qui possède des fonctionnalités qui peuvent être défilées horizontalement. J'ai ajouté un ontouchlistener à horizontalscrollview pour gérer les événements tactiles et forcer la vue à "s'aligner" sur l'image la plus proche de l'événement ACTION_UP.

L’effet recherché correspond donc à l’écran d'accueil Android où vous pouvez faire défiler l'écran de l'un à l'autre et qui s'aligne sur un seul écran lorsque vous levez la main.

Tout cela fonctionne très bien, sauf un problème: je dois glisser de gauche à droite presque parfaitement horizontalement pour qu'un ACTION_UP puisse jamais être enregistré. Si je glisse le moins possible verticalement (ce que je pense que beaucoup de gens ont tendance à faire sur leur téléphone lorsqu'ils glissent 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 horizontalscrollview se trouve dans une vue à défilement et que celle-ci détourne le contact vertical pour permettre le défilement vertical.

Comment puis-je désactiver les événements tactiles pour le défilement à partir de mon champ de défilement horizontal, tout en permettant un défilement vertical normal ailleurs dans le 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;
        }
    }
}
223
Joel

Mise à jour: j'ai compris cela. Sur ma ScrollView, je devais remplacer la méthode onInterceptTouchEvent pour n'intercepter l'événement tactile que 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 quelconque en Y. Ainsi, avec le correctif, ScrollView n'interceptera l'événement que si l'utilisateur fait défiler délibérément dans la direction Y et, dans ce cas, transmet l'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);
        }
    }
}
278
Joel

Merci Joel de me donner un indice sur la façon de résoudre ce problème.

J'ai simplifié le code (sans avoir 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);
    }
}
175
neevek

Je pense avoir trouvé une solution plus simple, mais elle utilise une sous-classe de ViewPager au lieu de (son parent) ScrollView.

UPDATE 2013-07-16 : J'ai également ajouté un remplacement pour onTouchEvent. Cela pourrait peut-être 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 () d'Android.widget.Gallery. La présentation de Google I/O 2013 explique les vues personnalisées pour Android .

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

60
Giorgos Kylafas

J'ai découvert que parfois un ScrollView regagne le focus et que l'autre perd le focus. Vous pouvez empêcher cela en n'accordant que l'un des focus de 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;
        }
    });
11
Marius Hilarious

Cela ne fonctionnait pas bien pour moi. Je l'ai changé et maintenant cela fonctionne 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;
        }
    }
}
8
snapix

Grâce à Neevek, sa réponse a fonctionné pour moi, mais elle ne verrouille pas le défilement vertical lorsque l'utilisateur a commencé à faire défiler la vue horizontale (ViewPager) dans le sens horizontal. Ensuite, sans lever le doigt, le défilement vertical commence à faire défiler la vue du conteneur sous-jacent (ScrollView). . Je l'ai corrigé en apportant une légère modification au 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);

}
6
Saqib

Ceci est finalement devenu une partie de la bibliothèque de support v4, NestedScrollView . Donc, je suppose que la plupart des cas ne nécessitent plus de piratage local.

5
Ebrahim Byagowi

La solution de Neevek fonctionne mieux que celle de Joel sur les appareils fonctionnant à partir de 3.2. Il y a un bogue dans Android qui entraînera une exception Java.lang.IllegalArgumentException: pointerIndex si un détecteur de gestes est utilisé dans une vue de défilement. Pour dupliquer le problème, implémentez une vue de défilement personnalisée comme suggéré par Joel et insérez un pager de vue à l'intérieur. Si vous traînez (ne soulevez pas votre silhouette) dans une direction (gauche/droite), puis dans le sens opposé, vous verrez le crash. Toujours dans la solution de Joel, si vous faites glisser le pager de vue en déplaçant votre doigt en diagonale, une fois que celui-ci quitte la zone de visualisation du contenu du pager de vue, celui-ci 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 en soi un code intelligent et concis.

http://code.google.com/p/Android/issues/detail?id=1899

1
Don