SwipeRefreshLayout + ViewPager, batasi gulir horizontal saja?

94

Saya telah menerapkan SwipeRefreshLayoutdan ViewPagerdi aplikasi saya, tetapi ada masalah besar: kapan pun saya akan menggeser ke kiri / kanan untuk beralih antar halaman, pengguliran terlalu sensitif. Sedikit gesekan ke bawah akan memicu SwipeRefreshLayoutpenyegaran juga.

Saya ingin menetapkan batas kapan gesekan horizontal dimulai, lalu paksa horizontal hanya sampai gesekan selesai. Dengan kata lain, saya ingin membatalkan gesekan vertikal saat jari bergerak secara horizontal.

Masalah ini hanya terjadi pada ViewPager, jika saya menggesek ke bawah dan SwipeRefreshLayoutfungsi refresh dipicu (bilah ditampilkan) dan kemudian saya menggerakkan jari saya secara horizontal, itu masih hanya memungkinkan gesekan vertikal.

Saya sudah mencoba untuk memperluas ViewPagerkelas tetapi tidak berhasil sama sekali:

public class CustomViewPager extends ViewPager {

    public CustomViewPager(Context ctx, AttributeSet attrs) {
        super(ctx, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean in = super.onInterceptTouchEvent(ev);
        if (in) {
            getParent().requestDisallowInterceptTouchEvent(true);
            this.requestDisallowInterceptTouchEvent(true);
        }
        return false;
    }

}

Tata letak xml:

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/viewTopic"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.myapp.listloader.foundation.CustomViewPager
        android:id="@+id/topicViewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>

bantuan apa pun akan dihargai, terima kasih

pengguna3896501
sumber
Apakah skenario yang sama berfungsi jika salah satu fragmen Anda di dalam viewpager memiliki SwipeRefreshLayout?
Zapnologica

Jawaban:

160

Saya tidak yakin apakah Anda masih memiliki masalah ini tetapi aplikasi Google I / O iosched menyelesaikan masalah ini dengan demikian:

    viewPager.addOnPageChangeListener( new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled( int position, float v, int i1 ) {
        }

        @Override
        public void onPageSelected( int position ) {
        }

        @Override
        public void onPageScrollStateChanged( int state ) {
            enableDisableSwipeRefresh( state == ViewPager.SCROLL_STATE_IDLE );
        }
    } );


private void enableDisableSwipeRefresh(boolean enable) {
    if (swipeContainer != null) {
            swipeContainer.setEnabled(enable);
    }
}

Saya telah menggunakan yang sama dan bekerja dengan cukup baik.

EDIT: Gunakan addOnPageChangeListener () sebagai ganti setOnPageChangeListener ().

nhasan
sumber
3
Ini adalah jawaban terbaik karena memperhitungkan status ViewPager. Itu tidak mencegah seret ke bawah yang berasal dari ViewPager, yang dengan jelas menunjukkan niat untuk menyegarkan.
Andrew Gallasch
4
Jawaban terbaik, namun mungkin bagus untuk memposting kode untuk enableDisableSwipeRefresh (ya sudah jelas dari nama fungsinya .. tetapi untuk memastikan saya harus google itu ...)
Greg Ennis
5
Bekerja dengan sempurna, tetapi setOnPageChangeListener disusutkan sekarang. gunakan addOnPageChangeListener sebagai gantinya.
Yon Kornilov
@nhasan Pada pembaruan terkini jawaban ini tidak berfungsi lagi. Menyetel status aktif swiperefresh ke false akan menghapus swiperefresh seluruhnya sedangkan sebelumnya jika status gulir viewpager berubah saat swiperefresh menyegarkan itu tidak akan menghapus tata letak, tetapi akan menonaktifkannya sambil menyimpannya dalam keadaan penyegaran yang sama seperti sebelumnya.
Michael Tedla
2
Tautan ke kode sumber untuk 'enableDisableSwipeRefresh' di aplikasi google i / o: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo
37

Dipecahkan dengan sangat sederhana tanpa memperpanjang apa pun

mPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        mLayout.setEnabled(false);
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                mLayout.setEnabled(true);
                break;
        }
        return false;
    }
});

bekerja seperti pesona

