【问题标题】:How to disable swiping in specific direction in ViewPager2如何在 ViewPager2 中禁用特定方向的滑动
【发布时间】:2019-06-18 11:21:16
【问题描述】:

我想在ViewPager2 中禁用从右向左滑动。 我的导航抽屉中基本上有一个带有 2 页的 viewpager2 元素。我希望我的第二页仅在我单击第一页中的某个元素时显示(从第一页从右向左滑动不应打开第二页),而当我在第二页中时,viewpager2 滑动(向左向右滑动)应该像在 viewpager 中那样滑动。

我已经尝试扩展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);

【问题讨论】:

  • 您可以尝试在页面更改时添加监听器,如果当前页面不可见则回滚

标签: android kotlin android-viewpager2


【解决方案1】:

我找到了一个监听器,它可以在用户尝试滑动时监听,然后它会检查当前页面,如果是第一页,则禁用用户输入,否则默认启用它。

这是代码 sn-p

在 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) listener viewpager2 的值,并根据positionpositionOffset 的正值或负值处理所需的场景。

【讨论】:

  • 操作流畅吗?喜欢它在禁用之前立即禁用滚动或显示一些抖动?
  • @UmarHussain 是的,它绝对流畅,完全没有抖动,实际上这个监听器会在用户实际打算滑动时进行监听,因此它会在实际滑动发生之前处理行为
  • 太好了,然后它比旧实现更容易处理滑动。
  • 多个片段的最简单解决方案stackoverflow.com/a/70666569/853140
【解决方案2】:

超过 2 个片段的解决方案。 (向下滚动查看解决方案,最后更新代码以纠正一些错误。)

这个有点棘手。

我认为,我认为人们希望在一个方向上禁用滑动的原因是因为无法在运行时添加片段,同时保持之前显示的片段的状态。

所以人们正在做的是他们正在预加载所有片段,并让那些没有显示的片段根本不存在。

现在,如果团队让适配器不受 ViewLifeCycle 的限制,这可以通过使用 ListDiffer 选项轻松解决,该选项可以正确地将更新传播到 RecyclerView 适配器,但是因为每个 dataSetChanged 都需要一个新的适配器ViewPager2,需要重新创建整个Fragments,ListDiffer对ViewPager2没有影响。

但也许不完全是因为我不确定 ListDiffer 是否能够识别“位置交换”以保留状态。

现在,关于建议使用registerOnPageChangeCallback() 的答案。

使用超过 2 个 Fragment 来 registerOnPageChangeCallback() 没有用的原因是,当这个方法被调用时,做某事已经太晚了,这造成的是窗口在中途变得无响应,与 addOnItemTouchListener() 不同;它能够在触摸到达视图之前拦截它们。

从某种意义上说,阻止和允许滑动的完整事务将由 registerOnPageChangeCallback() 和 addOnItemTouchListener() 这两个方法执行。

registerOnPageChangeCallback() 将告诉我们的适配器应该在哪个方向停止工作(通常从左到右(我将称之为“左”))以及在哪个页面,而addOnItemTouchListener() 会告诉视图在在我们想要的方向上的正确时刻。

问题在于,要使用 TouchListener,我们需要访问 ViewPager2 中的内部 RecyclerView。

执行此操作的方法是从 FragmentStateAdapter 覆盖 onAttachedToWindow() 方法。

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

现在附加到 RecyclerView 的正确侦听器称为 RecyclerView.SimpleOnItemTouchListener(),问题是侦听器无法区分“右”抛物和“左”抛物。

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

我们需要混合两种行为来获得想要的结果:

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() 方法在rv.getScrollState() 变为SCROLL_STATE_DRAGGING 之前至少执行了3 次以上

一旦定义了这个类,整个代码应该是这样的(在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 进行了一些更新,以解决 sme 错误和更多功能:

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

             }
         }
    );

【讨论】:

  • 不幸的是,这根本不顺利。启用后,很难滚动 viewpager,它不能按预期工作。
  • 完全同意这并不完美,主要是因为如果有任何事情需要从屏幕事件中进行浮动操作,那么做任何事情都已经太迟了。我注意到如果布局非常干净而没有任何冗余(这是 android 的另一个问题,太多属性在 XML 上覆盖自身),则该组件运行良好,因此如果父属性以任何方式拦截你的 recyclerView 拦截的浮动将给出一个意想不到的值。
  • 我没有足够的时间检查这段代码,但它对我很有用,虽然老实说,我已经让滑动功能更多地作为一个侧面导航选项,制作一个项目-选择 + onBackPressed()(树状索引)是导航 viewPager 的主要方式。添加和删​​除按钮的底部导航也是有效的。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    扩展 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://stackoverflow.com/a/34111034/4428159

    【讨论】:

    • 正如我的问题所述,我已经尝试扩展 ViewPager2 类并覆盖触摸事件,但不幸的是 ViewPager2 是最终类,所以我无法扩展它。
    • 你试过androidx.viewpager.widget.ViewPager类
    • 是的,我最初是这样做的,但我想尝试一下ViewPager2,因为它比它的前身ViewPager 更高效和优越
    猜你喜欢
    • 2019-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多