Как отключить смахивание в определенном направлении в ViewPager2

Я хочу отключить свайп справа налево в ViewPager2. У меня в основном есть элемент viewpager2 с двумя страницами в моем навигационном ящике. Я хочу, чтобы моя вторая страница отображалась только тогда, когда я щелкаю какой-либо элемент на моей первой странице (смахивание справа налево с первой страницы не должно открывать вторую страницу), а когда я нахожусь на второй странице, смахиваю viewpager2 (слева вправо) следует проводить, как и в окне просмотра.

Я пробовал расширить класс ViewPager2 и переопределить события касания, но, к сожалению, это ViewPager2 последний класс, поэтому я не могу его расширить.

Во-вторых, я попытался использовать метод setUserInputEnabled для false, но это полностью отключило все смахивания (я просто хочу отключить смахивание справа налево). Если бы я мог найти слушателя, который проверяет текущую страницу перед смахиванием и в противном случае отключил бы смахивание, это, вероятно, сработало бы.

     implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha05'

Код для настройки ViewPager2

      ViewPager2 pager = view.findViewById(R.id.pager);
      ArrayList<Fragment> abc = new ArrayList<>();
      abc.add(first);
      abc.add(second);
      navigationDrawerPager.setAdapter(new DrawerPagerAdapter(
                this, drawerFragmentList));
      pager.setAdapter(new FragmentStateAdapter(this), abc);

person Shahbaz Hussain    schedule 18.06.2019    source источник
comment
Вы можете попробовать добавить слушателя при смене страницы, и, если текущая страница не должна быть видна, просто откатитесь   -  person Antonis Radz    schedule 18.06.2019


Ответы (3)


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

Вот фрагмент кода для этого

В Java:

pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrollStateChanged(int state) {
        super.onPageScrollStateChanged(state);

        if (state == SCROLL_STATE_DRAGGING && pager.getCurrentItem() == 0) {
            pager.setUserInputEnabled(false);
        } else {
            pager.setUserInputEnabled(true);
        }
    }
});

В Котлине:

viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
    override fun onPageScrollStateChanged(state: Int) {
        super.onPageScrollStateChanged(state)

        viewPager.isUserInputEnabled = !(state == SCROLL_STATE_DRAGGING && viewPager.currentItem == 0)
    }
})

Поскольку в моем сценарии было только 2 страницы, проверка номера страницы была бы для меня полезной, но в случае, если у нас более 2 страниц и нам нужно отключить смахивание в одном конкретном направлении, мы можем использовать onPageScrolled(int position, float positionOffset, int positionOffsetPixels) прослушиватель viewpager2 и обрабатывать желаемый сценарий в соответствии с положительными или отрицательными значениями position и positionOffset.

person Shahbaz Hussain    schedule 18.06.2019
comment
Операция гладкая? Как он мгновенно отключает прокрутку или показывает некоторую дрожь перед отключением? - person Umar Hussain; 20.06.2019
comment
@UmarHussain да, это абсолютно гладко, никаких дрожаний, на самом деле этот слушатель слушает, когда пользователь на самом деле намеревается провести, поэтому он обрабатывает поведение до того, как произойдет фактическое смахивание - person Shahbaz Hussain; 20.06.2019
comment
Это здорово, тогда обработка свайпов намного проще, чем в старой реализации. - person Umar Hussain; 20.06.2019

Решение для более чем двух фрагментов. (прокрутите вниз, чтобы найти решение, в конце обновите код, чтобы исправить некоторые ошибки.)

Это было немного сложно.

Я думаю, что причина, по которой люди хотят отключить смахивание в одном направлении, на мой взгляд, заключается в том, что нет возможности добавлять фрагменты во время выполнения, сохраняя при этом состояние предыдущих фрагментов на дисплее.

Итак, что люди делают, так это то, что они предварительно загружают все фрагменты и делают так, как будто тех, которые не отображаются, просто нет.

Теперь, если бы команда сделала адаптер, не ограниченный ViewLifeCycle, это можно было бы легко решить, используя параметр ListDiffer, который правильно распространял бы обновления на адаптер RecyclerView, но потому что новый адаптер требуется для каждого dataSetChanged ViewPager2. необходимо воссоздать все фрагменты, а ListDiffer не влияет на ViewPager2.

Но, возможно, не совсем, поскольку я не уверен, что ListDiffer может распознать обмен позиции для сохранения состояния.

Теперь об ответе, который советует использовать registerOnPageChangeCallback().

