【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);
    }
}
相关推荐
极梦网络无忧几秒前
Android无障碍服务实现抖音直播间界面监控(场控助手核心原理)
android
海兰15 分钟前
使用 Spring AI 打造企业级 RAG 知识库第二部分:AI 实战
java·人工智能·spring
历程里程碑32 分钟前
二叉树---二叉树的中序遍历
java·大数据·开发语言·elasticsearch·链表·搜索引擎·lua
小信丶1 小时前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
无限进步_1 小时前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神1 小时前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe1 小时前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿1 小时前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
call me by ur name1 小时前
ERNIE 5.0 Technical Report论文解读
android·开发语言·人工智能·机器学习·ai·kotlin
阿维的博客日记1 小时前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java