HorizontalScrollView w ScrollView Touch Handling

226

Mam ScrollView, który otacza cały mój układ, więc cały ekran można przewijać. Pierwszym elementem, który mam w tym ScrollView, jest blok HorizontalScrollView, który ma funkcje, które można przewijać w poziomie. Dodałem ontouchlistener do widoku przewijania w poziomie, aby obsługiwać zdarzenia dotykowe i zmusić widok do „przyciągania” do najbliższego obrazu w zdarzeniu ACTION_UP.

Tak więc efekt, który zamierzam, jest podobny do zwykłego ekranu głównego Androida, w którym można przewijać od jednego do drugiego, i przesuwa się na jeden ekran po podniesieniu palca.

To wszystko działa świetnie, z wyjątkiem jednego problemu: muszę przesuwać od lewej do prawej prawie idealnie poziomo, aby ACTION_UP kiedykolwiek się zarejestrował. Jeśli przynajmniej przesunę w pionie (co myślę, że wiele osób ma tendencję do robienia na swoich telefonach podczas przesuwania na boki), otrzymam ACTION_CANCEL zamiast ACTION_UP. Moja teoria jest taka, że ​​dzieje się tak, ponieważ widok poziomy jest przewijany w widoku przewijania, a widok przewijania przejmuje dotyk pionowy, aby umożliwić przewijanie w pionie.

Jak mogę wyłączyć zdarzenia dotykowe dla widoku przewijania bezpośrednio z mojego poziomego widoku przewijania, ale nadal pozwolić na normalne przewijanie w pionie w innym miejscu widoku przewijania?

Oto przykład mojego kodu:

   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
źródło
Próbowałem wszystkich metod w tym poście, ale żadna z nich nie działa dla mnie. Korzystam z MeetMe's HorizontalListViewbiblioteki.
The Nomad
Jest artykuł z podobnym kodem ( HomeFeatureLayout extends HorizontalScrollView) tutaj velir.com/blog/index.php/2010/11/17/... Istnieją dodatkowe komentarze na temat tego, co się dzieje, gdy tworzona jest niestandardowa klasa przewijania.
CJBS

Odpowiedzi:

280

Aktualizacja: rozgryzłem to. W moim ScrollView musiałem przesłonić metodę onInterceptTouchEvent, aby przechwycić zdarzenie dotyku tylko wtedy, gdy ruch Y jest> ruchem X. Wygląda na to, że domyślnym zachowaniem ScrollView jest przechwytywanie zdarzenia dotyku, ilekroć występuje KAŻDY ruch Y. Dzięki tej poprawce ScrollView przechwyci zdarzenie tylko wtedy, gdy użytkownik celowo przewinie w kierunku Y iw takim przypadku przekaże ACTION_CANCEL dzieciom.

Oto kod mojej klasy Scroll View, która zawiera 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
źródło
używam tego samego, ale w czasie przewijania w kierunku x otrzymuję wyjątek WYJĄTEK NULL POINTER na publicznej wartości logicznej onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } Użyłem przewijania w poziomie wewnątrz przewijania
Vipin Sahu
@Wszystkie dzięki, że działa, zapomniałem zainicjować detektor gestów w konstruktorze scrollview
Vipin Sahu
Jak powinienem użyć tego kodu do wdrożenia ViewPager w ScrollView
Harsha MV,
Miałem pewne problemy z tym kodem, gdy miałem w nim zawsze powiększony widok siatki, czasem się nie przewijał. Musiałem zastąpić metodę onDown (...) w YScrollDetector, aby zawsze zwracać wartość true, jak sugerowano w całej dokumentacji (jak tutaj developer.android.com/training/custom-views/... ) To rozwiązało mój problem.
Nemanja Kovacevic,
3
Właśnie natrafiłem na mały błąd, o którym warto wspomnieć. Uważam, że kod w onInterceptTouchEvent powinien rozdzielić dwa wywołania boolowskie, aby zagwarantować, że mGestureDetector.onTouchEvent(ev)zostaną wywołane. W tej chwili nie zostanie wywołany, jeśli super.onInterceptTouchEvent(ev)jest fałszywy. Właśnie natrafiłem na przypadek, w którym klikalne dzieci w widoku przewijania mogą przechwytywać zdarzenia dotykowe, a onScroll w ogóle nie zostanie wywołany. W przeciwnym razie dzięki, świetna odpowiedź!
GLee,
176

Dziękuję Joel za udzielenie mi wskazówek, jak rozwiązać ten problem.

Uprościłem kod (bez potrzeby użycia GestureDetector ), aby osiągnąć ten sam efekt:

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
źródło
świetnie, dziękuję za ten refaktor. Miałem problemy z powyższym podejściem podczas przewijania na dół listyView, ponieważ jakiekolwiek zachowanie dotykowe elementów potomnych zaczęło nie być przechwytywane bez względu na stosunek ruchu Y / X. Dziwne!
Dori,
1
Dzięki! Pracował również z ViewPager wewnątrz ListView, z niestandardowym ListView.
Sharief Shaik,
2
Właśnie zastąpiłem zaakceptowaną odpowiedź na to i teraz działa dla mnie znacznie lepiej. Dzięki!
David Scott
1
@VipinSahu, aby wskazać kierunek ruchu dotyku, możesz wziąć deltę bieżącej współrzędnej X i lastX, jeśli jest ona większa niż 0, dotyk porusza się od lewej do prawej, w przeciwnym razie od prawej do lewej. A następnie zapisujesz bieżący X jako lastX do następnego obliczenia.
neevek
1
co z przewijaniem w poziomie?
Zin Win Htet,
60

