android触摸事件的分发

本文根据源码来梳理流程,将事件分发分为down、move、up、cancel事件分别分析梳理。算是比较详尽的分析了整个流程。

 

一、触摸事件传递流程

 用户触摸屏幕,Ativity是最先接触到屏幕的,然后将触摸事件传递到DecorView,然后由DecorView处理具体的事件分发。DecorView也是一个ViewGroup,接下来我们将从ViewGroup开始分析触摸事件分发。

 

 二.ViewGroup和View事件分发逻辑

   ViewGroup的事件分发我们将分为三个部分分析,分别是 DOWN事件、MOVE事件和UP事件、CANCEL事件,因为这三个事件的逻辑流程是不同的。一定要打开ViewGroup的源码看。以下所说控件皆指View及其子类。

    1.触摸事件的down动作的执行分发流程。

       在ViewGroup接收到down触摸事件的时候首先会去执行dispatchTouchEvent(MotionEvent ev)方法,在方法里面首先会去清空mFirstTouchTarget和mGroupFlags对应的打断触摸事件的标志位,每个down动作都会清空之前事件保存的状态。

 

if (actionMasked == MotionEvent.ACTION_DOWN) {
    //清空mFirstTouchTarget 
    cancelAndClearTouchTargets(ev);
    //设置mGroupFlags里面可以执行onInterceptTouchEvent()方法的标志位
    resetTouchState();
}

接下来会执行下面代码,FLAG_DISALLOW_INTERCEPT是一个掩码,通过与运算判断mGroupFlags标识对应二进制位是否是被打断的,我猜mGroupFlags的最高位也就是第20位是触摸事件的是否允许被打断的标志位。android很多地方都运用了16进制的整形来存储状态,通过与或非的方式来存取这些状态,这是一个非常聪明的方法,值得学习。有空我可以分享一下16进制整形存储状态在android中的运用。

 

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;
}

开发者也可以通过调用ViewGroup的requestDisallowInterceptTouchEvent(boolean disallowIntercept)来设置是否允许被打断,这个方法里面就是修改mGroupFlags的最高位来存储是否允许打断的标识。

 

接下来,如果intercepted==false,ViewGroup不打断事件传递,那么就会向子控件遍历寻找对应的子控件然后分发down动作。

if (!canceled && !intercepted) {

    // If the event is targeting accessiiblity focus we give it to the
    // view that has accessibility focus and if it does not handle it
    // we clear the flag and dispatch the event to all children as usual.
    // We are looking up the accessibility focused host to avoid keeping
    // state since these events are very rare.
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;
    //一定是要down动作或者鼠标停留在Viewgroup才会执行下面代码,move、up、cancel是不会遍历子控件的
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        final int actionIndex = ev.getActionIndex(); // always 0 for down
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                : TouchTarget.ALL_POINTER_IDS;

        // Clean up earlier touch targets for this pointer id in case they
        // have become out of sync.
        removePointersFromTouchTargets(idBitsToAssign);

        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            final float x = ev.getX(actionIndex);
            final float y = ev.getY(actionIndex);
            // Find a child that can receive the event.
            // Scan children from front to back.
            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
            final boolean customOrder = preorderedList == null
                    && isChildrenDrawingOrderEnabled();
            final View[] children = mChildren;
            //遍历循环子控件,倒序遍历是为了找到最上层的子控件,
            //假如在触摸的同一个点有多个控件在此处重叠,这个是就先找到最上面的子控件然后分发事件
//这也是为什么mFirstTouchTarget是一个链表的原因。mFirstTouchTarget链表里面是能消耗所有此事件的子控件
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = getAndVerifyPreorderedIndex(
                        childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(
                        preorderedList, children, childIndex);

                // If there is a view that has accessibility focus we want it
                // to get the event first and if not handled we will perform a
                // normal dispatch. We may do a double iteration but this is
                // safer given the timeframe.
                if (childWithAccessibilityFocus != null) {
                    if (childWithAccessibilityFocus != child) {
                        continue;
                    }
                    childWithAccessibilityFocus = null;
                    i = childrenCount - 1;
                }
                //判断该子控件是否是要执行事件分发的子控件,
                //如果不是就继续遍历循环,如果是就执行后续代码
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
                
                newTouchTarget = getTouchTarget(child);
                if (newTouchTarget != null) {
                    // Child is already receiving touch within its bounds.
                    // Give it the new pointer in addition to the ones it is handling.
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                    break;
                }

                resetCancelNextUpFlag(child);

 如果down动作被子控件给消耗掉了(返回true),就会执行下面if语句里面的代码,会在addTouchTarget()方法里面给mFirstTouchTarget赋值,alreadyDispatchedToNewTouchTarget = true;

//正式将事件传递个子控件,如果if判断为true则消耗此事件,false则不消耗此事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    // Child wants to receive touch within its bounds.
                    mLastTouchDownTime = ev.getDownTime();
                    if (preorderedList != null) {
                        // childIndex points into presorted list, find original index
                        for (int j = 0; j < childrenCount; j++) {
                            if (children[childIndex] == mChildren[j]) {
                                mLastTouchDownIndex = j;
                                break;
                            }
                        }
                    } else {
                        mLastTouchDownIndex = childIndex;
                    }
                    mLastTouchDownX = ev.getX();
                    mLastTouchDownY = ev.getY();

                    //找到了当前对应的子控件,将child控件封装到TouchTarget里面,
                    //并将这个TouchTarget作为链头添加到mFirstTouchTarget中
                    //重点,当子控件消耗了此事件,此时mFirstTouchTarget已经不为null了
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    //重点,当子控件消耗了此事件,alreadyDispatchedToNewTouchTarget 是 true
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }

                // The accessibility focus didn't handle the event, so clear
                // the flag and do a normal dispatch to all children.
                ev.setTargetAccessibilityFocus(false);
            }
            if (preorderedList != null) preorderedList.clear();
        }

        if (newTouchTarget == null && mFirstTouchTarget != null) {
            // Did not find a child to receive the event.
            // Assign the pointer to the least recently added target.
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
    }
}

