罪过啊,已经有一个月没有写blog了(转载不算)。年末了各种加班,搞得身累心累的,也没有那心情写。但是春节临近,适逢今天放假,我再也不能抑制内心的那份冲动,必须得写几篇blog抒发一下新春佳节即将到来的这份愉快。
这几篇blog主要就是android的源码和自己的总结,有可能还有demo。我发现阅读android的源码是件很令人开心的事情,因为android源码够难,需要理清作者思路,揣测作者的意图,这是很有趣的事情,而且楼下还有一群大妈在跳舞助兴,一首不知道叫什么名的歌单曲循环了一个下午,我也是醉了,醉了一下午。
Android的Touch事件传递虽然代码量并不是很大,但是这块内容应该算是比较复杂的吧,尤其对我等菜鸟而言。鉴于我之前blog的内容表达不清除,有时我再看自己写的内容都不能理解。所以,我觉得要多附上代码,把我是怎么一步一步看代码的过程描述出来,少写一些我对代码的总结,如果写,也要斟酌再斟酌。
第一篇文章就写点简单的吧。1、OnTouchListener和onTouchEvent的关系。2、touch和click/longclick的关系(以前的blog好像写过他们之间的关系,只是那时的我还年轻,看到的是表象,现在从源码的角度来理解)。
PS:我用的是4.0的源码,不同版本的源码会有点去别。
OnTouchListener和onTouchEvent的关系
对于一个view来说,touch事件来了,最先到达的就是dispatchTouchEvent这个方法,那么我们来看这个方法(下面的代码记作代码A):
代码A:
public boolean <span style="white-space:pre">dispatchTouchEvent</span>(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }从代码A第10行的代码可以看到,现执行listener的onTouch方法,如果该方法返回true,那么onTouchEvent方法就不会被执行。他们的先后关系已经很明显。
touch和click/longclick的关系
onTouchEent方法,代码B:
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true); } if (!mHasPerformedLongPress) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }代码B的4-12行,如果该view是DISABLED的,那么基本不做处理,结束该方法。代码B的20行开始,如果该view是CLICKABLE或者LONG_CLICKABLE,后面的代码是对该投产事件进行处理的。根据touch的action来分别进行处理。
a、down(代码B的74-97行)
在第75行把mHasPerformedLongPress设置为false,这个量的作用从名字就可以看出来。代码82到96行,根据isInScrollingContaine这个变量不同处理,isInScrollingContaine值为false,执行94行和95行,94行设置该view的Pressed状态为true。我们先把95行这个方法搁置,来看isInScrollingContaine为true的时候,执行的内容是:把CheckForTap放入队列,在150毫秒后执行。来看的内容:
代码C:
private final class CheckForTap implements Runnable { public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; setPressed(true); checkForLongClick(ViewConfiguration.getTapTimeout()); } }可以看出来执行的方法和isInScrollingContaine值为false的时候代码基本一样,只是延迟了150毫秒执行。
那么我们来看checkForLongClick这个方法:
代码D:
private void checkForLongClick(int delayOffset) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }代码D第6行用到CheckForLongPress,从代码可以看到就是把CheckForLongPress的一个对象加载到队列里,在500毫秒之后(如果isInScrollingContaine值为true时,延迟350毫秒)执行这个CheckForLongPress。那么我们来看CheckForLongPress的内容。
代码E:
class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { mHasPerformedLongPress = true; } } }可以看到CheckForLongPress的核心内容就是第8行的执行performLongClick这个方法。
down的总结:dwon事件处理,设置该view的longclick还没有被处理、修改该view显示的内容、在Runnable的队列里布置一个Runnable,再500毫秒后处理longclick事件。
b、move事件(代码B的105-120行)
110行,判断该touch是否还在这个view里面,这个方法里牵扯到一个mTouchSlop这里就不介绍它了(其实mTouchSlop我也还没弄明白)。112行取消掉代码B的91行布置的那个Runnable,第115行取消掉代码D第9行布置的那个Runnble。
c、cancel事件(代码B的100-103行)
取消之前布置的Runnable。
d、up事件(代码B的24-72行)
第24行代码,获得prepressed,prepressed值为true代表现在事件是在touch down之后的150毫秒之内,prepressed值为true代表touch down事件已经发生了超过150毫秒。至于这是怎么来的,通过跟踪mPrivateFlags与PFLAG_PREPRESSED之间的运算可得出,这里不方面演示出来。
第25行的判断,意思是说当前view已经被按下,两种情况:1.左边的很显然.2.当前view处在touch down之后的150毫秒以内,虽然(mPrivateFlags & PFLAG_PRESSED) != 0为true,但是该view在事实上已经被按下来。第28-31行,改变焦点状态。第33-39行,注释很清楚。
第41-57行,如果该view没有被处理onLongClick,即touch down后不到500毫秒,那么:取消布置在队列里的Runnable,第50-55行代码,注释很清楚,就是执行performClick。
第59-69行,设置该view的Pressed状态为false。第79行,取消布置的Runnable。
总结
代码就看到这里,基本已经明了了。梳理一下处理过程:
1.touch down时间后会布置一个名为mPendingCheckForLongPress的Runnable,让该Runnable在500毫秒后执行。会根据该view是否在能滚动的组件里面,如果该view的父view(递归父view)存在可滚动的,那么还会布置一个名为mPendingCheckForTap的Runnable,该Runnable在150毫秒后执行。
2.touch cancel 取消touch down布置的Runnable。
3.touch move 根据情况是否取消touch down布置的Runnable。
4.touch up 改变焦点状态、根据情况是否取消touch down布置的名为mPendingCheckForLongPress的Runnable、执行performClick()、取消touch down布置的名为mPendingCheckForTap的Runnable。