触摸事件的派发

本文深入解析Android触摸事件处理流程,包括事件的捕获、分发、处理机制,以及MotionEvent的详细解读。探讨多点触控下的事件序列、拆分与绑定,View和ViewGroup在触摸事件中的角色和交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.如果这是一个按下事件或者ACTION_SCROLL,将会进入触摸模式,此时将会重新设置控件焦点;

触摸事件位于窗口的坐标系下:event.offsetLocation(0, mCurScrollY);

将事件派发给控件树:mView.dispatchPointerEvent(event);

如果事件被消费,结束派发工作;

2.mView.dispatchPointerEvent

如果是触摸事件,执行dispatchTouchEvent,否则dispatchGenericMotionEvent;

3.MotionEvent与触摸事件的序列

MotionEvent继承于InputEvent,包含了多种用于描述一次触摸的详细信息,其中最基本的信息可以通过getAction或者getX/Y来获取;

由于多点触摸的存在,getAction所获得动作信息时一个复合值,它在低八位描述的是一个动作,9到16位描述了触发此事件的触控点从0开始的索引;实际应用中,需要通过getActionMasked以及getActionIndex将两个信息分离,虽然一个MotionEvent由一个触控点触发,但是这个触控点中包含了所有触控点的信息,所以getX/Y可以接受触控点的索引号作为参数,返回特定触摸点的触摸位置;

在多点触摸的过程中,索引号并不是一成不变的,因此触控点的索引号并不能用来识别或追踪一个特定触控点,开发者需要通过触控点的id达到识别或追踪一个特定触控点的效果,可通过getActionId获取,需要索引号作为索引号作为参数;

开发者在开发时,一般需要通过getActionMasked获取动作,通过getActionIndex获取索引,在根据索引获取id和位置信息;

当MotionEvent所携带的动作是ACTION_MOVE时,其getAction所获得的动作并不包含触控点的索引,不过它仍然包含了所有触控点的位置/id等信息;开发者可以通过getPointerCount来获取有多少个触控点处于活动状态,然后for循环;从这一事实可以得知,getAction中所包含的触控点索引号,只是为了通知开发者是否产生了新的触控点或某个触控点被移除;

触摸事件的序列:用户第一个手指按下开始到最后一个手指抬起这一过程中所产生的MotionEvent序列;

多点触摸下的事件序列中的每一个触控点的信息都足以独立形成一个事件序列,这个时候就产生了拆分,将这两条序列被称为原始序列的子序列;拆分由split()方法来完成;

事件序列的不可中断性:一旦一个控件决定接收一个触控点的ACTION_DOWN/或ACTION_POINTER_DOWN事件,表示控件将接收序列的所有的后续事件;

触摸事件序列除了ACTION_UP外还有一个结束标志,ACTION_CANCEL,比如,一个正在接收事件序列的控件从控件树中被移除,或者发生了ACTIVITY切换等,此时控件就需要中断对事件的处理;

View的dispatchTouchEvent:

首先触摸事件必须经过onFilterTouchEventForSecurity()过滤,这是一个安全性检查,当本窗口位于一个非全屏窗口之下时,可能会阻止控件处理触摸事件,可以在执行敏感行为的控件上时通过setFilterTouchesWhenObscured() 方法 在 mViewFlags 中 添加 FILTER_ TOUCHES_ WHEN_ OBSCURED 标记;

尝试让onTouchListener处理:

if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

倘若onTouchListener对该事件不感兴趣,交由onTouchEvent处理;

ViewGroup的dispatchTouchEvent:

事件的派发分为确定派发目标和执行派发两个工作;

根据序列的不可中断性,确定派发目标发生在收到ACTION_DOWN或ACTION_POINTER_DOWN的时刻,此时,ViewGroup会按照逆绘制顺序依次查找事件坐标所落在的子控件,并将事件发送给子控件的dispatchTouchEvent,然后根据返回值确定第一个愿意接受这一序列的子控件,将其确定为后续事件的派发目标;一旦通过ACTION_DOWN或ACTION_POINTER_DOWN确定派发目标,ViewGroup会将此触控点的id与目标建立绑定关系,属于此触控点的事件序列都会发送给这一目标;ViewGroup通过一个TouchTarget类的实例来描述这一绑定,这个类保存了一个触控点id的列表以及一个View实例;

由于多点触摸的存在,执行派发时可能需要将MotionEvent进行拆分,将ViewGroup收到的事件序列划分为多个子序列并发给多个子控件,所以在ViewGroup在派发过程中可能维护着多个TouchTarget的实例;ViewGroup将所有的TouchTarget存储在一个以mFirstTouchTarget为表头的单项链表中;