Причина, по которой бесполезно регистрироватьOnPageChangeCallback () с более чем двумя фрагментами, потому что к тому времени, когда этот метод вызывается, уже слишком поздно что-то делать, это создает то, что окно перестает отвечать на запросы на полпути, в отличие от addOnItemTouchListener (); который способен перехватывать касания до того, как они достигнут поля зрения.

В некотором смысле полная транзакция блокировки и разрешения пролистывания будет выполняться двумя методами, registerOnPageChangeCallback () и addOnItemTouchListener ().

registerOnPageChangeCallback() Сообщит нашему адаптеру, в каком направлении следует перестать работать (обычно слева направо (я назову это просто left)) и на какой странице, а addOnItemTouchListener() сообщит представлению, что нужно перехватить бросок в нужный момент в нужном нам направлении.

Проблема в том, что для использования TouchListener нам нужно получить доступ к внутреннему RecyclerView внутри ViewPager2.

Для этого нужно переопределить метод onAttachedToWindow() из FragmentStateAdapter.

@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
    super.onAttachedToRecyclerView(recyclerView);
}

Теперь правильный слушатель для присоединения к RecyclerView называется RecyclerView.SimpleOnItemTouchListener(), проблема в том, что слушатель не отличает правый бросок от левого.

public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e)

Нам нужно смешать 2 поведения, чтобы получить желаемый результат:

a) rv.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING

b) e.getX()

Нам также нужно отслеживать последнюю точку x, потому что это работает, потому что слушатель сработает несколько раз, прежде чем rv.getScrollState() превратится SCROLL_STATE_DRAGGING.

РЕШЕНИЕ.

Класс, который я использовал для обозначения слева направо:

public class DirectionResolver {

    private float previousX = 0;

    public Direction resolve(float newX) {
        Direction directionResult = null;
            float result = newX - previousX;
            if (result != 0) {
                directionResult = result > 0 ? Direction.left_to_right : Direction.right_to_left ;
            }
        previousX = newX;
        return directionResult;
    }

    public enum Direction {
        right_to_left, left_to_right
    }

}

Нет необходимости обнулять previousX int после транзакции, потому что метод resolve () выполняется как минимум более 3 раз, прежде чем rv.getScrollState() станет SCROLL_STATE_DRAGGING

После определения этого класса весь код должен быть таким (внутри FragmentStateAdapter):

private final DirectionResolver resolver = new DirectionResolver();


private final AtomicSupplier<DirectionResolver.Direction> directionSupplier = new AtomicSupplier<>();

@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {

    recyclerView.addOnItemTouchListener(
            new RecyclerView.SimpleOnItemTouchListener(){
                @Override
                public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {

                    boolean shouldIntercept = super.onInterceptTouchEvent(rv, e);

                    DirectionResolver.Direction direction = directionSupplier.get();

                    if (direction != null) {
                        DirectionResolver.Direction resolved = resolver.resolve(e.getX());
                        if (rv.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
                            //resolved will never be null if state is already dragging
                            shouldIntercept = resolved.equals(direction);
                        }
                    }
                    return shouldIntercept;
                }
            }
    );

    super.onAttachedToRecyclerView(recyclerView);
}

public void disableDrag(DirectionResolver.Direction direction) {
    Log.println(Log.WARN, TAG, "disableDrag: disabling swipe: " + direction.name());
    directionSupplier.set(() -> direction);
}

public void enableDrag() {
    Log.println(Log.VERBOSE, TAG, "enableDrag: enabling swipe");
    directionSupplier.set(() -> null);
}

Если вы спрашиваете, что такое AtomicSupplier, это что-то похожее на AtomicReference ‹›, поэтому, если вы хотите использовать это вместо этого, он даст те же результаты. Идея состоит в том, чтобы повторно использовать тот же SimpleOnItemTouchListener(), и для этого нам нужно указать его с параметром.

нам нужно проверить наличие нулей, потому что поставщик будет нулевым в первый раз (если вы не укажете ему начальное значение), recyclerView сначала прикрепляется к окну.

Теперь пользуюсь.

    binding.journalViewPager.registerOnPageChangeCallback(
            new ViewPager2.OnPageChangeCallback() {
                @Override
                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                        if (conditionToDisableLeftSwipe.at(position)) {
                            adapter.disableDrag(DirectionResolver.Direction.right_to_left);
                        } else {
                            adapter.enableDrag();
                        }
                    }

             }
         }
    );

Обновлять


Некоторые обновления были внесены в класс DirectionResolver.class для учета ошибок и дополнительных функций:

private static class DirectionResolver {

    private float previousX = 0;
    private boolean right2left;