以上代码,在向下分发事件的时候要调用dispatchTransformedTouchEvent()方法,它是真正控制分发事件的方法,究竟是当前控件自己处理还是对应的子控件来处理。

如果交给自己处理就会调用View.dispatchTouchEvent()处理,就会执行onTouchEvent()方法,onTouchEvent()方法就是处理具体的触摸事件的。但是onTouchEvent()的执行是有条件的,要根据当前控件是否添加OnTouchListener和OnTouchListener.onTouch()是否消耗了此事件来决定是否执行onTouchEvent()。用户的添加的点击事件监听和长按监听也是在onTouchEvent()里面处理的,所以自定义控件重写OnTouchEvent()的时候需要注意。可以查看源码了解逻辑。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();

    // cancel这个变量很重要,当cancel为true时,这表示要执行cancel事件,这个后面会分析到
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        //将当前事件变成cancel事件
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        //再还原之前的事件
        event.setAction(oldAction);
        return handled;
    }

    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // If for some reason we ended up in an inconsistent state where it looks like we
    // might produce a motion event with no pointers in it, then drop the event.
    if (newPointerIdBits == 0) {
        return false;
    }

    // If the number of pointers is the same and we don't need to perform any fancy
    // irreversible transformations, then we can reuse the motion event for this
    // dispatch as long as we are careful to revert any changes we make.
    // Otherwise we need to make a copy.
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }

    // 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()) {
            //然后计算出触摸事件在子控件里面对应的x,y坐标值
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

 

接下来在往后面执行代码,如果down动作被拦截或者向下分发的时候不消耗此事件,这个时候mFirstTouchTarget == null。这个时候回去执行下面代码


            //如果down动作被拦截或者向下分发的时候不消耗此事件,mFirstTouchTarget==null
            if (mFirstTouchTarget == null) {

                //在之前分析dispatchTransformedTouchEvent()方法,我们知道当前控件回去调用自己的dispatchTouchEvent()方法来处理此次的DOWN事件
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {//down事件被子控件控件消耗掉了
             
                TouchTarget predecessor = null
                TouchTarget target = mFirstTouchTarget;
                //循环所有能处理此事件的子控件
                while (target != null) {
                    final TouchTarget next = target.next;
                    
                    //down事件时,设置handle为true,代表已经消耗此事件
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {//当move、up、cancel事件时,会执行下面代码
                        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;
                }
            }

上面child的参数是null,这个时候回去执行父类也就是Viewd的dispatchTouchEvent()方法,最后会调用到自己的onTouchEvent(),这也就是当ViewGroup的子控件不消费这个触摸事件的时候或者当前容器拦截了触摸事件就会执行自己的onTouchEvent()方法。

如果mFirstTouchTarget != null,就表示子控件消耗了此事件,就会设置handled为true

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    handled = true;
}

 

2.触摸事件的move和up动作的分发流程。

move和up动作在分发流程上大致是一致的,但是不会去遍历子控件查找对应的可以消耗此事件的子控件,这个操作之前的down动作已经做了。如果有子控件消耗了down事件就会将对应的子控件保存在mFirstTouchTarget里面。move up事件的执行就直接会找mFirstTouchTarget里面的子控件去进行分发。

当move和up动作在ViewGroup的dispatchTouchEvent()里面开始执行的时候,有两种情况,是根据之前move动作的执行结果而受到影响的,即mFirstTouchTarget是否为空。我们分这两种情况来分析

(1).mFirstTouchTarget == null,即move/up动作的时候,被viewgroup的onInterceptTouchEvent(ev)打断 或者 子控件不消费此事件。

    开始的时候,如果用户没有修改mGroupFlags的拦截标志位话,默认是会执行onInterceptTouchEvent();

 

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;
}

