SwipeRefreshLayout + ViewPager, обмежити лише горизонтальну прокрутку?


94

Я реалізував SwipeRefreshLayoutі ViewPagerв моєму додатку, але виникають великі проблеми: коли я проведу пальцем вліво / вправо, щоб переключитися між сторінками, прокрутка занадто чутлива. Трохи пальцем вниз також спричинить SwipeRefreshLayoutоновлення.

Я хочу встановити обмеження, коли починається горизонтальне проведення, а потім застосовувати силу лише по горизонталі, поки не закінчиться. Іншими словами, я хочу скасувати вертикальне перенесення, коли палець рухається горизонтально.

Ця проблема виникає лише в тому випадку ViewPager, якщо я проведіть пальцем вниз і SwipeRefreshLayoutзапускається функція оновлення (відображається смужка), а потім я рухаю пальцем по горизонталі, вона все ще дозволяє лише вертикальні пальці.

Я намагався розширити ViewPagerклас, але він взагалі не працює:

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

}

Макет 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>

будь-яка допомога буде вдячна, дякую


Чи працює той же самий сценарій, якщо один з ваших фрагментів всередині оглядача має SwipeRefreshLayout?
Zapnologica

Відповіді:


160

Я не впевнений, що у вас все ще є ця проблема, але додаток Google I / O iosched вирішує цю проблему таким чином:

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

Я використовував те саме і працює досить добре.

РЕДАГУВАТИ: Використовуйте addOnPageChangeListener () замість setOnPageChangeListener ().


3
Це найкраща відповідь, оскільки вона враховує стан ViewPager. Це не заважає перетягуванню вниз, яке виникає на ViewPager, що чітко демонструє намір оновити.
Ендрю Галлах

4
Найкраща відповідь, однак, можливо, буде приємно опублікувати код, щоб включитиDisableSwipeRefresh (так, це зрозуміло з назви функції .. але щоб бути впевненим, я мусив його гугл ...)
Грег Енніс

5
Працює чудово, але setOnPageChangeListener зараз знецінюється. замість цього використовуйте addOnPageChangeListener.
Йон Корнілов

@nhasan Станом на останнє оновлення ця відповідь більше не працює. Якщо встановити включене стан swiperefresh в false, повністю видаляє swiperefresh, тоді як, якщо стан прокрутки viewpager змінився, коли оновлення swiperefresh оновляється, воно не видалить макет, але відключить його, зберігаючи його в тому ж стані оновлення, в якому він був раніше.
Майкл Тедла,

2
Посилання на вихідний код для 'enableDisableSwipeRefresh' у програмі вводу-виводу google: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo

37

Вирішується дуже просто, не розширюючи нічого

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

працювати як оберіг


Гаразд, але майте на увазі, що у вас буде така сама проблема для будь-якої прокручуваної, Viewяка може бути у вас всередині ViewPager, оскільки SwipeRefreshLayoutдозволяє навіть вертикальну прокрутку лише для її дочірнього рівня верхнього рівня (а в API нижчих за ICS, лише якщо це a ListView) .
corsair992

@ corsair992less Дякуємо за ваші поради
user3896501

@ corsair992 стикається з проблемою. У мене ViewPagerвсередині SwipeRefrestLayoutі ViewPager є Listview! SwipeRefreshLayoutдозвольте мені прокрутити вниз, але при прокрутці вгору це запускає поновлення. Будь-яка пропозиція?
Мухаммед Бабар

1
Ви можете трохи більше розробити те, що таке mLayout?
desgraci

