【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);
    }
}
相关推荐
Wang's Blog3 小时前
Java框架快速入门: Spring Security+OAuth2之元注解简化权限表达式
java·数据库·spring
xifangge20253 小时前
AGENTS.md 怎么写?涵盖 Java、Python、Vue、Go 的 8 套开箱即用模板
java·vue.js·python
Wang's Blog3 小时前
Java框架快速入门: Spring Security+OAuth2之云服务集成与多因子认证设计
java·开发语言·spring
天空之城--3 小时前
Android逆向安全合规接单指南
android·安全
liangsheng_g4 小时前
SpringAOP拦截器链递归与事务钩子补偿源码实战
java·spring
SL_staff4 小时前
制造业私有化文档平台的技术实践:从知识孤岛到可追溯知识资产
java·spring·开源
消失的旧时光-19435 小时前
Android 系统层扫盲 09:AMS、ATMS、WMS、PMS 到底分别管什么?
android·wms·pms·ams·aosp·atms
SL_staff5 小时前
3天上线OKR系统:一名HR与1名工程师如何用JVS完成全栈交付
java·低代码·开源
Godikov5 小时前
Android 系统级设备应用踩坑实录:sharedUserId 签名、SDK 授权失败 -4、开机自启与 U 盘 OTA 升级
android
wzdark7 小时前
基于链表的内存池设计与内存复用机制4
java·数据结构·链表