Myślę, że znalazłem prostsze rozwiązanie, tylko to wykorzystuje podklasę ViewPager zamiast (macierzystego) ScrollView.

AKTUALIZACJA 2013-07-16 : Dodałem również przesłonięcie dla onTouchEvent. Może to pomóc w kwestiach wymienionych w komentarzach, chociaż 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;
    }
}

Jest to podobne do techniki stosowanej w onScroll () w android.widget.Gallery . Jest to dodatkowo wyjaśnione w prezentacji Google I / O 2013 Pisanie niestandardowych widoków na Androida .

Aktualizacja 2013-12-10 : Podobne podejście opisano również w poście od Kirilla Grouchnikova na temat (wtedy) aplikacji Android Market .

Giorgos Kylafas
źródło
boolean ret = super.onInterceptTouchEvent (ev); tylko dla mnie zawsze było fałszywe. Korzystanie z UninterceptableViewPager w Scrollview
scottyab
To nie działa dla mnie, ale podoba mi się jego prostota. Używam ScrollViewz LinearLayoutw którym UninterceptableViewPagerjest umieszczony. Rzeczywiście, retzawsze jest fałsz ... Jakiś pomysł, jak to naprawić?
Peterdk
@scottyab & @Peterdk: Cóż, moje jest w tym, TableRowco jest w środku, TableLayoutktóre jest w środku ScrollView(tak, wiem ...), i działa zgodnie z przeznaczeniem. Może onScrollzamiast tego możesz spróbować zastąpić onInterceptTouchEvent, tak jak robi to Google (linia 1010)
Giorgos Kylafas
Właśnie zapisałem ret jako prawdę i działa dobrze. Wcześniej było to fałszywe, ale wierzę, że to dlatego, że scrollview ma liniowy układ, który mieści wszystkie dzieci
Amanni
11

Dowiedziałem się, że czasami jeden ScrollView odzyskuje ostrość, a drugi traci ostrość. Można temu zapobiec, przyznając tylko jeden punkt przewijania 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
źródło
jaki adapter mijasz?
Harsha MV,
Sprawdziłem ponownie i zdałem sobie sprawę, że tak naprawdę nie używam ScrollView, ale ViewPager i przekazuję mu FragmentStatePagerAdapter, który zawiera wszystkie obrazy z galerii.
Marius Hilarious,
8

Nie działało to dla mnie dobrze. Zmieniłem to i teraz działa płynnie. Jeśli ktoś zainteresowany.

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
źródło
To jest odpowiedź na mój projekt. Mam pager widoku, który działa jak galeria, można kliknąć w widoku przewijania. Korzystam z powyższego rozwiązania, działa ono na przewijaniu w poziomie, ale po kliknięciu obrazu pagera, który rozpoczyna nową aktywność i wraca, pager nie może się przewijać. Działa to dobrze, tks!
longkai
Działa dla mnie idealnie. Miałem niestandardowy widok „przeciągnij, aby odblokować” w widoku przewijania, co sprawiało mi ten sam problem. To rozwiązanie rozwiązało problem.
hybrydowy
6

Dzięki Neevek jego odpowiedź zadziałała dla mnie, ale nie blokuje przewijania w pionie, gdy użytkownik zaczął przewijać widok poziomy (ViewPager) w kierunku poziomym, a następnie bez podnoszenia w pionie przewijania palcem zaczyna przewijać leżący poniżej widok kontenera (ScrollView) . Naprawiłem to, wprowadzając niewielką zmianę w kodzie Neevaka:

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
źródło
5

To w końcu stało się częścią biblioteki wsparcia v4, NestedScrollView . Więc, jak sądzę, w większości przypadków nie są już potrzebne lokalne hacki.

Ebrahim Byagowi
źródło
1

Rozwiązanie Neevek działa lepiej niż Joel na urządzeniach z wersją 3.2 i nowszą. W systemie Android występuje błąd, który powoduje wyjątek java.lang.IllegalArgumentException: pointerIndex poza zasięgiem, jeśli wykrywacz gestów jest używany w widoku scollview. Aby zduplikować problem, zaimplementuj niestandardowy widok scollview zgodnie z sugestią Joela i umieść w nim pager widoków. Jeśli przeciągniesz (nie unoś figury) w jednym kierunku (w lewo / w prawo), a następnie w przeciwnym kierunku, zobaczysz awarię. Również w rozwiązaniu Joela, jeśli przeciągniesz pager widoku, przesuwając palec po przekątnej, gdy palec opuści obszar widoku zawartości pager widok, pager powróci do poprzedniej pozycji. Wszystkie te problemy dotyczą bardziej wewnętrznego projektu Androida lub jego braku niż implementacji Joela, która sama w sobie jest sprytnym i zwięzłym kodem.

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

Don
źródło