Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to disable swiping in specific direction in ViewPager2

I want to disable right to left swipe in ViewPager2. I basically have a viewpager2 element with 2 pages in my navigation drawer. I want my second page to show up only when I click some element in my first page (right to left swipe from the first page should not open the second page), while when I'm in the second page, the viewpager2 swipe (left to right swipe) should swipe as it should do in viewpager.

I've tried extending the ViewPager2 class and override the touch events, but unfortunately it ViewPager2 is a final class, so I cannot extend it.

Secondly, I tried to use setUserInputEnabled method to false, but this disabled all swipes altogether (I just want to disable right to left swipe). If I could find some listener which checks for the current page before swiping and disable swipe otherwise, it would probably work.

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

Code for setting up of 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);
like image 960
Shahbaz Hussain Avatar asked Jun 18 '19 11:06

Shahbaz Hussain


2 Answers

I found a listener which can listen when the user tries to swipe, it'll then check the current page, if it's the first page, disable the user input else enable it as it was by default.

Here's the code snippet for that

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

In Kotlin:

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

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

Since my scenario was of 2 pages only, checking the page number would be good for me, but in case we have more than 2 pages and we need to disable the swipe in one particular direction, we may use onPageScrolled(int position, float positionOffset, int positionOffsetPixels) listener of viewpager2 and handle the desired scenario according to the positive or negative values of position and positionOffset.

like image 161
Shahbaz Hussain Avatar answered Oct 12 '22 08:10

Shahbaz Hussain


Solution for more than 2 Fragments. (scroll down for solution, Updates to the code at the end to correct some bugs.)

This one was a little tricky.

I think the reason why people are looking to disable swipes in one direction, in my opinion, is because there is no way to add Fragments at run-time, while maintaining the state of previous Fragments on display.

So what people are doing is that they are preloading all Fragments, and making it as if the ones not on display are simply not there.

Now if the team would've made the Adapter not bounded by the ViewLifeCycle, this could be easily solved by using the ListDiffer option, that would correctly propagate updates to the RecyclerView Adapter, but because a new Adapter is required for each dataSetChanged of the ViewPager2, the entirety of the Fragments needs to be recreated, and the ListDiffer has no effect on the ViewPager2.

But maybe not entirely as I'm not sure the ListDiffer is able to recognize a "position swap" to preserve state.

Now, on the answer that advices the use of registerOnPageChangeCallback().

The reason why it's of no use to registerOnPageChangeCallback() with more than 2 Fragments, its because by the time this method gets called, it's already too late to do something, what this creates is that the window becomes unresponsive mid way, as opposed to the addOnItemTouchListener(); which is able to intercept touches before they reach the view.

In some sense the complete transaction of blocking and allowing swipes, are going to be performed by the two methods, registerOnPageChangeCallback() and addOnItemTouchListener().

registerOnPageChangeCallback() Will tell our adapter what direction should stop working (usually from left to right (I will call this simply "left")) and at what page, while addOnItemTouchListener() would tell the view to intercept the fling at the right moment in the direction we want.

The problem is that to use that TouchListener we need to access the inner RecyclerView inside the ViewPager2.

The way to do this is by overriding the onAttachedToWindow() method from the FragmentStateAdapter.

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

Now the correct listener to attach to the RecyclerView is called RecyclerView.SimpleOnItemTouchListener(), the problem is that the listener does not distinguish a "right" fling from a "left" one.

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

We need to mix 2 behaviours to get the desired result:

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

b) e.getX()

We also need to keep track of the last x point, the reason this works is because the listener will trigger multiple times before the rv.getScrollState() turns SCROLL_STATE_DRAGGING.

SOLUTION.

The class I used to identify left from right:

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
    }

}

There is no need to enzero the previousX int after the transaction because the resolve() method is at least executed more than 3 times before the rv.getScrollState() becomes SCROLL_STATE_DRAGGING

Once this class is defined the whole code should be like this (inside the 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);
}

If you are asking what AtomicSupplier is, it is something similar to an AtomicReference<> so if you want to use that instead it will give the same results. The idea is to reuse the same SimpleOnItemTouchListener() and in order to do that we need to supply it with the parameter.

we need to check for nulls because the supplier will be null the first time (unless you rpovide it an initial value) the recyclerView is firstly attached to the window.

Now using it.

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

             }
         }
    );

Update


Some updates have been done to the DirectionResolver.class to account for sme bugs and more functionality:

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


}

Direction enum:

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

}

Implementation:

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

Use:

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

             }
         }
    );
like image 32
Delark Avatar answered Oct 12 '22 07:10

Delark