现在的移动设备,最最主流的交互方式就是手指触摸屏幕了啦,当然语音输入,传感器识别也是移动终端上常用的交互方式。要想写好移动端的程序,了解操作系统如何处理触摸事件的整个过程是至关重要的。
大家都知道ViewGroup类是继承View类的,也就是说ViewGroup其实也是一个View,但ViewGroup的主要功能当然是作为一个容器,可以装载其它的View。
写应用层程序,大多数时候我们只关心我们的View是如何来处理一个TouchEvent的就行了,当然要深入了解操作系统的触摸事件消息读取队列对于提升我们的编程境界会更有帮助(目前我是没有这个境界的)。
先来说说我们的应用程序布局,大多数情况下根节点都是一个ViewGroup(如LinearLayout,RelativeLayout),然后在里面再装载着各种各样的View(Button,TextView,XXXLayout等)。当有TouchEvent产生时,系统会把TouchEvent传递给当前活跃的Application来处理,如果当前的Application没有消费该事件,或者是该事件不在Application的有效界面内(例如点击通知栏),则交由系统来处理。(这里有点想当然了,没有看过底层源码)。
如果TouchEvent在当前活跃的Application有效界面内,则交由当前处于栈顶的activity来处理,activity将TouchEvent分发(dispatch)给RootView来处理,rootView一般说来也就是当前布局的根节点。
分发一个TouchEvent给一个View来处理调用的是View的dispatchTouchEvent()方法。假设当前的根节点是一个ViewGroup, 则默认情况下会做如下处理
转换代码@Override public boolean dispatchTouchEvent(MotionEvent ev) { final int action = ev.getAction(); final float xf = ev.getX(); final float yf = ev.getY(); final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { // this is weird, we got a pen down, but we thought it was // already down! // XXX: We should probably send an ACTION_UP to the current // target. mMotionTarget = null; } // If we're disallowing intercept or if we're allowing and we didn't // intercept if (disallowIntercept || !onInterceptTouchEvent(ev)) { // reset this event's action (just to protect ourselves) ev.setAction(MotionEvent.ACTION_DOWN); // We know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); if (frame.contains(scrolledXInt, scrolledYInt)) { // offset the event to the view's coordinate system final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. mMotionTarget = child; return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { // Note, we've already copied the previous state to our local // variable, so this takes effect on the next event mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; if (target == null) { // We don't have a target, this means we're handling the // event as a regular view. ev.setLocation(xf, yf); if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } return super.dispatchTouchEvent(ev); } // if have a target, see if we're allowed to and want to intercept its // events if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; ev.setAction(MotionEvent.ACTION_CANCEL); ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { // target didn't handle ACTION_CANCEL. not much we can do // but they should have. } // clear the target mMotionTarget = null; // Don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal onTouchEvent(). return true; } if (isUpOrCancel) { mMotionTarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; mMotionTarget = null; } return target.dispatchTouchEvent(ev); }
如果是Action_Down事件的话,则disallowIntercept 一般情况下为false(每次是up或cancel事件时,mGroupFlags会重置FLAG_DISALLOW_INTERCEPT对应的位,代码如上),则会调用onInterceptTouchEvent(ev),这个函数是当前ViewGroup是否需要拦截事件,如果返回true,表示拦截事件,事件不再往下传递给子节点的view(默认返回false),否则遍历子节点,如果有子节点消费了该TouchEvent,则把target设为该子节点,并返回true,否则调用super.dispatchTouchEvent(即View的dispatchTouchEvent(ev)), 表示当前的viewGroup来消费该事件。View的dispatchTouchEvent(ev)如下:
转换代码public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
如果当前viewGroup监听了TouchListener且是enabled则调用TouchListener的onTouch方法来处理,如果onTouch方法返回true,表示消费了该事件,返回true给上层。否则调用自身的onTouchEvent(ev)事件。View的onTouchEvent也就是处理生成click或者longclick等事件,根据响应的时间或者具体触摸点生成相应的事件,具体可看源码。
通过上面的描述,我们就大概了解了触摸事件的消费过程,消费了返回true,否则返回false,传递给上层处理。如果我们想做额外的处理,就要拷贝一份当前的event,然后再继续往下传递,,如果我们想取消向下传递,可以将event设为ACTION_CANCEL或者在onInterceptTouchEvent里面返回true进行拦截。