HorizontalScrollView dalam ScrollView Touch Handling

226

Saya memiliki ScrollView yang mengelilingi seluruh tata letak saya sehingga seluruh layar dapat digulir. Elemen pertama yang saya miliki dalam ScrollView ini adalah blok HorizontalScrollView yang memiliki fitur yang dapat digulir secara horizontal. Saya telah menambahkan daftar ontouchlist ke horizontalscrollview untuk menangani acara sentuh dan memaksa tampilan untuk "snap" ke gambar terdekat di acara ACTION_UP.

Jadi efek yang saya tuju adalah seperti homescreen android stok di mana Anda dapat menggulir dari satu ke yang lain dan terkunci ke satu layar ketika Anda mengangkat jari Anda.

Ini semua berfungsi dengan baik kecuali untuk satu masalah: Saya perlu menggesek dari kiri ke kanan hampir sempurna secara horizontal agar ACTION_UP pernah mendaftar. Jika saya menggesek paling tidak secara vertikal (yang saya pikir banyak orang cenderung lakukan di ponsel mereka ketika menggesek sisi ke sisi), saya akan menerima ACTION_CANCEL bukan ACTION_UP. Teori saya adalah ini karena horizontalscrollview berada dalam scrollview, dan scrollview membajak sentuhan vertikal untuk memungkinkan pengguliran vertikal.

Bagaimana saya bisa menonaktifkan acara sentuh untuk scrollview hanya dari dalam scrollview horizontal saya, tetapi masih memungkinkan untuk bergulir vertikal normal di tempat lain di scrollview?

Ini contoh kode saya:

   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
sumber
Saya sudah mencoba semua metode dalam posting ini, tetapi tidak ada yang bekerja untuk saya. Saya menggunakan MeetMe's HorizontalListViewperpustakaan.
The Nomad
Ada artikel dengan beberapa kode serupa ( HomeFeatureLayout extends HorizontalScrollView) di sini velir.com/blog/index.php/2010/11/17/... Ada beberapa komentar tambahan tentang apa yang terjadi ketika kelas gulir khusus dibuat.
CJBS

Jawaban:

280

Pembaruan: Saya menemukan jawabannya. Pada ScrollView saya, saya perlu mengganti metode onInterceptTouchEvent untuk hanya mencegat acara sentuh jika gerakan Y adalah> gerakan X. Sepertinya perilaku default ScrollView adalah mencegat acara sentuh setiap kali ada gerakan APAPUN Y. Jadi dengan perbaikannya, ScrollView hanya akan mencegat acara jika pengguna dengan sengaja menggulir ke arah Y dan dalam kasus itu memberikan ACTION_CANCEL kepada anak-anak.

Ini adalah kode untuk kelas Tampilan Gulir saya yang berisi 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
sumber
saya menggunakan ini sama tetapi pada saat bergulir di x arah saya mendapatkan pengecualian NULL POINTER EXCEPTION di boolean publik diInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } Saya telah menggunakan scroll horizontal pada scrollview
Vipin Sahu
@Semua terima kasih itu berfungsi, saya lupa menginisialisasi detektor gerakan di scrollview constructor
Vipin Sahu
Bagaimana saya harus menggunakan kode ini untuk mengimplementasikan ViewPager di dalam ScrollView
Harsha MV
Saya memiliki beberapa masalah dengan kode ini ketika saya memiliki tampilan kisi yang selalu diperluas di dalamnya, terkadang kode itu tidak akan bergulir. Saya harus mengganti metode onDown (...) di YScrollDetector untuk selalu mengembalikan true seperti yang disarankan di seluruh dokumentasi (seperti di sini developer.android.com/training/custom-views/… ) Ini menyelesaikan masalah saya.
Nemanja Kovacevic
3
Saya baru saja menemukan bug kecil yang layak disebut. Saya percaya bahwa kode di onInterceptTouchEvent harus membagi dua panggilan boolean, untuk menjamin bahwa mGestureDetector.onTouchEvent(ev)akan dipanggil. Seperti sekarang, itu tidak akan dipanggil jika super.onInterceptTouchEvent(ev)itu salah. Saya baru saja menemukan sebuah kasus di mana anak-anak yang dapat diklik di layar gulir dapat mengambil acara sentuh dan onScroll tidak akan dipanggil sama sekali. Kalau tidak, terima kasih, jawaban yang bagus!
GLee
176

Terima kasih Joel karena memberi saya petunjuk tentang cara mengatasi masalah ini.

Saya telah menyederhanakan kode (tanpa perlu GestureDetector ) untuk mencapai efek yang sama:

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
sumber
luar biasa, terima kasih untuk refactor ini. Saya mendapatkan masalah dengan pendekatan di atas ketika menggulir ke bagian bawah listView karena perilaku sentuhan atas elemen anak mulai tidak dicegat tidak peduli apa rasio gerakan Y / X. Aneh!
Dori
1
Terima kasih! Juga bekerja dengan ViewPager di dalam ListView, dengan ListView kustom.
Sharief Shaik
2
Baru saja mengganti jawaban yang diterima dengan ini dan itu bekerja jauh lebih baik untuk saya sekarang. Terima kasih!
David Scott
1
@ VipinSahu, untuk mengetahui arah gerakan sentuh, Anda dapat mengambil delta koordinat X saat ini dan lastX, jika lebih besar dari 0, sentuh bergerak dari kiri ke kanan, jika tidak dari kanan ke kiri. Dan kemudian Anda menyimpan X saat ini sebagai lastX untuk perhitungan selanjutnya.
neevek
1
bagaimana dengan scrollview horizontal?
Zin Win Htet
60

