如何在ViewPager2中禁用特定方向的滑动

6
我希望在ViewPager2中禁用右向左划的功能。我有一个导航抽屉,其中包含了一个具有两个页面的viewpager2元素。我想让第二页只有在我点击第一页中的某个元素时才会显示出来(从第一页向右滑动不应该打开第二页),而当我在第二页时,viewpager2的向左滑动应该像在viewpager中一样正常工作。
我尝试扩展ViewPager2类并重写触摸事件,但不幸的是,ViewPager2是一个final类,所以我无法继承它。
其次,我尝试使用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);

您可以尝试在页面更改时添加监听器,如果当前页面不可见,则回滚。 - Antonis Radz
4个回答

9
我找到了一个监听器,可以监听用户尝试滑动的操作。它会检查当前页面,如果是第一页,则禁用用户输入,否则将其恢复为默认设置。

以下是相关代码:

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

由于我的情景只有两页,检查页数对我来说很好,但是如果我们有超过两页并且需要禁用某个特定方向的滑动,则可以使用 viewpager2onPageScrolled(int position, float positionOffset, int positionOffsetPixels) 监听器,并根据 positionpositionOffset 的正数或负数值处理所需的情景。


操作是否流畅?它是立即禁用滚动还是在禁用之前会出现一些抖动? - Umar Hussain
3
@UmarHussain 是的,它非常流畅,没有任何抖动,实际上这个监听器会监听用户何时打算滑动,因此它会在实际滑动发生之前处理行为。 - Shahbaz Hussain
1
太棒了,那么相比旧的实现方式,处理轻扫手势就容易多了。 - Umar Hussain
多个片段的最简解决方案 https://dev59.com/aLTma4cB1Zd3GeqP8pE_#70666569 - Pavan Kumar N

2

解决方案,针对超过 2 个片段。

如果您已足够了解 Android,请直接查看代码;否则,请参考以下内容:

实际上,这个解决方案旨在模拟在给定方向上没有页面的情况。

因此,我建议使用下面的代码来禁用 ViewPager2 的滑动功能,并允许通过选项卡进行导航。可以添加或删除选项卡,使其看起来就像是正在添加或删除片段。

禁用整个 ViewPager2 的滑动功能,并允许仅通过选项卡进行导航。

可以添加或删除选项卡,使其看起来像是正在添加或删除片段。

为了使用 100% 功能完整的滑动功能,还需要解决至少两个行为问题,这些问题将在代码之后讨论。

代码

截至 2022 年 8 月 21 日,我终于花时间测试了一些代码问题,并想出了更好的解决方案:

public enum Direction {
    allow_all(null),
    right_to_left(Resolve.r2L()),
    left_to_right(Resolve.l2R()),
    left_and_right(Resolve.lR()); // NOT TESTED SHOULD IGNORE

    Direction(Resolve resolve) {
        this.resolve = resolve;
    }

    @FunctionalInterface
    private interface Resolve {
        boolean resolve(float prev, float next);

        static Resolve r2L() {
            return (prev, next) -> prev > next;
        }
        static Resolve l2R() {
            return (prev, next) -> prev < next;
        }
        static Resolve lR() {
            return (prev, next) -> prev != next; //THIS REQUIRES TESTING
        }
    }

    private final Resolve resolve;

    public static class Resolver {
        float prev;
        long prevTime;
        Direction toBlock = allow_all;
        public boolean shouldIntercept(MotionEvent event) {
            if (toBlock == allow_all) return false;
            long nextTime = event.getDownTime();
            float next = event.getX();
            boolean intercept = false;
            if (prevTime == nextTime) {
                intercept = toBlock.resolve.resolve(prev, next);
            } else {
                prevTime = nextTime;
            }
            prev = next;
            return intercept;
        }

        public void setToBlock(Direction toBlock) {
            if (this.toBlock != toBlock) {
                this.toBlock = toBlock;
                prev = 0;
            }
        }
    }
}

在适配器内部...

public class MyAdapter extends FragmentStateAdapter {

    private final Direction.Resolver resolver = new Direction.Resolver();

    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        recyclerView.addOnItemTouchListener(
                new RecyclerView.SimpleOnItemTouchListener(){

                    @Override
                    public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
                        return resolver.shouldIntercept(e);
                    }
                }
        );

        super.onAttachedToRecyclerView(recyclerView);
    }

    public void disableDrag(Direction direction) {
        resolver.setToBlock(direction);
    }

    public void enableDrag() {
        resolver.setToBlock(Direction.allow_all);
    }
}

缺点:

**

  • A) onPageSelected:

**

在页面切换时应执行setToBlock(Direction)方法。 问题是:谁来调用它/何时调用它?

我不知道答案...... 我最好的猜测是将该方法放置在ViewPager的onPageSelected回调侦听器中是一个好地方.... 但是这个侦听器存在问题。

        viewPager2.registerOnPageChangeCallback(
                new ViewPager2.OnPageChangeCallback() {

                    @Override
                    public void onPageSelected(int position) {
                        if (position == 2) mAdapter.enableDrag(); return;
                        if (position == 1) mAdapter.disableDrag(left_to_right)

                    }
                }
        );

