ScrollView Dokunmatik İşleme içinde HorizontalScrollView


226

Tüm ekranın kaydırılabilir olması için tüm düzenimi çevreleyen bir ScrollView var. Bu ScrollView'da sahip olduğum ilk öğe, yatay olarak kaydırılabilen özelliklere sahip bir HorizontalScrollView bloğudur. Dokunma olaylarını işlemek ve görünümü ACTION_UP olayındaki en yakın görüntüye "yapışmaya" zorlamak için yatay kaydırma görünümüne bir onto-listenener ekledim.

Bu yüzden gideceğim etki, birinden diğerine geçebileceğiniz hisse senedi android ana ekranı gibidir ve parmağınızı kaldırdığınızda bir ekrana yaslanır.

Bu bir sorun dışında tüm harika çalışıyor: Bir ACTION_UP hiç kayıt için hemen hemen yatay olarak soldan sağa hızlıca kaydırmak gerekiyor. En azından dikey olarak kaydırırsam (birçok kişinin yan yana kaydırırken telefonlarında yapma eğiliminde olduğunu düşünüyorum), ACTION_UP yerine bir ACTION_CANCEL alırım. Benim teorim, bunun nedeni yatay kaydırma görüntüsünün bir kaydırma görüntüsü içinde olması ve kaydırma görüntüsünün dikey kaydırmaya izin vermek için dikey dokunuşu ele geçirmesidir.

Kaydırma görünümünün dokunma olaylarını yatay kaydırma görünümümden nasıl devre dışı bırakabilirim, ancak yine de kaydırma görüntüsünün başka bir yerinde normal dikey kaydırmaya izin verebilirim?

İşte benim kod bir örnek:

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

Bu yazıda tüm yöntemleri denedim, ama hiçbiri benim için çalışmıyor. MeetMe's HorizontalListViewKütüphane kullanıyorum .
Göçebe

Bazı benzer kodlara sahip bir makale ( HomeFeatureLayout extends HorizontalScrollView) burada velir.com/blog/index.php/2010/11/17/… Özel kaydırma sınıfı oluşturuldukça neler olduğu hakkında ek yorumlar var.
CJBS

Yanıtlar:


280

Güncelleme: Bunu anladım. Benim ScrollView, sadece Y hareket> X hareket ise touch olayı durdurmak için onInterceptTouchEvent yöntemini geçersiz kılmak gerekiyordu. ScrollView'in varsayılan davranışı, HERHANGİ BİR hareket olduğunda dokunma olayını durdurmaktır. Bu nedenle, düzeltme ile ScrollView yalnızca kullanıcı kasıtlı olarak Y yönünde ilerliyorsa ve bu durumda ACTION_CANCEL öğesini çocuklara iletirse olayı keser.

İşte HorizontalScrollView içeren Scroll View sınıfımın kodu:

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

Bunu aynı kullanıyorum ama x yönünde kaydırma sırasında ben istisna alıyorum Genel boolean onInterceptTouchEvent (MotionEvent ev) {dönüş super.onInterceptTouchEvent (ev) & & mGestureDetector.onTouchEvent (ev); } Scrollview içinde yatay kaydırma kullandım
Vipin Sahu

@ Teşekkürler çalışıyor, scrollview yapıcısında jest dedektörünü başlatmayı unutuyorum
Vipin Sahu

ScrollView içinde bir ViewPager uygulamak için bu kodu nasıl kullanmalıyım
Harsha MV

Ben her zaman genişletilmiş bir ızgara görünümü vardı bu kod ile bazı sorunlar vardı, bazen kaydırma olmaz. Ben belgelerine boyunca (buradaki gibi önerildiği gibi (...) YScrollDetector yöntemi her zaman doğru dönmek onDown geçersiz kılmak zorunda developer.android.com/training/custom-views/... Bu benim sorunu çözüldü).
Nemanja Kovacevic

3
Sadece bahsetmeye değer küçük bir hataya rastladım. Ben onInterceptTouchEvent kodu çağrılacak garanti için iki boolean çağrıları bölünmesi gerektiğine inanıyorum mGestureDetector.onTouchEvent(ev). Şimdi olduğu gibi super.onInterceptTouchEvent(ev), yanlış ise çağrılmaz . Kaydırma görünümündeki tıklanabilir çocukların dokunma olaylarını yakalayabileceği ve onScroll'un hiç çağrılmayacağı bir durumla karşılaştım. Aksi takdirde, teşekkürler, harika cevap!
GLee

176

Joel, bana bu sorunun nasıl çözüleceğine dair bir ipucu verdiği için teşekkür ederim.

