227 votes

Android - HorizontalScrollView dans le ScrollView Touch Manutention

J'ai un ScrollView qui entoure toute ma mise en page, de sorte que la totalité de l'écran est scrollable. Le premier élément que j'ai dans ce ScrollView est un HorizontalScrollView bloc qui a des caractéristiques qui peuvent être défiler horizontalement. J'ai ajouté une ontouchlistener à la horizontalscrollview pour gérer les événements tactiles et forcer l'affichage à "snap" le plus proche de l'image sur le ACTION_UP événement.

Ainsi, l'effet que je vais faire, c'est comme le stock android à l'écran d'accueil où vous pouvez vous déplacer de l'un à l'autre et il s'aligne sur un seul écran lorsque vous soulevez votre doigt.

Tout cela fonctionne très bien, sauf pour un seul problème: j'ai besoin de passer de gauche à droite, presque parfaitement à l'horizontale pour un ACTION_UP à jamais de vous inscrire. Si j'ai glisser verticalement dans le moins (je pense que beaucoup de gens ont tendance à le faire sur leur téléphone lors de glisser un côté à l'autre), je reçois un ACTION_CANCEL au lieu d'une ACTION_UP. Ma théorie est que c'est parce que le horizontalscrollview est dans un scrollview, et la scrollview est le détournement de la verticale tactile pour permettre le défilement vertical.

Comment puis-je désactiver les événements tactiles pour la scrollview de juste dans mon horizontale scrollview, mais permettent tout de même normal de défilement vertical ailleurs dans le scrollview?

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

}

285voto

Joel Points 3335

Mise à jour: j'ai compris cela. Sur mon ScrollView, j'avais besoin de remplacer la onInterceptTouchEvent méthode à seulement intercepter l'événement tactile si l'axe de mouvement est > le X de mouvement. Il semble que le comportement par défaut d'un ScrollView est d'intercepter l'événement de touche chaque fois qu'il est Y de mouvement. Donc, avec le correctif, la ScrollView ne intercepter l'événement, si l'utilisateur est délibérément défilement dans la direction Y et dans ce cas passer au large de la ACTION_CANCEL pour les enfants.

Voici le code de mon Défiler la Vue de la classe qui contient la HorizontalScrollView:

public class CustomScrollView extends ScrollView {
    private GestureDetector mGestureDetector;
    View.OnTouchListener mGestureListener;

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

178voto

neevek Points 4794

Merci Joel pour me donner une idée sur comment 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);
}
}

60voto

Giorgos Kylafas Points 1077

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

Mise à JOUR 2013-07-16: j'ai ajouté un remplacement pour onTouchEvent . Il pourrait peut-être contribuer à la résolution des 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 android.widget de.Galerie onScroll(). Il est expliqué plus en détail par la Google I/O 2013 présentation de l'Écriture de Vues Personnalisées pour Android.

Mise à jour 2013-12-10: Une approche similaire est décrit dans un post de Kirill Grouchnikov à propos de l'application Android Market.

14voto

Marius Hilarious Points 432

J'ai trouvé que somethimes un ScrollView reprend focus et l'autre perd le focus. Vous pouvez empêcher que, par l'octroi d'un de la scrollView focus:

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

6voto

Saqib Points 630

Grâce à Neevek sa réponse a fonctionné pour moi, mais il n'a pas de verrouillage du défilement vertical lorsque l'utilisateur a commencé à faire défiler la vue horizontale(ViewPager) dans le sens horizontal et puis, sans lever le doigt de défilement verticale, il commence à faire défiler le conteneur sous-jacent de la vue(le ScrollView). Je l'ai corrigé en faisant un léger changement dans la Neevak code:

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

}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X