Saya pikir saya menemukan solusi yang lebih sederhana, hanya ini menggunakan subkelas dari ViewPager bukan (induknya) ScrollView.

UPDATE 2013-07-16 : Saya menambahkan override onTouchEventjuga. Mungkin bisa membantu dengan masalah yang disebutkan dalam komentar, meskipun 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;
    }
}

Ini mirip dengan teknik yang digunakan di android.widget.Gallery's onScroll () . Lebih lanjut dijelaskan oleh presentasi Google I / O 2013 Menulis Tampilan Kustom untuk Android .

Pembaruan 2013-12-10 : Pendekatan serupa juga dijelaskan dalam posting dari Kirill Grouchnikov tentang aplikasi (saat itu) Android Market .

Giorgos Kylafas
sumber
boolean ret = super.onInterceptTouchEvent (ev); hanya pernah kembali salah untuk saya. Menggunakan pada UninterceptableViewPager dalam Scrollview
scottyab
Ini tidak berhasil untuk saya, meskipun saya suka kesederhanaannya. Saya menggunakan ScrollViewdengan LinearLayoutdi mana UninterceptableViewPagerditempatkan. Memang, retselalu salah ... Ada petunjuk bagaimana cara memperbaikinya?
Peterdk
@scottyab & @Peterdk: Ya, milik saya ada di TableRowdalam TableLayoutyang ada di dalam ScrollView(ya, saya tahu ...), dan berfungsi sebagaimana mestinya. Mungkin Anda bisa mencoba onScrollmengganti alih-alih onInterceptTouchEvent, seperti Google melakukannya (baris 1010)
Giorgos Kylafas
Saya baru saja hardcoded ret sebagai benar dan berfungsi dengan baik. Itu kembali salah sebelumnya, tapi saya percaya itu karena scrollview memiliki tata letak linier yang menampung semua anak
Amanni
11

Saya telah menemukan bahwa kadang-kadang satu ScrollView mendapatkan kembali fokus dan yang lainnya kehilangan fokus. Anda dapat mencegahnya, dengan hanya memberikan salah satu fokus 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 Lucu
sumber
adaptor apa yang Anda lewati?
Harsha MV
Saya memeriksa lagi dan menyadari bahwa itu sebenarnya bukan ScrollView yang saya gunakan, tetapi sebuah ViewPager dan saya memberikannya FragmentStatePagerAdapter yang mencakup semua gambar untuk galeri.
Marius Hilarious
8

Itu tidak bekerja dengan baik untuk saya. Saya mengubahnya dan sekarang bekerja dengan lancar. Kalau ada yang tertarik.

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;
        }
    }
}
bidik
sumber
Ini adalah jawaban untuk proyek saya. Saya memiliki pager tampilan yang berfungsi sebagai galeri yang dapat diklik di scrollview. Saya menggunakan solusi yang disediakan di atas, ini bekerja pada gulir horizontal, tetapi setelah saya mengklik gambar pager yang memulai aktivitas baru dan kembali, pager tidak dapat menggulir. Ini bekerja dengan baik, tks!
longkai
Berfungsi sempurna untuk saya. Saya memiliki tampilan "swipe untuk membuka" kustom di dalam tampilan gulir yang memberi saya masalah yang sama. Solusi ini menyelesaikan masalah.
hibrida
6

Berkat Neevek, jawabannya bekerja untuk saya tetapi tidak mengunci pengguliran vertikal ketika pengguna mulai menggulir tampilan horizontal (ViewPager) ke arah horizontal dan kemudian tanpa mengangkat gulir jari secara vertikal, ia mulai menggulir tampilan wadah yang mendasarinya (ScrollView) . Saya memperbaikinya dengan membuat sedikit perubahan pada kode 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);

}
Saqib
sumber
5

Ini akhirnya menjadi bagian dari dukungan perpustakaan v4, NestedScrollView . Jadi, tidak diperlukan lagi peretasan lokal untuk sebagian besar kasus.

Ebrahim Byagowi
sumber
1

Solusi Neevek bekerja lebih baik daripada Joel pada perangkat yang menjalankan versi 3.2 ke atas. Ada bug di Android yang akan menyebabkan java.lang.IllegalArgumentException: pointerIndex di luar jangkauan jika detektor gerakan digunakan di dalam scollview. Untuk menduplikasi masalah, terapkan scollview khusus seperti yang disarankan Joel dan letakkan pager tampilan di dalamnya. Jika Anda menyeret (jangan mengangkat sosok Anda) ke satu arah (kiri / kanan) lalu ke sebaliknya, Anda akan melihat tabrakan. Juga dalam solusi Joel, jika Anda menyeret pager tampilan dengan menggerakkan jari Anda secara diagonal, begitu jari Anda meninggalkan area tampilan konten pager tampilan, pager akan kembali ke posisi sebelumnya. Semua masalah ini lebih berkaitan dengan desain internal Android atau kurang dari itu daripada implementasi Joel, yang itu sendiri adalah bagian dari kode yang cerdas dan ringkas.

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

Mengenakan
sumber