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

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

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

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

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

5 ответов

Решение

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

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

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

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

Самое простое решение для более чем 2 фрагментов:

      int previousPage = 0;
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        super.onPageScrolled(position, positionOffset, positionOffsetPixels);
        if(position < previousPage){
            pager.setCurrentItem(previousPage, false);
        } else {
            previousPage = position;
        }
    }
});

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

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

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

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

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

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

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

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

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

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

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

Это можно сделать, переопределив onAttachedToWindow() метод из.

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

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

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

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

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

б) e.getX()

Нам также нужно отслеживать последнюю точку x, потому что это работает, потому что слушатель срабатывает несколько раз перед поворотами.

РЕШЕНИЕ.

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

      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
    }

}

Нет необходимости преобразовывать предыдущее значение 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 сначала прикрепляется к окну.

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


Обновлять


Некоторые обновления были внесены в DirectionResolver.class, чтобы учесть ошибки SEM и другие функции:

      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) && !result;
        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();
                        }
                    }

             }
         }
    );

Переопределите методы смахивания, как MotionEvent.

Расширьте класс 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);
}

Я изменил код из этого ответа, если вам нужно больше объяснений, пожалуйста, смотрите это: /questions/2937987/prolistajte-napravlenie-v-viewpager/2937992#2937992

Другие вопросы по тегам