2
viewPager.setOnTouchListener {_, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov 13.03.18

22

Я зустрів вашу проблему. Налаштування SwipeRefreshLayout вирішить проблему.

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

Див реф: посилання


Це найкраще рішення для включення нахилу у виявлення.
нафсака

Чудове рішення +1
Трамвай Нгуен

12

Я базував це на попередній відповіді, але виявив, що це працює трохи краще. Рух починається з події ACTION_MOVE і закінчується ACTION_UP або ACTION_CANCEL на мій досвід.

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

Thnxx для рішення
Hitesh Kushwah

9

З деяких причин, найбільш відомих їм, команда розробників бібліотеки підтримки вважала за потрібне примусово перехоплювати всі події руху вертикального перетягування з SwipeRefreshLayoutдочірнього макета, навіть коли дитина спеціально вимагає права власності на подію. Єдине, що вони перевіряють, це те, що стан вертикальної прокрутки його основної дочірньої системи дорівнює нулю (у випадку, якщо це дочірнє місце можна вертикально прокручувати). requestDisallowInterceptTouchEvent()Метод був перевизначений з порожнім тілом, і (не так) висвітлюючи коментар «Ні».

Найпростішим способом вирішити цю проблему було б просто скопіювати клас із бібліотеки підтримки у свій проект та видалити метод заміщення. ViewGroupРеалізація використовує внутрішній стан для обробки onInterceptTouchEvent(), тому ви не можете просто перекрити метод знову і дублювати його. Якщо ви дійсно хочете перекрити реалізацію бібліотеки підтримки, вам доведеться встановити спеціальний прапор під час викликів requestDisallowInterceptTouchEvent(), а також змінити onInterceptTouchEvent()та onTouchEvent()(або, можливо, зламати canChildScrollUp()) поведінку на основі цього.


Чоловіче, це грубо. Я б дуже хотів, щоб вони цього не зробили. У мене є список, який я хочу ввімкнути, щоб оновити, і можливість гортати елементи рядка. Те, як вони створили SwipeRefreshLayout, робить це майже неможливим без шалених робочих процедур.
Джессі А. Морріс

3

Я знайшов рішення для ViewPager2. Я використовую відображення для зменшення чутливості до перетягування так:

/**
 * 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
}

Це працює як шарм для мене.


2

Є одна проблема з рішенням nhasan:

Якщо горизонтальне проведення пальцем, яке запускає setEnabled(false)виклик SwipeRefreshLayoutу, OnPageChangeListenerвідбувається в тому випадку, коли SwipeRefreshLayoutви вже розпізнали функцію "Перетягування для перезавантаження", але ще не викликали зворотний виклик сповіщень, анімація зникає, але внутрішній стан SwipeRefreshLayoutзалишається "оновленням" назавжди як ні викликаються зворотні виклики сповіщень, які можуть скинути стан. З точки зору користувача це означає, що функція Pull-to-Reload більше не працює, оскільки всі жести тяги не розпізнаються.

Проблема тут полягає в тому, що disable(false)виклик прибирає анімацію спінера, а зворотний виклик сповіщення викликається із onAnimationEndметоду внутрішнього AnimationListener для цього спінера, який виведений з ладу таким чином.

Справді, наш тестер мав найшвидші пальці, щоб спровокувати цю ситуацію, але це може траплятися раз у раз і в реалістичних сценаріях.

Рішенням для виправлення цього є перевизначення onInterceptTouchEventметоду SwipeRefreshLayoutнаступним чином:

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

Використовуйте MySwipeRefreshLayoutу вашому макеті - файл і змініть код у розчині mhasan на

...

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

...

1
У мене теж була така сама проблема, як і у вас. Щойно змінене рішення nhasan за допомогою цього pastebin.com/XmfNsDKQ Примітка макета оновлення пальцем не створює проблем при його оновленні, тому саме тому перевірка.
Amit Jayant

0

Може виникнути проблема з відповіддю @huu duy, коли ViewPager поміщається у вертикально прокручуваний контейнер, який, у свою чергу, розміщується в SwiprRefreshLayout. Якщо контейнер, що прокручується, не прокручується повністю, можливо, неможливо активуйте пальцем для оновлення в тому самому жесті прокрутки. Дійсно, коли ви починаєте прокручувати внутрішній контейнер і ненавмисно рухаєте пальцем горизонтально більше, ніж mTouchSlop (що за замовчуванням становить 8dp), запропонований CustomSwipeToRefresh відхиляє цей жест. Тож користувач повинен ще раз спробувати, щоб розпочати оновлення. Для користувача це може здатися дивним. Я витягнув вихідний код оригінального SwipeRefreshLayout з бібліотеки підтримки до свого проекту і переписав 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;
}

Див. Мій приклад проекту на Github .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.