pengguna3896501
sumber
Ok, namun perlu diingat bahwa Anda akan memiliki masalah ini sama untuk setiap digulir Viewbahwa Anda mungkin memiliki di dalam ViewPager, sebagai SwipeRefreshLayoutmemungkinkan bergulir bahkan vertikal hanya untuk itu anak tingkat atas (dan API lebih rendah dari ICS hanya jika hal itu terjadi untuk menjadi ListView) .
corsair992
@ corsair992less Terima kasih atas tip Anda
pengguna3896501
@ corsair992 menghadapi masalah. Saya memiliki ViewPagerbagian dalam SwipeRefrestLayoutdan ViewPager memiliki Listview! SwipeRefreshLayoutbiarkan saya menggulir ke bawah tetapi saat menggulir ke atas itu memicu kemajuan penyegaran. Ada saran?
Muhammad Babar
1
dapatkah Anda mengembangkan lebih banyak lagi tentang apa itu mLayout?
desgraci
2
viewPager.setOnTouchListener {_, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov
22

Saya telah menemui masalah Anda. Sesuaikan SwipeRefreshLayout akan menyelesaikan masalah.

public class CustomSwipeToRefresh extends SwipeRefreshLayout {

private int mTouchSlop;
private float mPrevX;

public CustomSwipeToRefresh(Context context, AttributeSet attrs) {
    super(context, attrs);

    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mPrevX = MotionEvent.obtain(event).getX();
            break;

        case MotionEvent.ACTION_MOVE:
            final float eventX = event.getX();
            float xDiff = Math.abs(eventX - mPrevX);

            if (xDiff > mTouchSlop) {
                return false;
            }
    }

    return super.onInterceptTouchEvent(event);
}

Lihat ref: link

huu duy
sumber
Ini adalah solusi terbaik, dengan memasukkan kemiringan dalam pendeteksian.
nafsaka
Solusi hebat +1
Trem Nguyen
12

Saya mendasarkan ini dari jawaban sebelumnya tetapi menemukan ini bekerja sedikit lebih baik. Mosi dimulai dengan acara ACTION_MOVE dan diakhiri dengan ACTION_UP atau ACTION_CANCEL menurut pengalaman saya.

mViewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mSwipeRefreshLayout.setEnabled(false);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mSwipeRefreshLayout.setEnabled(true);
                break;
        }
        return false;
    }
});
Sean Abraham
sumber
Thnxx untuk solusinya
Hitesh Kushwah
9

Untuk beberapa alasan yang paling hanya diketahui oleh mereka, tim pengembang pustaka dukungan merasa cocok untuk secara paksa mencegat semua peristiwa gerakan tarik vertikal dari SwipeRefreshLayouttata letak anak, bahkan ketika seorang anak secara khusus meminta kepemilikan acara tersebut. Satu-satunya hal yang mereka periksa adalah bahwa status gulir vertikal dari turunan utamanya berada di nol (dalam kasus anak itu dapat digulir secara vertikal). The requestDisallowInterceptTouchEvent()Metode telah ditimpa dengan tubuh kosong, dan (tidak begitu) menerangi komentar "Tidak".

Cara termudah untuk mengatasi masalah ini adalah dengan menyalin kelas dari pustaka dukungan ke dalam proyek Anda dan menghapus penggantian metode. ViewGroupImplementasi menggunakan status internal untuk penanganannya onInterceptTouchEvent(), jadi Anda tidak bisa begitu saja mengganti metode itu lagi dan menduplikasinya. Jika Anda benar-benar ingin mengganti implementasi support library, Anda harus menyiapkan flag kustom saat panggilan ke requestDisallowInterceptTouchEvent(), dan mengganti onInterceptTouchEvent()dan onTouchEvent()(atau mungkin meretas canChildScrollUp()) perilaku berdasarkan itu.

corsair992
sumber
Man, itu kasar. Saya benar-benar berharap mereka tidak melakukan itu. Saya memiliki daftar yang ingin saya aktifkan tarik untuk menyegarkan dan kemampuan untuk menggesek item baris. Cara mereka membuat SwipeRefreshLayout membuatnya hampir tidak mungkin tanpa solusi gila-gilaan.
Jessie A. Morris
3

Saya telah menemukan solusi untuk ViewPager2. Saya menggunakan refleksi untuk mengurangi sensitivitas drag seperti ini:

/**
 * Reduces drag sensitivity of [ViewPager2] widget
 */
fun ViewPager2.reduceDragSensitivity() {
    val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
    recyclerViewField.isAccessible = true
    val recyclerView = recyclerViewField.get(this) as RecyclerView

    val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
    touchSlopField.isAccessible = true
    val touchSlop = touchSlopField.get(recyclerView) as Int
    touchSlopField.set(recyclerView, touchSlop*8)       // "8" was obtained experimentally
}

Ini bekerja seperti pesona bagi saya.

Alex Shevelev
sumber
2

Ada satu masalah dengan solusi nhasan:

Jika gesekan horizontal yang memicu setEnabled(false)panggilan di SwipeRefreshLayoutdalam OnPageChangeListenerterjadi saat SwipeRefreshLayouttelah mengenali Tarik-untuk-Muat Ulang tetapi belum memanggil callback notifikasi, animasi menghilang tetapi status internal SwipeRefreshLayouttetap "menyegarkan" selamanya karena tidak panggilan balik notifikasi yang dapat mengatur ulang status. Dari sudut pandang pengguna, ini berarti Tarik-untuk-Muat Ulang tidak berfungsi lagi karena semua gerakan tarik tidak dikenali.

Masalahnya di sini adalah bahwa disable(false)panggilan tersebut menghapus animasi spinner dan callback notifikasi dipanggil dari onAnimationEndmetode AnimationListener internal untuk spinner tersebut yang disetel tidak berurutan seperti itu.