    public Direction resolve(float newX) {
        Direction directionResult = null;
        float result = newX - previousX;
        if (result != 0) {
            directionResult = result > 0 ? Direction.left_to_right : Direction.right_to_left;
        } else {
            directionResult = Direction.left_and_right;
        }
        previousX = newX;
        return right2left ? Direction.right_to_left : directionResult;
    }


    public void reset(Direction direction) {
        previousX = direction == Direction.left_to_right ? previousX : 0;
    }

    public void reset() {
        right2left = false;
    }


}

Перечисление направлений:

public enum Direction {
    right_to_left, left_to_right, left_and_right;

    //Nested RecyclerViews generate a wrong response from the resolve() method in the direction resolver.
    public boolean equals(Direction direction, DirectionResolver resolver) {
        boolean result = direction == left_and_right || super.equals(direction);
        resolver.right2left = !result && direction == left_to_right;
        return result;
    }

}

Реализация:

@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {

    recyclerView.addOnItemTouchListener(
            new RecyclerView.SimpleOnItemTouchListener(){
                @Override
                public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {

                    boolean shouldIntercept = super.onInterceptTouchEvent(rv, e);

                    Direction direction = directionSupplier.get();

                    if (direction != null) {
                        Direction resolved = resolver.resolve(e.getX());
                        if (rv.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
                            //resolved will never be null if state is already dragging
                            shouldIntercept = resolved.equals(direction, resolver);
                            resolver.reset(direction);
                        }
                    }

                    return shouldIntercept;
                }
            }
    );

    super.onAttachedToRecyclerView(recyclerView);
}

public void disableDrag(Direction direction) {
    directionSupplier.set(() -> direction);
    resolver.reset();
}

public void enableDrag() {
    directionSupplier.set(() -> null);
}

Использовать:

    binding.journalViewPager.registerOnPageChangeCallback(
            new ViewPager2.OnPageChangeCallback() {
                @Override
                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                        if (conditionToDisableLeftSwipe.at(position)) {
                            adapter.disableDrag(DirectionResolver.Direction.right_to_left);
                        } else {
                            adapter.enableDrag();
                        }
                    }

             }
         }
    );
person Delark    schedule 15.12.2020
comment
К сожалению, это совсем не гладко. Когда он включен, очень сложно прокручивать окно просмотра, он работает не так, как задумано. - person Felipe Ribeiro R. Magalhaes; 30.05.2021
comment
Полностью согласен, что это не идеально, в основном потому, что если что-то требует операций с плавающей запятой из событий экрана, уже слишком поздно что-либо делать вообще. Я заметил, что компонент работает хорошо, если макет очень чистый без каких-либо избыточностей (это еще одна проблема Android, которая слишком много свойств переопределяет себя в XML), поэтому, если родительское свойство каким-либо образом перехватывает ваш recyclerView, перехваченный поплавок будет дать неожиданное значение. - person Delark; 30.05.2021
comment
У меня не было достаточно времени, чтобы проверить этот код, но он сработал для меня afaik, хотя, честно говоря, я позволил функции прокрутки больше как опции боковой навигации, сделав выбор элемента + onBackPressed () (дерево, подобное index) основной способ навигации по viewPager. Также действительна нижняя навигация, которая добавляет и удаляет кнопки. - person Delark; 30.05.2021

Расширьте класс viewpager и переопределите функции onInterceptTouchEvent и onTouchEvent. Затем определите направление смахивания и верните false, если вы не хотите смахивать.

Вы можете использовать этот вспомогательный метод для обнаружения смахивания:

float downX;  // define class level variable in viewpager 
private boolean wasSwipeToLeftEvent(MotionEvent event){
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = event.getX();
            return false;

        case MotionEvent.ACTION_MOVE:
        case MotionEvent.ACTION_UP:
            return event.getX() - downX > 0;

        default:
            return false;
    }
}

Затем в вашем методе для событий касания:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {


    return !this.wasSwipeToLeftEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    return return !this.wasSwipeToLeftEvent(event);
}

Я изменил код из этого ответа, если вам нужно больше объяснений, см. Это: https://stackoverflow.com/a/34111034/4428159

person Umar Hussain    schedule 18.06.2019
comment
Как указано в моем вопросе, я уже пробовал расширить класс ViewPager2 и переопределить события касания, но, к сожалению, ViewPager2 является последним классом, поэтому я не могу его расширить. - person Shahbaz Hussain; 18.06.2019
comment
Вы пробовали класс androidx.viewpager.widget.ViewPager - person Umar Hussain; 19.06.2019
comment
Да, сначала я это сделал, но я хотел попробовать ViewPager2, так как он более эффективен и превосходит своего предшественника ViewPager - person Shahbaz Hussain; 19.06.2019