有时候,当使用某种手指快速滑动时,监听器在交换动画结束之前就会注册页面更改。这意味着在瞬间内,快速滑动会受到传入页面方向规则的影响,但实际上交换仍在前一页进行。
在上面的示例中,mAdapter.enableDrag(); 发生在位置2。假设位置0应该被禁止,因此位置1的规则是 mAdapter.disableDrag(left_to_right),以使位置0无法访问。
如果我用这种方式轻轻地滑动手指,使ViewPager注册一个位置更改到2(eanbleDrag()),然后朝相反方向滑动而不离开手指,则页面从半渲染的位置2返回到不可访问的位置0。
这不难重现,每5次尝试中就有1次,但您需要积极想要重现它。
我不知道如何解决这个问题。
也许应该在交换的后期才调用disableDrag(),但这意味着要访问页面片段的生命周期。这意味着使用setTargetFragment()方法(“干净”的方法),而我宁愿死也不用那个。
另一种选择是使用绑定到ViewPagerFrag到PageFrag的后退堆栈条目的共享ViewModel进行Fragment到Fragment通信。
当然...暂且不管这两种方法都在幕后使用静态字段进行引用存储...这意味着您完全可以采用公共静态方式,当然前提是保持代码整洁...
B)限制一个方向的滑动。
实际上,这个解决方案旨在模仿给定方向上页面的缺席,问题在于,如果我们仔细分解页面缺席的行为,我们会注意到动画只有在达到给定轴之后才限制运动,更准确地说,一旦最后一页(索引0)完全显示,限制就成为现实,如果索引0的页面向索引1滑动并稍微偏移屏幕,您仍然可以向0滑动页面。
通过限制一个方向的移动,并让此规则统治整个页面,会出现不需要的行为:
例如:[位置0(禁止)] - [1(允许)] - [2(允许)]
为了模拟位置0的缺席,位置1必须disableDrag(left_to_rigth);
如果我们从1向2拖动手指,然后再轻轻地拖回来(可能是因为用户改变主意决定留在页面1)...,那么,由于整个页面受到toBlock方向==left_to_rigth的支配,页面将拒绝返回,并且动画将卡在两个片段之间。

我的猜测是,在给定的Fragment Y轴达到屏幕上的给定触发Y轴时(即:使用屏幕坐标监听器),应该执行禁用操作。这意味着我们需要更多了解组件提供给我们访问的所有不同可用的监听器。

...

所以...

目前我能做的最好的就是,我真的很希望能得到任何关于如何解决这个问题或者处理主要问题的建议,即添加和删除Fragment而不会丢失状态。尽管我几乎看不到这种可能性(至少在StateAdapter中),因为DiffUtil需要推断重新排序的更改,而我认为Mayer的算法并不适用于处理那种级别的推断(重新排序推断(“索引跳跃”)。相反,Mayers仅通过推断整个段重新排序来工作。

此外,Fragment集合需要同时作为LIFO和FIFO(我认为术语是旋转木马?),以支持从两端添加和删除,顶部(禁止从左到右访问)和底部(禁止从右到左访问)。

我认为这太过具体和繁琐,不是ViewPager2工具必要的增强功能。

因为我缺乏所有这些细节的知识...我认为最好的解决方案是:

禁用整个ViewPager2的滑动功能,只允许通过选项卡进行导航。然后可以添加或删除选项卡,使其看起来像是添加或删除了Fragment。


很不幸,这一点都不流畅。启用后,很难滚动视图页面,它不能按预期工作。 - Felipe Ribeiro R. Magalhaes
完全同意这不是完美的,主要是因为如果任何东西需要从屏幕事件进行浮点运算,那么现在已经太晚了,无法做任何事情。我注意到,如果布局非常干净,没有任何冗余(这是 Android 的另一个问题,因为太多属性在 XML 上互相覆盖),那么如果父属性以任何方式拦截您的 recyclerView,则拦截的浮动值将会给出意外的结果。 - Delark
我还没有足够的时间检查这段代码,但据我所知它已经可以工作了,虽然说实话,我更把滑动功能当作是一个辅助导航选项,主要是借助 on-item-chosed + onBackPressed() (类似树状索引) 的方式来浏览 viewPager。此外,添加和删除按钮的底部导航也是有效的。 - Delark

1

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

-1

扩展viewpager类并覆盖函数onInterceptTouchEventonTouchEvent。然后识别滑动的方向,如果不想滑动,则返回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://dev59.com/W2Af5IYBdhLWcg3wXhyz#34111034


正如我在问题中所述,我已经尝试扩展ViewPager2类并覆盖触摸事件,但不幸的是ViewPager2是一个final类,因此我无法扩展它。 - Shahbaz Hussain
你尝试过使用 androidx.viewpager.widget.ViewPager 类吗? - Umar Hussain
3
是的,起初我使用了ViewPager,但我想尝试使用ViewPager2,因为它比前者更高效和优越。 - Shahbaz Hussain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接