如何禁用 ViewPager2 中特定方向的滑动
How to disable swiping in specific direction in ViewPager2
我想在 ViewPager2
中禁用从右向左滑动。
我的导航抽屉中基本上有一个包含 2 页的 viewpager2 元素。我希望我的第二页仅在我单击第一页中的某个元素时显示(从第一页从右向左滑动不应打开第二页),而当我在第二页时,viewpager2 滑动(左向右滑动)应该像在 viewpager 中那样滑动。
我试过扩展 ViewPager2
class 并覆盖触摸事件,但不幸的是它 ViewPager2
是最终的 class,所以我无法扩展它。
其次,我尝试使用 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);
扩展 viewpager class 并覆盖函数 onInterceptTouchEvent
和 onTouchEvent
。然后确定滑动的方向,如果不想滑动则return 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);
}
我修改了这个答案的代码,如果您需要更多解释,请参阅:
我找到了一个侦听器,它可以在用户尝试滑动时侦听,然后它会检查当前页面,如果是第一页,则禁用用户输入,否则默认启用它。
这是它的代码片段
在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);
}
}
});
在 Kotlin 中:
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
.
的正负值处理所需的场景
超过 2 个片段的解决方案。
(向下滚动查看解决方案,最后更新代码以更正一些错误。)
这个有点棘手。
我认为人们希望在一个方向上禁用滑动的原因,在我看来,是因为无法在 运行 时间添加片段,同时保持先前片段的状态显示。
所以人们正在做的是他们预加载所有片段,并使那些没有显示的片段根本不存在。
现在,如果团队让适配器不受 ViewLifeCycle 的限制,这可以通过使用 ListDiffer 选项轻松解决,这会正确地将更新传播到 RecyclerView 适配器,但是因为需要一个新的适配器ViewPager2的每个dataSetChanged,需要重新创建整个Fragments,ListDiffer对ViewPager2没有影响。
但可能不完全是因为我不确定 ListDiffer 是否能够识别“位置交换”以保留状态。
现在,关于建议使用 registerOnPageChangeCallback()
的答案。
注册 OnPageChangeCallback() 超过 2 个 Fragments 没有用的原因是,当调用此方法时,已经来不及做某事了,这造成的是 window 在中途变得无响应,这与 addOnItemTouchListener() 不同;它能够在触摸到达视图之前拦截它们。
在某种意义上,阻止和允许滑动的完整事务将由两种方法执行,即 registerOnPageChangeCallback() 和 addOnItemTouchListener()。
registerOnPageChangeCallback()
会告诉我们的适配器应该停止工作的方向(通常是从左到右(我将其简称为“左”))和在哪个页面,而 addOnItemTouchListener()
会告诉视图在正确的时刻朝我们想要的方向拦截投掷。
问题是要使用 TouchListener,我们需要访问 ViewPager2 中的内部 RecyclerView。
方法是覆盖 FragmentStateAdapter
中的 onAttachedToWindow()
方法。
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
}
现在附加到 RecyclerView 的正确侦听器称为 RecyclerView.SimpleOnItemTouchListener()
,问题是侦听器无法区分“右”fling 和“左”fling。
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
之前触发多次。
解决方案。
我用的class左右识别:
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进行enzero,因为在rv.getScrollState()
变为SCROLL_STATE_DRAGGING
之前resolve()方法至少执行了3次以上
一旦这个 class 被定义,整个代码应该是这样的(在 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 首先附加到 window。
正在使用中。
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();
}
}
}
}
);
超过 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;
}
}
});
我想在 ViewPager2
中禁用从右向左滑动。
我的导航抽屉中基本上有一个包含 2 页的 viewpager2 元素。我希望我的第二页仅在我单击第一页中的某个元素时显示(从第一页从右向左滑动不应打开第二页),而当我在第二页时,viewpager2 滑动(左向右滑动)应该像在 viewpager 中那样滑动。
我试过扩展 ViewPager2
class 并覆盖触摸事件,但不幸的是它 ViewPager2
是最终的 class,所以我无法扩展它。
其次,我尝试使用 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);
扩展 viewpager class 并覆盖函数 onInterceptTouchEvent
和 onTouchEvent
。然后确定滑动的方向,如果不想滑动则return 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);
}
我修改了这个答案的代码,如果您需要更多解释,请参阅:
我找到了一个侦听器,它可以在用户尝试滑动时侦听,然后它会检查当前页面,如果是第一页,则禁用用户输入,否则默认启用它。
这是它的代码片段
在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);
}
}
});
在 Kotlin 中:
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
.
超过 2 个片段的解决方案。 (向下滚动查看解决方案,最后更新代码以更正一些错误。)
这个有点棘手。
我认为人们希望在一个方向上禁用滑动的原因,在我看来,是因为无法在 运行 时间添加片段,同时保持先前片段的状态显示。
所以人们正在做的是他们预加载所有片段,并使那些没有显示的片段根本不存在。
现在,如果团队让适配器不受 ViewLifeCycle 的限制,这可以通过使用 ListDiffer 选项轻松解决,这会正确地将更新传播到 RecyclerView 适配器,但是因为需要一个新的适配器ViewPager2的每个dataSetChanged,需要重新创建整个Fragments,ListDiffer对ViewPager2没有影响。
但可能不完全是因为我不确定 ListDiffer 是否能够识别“位置交换”以保留状态。
现在,关于建议使用 registerOnPageChangeCallback()
的答案。
注册 OnPageChangeCallback() 超过 2 个 Fragments 没有用的原因是,当调用此方法时,已经来不及做某事了,这造成的是 window 在中途变得无响应,这与 addOnItemTouchListener() 不同;它能够在触摸到达视图之前拦截它们。
在某种意义上,阻止和允许滑动的完整事务将由两种方法执行,即 registerOnPageChangeCallback() 和 addOnItemTouchListener()。
registerOnPageChangeCallback()
会告诉我们的适配器应该停止工作的方向(通常是从左到右(我将其简称为“左”))和在哪个页面,而 addOnItemTouchListener()
会告诉视图在正确的时刻朝我们想要的方向拦截投掷。
问题是要使用 TouchListener,我们需要访问 ViewPager2 中的内部 RecyclerView。
方法是覆盖 FragmentStateAdapter
中的 onAttachedToWindow()
方法。
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
}
现在附加到 RecyclerView 的正确侦听器称为 RecyclerView.SimpleOnItemTouchListener()
,问题是侦听器无法区分“右”fling 和“左”fling。
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
之前触发多次。
解决方案。
我用的class左右识别:
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进行enzero,因为在rv.getScrollState()
变为SCROLL_STATE_DRAGGING
一旦这个 class 被定义,整个代码应该是这样的(在 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 首先附加到 window。
正在使用中。
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();
}
}
}
}
);
超过 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;
}
}
});