因为只有down动作的时候才会去遍历子控件分发分发事件,当是move和up动作的时候newTouchTarget == null,alreadyDispatchedToNewTouchTarget == false;

接下来会执行下面代码

 

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

这个时候会去执行父类也就是View里面的dispatchTouchEvent()方法,最后会调用到自己的onTouchEvent()。

(2).mFirstTouchTarget != null,即ViewGroup的子控件消耗了此事件。

    开始的时候,如果用户没有修改mGroupFlags的拦截标志位话,默认是会执行onInterceptTouchEvent();

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;
}

因为只有down动作的时候才会去遍历子控件分发分发事件,当是move和up动作的时候newTouchTarget == null,alreadyDispatchedToNewTouchTarget == false;

这个时候就会执行如下代码,会将move、up事件通过dispatchTransformedTouchEvent(ev,cancekChild,target.child,target.pointerIdBits)传递个对应的子控件处理或者自己处理。

if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                //遍历所有能执行此事件的子控件
                while (target != null) {
                    final TouchTarget next = target.next;

                    //在down事件时,alreadyDispatchedToNewTouchTarget == true,target == newTouchTarget
                    //但是在move up cancel事件时由于不会遍历子控件,alreadyDispatchedToNewTouchTarget == false
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {//move up cancel事件会执行里面的方法
                        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;
                }
            }
 

3.触摸事件的cancel动作的执行流程

    一般情况下cancel事件的产生是由于viewgroup向下分发事件的时候,之前的事件能正常传递到子控件(比如down事件),但是在中途某个事件(比如move事件)被ViewGroup的onInterceptTouchEvent()拦截了(返回值为true),这个时候VIewGroup就要处理这个事件了,那么就需要向子控件传递cancel事件,让子控件根据cancel做相应的处理。我们还是看while循环里面的代码

 

if (mFirstTouchTarget == null) {
                
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        //因为intercepted == true,此时cancelChild就变成true了
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //此时dispatchTransformedTouchEvent()方法就会将cancel事件分发到子控件当中去,方法里面的流程在前面的代码分析里面又=有呈现
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        //如果是cancel事件会回收清空mFirstTouchTarget里面所有的能处理事件的TouchTarget
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

此时cancelChild = true,再到dispatchTransforedTouchEvent()里面看代码。它会修改当前的事件的动作为cancel,然后向下传递给子控件,最后还原当前事件的动作。

 

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
}

 

总结:

在Viewgroup的dispatchTouchEvent()里面会默认执行onInterceptTouchEvent()方法。用户可以通过调用requestDisallowInterceptTouchEvent()来决定是否执行onInterceptToucheEvent()方法。

对于事件分发流程一定要分为down、move、up、cancel来分析这样才会清楚。

1.down事件在Viewgroup的dispatchTouchEvent()里面做了遍历查找是否有子控件能消费此事件,如果消费了此事件就保存子控件,如果没有就自己消费,具体消费就是将触摸事件传递给自己的onTouchEvent()。

2.move和up事件在Viewgroup的dispatchTouchEvent()里面直接根据有没有子控件消费之前的down事件来来判断是将事件传递给自己的onTouchEvent()还是传递给子控件的dispatchTouchEvent().

3.ViewGroup和View的dispatchTouchEvent()又要查看源码。

4.ViewGroup的具体事件分发是由dispatchTransformedTouchEvent()来完成的。

 

最后附上分析事件分发的流程图,看完本文,在对照着流程图分析代码两遍应该就没什么问题了。如有错误,忘朋友们不令指正~

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值