dispatchTouchEvent:

            //剔除索引号,获取实际动作
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
			//MotionEvent.ACTION_DOWN意味着一条崭新的事件序列的开始,此时ViewGroup会重置所有的状态;
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            final boolean intercepted;
			//检查viewGroup是否需要对事件进行截获
            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;
            }
            //canceled表示viewGroup收到这一事件是否被取消
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
			//split表示此控件树是否启动事件的拆分机制
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
			//保存派发目标(派发目标在down或者pointer_down时确定)
            TouchTarget newTouchTarget = null;
			//当确定了派发目标后这一事件事件上已经完成派发了,会被置为true,跳过后续的派发过程
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不阻断,不取消
            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;

				//ACTION_DOWN或者ACTION_POINTER_DOWN时,确定派发目标
                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
					//通过索引号获取触控点的id,如果没有拆分,所有触控点的事件都会被派发给后续被确定的目标控件
                    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 = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
						//按照逆绘制的顺序遍历所有子控件
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(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;
                            }

							//从mFirstTouchTarget列表中查找控件所对应的TouchTarget,倘若已经存在,表明此控件已经在接收另外一个事件子序列,ViewFroup会默认此控件对这一序列也感兴趣,此时将触控点id绑定在其上,并终止派发目标的查找,后续的派发工作会据此将此事件派发给这一控件
                            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);
							//尝试将控件派发给当前子控件,
							//根据idBitsToAssign将其指定的触控点的信息从原始事件中分离出来,并且产生一个新的MotionEvent
							//如果有必要,修改新MotionEvent的action;
							//将事件坐标转换到子控件的坐标系下
							//将新的MotionEvent发给子控件
                            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();
								//当子控件接收这一事件时,为其创建一个TouchTarget并保存在mFirstTouchTarget链表中,从此,事件都会传递给这个子控件
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                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();
                    }

					//如果没有找到合适的子控件处理事件,viewgroup会强行将这一事件交给最后一次接收事件序列的子控件;
                    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;
                        }
						//将触控点的id绑定到控件的TouchTarget中
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
            //没有合适子控件处理,只能自己处理
            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;
				//遍历mFirstTouchTarget链表,为每一个TouchTarget派发事件
                while (target != null) {
                    final TouchTarget next = target.next;
					//倘若TouchTarget是新确定的TouchTarget,之前派发过,不需要在派发
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
						//目前控件即将被移出控件树,或者ViewGroup决定截取此事件序列,此时仍会将事件发送给目标控件,但是其动作会被会被改成ACTION_CANCEL
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
						//派发给目标控件
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
						//倘若中止目标控件继续接受事件序列,则将其对应的TouchTarget从链表中删除并回收,下次事件到来时将不会为其进行事件派发
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
       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.
		//首先处理当需要终止子控件对事件序列进行处理的情况,此时仅需要将事件的动作替换成ACTION_CANCEL并调用子空间的dispatchTouchEvent即可;
		//此时并没有变换坐标;
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
				//派发给ViewGroup自己
                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();
		//目标希望接受的触控点的列表,它是oldPointerIdBits的子集,
        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;
		//如果相同,表示目标对原始事件的所有触控点全盘接受,因此transformedEvent仅仅是原始事件的一个复制
        if (newPointerIdBits == oldPointerIdBits) {
			//当目标控件不存在通过setScaleX()等方法进行变换时,为了效率会将原始事件简单的进行控件位置与滚动量变换后发送给目标控件并返回
            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将这一个子集分离出来构成transformedEvent
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
		//transformedEvent进行坐标系转换,并发送给目标
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
			//对transformedEvent进行坐标系变换,使之位于派发的坐标系之中
			//首先是计算viewGroup的滚动量以及目标控件的位置
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
			//然后当目标控件中存在使用setScaleX()等方法设置的矩阵变换时,将对事件坐标进行变换。此次变换完成之后,事件坐标点便位于目标控件的坐标系中了
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

			//发送给目标控件
            handled = child.dispatchTouchEvent(transformedEvent);
        }

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

修改事件的action:这一工作发生在event.split中;

为什么要修改事件的action:将ACTION_POINTER_DOWN/UP修改为ACTION_DOWN/UP或者将ACTION_POINTER_DOWN/UP修改为ACTION_MOVE以及修改索引号;这三种情况

移除派发目标

            //移除派发目标
			//当viewGroup收到一个MotionEvent.ACTION_UP或者CANCLE事件时,整个事件序列已经结束,删除所有的TouchTarget
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
				//当事件是一个ACTION_POINTER_UP时,将其对应的触控点ID从对应的TouchTarget中移除;removePointersFromTouchTargets会在TouchTarget的最后一个触控点ID被移除的同时,将这个TouchTarget从mFirstTouchTarget链表中删除并销毁;
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }

 

转载于:https://my.oschina.net/u/3491256/blog/911640

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值