HorizontalScrollView in ScrollView Touch Handling

226

Ich habe eine ScrollView, die mein gesamtes Layout umgibt, sodass der gesamte Bildschirm scrollbar ist. Das erste Element in dieser ScrollView ist ein HorizontalScrollView-Block mit Funktionen, durch die horizontal gescrollt werden kann. Ich habe der horizontalen Bildlaufansicht einen Ontouchlistener hinzugefügt, um Berührungsereignisse zu verarbeiten und die Ansicht zu zwingen, am nächsten Bild des ACTION_UP-Ereignisses zu "fangen".

Der Effekt, den ich anstrebe, ist also wie der Standard-Android-Homescreen, auf dem Sie von einem zum anderen scrollen können und der auf einem Bildschirm einrastet, wenn Sie Ihren Finger heben.

Dies alles funktioniert bis auf ein Problem hervorragend: Ich muss fast perfekt horizontal von links nach rechts wischen, damit sich ein ACTION_UP jemals registriert. Wenn ich im geringsten vertikal wische (was meiner Meinung nach viele Leute auf ihren Handys tun, wenn sie von einer Seite zur anderen wischen), erhalte ich eine ACTION_CANCEL anstelle einer ACTION_UP. Meine Theorie ist, dass dies daran liegt, dass sich die horizontale Bildlaufansicht innerhalb einer Bildlaufansicht befindet und die Bildlaufansicht die vertikale Berührung entführt, um ein vertikales Bildlauf zu ermöglichen.

Wie kann ich die Berührungsereignisse für die Bildlaufansicht nur in meiner horizontalen Bildlaufansicht deaktivieren und trotzdem das normale vertikale Bildlauf an anderer Stelle in der Bildlaufansicht zulassen?

Hier ist ein Beispiel meines Codes:

   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
quelle
Ich habe alle Methoden in diesem Beitrag ausprobiert, aber keine davon funktioniert für mich. Ich benutze die MeetMe's HorizontalListViewBibliothek.
Der Nomade
Es gibt einen Artikel mit ähnlichem Code ( HomeFeatureLayout extends HorizontalScrollView) hier velir.com/blog/index.php/2010/11/17/… Es gibt einige zusätzliche Kommentare dazu, was passiert , wenn die benutzerdefinierte Bildlaufklasse zusammengesetzt wird.
CJBS

Antworten:

280

Update: Ich habe das herausgefunden. In meiner ScrollView musste ich die onInterceptTouchEvent-Methode überschreiben, um das Berührungsereignis nur abzufangen, wenn die Y-Bewegung> die X-Bewegung ist. Es scheint, als ob das Standardverhalten einer ScrollView darin besteht, das Berührungsereignis abzufangen, wenn eine Y-Bewegung vorliegt. Mit dem Fix fängt die ScrollView das Ereignis nur ab, wenn der Benutzer absichtlich in die Y-Richtung scrollt und in diesem Fall die ACTION_CANCEL an die untergeordneten Elemente weitergibt.

Hier ist der Code für meine Scroll View-Klasse, die die HorizontalScrollView enthält:

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
quelle
Ich verwende dasselbe, aber zum Zeitpunkt des Bildlaufs in x-Richtung erhalte ich die Ausnahme NULL POINTER EXCEPTION bei public boolean onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } Ich habe horizontale Schriftrolle in der Bildlaufansicht verwendet
Vipin Sahu
@All danke, es funktioniert, ich vergesse, den Gesten-Detektor im Scrollview-Konstruktor zu initialisieren
Vipin Sahu
Wie soll ich diesen Code für die Implementierung eines ViewPagers in ScrollView
Harsha MV
Ich hatte einige Probleme mit diesem Code, als ich eine immer erweiterte Rasteransicht darin hatte, manchmal wurde nicht gescrollt. Ich musste die onDown (...) -Methode in YScrollDetector überschreiben, um immer true zurückzugeben, wie in der gesamten Dokumentation vorgeschlagen (wie hier developer.android.com/training/custom-views/… ). Dies löste mein Problem.
Nemanja Kovacevic
3
Ich bin gerade auf einen kleinen Fehler gestoßen, der erwähnenswert ist. Ich glaube, dass der Code in onInterceptTouchEvent die beiden booleschen Aufrufe aufteilen sollte, um zu gewährleisten, dass mGestureDetector.onTouchEvent(ev)dieser aufgerufen wird. So wie es jetzt ist, wird es nicht aufgerufen, wenn super.onInterceptTouchEvent(ev)es falsch ist. Ich bin gerade auf einen Fall gestoßen, in dem anklickbare Kinder in der Bildlaufansicht die Berührungsereignisse erfassen können und onScroll überhaupt nicht aufgerufen wird. Ansonsten danke, tolle Antwort!
GLee
176

Vielen Dank, Joel, dass Sie mir einen Hinweis gegeben haben, wie ich dieses Problem lösen kann.

Ich habe den Code vereinfacht (ohne dass ein GestureDetector erforderlich ist ), um den gleichen Effekt zu erzielen:

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
quelle
ausgezeichnet, danke für diesen Refaktor. Beim Scrollen zum Ende der Listenansicht traten Probleme mit dem obigen Ansatz auf, da das Berührungsverhalten über untergeordnete Elemente unabhängig vom Y / X-Bewegungsverhältnis nicht mehr abgefangen wurde. Seltsam!
Dori
1
Vielen Dank! Arbeitete auch mit einem ViewPager in einer ListView mit einer benutzerdefinierten ListView.
Sharief Shaik
2
Ich habe gerade die akzeptierte Antwort durch diese ersetzt und es funktioniert jetzt viel besser für mich. Vielen Dank!
David Scott
1
@VipinSahu, um die Bewegungsrichtung der Berührung zu bestimmen, können Sie das Delta der aktuellen X-Koordinate und lastX nehmen. Wenn es größer als 0 ist, bewegt sich die Berührung von links nach rechts, andernfalls von rechts nach links. Und dann speichern Sie das aktuelle X als lastX für die nächste Berechnung.
Neevek
1
Was ist mit der horizontalen Bildlaufansicht?
Zin Win Htet
60

