【Android】模板化解决复杂场景的滑动冲突问题

仿写项目的业务场景刚好覆盖有两种复杂滑动冲突场景:

Horizontal ViewPager2 嵌套 Vertical RecyclerView (OuterRecyclerView ) 嵌套 Horizontal RecyclerView (InnerRecyclerView) 的横纵横场景和

Horizontal ViewPager2 嵌套 Horizontal ViewPager2 (InnerViewPager) 嵌套 Vertical RecyclerView 的横横纵场景。

同时要让内层的 InnerRecyclerView 和 InnerViewPager 在横向滑动方向尚可滑动接管事件,不可滑动时将事件交给外层的 Horizontal ViewPager2。

背景原理

我们知道 MotionEvent 事件传递是由 DecorView 到 ViewGroup 再到 View,完整事件序列必定由 MotionEvent.DOWN 开始,ViewGroup 在事件传递过程中会判断是否拦截该事件序列,如果 DOWN 被拦截则该 ViewGroup 会完全接管后续所有事件。

java 复制代码
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action);
    } else {
        intercepted = false;
    }
} else {
    intercepted = true;
}

由外层条件语句知,child 的 requestDisallowInterceptTouchEvent 方法无法影响 ViewGroup 对 DOWN 事件的拦截处理,所以大部分情况 ViewGroup 派生类都不会拦截 DOWN 事件,通常逻辑是让 child 消费 DOWN 事件,再根据后续 MOVE 方向判断是否拦截。

requestDisallowInterceptTouchEvent

该方法是 child 发送给父容器的请求,参数的布尔值表示是否期望父容器拦截事件,父容器接收到请求后会将 FLAG_DISALLOW_INTERCELT 标志位设置为 1,内部的 disallowIntercept 字段进而得到 true。

java 复制代码
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}
java 复制代码
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
} /* mGroupFlags可以看成标记位大集合 */

所以解决滑动冲突的方法很明确:父布局不要拦截向下传递的 DOWN 事件,让 child 在 DOWN 事件禁止父布局拦截事件,根据后续 MOVE 事件的滑动方向和 child 本身的情况判断是否允许父布局拦截事件,在 CANCEL 或 UP 事件恢复原状。

横纵横

InnerRecyclerView 判断是否为横向滑动且滑动方向有内容在屏幕外,是则由自己接管,否则交给 ViewPager2,这里利用 View 树的特点冒泡获得以 ViewPager2 为父容器的 View,对该 View 进行 requestDisallowInterceptTouchEvent,隔离了 OuterRecyclerView 和 ViewPager2。

如果为纵向滑动则设置 requestDisallowInterceptTouchEvent(false),让 OuterRecyclerView 来禁止父容器拦截,保证了 InnerRecyclerView 和 OuterRecyclerView 互不冲突。

java 复制代码
public class InnerRecyclerView extends RecyclerView {

    private double x0, y0;
    private final int touchSlop;

    public InnerRecyclerView(Context context) {
        super(context);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public InnerRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public InnerRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        ViewGroup v = this;
        while (v != null && !(v.getParent() instanceof ViewPager2)) {
            v = (ViewGroup) v.getParent();
        }

        switch (e.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                x0 = e.getX();
                y0 = e.getY();
                v.requestDisallowInterceptTouchEvent(true);
                break;

            case MotionEvent.ACTION_MOVE:
                double dx = e.getX() - x0;
                double dy = e.getY() - y0;

                if (Math.abs(dx) < touchSlop && Math.abs(dy) < touchSlop) break;

                if (Math.abs(dx) > Math.abs(dy)) {
                    if (dx > 0) {
                        if (!canScrollHorizontally(-1)) {
                            v.requestDisallowInterceptTouchEvent(false);
                        } else {
                            v.requestDisallowInterceptTouchEvent(true);
                        }
                    } else {
                        if (!canScrollHorizontally(1)) {
                            v.requestDisallowInterceptTouchEvent(false);
                        } else {
                            v.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                } else {
                    v.requestDisallowInterceptTouchEvent(false);
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                v.requestDisallowInterceptTouchEvent(false);
                break;
        }

        return super.onInterceptTouchEvent(e);
    }
}

OuterRecyclerView 判断是否为纵向滑动,是则由自己接管,否则交给 ViewPager2,InnerRecycler 和 OuterRecyclerView 的拦截决策均只影响 ViewPager2 是否拦截,所以保证 ViewPager2 和内层整体互不冲突,进而解决横纵横方向的滑动冲突。

java 复制代码
public class OuterRecyclerView extends RecyclerView {

    private double x0, y0;
    private final int touchSlop;

    public OuterRecyclerView(@NonNull Context context) {
        super(context);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public OuterRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public OuterRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                x0 = ev.getX();
                y0 = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                double dx = ev.getX() - x0;
                double dy = ev.getY() - y0;

                if (Math.abs(dx) < touchSlop && Math.abs(dy) < touchSlop) break;

                if (Math.abs(dy) > Math.abs(dx)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                } else getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

横横纵

因为 ViewPager2 是 final 类,无法通过重写 onInterceptTouchEvent 方法解决冲突,换种思路想,我们可以用 FrameLayout 包裹 InnerPager2,重写 FrameLayout 的 onInterceptTouchEvent 方法来解决,其内部逻辑和横纵横的 InnerRecyclerView 类似。

java 复制代码
public class InnerViewPagerContainer extends FrameLayout {

    private int touchSlop;
    private float startX, startY;

    public InnerViewPagerContainer(Context context) {
        super(context);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public InnerViewPagerContainer(Context context, AttributeSet attrs) {
        super(context, attrs);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        ViewGroup v = this;
        while (v != null && !(v.getParent() instanceof ViewPager2)) {
            v = (ViewGroup) v.getParent();
        }

        switch (e.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                startX = e.getX();
                startY = e.getY();
                v.requestDisallowInterceptTouchEvent(true);
                break;

            case MotionEvent.ACTION_MOVE:
                float dx = e.getX() - startX;
                float dy = e.getY() - startY;

                if (Math.abs(dx) < touchSlop && Math.abs(dy) < touchSlop) break;

                if (Math.abs(dx) > Math.abs(dy)) {
                    if (dx > 0) {
                        if (!getChildAt(0).canScrollHorizontally(-1)) {
                            v.requestDisallowInterceptTouchEvent(false);
                        } else {
                            v.requestDisallowInterceptTouchEvent(true);
                        }
                    } else {
                        if (!getChildAt(0).canScrollHorizontally(1)) {
                            v.requestDisallowInterceptTouchEvent(false);
                        } else {
                            v.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                } else {
                    v.requestDisallowInterceptTouchEvent(false);
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                v.requestDisallowInterceptTouchEvent(false);
                break;
        }

        return super.onInterceptTouchEvent(e);
    }
}
相关推荐
Ray Liang1 小时前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
Java水解1 小时前
Java 中间件:Dubbo 服务降级(Mock 机制)
java·后端
砖厂小工3 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心4 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心4 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
SimonKing5 小时前
OpenCode AI辅助编程,不一样的编程思路,不写一行代码
java·后端·程序员
FastBean5 小时前
Jackson View Extension Spring Boot Starter
java·后端
Kapaseker6 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴7 小时前
Android17 为什么重写 MessageQueue
android
Seven977 小时前
剑指offer-79、最⻓不含重复字符的⼦字符串
java