Aynı etkiyi elde etmek için kodu (bir GestureDetector'a ihtiyaç duymadan) basitleştirdim :

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

mükemmel, bu refactor için teşekkürler. Y / X hareket oranı ne olursa olsun alt öğeler üzerinde herhangi bir dokunma davranışı yakalanmaya başlamaması nedeniyle listenin en altına kaydırırken yukarıdaki yaklaşımla ilgili sorunlar alıyordum. Tuhaf!
Dori

1
Teşekkürler! Ayrıca özel bir ListView ile bir ListView içinde bir ViewPager ile çalıştı.
Sharief Shaik

2
Kabul edilen cevabı bununla değiştirdim ve şimdi benim için çok daha iyi çalışıyor. Teşekkürler!
David Scott

1
@VipinSahu, dokunma hareketinin yönünü söylemek için, mevcut X koordinatının ve lastX'in deltasını alabilirsiniz, eğer 0'dan büyükse, dokunma soldan sağa, aksi takdirde sağdan sola hareket ediyordur. Ve sonra bir sonraki hesaplama için geçerli X'i lastX olarak kaydedersiniz.
neevek

1
yatay kaydırma görüntüsü ne olacak?
Zin Win Htet

60

Ben daha basit bir çözüm buldum, sadece bu (üst) ScrollView yerine ViewPager bir alt sınıf kullanır.

GÜNCELLEME 2013-07-16 : Ben de bir geçersiz kılma ekledim onTouchEvent. YMMV olmasına rağmen muhtemelen yorumlarda bahsedilen sorunlara yardımcı olabilir.

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

Bu android.widget.Gallery'nin onScroll () 'da kullanılan tekniğe benzer . Ayrıca Android için Özel Görünüm Yazma Google I / O 2013 sunumu ile açıklanmaktadır .

Güncelleme 2013-12-10 : Benzer bir yaklaşım Kirill Grouchnikov'un (o zaman) Android Market uygulamasıyla ilgili bir yazısında da açıklanıyor .


boolean ret = super.onInterceptTouchEvent (ev); sadece benim için yanlış döndü. Bir Kaydırma Görünümünde UninterceptableViewPager üzerinde kullanma
scottyab

Bu benim için işe yaramıyor, ama ben basitliği seviyorum. Bir kullanmak ScrollViewbir ile LinearLayouthangi UninterceptableViewPageryerleştirilir. Gerçekten, rether zaman yanlıştır ... Bunu nasıl düzeltebileceğime dair bir ipucu var mı?
Peterdk

@scottyab & @Peterdk: Benim TableRow, içinde TableLayoutolan bir içinde ScrollView(evet, biliyorum ...), ve amaçlandığı gibi çalışıyor. Belki geçersiz kılma deneyebilirsiniz onScrollyerine onInterceptTouchEventgibi, Google bunu yapar (hat 1010)
Giorgos Kylafas

Ben sadece doğru olarak hardcoded ret var ve iyi çalışıyor. Daha önce yanlış dönüyordu ama bunun nedeni,
scrollview'in

11

Bir zamanlar ScrollView'ın odaklanmaya, diğerinin odaklanmaya başladığını öğrendim. Bunu, scrollView odağından yalnızca birini vererek önleyebilirsiniz:

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

hangi adaptörü geçiyorsun?
Harsha MV

Tekrar kontrol ettim ve aslında kullandığım bir ScrollView değil, bir ViewPager ve galeri için tüm görüntüleri içeren bir FragmentStatePagerAdapter geçirdiğimi fark ettim.
Marius Hilarious

8

Benim için iyi çalışmadı. Ben değiştirdim ve şimdi sorunsuz çalışıyor. İlgilenen varsa.

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

Bu projemin cevabı. Bir galeri gibi hareket eden bir görünümde kaydırma görünümünde tıklatılabilir. Yukarıda verilen çözümü kullanıyorum, yatay kaydırma üzerinde çalışıyor, ancak yeni bir etkinlik başlatan ve geri dönen çağrı cihazının görüntüsünü tıkladıktan sonra çağrı cihazı kaydırma yapamıyor. Bu iyi çalışıyor, tks!
longkai

Benim için mükemmel çalışıyor. Bana aynı sorunu veren bir kaydırma görünümü içinde özel bir "kilidini tokatlamak" görünümü vardı. Bu çözüm sorunu çözdü.
melez

6

Neevek sayesinde yanıtı benim için çalıştı, ancak kullanıcı yatay görünümü (ViewPager) yatay yönde kaydırmaya başladığında dikey kaydırmayı kilitlemiyor ve daha sonra parmak kaydırmasını dikey olarak kaldırmadan alttaki konteyner görünümünü kaydırmaya başlıyor (ScrollView) . Neevak'ın kodunda küçük bir değişiklik yaparak düzelttim:

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

}


1

Neevek'in çözümü, 3.2 ve üzeri sürümleri çalıştıran cihazlarda Joel'den daha iyi çalışıyor. Android'de, bir scollview içinde bir hareket dedektörü kullanılırsa java.lang.IllegalArgumentException: pointerIndex aralığına neden olacak bir hata var. Sorunu tekrarlamak için, Joel'in önerdiği gibi özel bir scollview uygulayın ve içine bir görüntüleme çağrı cihazı yerleştirin. Bir yöne (sola / sağa) ve sonra tersine sürüklerseniz (şekil kaldırmazsanız), çarpışmayı görürsünüz. Ayrıca Joel'in çözümünde, parmağınızı çapraz olarak hareket ettirerek görüntüleme çağrı cihazını sürüklerseniz, parmağınız görüntüleme çağrı cihazının içerik görüntüleme alanından ayrıldığında, çağrı cihazı önceki konumuna geri döner. Tüm bu sorunlar, Android'in iç tasarımıyla veya eksikliğiyle, Joel'in akıllı ve özlü bir kod parçası olan uygulamasından daha fazla.

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.