Android 滑动冲突处理笔记

本文深入探讨Android中点击事件的分发机制,包括事件冲突的场景、解决冲突的方法,以及如何通过外部拦截法和内部拦截法来控制事件的流向。文章提供了详细的伪代码和注意事项,帮助开发者更好地理解和应用事件传递机制。
摘要由CSDN通过智能技术生成

整理自:《Android 艺术探索》

关于事件传递机制部分:点击事件分发机制 关键源码笔记


1、冲突的几种场景
  1. 外部滑动与内部滑动方向不一致
  2. 外部滑动与内部滑动方向一致
  3. 上述两种情况的嵌套
2、解决冲突的前提

制定好规则,即什么情况由外部的父容器拦截处理,什么时候分发给内部的子控件处理。

3、解决方法

(1)外部拦截法

即事件先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就分发给子控件。

该方法的实现需要重写父容器的 onInterceptTouchEvent() 方法,伪代码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            // 根据实际情况来判断是否拦截
            if (父容器需要拦截) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        // 
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

需要注意的几个点:

  • 1、通常情况下,对于 DOWN 事件必须返回 false,因为如果父容器拦截了 DOWN 事件,则后续的 MOVE、UP 事件就不会分发给子控件处理,而直接交由父容器处理。

  • 2、对于 ACTION_UP 事件,在父容器中(onInterceptTouchEvent() 方法中)必须返回 false,因为 ACTION_UP 事件本身没有太多意义。

    注意,这里返回 false 是为了在父容器不拦截 MOVE 事件的时候能够使得 UP 事件正常传递到子控件,因为如果拦截父控件拦截了 MOVE 事件,则会把 mFirstTouchTarget 清空,此时对于 UP 事件本身就无法传递到子控件中了。

    但是在父控件没有拦截 MOVE 事件的时候,如果父容器在 ACTION_UP 事件时返回了 true,则子控件就无法接收到该事件了,此时子控件的 onClick 事件就无法触发。不过对于父容器来说,即使 ACTION_UP 事件在 onInterceptTouchEvent() 方法中返回了 false,但是此时已经传递到父容器了 dispatchTouchEvent() 中(因为是在 dispatchTouchEvent() 中通过 onInterceptTouchEvent() 来判断是否拦截事件)。

(2)内部拦截法

父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要该事件就直接消耗掉,否则就交由父容器进行处理。

注意,“父容器不拦截任何事件” 指的是在逻辑上不拦截任何事件,而由子元素的自行判断。

但是在具体代码的实现上,则稍有不同:

首先,需要重写子元素的 dispatchTouchEvent() 方法:

public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
       		// true 表示不允许父控件拦截
       		// 当传递到这里来的时候,表示父控件的 mGroupFlags 已经被重置过了
       		// 因此这里设置为不允许拦截时不会受重置的影响
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (父容器需要此类点击事件) {
            	// 不过此次的 MOVE 事件会交由当前控件的 super.onTouchEvent(event),
            	// 而不会回传给父容器的 onTouchEvent() 了
            	// 之后的第一次事件也不会传递给父容器的 onTouchEvent(),
            	// 因此第一次的要转换成 ACTION_CANCEL 事件传递给 mFirstTouchTarget 中的子 View,
            	// 并清空 mFirstTouchTarget,第二次及以后的才会正常的传递给父容器的 onTouchEvent()。
            	// 当然上述是在当前代码逻辑下的场景,否则视具体代码而定。
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
    }
    mLastX = x;
    mLastY = y;
    return super.onTouchEvent(event);
}

在子元素的 onTouchEvent() 中,需要对 ACTION_DOWN 事件调用父容器的 requestDisallowInterceptTouchEvent(true),将父容器的 mGroupFlags 设置为不允许拦截状态,同时该方法里面也会依次将父容器的父容器的 mGroupFlags 也设置为不允许拦截状态。

然后还需要重写父容器的 onInterceptTouchEvent() 方法,在实际的代码上对非 ACTION_DOWN 进行拦截。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}

需要进行上述两步的原因如下:

当父容器的 mGroupFlags 设置为不允许拦截状态时,此时对于 ACTION_DOWN 事件是无法干预的,因为每次 ACTION_DOWN 事件传递到父容器时,都会先重置其 mGroupFlags 为允许拦截状态(如后面源码所示)。

而当 mGroupFlags 为允许拦截状态时,ACTION_DOWN 事件又会先传递到父容器的 onInterceptTouchEvent() 去进行判断是否拦截,如果拦截了,则会导致子控件无法接收点击事件。

因此在父容器中必须不拦截 ACTION_DOWN 事件,而对于后续事件,如果子元素设置了:

getParent().requestDisallowInterceptTouchEvent(false);

则表示子元素希望父容器拦截,因此父容器的 onInterceptTouchEvent() 又要对非 ACTION_DOWN 事件默认进行拦截,否则父容器无法正常的对后续事件进行拦截。


补充源码

// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            // 重置 mGroupFlags 为可拦截状态
            resetTouchState();
        }
        // Check for interception.
        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); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }
        ...
	}
	...
}	        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值