Memang perlu penguji kami dengan jari tercepat untuk memprovokasi situasi ini, tetapi itu dapat terjadi sesekali dalam skenario realistis juga.

Solusi untuk memperbaikinya adalah dengan mengganti onInterceptTouchEventmetode SwipeRefreshLayoutsebagai berikut:

public class MySwipeRefreshLayout extends SwipeRefreshLayout {

    private boolean paused;

    public MySwipeRefreshLayout(Context context) {
        super(context);
        setColorScheme();
    }

    public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setColorScheme();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (paused) {
            return false;
        } else {
            return super.onInterceptTouchEvent(ev);
        }
    }

    public void setPaused(boolean paused) {
        this.paused = paused;
    }
}

Gunakan MySwipeRefreshLayoutdi Layout Anda - File dan ubah kode dalam solusi mhasan menjadi

...

@Override
public void onPageScrollStateChanged(int state) {
    swipeRefreshLayout.setPaused(state != ViewPager.SCROLL_STATE_IDLE);
}

...
Nantoka
sumber
1
Saya juga memiliki masalah yang sama dengan Anda. Baru saja memodifikasi solusi nhasan dengan pastebin.com/XmfNsDKQ Note tata letak penyegaran gesek tidak menimbulkan masalah saat menyegarkan, jadi itulah mengapa pemeriksaan.
Amit Jayant
0

Mungkin ada masalah dengan jawaban @huu duy saat ViewPager ditempatkan dalam wadah yang dapat di-scroll secara vertikal yang, pada gilirannya, ditempatkan di SwiprRefreshLayout Jika wadah konten yang dapat digulir tidak sepenuhnya di-scroll, maka itu mungkin tidak memungkinkan untuk aktifkan swipe-to-refresh dengan gerakan scroll-up yang sama. Memang, ketika Anda mulai menggulir wadah dalam dan menggerakkan jari secara horizontal lebih dari mTouchSlop secara tidak sengaja (yang merupakan 8dp secara default), CustomSwipeToRefresh yang diusulkan menolak gerakan ini. Jadi, pengguna harus mencoba sekali lagi untuk mulai menyegarkan. Ini mungkin terlihat aneh bagi pengguna. Saya mengekstrak kode sumber untuk SwipeRefreshLayout asli dari pustaka dukungan ke proyek saya dan menulis ulang onInterceptTouchEvent ().

private float mInitialDownY;
private float mInitialDownX;
private boolean mGestureDeclined;
private boolean mPendingActionDown;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();
    final int action = ev.getActionMasked();
    int pointerIndex;

    if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
        mReturningToStart = false;
    }

    if (!isEnabled() || mReturningToStart || mRefreshing ) {
        // Fail fast if we're not in a state where a swipe is possible
        if (D) Log.e(LOG_TAG, "Fail because of not enabled OR refreshing OR returning to start. "+motionEventToShortText(ev));
        return false;
    }

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
            mActivePointerId = ev.getPointerId(0);

            if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) >= 0) {

                if (mNestedScrollInProgress || canChildScrollUp()) {
                    if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. Set pending DOWN=true. "+motionEventToShortText(ev));
                    mPendingActionDown = true;
                } else {
                    mInitialDownX = ev.getX(pointerIndex);
                    mInitialDownY = ev.getY(pointerIndex);
                }
            }
            return false;

        case MotionEvent.ACTION_MOVE:
            if (mActivePointerId == INVALID_POINTER) {
                if (D) Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                return false;
            } else if (mGestureDeclined) {
                if (D) Log.e(LOG_TAG, "Gesture was declined previously because of horizontal swipe");
                return false;
            } else if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) < 0) {
                return false;
            } else if (mNestedScrollInProgress || canChildScrollUp()) {
                if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. "+motionEventToShortText(ev));
                return false;
            } else if (mPendingActionDown) {
                // This is the 1-st Move after content stops scrolling.
                // Consider this Move as Down (a start of new gesture)
                if (D) Log.e(LOG_TAG, "Consider this move as down - setup initial X/Y."+motionEventToShortText(ev));
                mPendingActionDown = false;
                mInitialDownX = ev.getX(pointerIndex);
                mInitialDownY = ev.getY(pointerIndex);
                return false;
            } else if (Math.abs(ev.getX(pointerIndex) - mInitialDownX) > mTouchSlop) {
                mGestureDeclined = true;
                if (D) Log.e(LOG_TAG, "Decline gesture because of horizontal swipe");
                return false;
            }

            final float y = ev.getY(pointerIndex);
            startDragging(y);
            if (!mIsBeingDragged) {
                if (D) Log.d(LOG_TAG, "Waiting for dY to start dragging. "+motionEventToShortText(ev));
            } else {
                if (D) Log.d(LOG_TAG, "Dragging started! "+motionEventToShortText(ev));
            }
            break;

        case MotionEvent.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mGestureDeclined = false;
            mPendingActionDown = false;
            mActivePointerId = INVALID_POINTER;
            break;
    }

    return mIsBeingDragged;
}

Lihat proyek contoh saya di Github .

Stanislav Perchenko
sumber