Ich glaube, ich habe eine einfachere Lösung gefunden. Nur diese verwendet eine Unterklasse von ViewPager anstelle von (der übergeordneten) ScrollView.

UPDATE 2013-07-16 : Ich habe auch eine Überschreibung für hinzugefügt onTouchEvent. Es könnte möglicherweise bei den in den Kommentaren genannten Problemen helfen, obwohl 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;
    }
}

Dies ähnelt der in onScroll () von android.widget.Gallery verwendeten Technik . Weitere Informationen finden Sie in der Google I / O 2013-Präsentation Schreiben von benutzerdefinierten Ansichten für Android .

Update 10.12.2013 : Ein ähnlicher Ansatz wird auch in einem Beitrag von Kirill Grouchnikov über die (damalige) Android Market App beschrieben .

Giorgos Kylafas
quelle
boolean ret = super.onInterceptTouchEvent (ev); nur immer falsch für mich zurückgegeben. Verwenden auf UninterceptableViewPager in einer Bildlaufansicht
Scottyab
Das funktioniert bei mir nicht, obwohl ich es einfach mag. Ich benutze ein ScrollViewmit einem, LinearLayoutin das das UninterceptableViewPagergelegt wird. In der Tat ist retimmer falsch ... Gibt es einen Hinweis, wie man das behebt?
Peterdk
@scottyab & @Peterdk: Nun, meine befindet sich in einer, TableRowdie sich in einer befindet, TableLayoutdie sich in einer befindet ScrollView(ja, ich weiß ...), und sie funktioniert wie beabsichtigt. Vielleicht könnten Sie versuchen, onScrollstattdessen zu überschreiben onInterceptTouchEvent, wie es Google tut (Zeile 1010)
Giorgos Kylafas
Ich habe gerade ret als wahr fest codiert und es funktioniert gut. Es war früher falsch, aber ich glaube, das liegt daran, dass die Bildlaufansicht ein lineares Layout hat, das alle Kinder enthält
Amanni
11

Ich habe herausgefunden, dass manchmal eine ScrollView den Fokus wiedererlangt und die andere den Fokus verliert. Sie können dies verhindern, indem Sie nur einen der scrollView-Fokusse gewähren:

    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 urkomisch
quelle
Welchen Adapter übergeben Sie?
Harsha MV
Ich habe erneut nachgesehen und festgestellt, dass es sich nicht um eine von mir verwendete ScrollView handelt, sondern um einen ViewPager, und ich übergebe ihm einen FragmentStatePagerAdapter, der alle Bilder für die Galerie enthält.
Marius Hilarious
8

Es hat bei mir nicht gut funktioniert. Ich habe es geändert und jetzt funktioniert es reibungslos. Wenn jemand interessiert ist.

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
quelle
Dies ist die Antwort für mein Projekt. Ich habe einen Ansichts-Pager, der als Galerie fungiert und in einer Bildlaufansicht angeklickt werden kann. Ich verwende die oben bereitgestellte Lösung, sie funktioniert beim horizontalen Scrollen, aber nachdem ich auf das Bild des Pagers geklickt habe, das eine neue Aktivität startet und zurückgeht, kann der Pager nicht scrollen. Das funktioniert gut, tks!
Longkai
Funktioniert perfekt für mich. Ich hatte eine benutzerdefinierte "Swipe to Unlock" -Ansicht in einer Bildlaufansicht, die mir die gleichen Probleme bereitete. Diese Lösung löste das Problem.
Hybrid
6

Dank Neevek hat seine Antwort für mich funktioniert, aber das vertikale Scrollen wird nicht gesperrt, wenn der Benutzer mit dem Scrollen der horizontalen Ansicht (ViewPager) in horizontaler Richtung begonnen hat. Ohne den Finger vertikal zu scrollen, wird die zugrunde liegende Containeransicht (ScrollView) gescrollt. . Ich habe das Problem behoben, indem ich Neevaks Code leicht geändert habe:

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
quelle
5

Dies wurde schließlich Teil der Support v4-Bibliothek NestedScrollView . Daher werden in den meisten Fällen keine lokalen Hacks mehr benötigt.

Ebrahim Byagowi
quelle
1

Die Lösung von Neevek funktioniert besser als die von Joel auf Geräten mit Version 3.2 und höher. Es gibt einen Fehler in Android, der java.lang.IllegalArgumentException verursacht: pointerIndex außerhalb des Bereichs, wenn ein Gestenmelder in einer Scollview verwendet wird. Um das Problem zu duplizieren, implementieren Sie eine benutzerdefinierte Ansicht, wie Joel vorgeschlagen hat, und fügen Sie einen Ansichtspager ein. Wenn Sie Ihre Figur in eine Richtung (links / rechts) und dann in die entgegengesetzte Richtung ziehen (nicht anheben), sehen Sie den Absturz. Wenn Sie in Joels Lösung den Ansichtspager ziehen, indem Sie Ihren Finger diagonal bewegen, springt der Pager an seine vorherige Position zurück, sobald Ihr Finger den Inhaltsansichtsbereich des Ansichtspagers verlässt. All diese Probleme haben mehr mit dem internen Design oder dem Fehlen von Android zu tun als mit Joels Implementierung, die selbst ein Stück intelligenter und prägnanter Code ist.

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

Don
quelle