Android事件分发机制之源码完美解析(上)

学事件分发是为了什么呢?还不是为了解决滑动冲突的。

实际上,如果仅仅是为了解决滑动冲突的,大可不必看源码,只需要掌握事件分发的外在规律即可。


只要记住这张图,再明白内部拦截法和外部拦截法,滑动冲突这一块,都可以轻松解决了。

分享一个非常好的滑动冲突的实例:http://blog.csdn.net/qq_36523667/article/details/78825810

只需要掌握上述的内容,事件分发再无难题了。


但是,如果想掌握的更通透一点,源码是不可或缺的。因为源码里还有很多细节,enable?clickable?performClick?。。。


最重要的一个问题,如果在自定义一个view的时候,onTouchEvent返回了true,代表是消费这系列事件。而且onTouchEvent不会再调用父容器的onTouchEvent;如果返回false,会转而调用父容器的onTouchEvent。这个问题是事件分发的核心问题,可是在源码中哪里有答案???

在view group里有一段核心的代码

for (int i = childrenCount - 1; i >= 0; i--) {
...
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}
上面会有一个for循环,进行手指触摸区域的查找,看看是否会落在child里面。找到那个手指所落在的那个child view里。然后执行child.dispatchTouchEvent。进行一个递归查找。(为什么自动就进行一个递归查找了呢?别看这里是view哦,以为是直接就调用了view的dispatchTouchEvent。错误!比如你这个child查找到是FrameLayout,然后你其实是一个view group,所以你的dispatch touch event 肯定是view group的dispatch touch event(因为view group重写了view的dispatchTouchEvent))

所以for循环的任务是:找到当前view group中所有view、view group的区域中包含该手指落点的view、viewgroup,并进行一个递归的分发。

不过不是还有一种找不到的情况嘛!那种情况大部分是mFirstTouchTarget为空。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}
也就是上面的这段代码。而下面的else总体上就是指代的CANCEL情况,不过本文讨论的重点不是CACEL。

这种情况就是1.没有child 2.child不需要 这个时候由这个view group 调用super.dispatchTouchEvent。即把这个view group作为view,调用他自身的view.dispatchTouchEvent。


下面着重讲讲子view不为空的情况,去解决那个核心问题。

重新提一下问题:如果子view返回true,为什么就不调用父view的逻辑了?如果子view返回false,为什么就调用父view的逻辑了?

其实上面已经给出了答案。上面的源码先是一个for循环,那个for循环考虑的就是子view的情况;下面有一个if,if考虑的就是父view的情况。

如果1.view group有子view 2.且这个子view返回了true,愿意接收这个事件 这种情况mFirstTouchTarget就不为空了。所以下面的if判断条件就不成立了。而如果1.view group没有子view 2.或者这个子view 返回了 false。这个时候mFirstTouchTarget就是空了。自然就只能交给view group来处理了。

现在用最正宗的伪代码概括一下view group的代码:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    for (;;) {
        递归所有的子View
        1.有子View返回了true进行消费 mFirstTouchTarget = xxx
        2.没有       
    }
    if (mFirstTouchTarget == null) {
        super.dispatchTouchEvent(ev);
    } else {
        CANCLE的情况
    }
}

接下来我们用源码作最后的证明:为什么子View的onTouchEvent的返回值可以决定mFirstTouchTarget?

上面的for循环的逻辑伪代码如下

 
 
TouchTarget newTouchTarget;
for (;;) {
    if (dispatchTransformedTouchEvent(...)) {
        newTouchTarget = addTouchTarget(child...);
    }
}
所以我们需要查看一下,addTouchTarget方法的源码

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
所以在这里就完成了mFirstTouchTarget的赋值。(这里解释下mFirstTouchTarget的概念,他在view group中的dispatch代码中起了十分重要的作用。least recently added target。意思就是最近在for循环中搜到的并且愿意消费的view。)


解决了上述问题,继续追溯伪代码中的dispatchTransformedTouchEvent方法:

// Perform any necessary transformations and dispatch.
if (child == null) {
    handled = super.dispatchTouchEvent(transformedEvent);
} else {
    final float offsetX = mScrollX - child.mLeft;
    final float offsetY = mScrollY - child.mTop;
    transformedEvent.offsetLocation(offsetX, offsetY);
    if (! child.hasIdentityMatrix()) {
        transformedEvent.transform(child.getInverseMatrix());
    }

    handled = child.dispatchTouchEvent(transformedEvent);
}
这里是dispatchTransformedTouchEvent中的核心代码。这里是递归调用逻辑的具体实现。直接看else吧。因为for循环中传入的参数是child。(if中传入的child参数才为空)所以直接看view的dispatch代码就可以了。


view的dispatch的核心代码

if (!result && onTouchEvent(event)) {
    result = true;
}
return result;
所以view的dispatch的返回值是由view的onTouchEvent来决定的。


所以,得出结论:如果view的onTouchEvent返回true,view的dispatch多半也会返回true,然后父view group的dispatchTransformedTouchEvent多半也会返回true,然后就mFirstTouchTarget也会被赋值,因此父view group的逻辑就不会被执行了;反之,view的onTouchEvent...然后mFirstTouchTarget依然为空,因此父view group的逻辑就会被得到执行。


到这里告一段落,这篇文章主要分析事件分发的核心机制,让我们不仅仅停留在会用的基础上,也知其所以然了。下篇文章会像流水账一样分析所有事件分发的代码。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值