转眼间近一年没更新博客了,工做一忙起来。很是难有时间来写博客了,由于现在也在从事Android开发相关的工做,所以之后的博文也会不少其它地专一于这一块。html
这篇文章准备从源代码层面为你们带来Touch事件的传递机制。我这里分析的源代码时Android4.4的。java
说到分析源代码,光看确定是不行的,必定要亲自去跟,并且要边跟边思考,因此在下一篇中。会有一个Demo来为你们详细分析源代码的走向。android
如下进入正题,先来看下Android中事件的分类:数组
一、键盘事件:主要是指按下虚拟键盘的某个按键、或者机身的物理按键时产生的事件。网络
二、鼠标事件:Android4.0以后添加了对鼠标事件的监控,如ACTION_HOVER_ENTER。app
三、触摸屏事件:凡是触摸屏幕而产生的事件都是触摸屏事件。触摸屏事件包含很是多,比方单点触控、多点触控)、轨迹球事件等。ide
咱们这里主要解说单点触控事件,也就是Touch事件的传递,首先看下Touch事件的完整传递过程:布局
一、首先需要明确,Android中,Touch事件的分发分服务端和应用端。在Server端由WindowManagerService(WMS,窗体管理服务。不懂的自行脑补)负责採集和分发的。在client端则是由ViewRootImpl(内部有个mView变量指向View树的根 ,负责控制View树的UI绘制和事件消息的分发)负责分发的。post
二、WMS在启动以后,通过逐层调用。会在native层启动两个线程:InputReaderThread和InputDispatchThread,前者用来读取输入事件,this
后者用来分发输入事件,输入事件通过nativie层的层层传递,终于会传递到java层的ViewRootImpl中。调用
ViewPostImeInputStage(ViewRootImpl的内部类)中的各个方法来分发不一样的事件,而Touch事件是在processPointerEvent方法进行分发的(这部分代码很是单,可自行查看)。
三、processPointerEvent方法中调用mView.dispatchPointerEvent(event)方法。这里的mView就是在建立窗体后经过调用root.setView传进
来的DecorView,而dispatchPointerEvent方法会对event进行推断,假设是Touch事件的话。就调用dispatchTouchEvent将该事件分发DecorView,这样。Touch事件就传递到了View树中了。
Touch事件从WMS到ViewRootImpl的传递
如下这张图(不是本身画的。网上找的,Android4.4中,InputManager变成了InputManagerService,ViewRoot变成了ViewRootImpl)展现了Touch事件
从WMS(sever端)传递到ViewRootImpl(client端)的流程。
这里需要特别注意的是。Touch事件从server端传递到client端採用的IPC方式并不是Binder。而是共享内存和管道,至于为何不採用Binder,应该是共享内存的效率更高,而管道(注意。两个管道。分别负责不一样方向的读和写)仅仅负责通知是否有事件发生,传递的仅仅是一个很是easy的字符串。所以并不会太多地影响到IPC的效率。
上图中,仅仅有WMS、ViewRootImpl、InputManagerService、InputQueue是在FrameWork层实现的。其它部分都是在native层实现的,native 层的代码没有细看,參考了些网上的一些资料和公司内部的一些分享,把整个流程串通了。这部分代码,假设时间充足,可以深刻研究下。
在sever端中。InputReader和InputDispatcher是native 层的两个线程,前者不断地从EventHub中读取事件(包含所有的事件。对不一样的事件会作推断处理)。后者则不断地分发InputReader读取到的事件。而实际的分发操做时在InputPublish中进行的,它里面保存的有一个指向server端InputChannel端的指针。和一个指向ShareMemory(共享内存)的指针。当有事件要分发时,它会将事件写入到ShareMemory中,并且传递一个特定的字符串给InputChannel。由inutChannel将该字符串写入到管道中。在client端,一旦InputChannel从管道中读取到有事件分发过来,便会通知InPutConsumer从ShareMemory中读取详细的事件。并传递到framework层的InputQueue中。相同。一旦事件消费完毕,client端会经过管道告诉server端,事件已经消费完毕,流程与上面的似。
大体的流程就是这样。
另外。顺便说下这里的两个InputChannel,这两个InputChannel是在native 层的。他们在framework层各自有一个相应的InputChannel类,对于这两个framework层的InputChannel。client端的是在ViewRootImpl中的setView中new出来的,但是并未作不论什么初始化操做(真正的初始化操做是跟server端的一块儿在WMS中运行的)。也就是构造方法里面为空。
// Schedule the first layout -before- adding to the window // manager, to make sure we do the relayout before receiving // any other events from the system. requestLayout(); if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { mInputChannel = new InputChannel(); }
server端的InputChannel尽管是在server端建立的,但其建立过程是在client端发起的。ViewRootImpl中有server端的Session的代理,相同是setview方法中。经过Session代理运行server端Session的addToDisplay方法。该方法接受了client端的InputChannel方法
mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel);
addToDisplay方法在Session类中,它会调用WindowManagerService的addWindow方法。而两个InputChannel的初始化操做都是在这里面的这这段代码中进行的。
if (outInputChannel != null && (attrs.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { String name = win.makeInputChannelName(); InputChannel[] inputChannels = InputChannel.openInputChannelPair(name); win.setInputChannel(inputChannels[0]); inputChannels[1].transferTo(outInputChannel); mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle); }
这里的InputChannel.openInputChannelPair方法会在native层建立两个InputChannel,也就是咱们上面看到的那两个,并返回相应的framework层InputChannel的两个对象,保存在iputChannels数组中,当中一个保留字server端,一个经过transferTo方法返回到client 端。
这里的InputChannel.openInputChannelPair方法和transferTo方法中都是直接调用来了native方法,这里再也不贴代码。
说了这么多。事实上就是一个Binder机制,关于Binder机制。你们自行在搜索吧,入门的资料仍是挺多的。
这里我依据源代码画了两份ViewRootImpl和WMS之间经过Binder 机制进行IPC的序列图,有兴趣的可以自行研究下代码,仅仅要能搞清楚Binder机制。这部分代码还就不难懂。
ViewRootImpl到WMS的链接,经过WMS提供给ViewRootImpl的IWinowSession成员,也就是Session在本地的代理来完毕:
WMS到 ViewRootImpl的链接,经过ViewRootImpl提供给WMS的Iwindow(ViewRootImpl的内部类)来完毕:
Touch事件在View树中的分发
当Touch事件传递到了ViewRootImpl中后,就会在View树中进行分发,要了解Touch事件在View树中的分发。首先需要了解View树的建立,这部分又可以写成一篇单独的博文了。详细的过程这里再也不详说,有兴趣的可以本身研究下。
View树建立完毕后的结构是这种(图片源自网络):
View树的根View永远是DecorView,它继承自FrameLyout。其内部会有一个LinearLayout,依据Window Feather的的不一样,LinearLayout内部的布局也不一样,当中每种不一样布局的xml(系统资源内部的xml布局)内都有一个id为content的FrameLayout。这就是咱们在本身的布局所attach的父容器。
Touch事件的传递天然是先从ViewRootImpl传递到DecorView中。这个前面的第三点也说到了,所以咱们这里就从DecorView入手,開始分析Touch事件的分发。
在開始分析以前,先大体梳理下Touch事件传递可能涉及到的一些基础:
一、普通状况下。每一个Touch事件,老是以ACTION_DOWN事件開始。中间穿插着一些ACTION_MOVE事件(取决因而否有手势的移动),而后以ACTION_UP事件结束。中间还会会有onTouch、Click、LongClick等事件。
二、事件分发过程当中,包含对MotionEvent事件的三种处理操做:
分发操做:dispatchTouchEvent方法,后面两个方法都是在该方法中被调用的。
拦截操做:onInterceptTouchEvent方法(ViewGroup)
消费操做:onTouchEvent方法和OnTouchListener的onTouch方法,当中onTouch的优先级高于onTouchEvent,若onTouch返回true。那么就不会调用onTouchEvent方法。
三、dispatchTouchEvent分发Touch事件是自顶向下,而onTouchEvent消费事件时自底向上。onTouchEvent和onIntercepteTouchEvent都是在dispatchTouchEvent
中被调用的。
如下,正式进入对Touch事件在View树中分发的分析:
首先来看DecorView(PhoneWindow的内部类)中dispatchTouchEvent方法:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final Callback cb = getCallback(); return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); }
这里的cb就是当前的Activity,Activity实现了Window.Callback接口,同一时候在Activity的attach方法中。建立PhoneWindow后。调用了
mWindow.setCallback(this)将PhoneWindow中的callback设置为当前的的Activity,所以这里cb.dispatchTouchEvent就是Activity的
dispatchTouchEvent方法,假设前面三个条件同一时候成立(一般是都成立的),则调用Activity的dispatchTouchEvent方法进行事件的分发。
不然。直接调用super.dispatchTouchEvent方法,也便是FrameLayout的dispatchTouchEvent方法,事实上即便调用了Activity的
dispatchTouchEvent方法。终于也是会调用到super.dispatchTouchEvent。咱们可以继续往下看Activity的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
前面if分支不用管,这里会调用PhoneWindow的superDispatchTouchEvent方法。进去看看:
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
调用了DecorView的superDispatchTouchEvent方法,再进去看看:
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
终于仍是调用来DecorView的super.dispatchTouchEvent。也就是说。无论如何。DecorView的dispatchTouchEvent终于都会调用到本身父亲FrameLayout的dispatchTouchEvent方法。而咱们在FrameLayout中找不到dispatchTouchEvent方法。因此,会去运行ViewGroup的
dispatchTouchEvent方法。
假设该dispatchTouchEvent返回true,说明后面有view消费掉了该事件,那就返回true,不会再去运行自身的onTouchEvent方法,不然,说明没有view消费掉该事件,会一路回传到Activity中,而后调用本身的onTouchEvent方法。该方法的实现比較简单。例如如下:
public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
public boolean shouldCloseOnTouch(Context context, MotionEvent event) { if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) && peekDecorView() != null) { return true; } return false; }
这里的大体意思是,假设设置了mCloseOnTouchOutside属性为true(相应xml中的android:windowCloseOnTouchOutside属性),且当前事件为down事件。且down事件发生在该Activity范围以外,并且DecorView不为null,就返回true。很是明显。dialog形的Activity可能会发生这种状况。
如下需要重点来看下ViewGroup中的dispatchTouchEvent方法了:
public boolean dispatchTouchEvent(MotionEvent ev) { //调试用的 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } //handled为返回值,表示是否有view消费了该事件。 boolean handled = false; //是否要过滤掉该Touch事件。大体是这个意思 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { //由于down事件表明一个系列事件的開始,所以假设是down事件, //一、就清空掉曾经消费事件的目标view,这里主要指清空掉mFirstTouchTarget链表(保存接受Touch事件的单链表,这点在后面的代码中会看到)。并将mFirstTouchTarget置为null。 //二、重置触摸状态,重置了disallowIntercept相应的标志位。该变量的值决定了onInterceptTouchEvent方法是否有效,这点后面咱们会看到;还有就是重置来View的mPrivateFlags标志位,这个没去了解详细是干吗用的。通常在发生app的切换。或者ANR等状况时。代码会走到这里,这一点源代码的凝视里也有。
cancelAndClearTouchTargets(ev); resetTouchState(); } // 标记是否要拦截该Touch事件,true表示拦截。false表示不拦截 final boolean intercepted; //假设当前事件为down事件。或者可接受Touch事件的链表不为空。就运行if语句里的逻辑。这里注意, //一、由于down事件是一个完整事件序列的的起点,所以当发生down事件时。逻辑走到这里。尚未找到消费down事件的view,所以mFirstTouchTarget为null, //二、而对后面的move和up事件,假设前面的down事件被某个view消费掉了,则mFirstTouchTarget不为null。
上面两种状况都会使代码进入if分支中来。 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //是否不一样意拦截。默以为false,也就是赞成该方法可以经过 requestDisallowInterceptTouchEvent方法来设置 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 假设赞成拦截,则onInterceptTouchEvent有效,依据咱们覆写的该方法的返回值来推断是否拦截,不然。onInterceptTouchEvent无效,不正确该事件进行拦截。 if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // 假设当前事件不是down事件。且以前在分发down事件的时候没有找到消费down事件的目标view,也即mFirstTouchTarget为null,则直接拦截该事件。 intercepted = true; } // 检查当前事件是否被取消 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; //保存消费事件的目标View所相应的 TouchTarget对象 TouchTarget newTouchTarget = null; //事件是否已经分发到了目标View中。
boolean alreadyDispatchedToNewTouchTarget = false; // 假设没有被取消。并且没有被拦截,就分发该事件,注意仅仅有down事件才会走到这里去分发。对于move和up事件。则会跳过这里。直接从 mFirstTouchTarget链表中找到以前消耗down事件的目标View,直接将move和up事件非法给它,后面的代码中咱们会分析到。
if (!canceled && !intercepted) { //仅仅有down事件会走到这里 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { //Touch事件的index,对于单点触控。一直为0,这里不用深究 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); //该ViewGroup中子View的个数 final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { //当前事件发生的位置 final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); //保存该ViewGroup中子View final View[] children = mChildren; final boolean customOrder = isChildrenDrawingOrderEnabled(); //遍历子View,找到能消费该down事件的子View,对于类型为ViewGroup的子View,在分发的时候。会递归调用到它的dispatchTouchEvent方法继续进行分发。 for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = children[childIndex]; //假设当前子View可以消费该down事件。并且该down事件发生的位置在当前子View的范围内,则继续运行。将down事件分发给它,不然,continue推断下一个子View能否接受该down事件。 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } //推断该能接受该down事件的child是否已经在mFirstTouchTarget链表中。假设在的话,说明child已经消费掉了该down事件,直接跳出循环。我在写demo跟代码时,没有一次走到这里的,临时不是很是清楚,如何的场景下,代码会运行到这里的break。 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //假设该child尚未消费掉该down事件,就直接调用dispatchTransformedTouchEvent方法将该down事件传递给该child,该方法里面会调用到child的dispatchTouchEvent方法,假设该方法返回true,则说明child消费掉了该down事件,那么就运行if语句里的逻辑,将child添加到mFirstTouchTarget链表的表头,并且将该表头赋值给newTouchTarget(參见addTouchTarget方法),同一时候 alreadyDispatchedToNewTouchTarget置为true。说明有子view消费掉了该down事件。 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = childIndex; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } //假设newTouchTarget为null。并且 mFirstTouchTarget不为null,也即没找到子View来消耗该事件,但是保存Touch事件的链表不为空,则把newTouchTarget赋值为最先加进(Least Recently added)mFirstTouchTarget链表的target。临时没全然搞明确这里的详细意思,跟代码都没有走到这里。 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; } } } //后面在处理MOVE和UP事件时。会直接依据上次的DOWN是否被消费掉来直接进行相应的处理。
if (mFirstTouchTarget == null) { // 假设没有子view接受该事件,则直接把当前的ViewGroup看成普通的View看待,把事件传递给本身(详见dispatchTransformedTouchEvent方法,注意第三个參数传递的是null)。
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 假设以前的DOWN事件被子view消费掉了。就会直接找到该子View相应的Target,将MOVE或UP事件传递给它们。 TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { //假设该事件已经被消费掉了,则再也不进行分发(该分支主要针对DOWN事件) handled = true; } else { //不然。就直接将DOWN或UP事件分发给目标Target(以前消费DOWN事件的view相应的target,注意dispatchTransformedTouchEvent的第三个參数为target.child),这里要注意的是,假设intercepted为true,也就是MOVE或UP事件被拦截了,则cancelChild为true,则会分发一次CANCLE事件(注意dispatchTransformedTouchEvent的第二个參数)。 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; } } // 假设当前事件是CANCLE或UP。会调用resetTouchState方法。清空Touch状态,这里会清空mFirstTouchTarget链表,并将mFirstTouchTarget置为null if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
另外,画了张ViewGroup中dispatchTouchEvent方法代码运行的流程图。可以有助于你们对代码整体逻辑的把握(Windows上viso中画的图,传到mac上就变成这样了,又一次保存成图片,清楚度过低,直接在PPT里面截出来了。凑合着看吧。没太大影响)。
关于上面提到的dispatchTransformedTouchEvent方法,这里就很少分析了,感兴趣可以本身分析下,另外,ViewGroup中没有复写onTouchEvent方法。
如下重点看下View中的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(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; }
很是明显。会先推断该View有没有绑定OnTouchListener监听器,假设绑定了,并且复写了当中的onTouch方法。假设onTouch方法返回了true,那么Touch事件就被消费掉了,后面的onTouchEvent方法就不会获得运行,而假设没有被消费掉。才会运行到onTouchEvent方法。依据其返回值来断定Touch时间是否被消费掉。
这里重点关注消费Touch事件的前后顺序:onTouch先于onTouchEvent。
如下就关键来看下View中的onTouchEvent方法了。
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; }
这里事实上没太多要说的,重点关注:
一、onClick和onLongClick运行的时机:onClick时在UP事件中运行的,onLongClick实在Down事件中运行的,仅仅是假设在Down事件中已经运行了onLongClick的话,则mHasPerformedLongPress变量会被置为true,这样在UP事件中。就会把onClick的回调remove掉。就不会再运行onClick了。
二、仅仅要该View是clickable的,就必定会消费掉Touch事件,仅仅是,假设该View是Disable的话,尽管消费掉了Touch事件。但是不作不论什么处理。
另外。有一点大体说下:
源代码的前面部分有一个mTouchDelegate变量(默以为null),假设它不为null的话。会将Touch事件分发给它。
详细的意思是这种。假设有两个视图v1和touchDelegate1。它们的布局相互之间不重叠。假设设置了v1.setTouchDelegate(touchDelegate1)的话,v1的触摸事件就会分发给touchDelegate1中的view(TouchDelegate中有一个view变量)。
为了便于整体上对源代码流程的把握,这里相同画了一个流程图
最后,关于整个Touch事件在View树中的传递流程,相同画了张流程图,看起来会更直观。有助于对整体流程的把控:
以上流程图中有些地方文字有错位。应该不影响对流程的整体理解和把握。
事实上相对来讲,事件的分发处理属于Android中比較基础的知识点。但想把整个流程完整地串通,仍是要花些时间的。这篇文章在10月份的时候就想写了,但是工做后,写博客的时间愈来愈少。人也变得愈来愈懒了。
。。
整篇文章断断续续坚持着写下来仍是挺费劲的。
好了,很少说了,下篇文章,将经过一个Demo,结合8种不一样的状况,对Touch事件在View树中传递时源代码的运行状况作一个详细的分析。
知乎上可以看到一个不同的我,欢迎关注:兰亭风雨的知乎首页
一直想作个公众号。但是考虑到在手机端看代码的用户体验确实太差。而我又比較喜欢正能量的东东。最后倒腾了一个鸡汤号,每日一篇正能量好文,同一时候各类互联网信息爆料。尽在当中。
。。
欢迎扫码关注。