android事件分发机制源码分析
- 没什么用的前言
- 责任链设计模式
- 流程图
- 源码分析
没什么用的前言
事件分发机制是面试中一道必问的题目,而我的应对方式则是,在网络上找一些博客看看,然后做一些笔记,最后在面试时将我自己记住的内容说出来。这种方式本身没有太大的问题,因为在看博客的过程中也学到了知识。但每次看完博客,我都没办法很好地理解整个流程,所以决定自己看一下源码,看完之后,决定通过博客这种形式将自己的笔记输出出来。
责任链设计模式
提到事件分发机制,永远都绕不开责任链设计模式,只有理解了责任链涉及模式,才能更好地理解整个分发流程。
责任链设计模式的定义(来自百度百科):责任链模式是一种设计模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。
这里的重点有:
- 每个对象只持有下一个对象的引用,而不会持有更下级别的引用
- 发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求
假设有对象A、B、C,A持有B的引用,B持有C的引用。当A获取到请求后,会判断自己是否需要处理该请求,如果不需要处理,就将请求交给B处理,B也会做一样的判断。
假设请求最终是由C处理,则可以看到,A并没有持有C的引用,但请求最终却可以在由C处理。这就是责任链设计模式的优势。
代码举例
// AbstractTask
public abstract class AbstractTask {private AbstractTask next;public AbstractTask(){}public AbstractTask(AbstractTask next){this.next = next;}public void handleRequest(Request request){// 如果当前的level可以处理,就交给自己处理if(getTaskLevel() == request.getTaskLevel()) {handle(request);}else {// 如果自己没办法处理,并且存在next,就交给next处理if(next != null) {next.handleRequest(request);}else {System.out.println("没有找到处理对象");}}}public void setNext(AbstractTask next){this.next = next;}protected abstract int getTaskLevel();protected abstract void handle(Request request);
}// ATask
// 还有B和C task,不过代码都差不多,我就不贴出来了
// B的taskLevel为2,C的taskLevel为3
public class ATask extends AbstractTask{@Overrideprotected int getTaskLevel() {return 1;}@Overrideprotected void handle(Request request) {System.out.println("这里是A task, request: " + request.toString());}
}// Request
public class Request {private final int taskLevel;private Object content;public Request(int taskLevel){this.taskLevel = taskLevel;}public void setContent(Object content){this.content = content;}public int getTaskLevel(){return taskLevel;}public Object getContent(){return content;}@Overridepublic String toString() {return "Request{" +"taskLevel=" + taskLevel +", content=" + content +'}';}
}
测试代码
public class Test {public static void main(String[] args) {ATask aTask = new ATask();BTask bTask = new BTask();CTask cTask = new CTask();aTask.setNext(bTask);bTask.setNext(cTask);Request request = new Request(3);request.setContent("new Request(3)");aTask.handleRequest(request);}
}
最后打印出来的是:这里是C task, request: Request{taskLevel=3, content=new Request(3)}。
可以看到,A没有持有C的引用,但执行了C的handle方法。
有一个必须说清楚的是,持有下一级的引用不只有链表这种方式,还有其他方式。比如用一个数组来存储,这就是ViewGroup的做法。
再看看ViewGroup几行寻找targetView的代码
// 成员变量
private View[] mChildren;// ViewGroup.dispatchTouchEvent
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);.... if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {....}
}
可以看到,ViewGroup是从children里面寻找的,而children是一个数组。所以没必要将子对象局限于链表这种方式,如果有更好的方式,可以使用更好的方式来实现。
而这里的dispatchTransformedTouchEvent方法最终会调用View的dispatchTouchEvent方法,如果调用的View是一个ViewGroup,就会执行相同的代码,直到调用的是一个View。这就是责任链设计模式在事件分发机制上的实现。可能有人会问,如果该ViewGroup没有子View呢?这个问题留到下面的源码分析,这里只是讲解责任链设计模式在事件分发上的应用。
从这里也可以想象得到,A ViewGroup持有B ViewGroup的引用,B ViewGroup持有C View的引用,而A ViewGroup没有直接持有C View的引用,和上面的例子有点像。
流程图
图里面,省略了DecorView等其他界面。
如果给View2设置onClick并且点击View2,那将发生。
- 传递一个ACTION_DOWN的event给Activity
- Activity调用dispatchTouchEvent最后将事件传递给ViewGroup
- ViewGroup调用dispatchTouchEvent
- 调用ViewGroup的onInterceptTouchEvent判断是否拦截该事件,如果否,则执行第5步
- 从上面的源码可以看到,ViewGroup是从最后一个View向第0个View的顺序调用。所以会判断View3的范围内是否包含MotionEvent的x值和y值,发现不包含,调用View2判断。
- 发现View2是目标View,则调用该View的onTouchEvent方法,并且将后续的事件都给该View
- 上面的第4步,如果确定拦截,则会调用该ViewGroup的onTouchEvent, No touch targets so treat this as an ordinary view。在源码搜索这句注释就能看到相应的代码,可以发现,最后调用的就是super的dispatchTouchEvent方法。
总结一下流程:Activity dispatchTouchEvent -> ViewGroup dispatchTouchEvent ->ViewGroup onInterceptTouchEvent->View dispatchTouchEvent ->View onTouchEvent
这个的就不画出来了,一看就知道怎么调用,下面写完源码分析之后,将画一个较为完整的图
以上就是点击一个View后的一个大概流程,接下来开始源码分析,从源码的角度去还原一个完整的调用流程。
源码分析
从上面的流程可以看到
- ViewGroup的调用顺序是:dispatchTouchEvent -> onInterceptTouchEvent -> onTouchEvent
- View的调用顺序是:dispatchTouchEvent ->onTouchEvent
所以先看ViewGroup的dispatchTouchEvent方法,再根据流程分析其他方法的代码。
// ViewGroup dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {...// 这个变量也是重点,不过先看完其他代码,再回头分析这个变量boolean handled = false;if (onFilterTouchEventForSecurity(ev)) {final int action = ev.getAction();// 这里的masked可以当action使用// 执行一次mask运算之后,就可以保证该action变量不存在action以外的数值final int actionMasked = action & MotionEvent.ACTION_MASK;// 如果是ACTION_DOWN,则清除和重置一些变量if (actionMasked == MotionEvent.ACTION_DOWN) {cancelAndClearTouchTargets(ev);resetTouchState();}// Check for interception.final boolean intercepted;// 该if是用于判断是否执行onInterceptTouchEvent// 当ACTION_DOWN找到目标之后,mFirstTouchTarget就不会为空// 所以进入if有两种可能,1:action为ACTION_DOWN 2:找到消费该事件的Viewif (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {// ViewGroup里面有一个方法,为requestDisallowInterceptTouchEvent,调用这个方法就可以修改mGroupFlags的值,具体看下面提供的源码// 如果为true,则将包含FLAG_DISALLOW_INTERCEPT这个flag,此时执行这里的&运算将为true,所以将不执行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;}} else {// There are no touch targets and this action is not an initial down// so this view group continues to intercept touches.intercepted = true;}...// Check for cancelation.// 判断是否为cancelfinal boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;// 这里的mouseEvent和split不是分析的重点,所以就不解释了,只将代码贴出来// Update list of touch targets for pointer down, if needed.final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0 && !isMouseEvent;TouchTarget newTouchTarget = null;boolean alreadyDispatchedToNewTouchTarget = false;// 上面之所以会将cancel贴出来,是因为这里需要用到// 如果没有cancel也没有interceptedif (!canceled && !intercepted) {...// 如果是ACTION_DOWN或者其他情况,这里的其他情况不包含ACTION_MOVE、ACTION_UP这些// 至于ACTION_MOVE、ACTION_UP这些,则会传递给消费了ACTION_DOWN的View// 而下面的代码,有一个重要的任务,就是寻找目标View// 也就是,只有ACTION_DOWN才会寻找目标View,而其他action则不会寻找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// down时getPointerId将返回0,并且这里的split为true,所以最后的结果为1// 该变量主要是用于解决多点触控的问题,这里不作讨论,只要知道这里的值是1即可final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex): TouchTarget.ALL_POINTER_IDS;removePointersFromTouchTargets(idBitsToAssign);final int childrenCount = mChildrenCount;// 如果有子Viewif (newTouchTarget == null && childrenCount != 0) {// 获取触摸的x值和y值final float x =isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);final float y = isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);// 代码已经贴再下面了,想看的话可以看看// 这里解释一下,从接收的类型上可以看到,该方法放回的是一个child List// 如果ViewGroup里面的任何一个child的getZ()不为0,就会返回一个非空的数组, 否则就会返回空数组// 该方法的作用是,按child的z值进行排序,z值最大的child放到list的最后final ArrayList<View> preorderedList = buildTouchDispatchChildList();// child drawing代码已经贴再下面// 该方法默认是false,可以调用方法设置为true,但设置的方法访问修饰符为protected// 从方法名称可以知道,该方法是用于判断是否支持按照定义的顺序final boolean customOrder = preorderedList == null&& isChildrenDrawingOrderEnabled();// private View[] mChildren;final View[] children = mChildren;// 这里需要注意,这里的遍历是倒序遍历,即从最后一个往第一个遍历for (int i = childrenCount - 1; i >= 0; i--) {// 上面的注释也提到,customOrder默认为false,false将返回传入的index,true的情况就不讨论了final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);// 如果preorderedList不为null,就从preorderedList取数据,否则就从children取数据final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);// 两个方法的代码都贴在下面// 第一个方法,判断View是否可见或虽不可见但在执行动画,只要有一个为true,就不会执行continue。而如果返回true,则会执行第二个方法// 第二个方法,判断x值和y值是否在child里面// 如果child不可见并且正在执行动画或者View可见但没有动画 && xy值在child里面,才不会执行continueif (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {continue;}// getTouchTarget()的作用是查找child是否存在于mFirstTouchTarget的单链表中。 // 如果为true,则返回相应的TouchTarget对象,否则返回nullnewTouchTarget = getTouchTarget(child);resetCancelNextUpFlag(child);// 重点!!!该方法会调用View的dispatchTouchEvent// 如果是一个ViewGroup,那就执行相同的代码,最后执行到这里// 如果是一个View,则会根据具体情况,判断是否执行onTouchEvent和其他代码// 代码我已经放到了下面,建议把该方法的代码看一看,这个方法真的很重要。在看该方法之前,请记住,action是ACTION_DOWN,cancel是false,child不为空// 如果不想看,那就必须知道,该方法会调用View的dispatchTouchEvent方法,尝试将event分发给View// 如果返回true,则说明child消费该down事件。返回true的情况有多种,可以是child的dispatchTouchEvent返回true// 也可以onTouchListener返回true,或者是onTouchEvent返回trueif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...// 找到child就将child添加到TouchTarget的链条里面,并返回新生成的target对象// 这里要记住,此时,newTouchTarget已经是一个非空对象,并且在调用该方法之后,会给mFirstTouchTarget变量赋值,所以此时的mFirstTouchTarget也不为空newTouchTarget = addTouchTarget(child, idBitsToAssign);// 该变量也要记住,这里是在该if里面唯一一行设置为true的代码alreadyDispatchedToNewTouchTarget = true;// 既然找到touchTarget,那就跳出循环把,没必要继续循环下去break;}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;}}}// 如果上面的ACTION_DOWN没有找到消费该event的view,这里的mFirstTouchTarget则为nullif (mFirstTouchTarget == null) {// 此时,可以继续带着参数看该方法,这里的canceled是false,child是null// 不过我还是解释一下吧,由于child是null,最终调用的是自己的super.dispatchT...方法// 也就是说ViewGroup将调用View的dispatchTou...方法// 如果返回true,则表明该事件被该ViewGroup消费,以后的事件都会执行下面的else代码// 为了防止有人懂不懂什么会执行下面的代码,我说清楚一点,一个ViewGroup也会被其他ViewGroup调用// 所以我的意思是,其他ViewGroup会在上面的for循环找到target,然后执行下面的else代码handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {// 如果mFirstTouchTarget不为空,则会执行这里的代码// ACTION_DOWN不为空时,表示找到了消费事件的View// 其他action则是,在ACTION_DOWN找到消费的ViewTouchTarget predecessor = null;TouchTarget target = mFirstTouchTarget;while (target != null) {final TouchTarget next = target.next;// 只有ACTION_DOWN时,并且找到消费的View,该变量才会为true,其他情况都是false// 由于ACTION_DOWN已经执行了dispatchTou...,所以这里没有执行是正确的if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {handled = true;} else {// 看了一下代码,只有调用performButtonActionOnTouchDown,reset方法才有可能返回true,而intercepted则不用说,只要ViewGroup不拦截该event,就wieldfalfinal boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;// cancelChild上面提到,child则是消费了ACTION_DOWN的Viewif (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}// 下面的if和cancel有关,不做讨论// 总结一下上面的if和else,ACTION_DOWN时,如果有View消费了该事件,上面的if就为true,就会将handled置为true// 并且在寻找目标View时,会调用View的dispatchTou..,所以上面的if不会调用dispatchTou...// 在此之上,其他action则会执行else的代码,从intercept可以看到,即便找到目标View,也可以拦截其他事件// 而不管是否拦截,都会执行View的dsipatchTou...方法,只不过如果拦截了,View拿到的action是ACTION_CANCEL,而不是event原本的actionif (cancelChild) {if (predecessor == null) {mFirstTouchTarget = next;} else {predecessor.next = next;}target.recycle();target = next;continue;}}predecessor = target;target = next;}}// Update list of touch targets for pointer up or cancel, if needed.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;
}
其他代码可不看 ,还是建议看看dispatchTransformedTouchEvent方法的代码。里面关键代码还是很多,不看的话,肯定没办法理解整体的执行流程。
方法列表
- requestDisallowInterceptTouchEvent
- buildTouchDispatchChildList
- isChildrenDrawingOrderEnabled
- canReceivePointerEvents和isTransformedTouchPointInView
- dispatchTransformedTouchEvent
既然return handled的代码在末尾,那就在这里总结一下,哪些地方可能为true。首先,默认值是false,所以必须手动置为true。
第一次,这段代码不能一定会置为true,只能说可能会,但还是有必要拿出来
if (mFirstTouchTarget == null) {handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
当ACTION_DOWN找不到target时,就会执行这里的代码,最终会调用super.dispatchTou…方法。此时,如果该ViewGroup要消费该事件,就可以返回true。
第二次,当ACTION_DOWN找到target view时,直接置为true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {handled = true;
第三次,这个if的else
} else {final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}
再探讨一下,为什么找到target view时,ViewGroup需要返回true。
我的理解是:既然是该ViewGroup的child消费事件,那也可以说是该ViewGroup消费了该事件。只不过上一级并不知道具体消费的对象。但这不重要,只要知道该VIewGroup需要就行,不需要理会具体是哪个对象。
可以思考一下,如果返回false会怎么样。如果返回false,那ViewGroup在调用dispatchTra…方法的时候,就没办法找到target view。所以ViewGroup必须返回true,然后再由该ViewGroup去调用自己的child,将事件传递给target view。
上面的dispatchTransformedTouchEvent方法一定要看,里面有很多很重要的代码。
简单梳理一下执行流程
- Activity接收到触摸事件之后,会调用ViewGroup的dispacthTouchEvent方法,让ViewGroup自己将触摸事件分发出去
- 如果是ACTION_DOWN,就清空touch相关的数据。并从最后一个到第一个寻找需要消费的子View。寻找的方式为,判断触摸的x值和y值是否在子View里面,并且子View的dispacthTouchEvent方法是否返回true。如果返回true,则会用该View的信息构建一个TouchTarget并将TouchTarget的值赋给mFirstTocuhTarget
- ACTION_DOWN的代码执行完成之后,将判断mFirstTouchTarget的值是否为空。如果不为空,就说明找到需要消费的View,ViewGroup的dispatchTouchEvent方法将返回true,表示该事件被该ViewGroup消费了。如果为空,则会调用super.dispatchTouchEvent方法,尝试将该事件分配给自己。
- ACTION_DOWN以外的事件将不寻找消费的子View,而是判断mmFristTouchTarget是否为空。如果不为空,继续调用child的dispatchTouchEvent方法并返回true。如果为空,则会调用当前ViewGroup的super.dipatchTouchEvent方法。
- 有一点需要注意,如果不是ACTION_DOWN并且没有找到消费的子View或者是调用了disallowIntercept方法,则不会调用onInterceptTouchEvent。其他情况都会调用onInterceptTocuhEvent方法,所以即使找到了消费的子View,也会尝试拦截触摸事件。
- 关于View的dispatchTouchEvent,在调用onTouchEvent之前,会判断onTouchListener是否为空。如果不为空,并且onTouch方法返回true,则会返回true,表明消费了该事件。否则会执行onTouchEvent方法。
- 在onTouchEvent方法里面,如果没有设置click或longClick或tooltip,就会返回false。longClick和tooltip的触发逻辑是一样的,都是在down的时候,向RunQueue放入一个延迟任务,如果延迟结束就会执行longClick。如果延迟还没结束就松开手指,即执行了up事件,则会将该任务移除。如果up时延迟还没有结束,或者没有设置longClick,并且设置了click,就会触发onClick。如果同时设置了longClick和click,并且长按超过400毫秒,就会执行longClick,此时松开手指也不会执行click。也就是,longClick的优先级是高于click。
最后根据上面分析的执行流程,重新画一个简单流程图。先说清楚,这个流程图只是用于理解,所以像第7步的longClick、click不会包含在这里面。
方法列表
requestDisallowInterceptTouchEvent
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {// We're already in this state, assume our ancestors are tooreturn;}if (disallowIntercept) {mGroupFlags |= FLAG_DISALLOW_INTERCEPT;} else {mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}...
}
回到源码分析
buildTouchDispatchChildList
/*** Provide custom ordering of views in which the touch will be dispatched.** This is called within a tight loop, so you are not allowed to allocate objects, including* the return array. Instead, you should return a pre-allocated list that will be cleared* after the dispatch is finished.* @hide*/
public ArrayList<View> buildTouchDispatchChildList() {return buildOrderedChildList();
}/*** Populates (and returns) mPreSortedChildren with a pre-ordered list of the View's children,* sorted first by Z, then by child drawing order (if applicable). This list must be cleared* after use to avoid leaking child Views.** Uses a stable, insertion sort which is commonly O(n) for ViewGroups with very few elevated* children.*/
ArrayList<View> buildOrderedChildList() {final int childrenCount = mChildrenCount;if (childrenCount <= 1 || !hasChildWithZ()) return null;if (mPreSortedChildren == null) {mPreSortedChildren = new ArrayList<>(childrenCount);} else {// callers should clear, so clear shouldn't be necessary, but for safety...mPreSortedChildren.clear();mPreSortedChildren.ensureCapacity(childrenCount);}final boolean customOrder = isChildrenDrawingOrderEnabled();for (int i = 0; i < childrenCount; i++) {// add next child (in child order) to end of listfinal int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View nextChild = mChildren[childIndex];final float currentZ = nextChild.getZ();// insert ahead of any Views with greater Zint insertIndex = i;while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {insertIndex--;}mPreSortedChildren.add(insertIndex, nextChild);}return mPreSortedChildren;
}
回到源码分析
isChildrenDrawingOrderEnabled
protected boolean isChildrenDrawingOrderEnabled() {return (mGroupFlags & FLAG_USE_CHILD_DRAWING_ORDER) == FLAG_USE_CHILD_DRAWING_ORDER;
}protected void setChildrenDrawingOrderEnabled(boolean enabled) {setBooleanFlag(FLAG_USE_CHILD_DRAWING_ORDER, enabled);
}
回到源码分析
canReceivePointerEvents和isTransformedTouchPointInView
protected boolean canReceivePointerEvents() {return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}// 这里的outLocalPoint应该是想将x值和y值设置到该变量里面,所以传入了一个引用
protected boolean isTransformedTouchPointInView(float x, float y, View child,PointF outLocalPoint) {final float[] point = getTempLocationF();point[0] = x;point[1] = y;transformPointToViewLocal(point, child);final boolean isInView = child.pointInView(point[0], point[1]);if (isInView && outLocalPoint != null) {outLocalPoint.set(point[0], point[1]);}return isInView;
}
回到源码分析
dispatchTransformedTouchEvent
// 在dispatchTouchEvent里面,由三个地方调用该方法,其中两个是child调用的,一个是当前的VieweGroup调用的
// child调用时:child不为空
// 当前ViewGroup调用时:child为null
// cancel就都当作false吧
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();// 虽说当作canel为false,但还是说一下代码的执行逻辑// 除了action是cancel之外,如果再ACTION_DOWN找到target view,并且当前的action不是ACTION_DOWN,并且ViewGroup的onInterceptTouchEvent返回true// cancel的值也是true的,此时,也会进入到该if,所以在进入之后,需要手动设置为cancel,因为进入之前的action不是cancelif (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);// 可以看到,如果child为空,就会调用ViewGroup自己的super.dispatchTou...方法。而如果不为空,则会调用child的该方法。// 而如果是调用super.dispatchTou...方法,则方法逻辑就是View的该方法,下面会将View的该方法的代码贴出来,分析代码的执行流程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;// 和多点触控有关,一般为true,上面的注释也写得很清楚。并且大部分情况下,会在该if里面执行if (newPointerIdBits == oldPointerIdBits) {// 不明白hasIdentityMatrix的作用,而且代码是native,看不了。总之,只要知道,该方法一般返回true// 所以如果child不为空,是可以进入到if的if (child == null || child.hasIdentityMatrix()) {// 下面的代码没什么好说的, 接下来看看View的dispatchTou...方法// 代码贴在这个方法的下面,滑下去就行了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()) {transformedEvent.transform(child.getInverseMatrix());}handled = child.dispatchTouchEvent(transformedEvent);}// Done.transformedEvent.recycle();return handled;
}// View的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {// 这里是accessibility相关的代码...// result可以当作ViewGroup的handled,都是默认为false,再根据情况改为true// 而如果为true,则表明该View要消费该事件,所以下面那些将resule设置为true的代码,都是表明要消费该事件// 不过有一点需要记住,一个View如果想要消费move、up这些事件,就需要在down时表明需要该事件// 不过也有其他解决方案,比如从ViewGroup入手,毕竟不是所有情况都需要down事件boolean result = false;...final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesturestopNestedScroll();}if (onFilterTouchEventForSecurity(event)) {// 如果enable为true,并且正在拖拽,就直接将resule设置为trueif ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}// noinspection SimplifiableIfStatement// li就是保存各种listener的类,比如onClick、onLongClick、onTouchListener等// 如果onTouchListener不为空,并且onTouch返回true,就将result设置为trueListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}// 如果上面的if没有执行,则会调用onTouchEvent代码// 此时,来回答一道在面试时会被问的题目。onTouch和onTouchEvent哪个会先被调用?// 从代码上看,答案已经出来了。不过如果没有看源码,可能就只知道这回事,但不知道为什么// 滑下去看onTouchEvent的代码// 这里还是提醒一下,如果onTouchEvent返回true,则表明消费该事件if (!result && onTouchEvent(event)) {result = true;}}if (!result && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);}// Clean up after nested scrolls if this is the end of a gesture;// also cancel it if we tried an ACTION_DOWN but we didn't want the rest// of the gesture.if (actionMasked == MotionEvent.ACTION_UP ||actionMasked == MotionEvent.ACTION_CANCEL ||(actionMasked == MotionEvent.ACTION_DOWN && !result)) {stopNestedScroll();}return result;
}// View的dispatchTouchEvent
public boolean onTouchEvent(MotionEvent event) {final float x = event.getX();final float y = event.getY();final int viewFlags = mViewFlags;final int action = event.getAction();// 代码有点长,不看也可以。该变量如果为true,那就可以click或者longClickfinal boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;// 如果View为disable,即enable为falseif ((viewFlags & ENABLED_MASK) == DISABLED) {if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;// A disabled view that is clickable still consumes the touch// events, it just doesn't respond to them.// 上面的注释已经说明了一切。说一下我的理解,觉得就算一个View disabled,只要设置了click或者longClick,那就必须消费该事件。// 只是,是否响应看的是View是否disablereturn clickable;}// 如果设置了delegate,则执行delegate的代码,并且当delegate返回true时,直接return。如返回false,那就继续执行下面的代码。if (mTouchDelegate != null) {if (mTouchDelegate.onTouchEvent(event)) {return true;}}// 只有设置了click或者tooltip才能进入if。记住,只要进入if,就一定会返回true,即消费该事件。没有进入则返回false// tooltip是android 26新出的一个功能,给一个View设置tooltip(String)之后,长按该View,就会出现一个提示// 所以tooltip就是一个longClick,这不是我瞎说的。实际上,tooltip和longClick就是调用同一段代码,具体可以看下面的源码分析// tooltip的设置方式:调用setTooltipText。如果参数是一个空字符串,则会清空flag,否则会设置tooltip的flagif (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {switch (action) {// 只分析ACTION_DOWN和ACTION_UP,可以先看ACTION_DOWN的代码,再回头看ACTION_UP的代码case MotionEvent.ACTION_UP:mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;if ((viewFlags & TOOLTIP) == TOOLTIP) {handleTooltipUp();}// 如果不能点击,则表明是因为设置了tooltip// 而如果看了checkForLongClick的代码,就会知道,显示tooltips不是在ACTION_UP执行的,而是由Runnable自己完成的// 所以到了这里直接移除callback是没有问题的。假设在up之前就显示tooltip,那移除肯定没问题。而up之后才需要执行,那更要移除,因为都已经松开手指了if (!clickable) {removeTapCallback();removeLongPressCallback();mInContextButtonPress = false;mHasPerformedLongPress = false;mIgnoreNextUpEvent = false;break;}boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;// 在ACTION_DWON,有一句代码调用了setPressed(true, x, y),并且执行了mPrivateFlags |= PFLAG_PRESSED,所以这里为trueif ((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, x, y);}// 在checkForLongClick我有提到,如果有handle longClick,该变量就会变为true,所以如果没有handle,并且不忽略up event,就可以进入eventif (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {// This is a tap, so remove the longpress check// 如果是clickable,上面的break则不会执行,所以需要在这里执行removeLongPressCallback();// Only perform take click actions if we were in the pressed state// 如果没有focus,就执行clickif (!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();}// Returns true if the Runnable was successfully placed in to the message queue. // Returns false on failure, usually because the looper processing the message queue is exiting.// 上面的注释是post方法的注释,不是原本写在这里的注释// 意思是,如果没办法成功向消息队列放入Runnable,则会执行下面的performClick方法// 而PerformClick从类名也看得出,就是用来执行click,所以效果是一样的if (!post(mPerformClick)) {performClickInternal();}}}if (mUnsetPressedState == null) {mUnsetPressedState = new UnsetPressedState();}if (prepressed) {postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {// If the post failed, unpress right nowmUnsetPressedState.run();}removeTapCallback();}mIgnoreNextUpEvent = false;break;case MotionEvent.ACTION_DOWN:...// ACTION_UP时,该变量用于判断是否调用onClick,只有false时,才可以调用onClickmHasPerformedLongPress = false;// 如果不能点击, 那就只有tooltip这种可能性了,直接break就行。记住,我上面提到了,只要break,就会返回true// 为什么现在就可以return,可以滑下去看看该方法的代码。顺便一提,该方法和longClick调用的是同一个方法,只要看一遍就可以了if (!clickable) {checkForLongClick(ViewConfiguration.getLongPressTimeout(),x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);break;}// Performs button-related actions during a touch down event. True if the down was consumed.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.// 如果正在滚动,则将延迟100ms执行if (isInScrollingContainer) {mPrivateFlags |= PFLAG_PREPRESSED;if (mPendingCheckForTap == null) {mPendingCheckForTap = new CheckForTap();}mPendingCheckForTap.x = event.getX();mPendingCheckForTap.y = event.getY();// 点击查看ViewConfiguration.getTapTimeout()的代码,可以发现,获取到的值是100mms// 最终,mPendingCheckForTap也会执行else里面的checkFor...代码,所以逻辑是一样的postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());} else {// Not inside a scrolling container, so show the feedback right awaysetPressed(true, x, y);checkForLongClick(ViewConfiguration.getLongPressTimeout(),x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);}break;case MotionEvent.ACTION_CANCEL:...break;case MotionEvent.ACTION_MOVE:...break;}return true;}return false;
}// View的checkForLongClick
private void checkForLongClick(long delay, float x, float y, int classification) {if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {mHasPerformedLongPress = false;if (mPendingCheckForLongPress == null) {mPendingCheckForLongPress = new CheckForLongPress();}mPendingCheckForLongPress.setAnchor(x, y);mPendingCheckForLongPress.rememberWindowAttachCount();mPendingCheckForLongPress.rememberPressedState();mPendingCheckForLongPress.setClassification(classification);// mPendingCheckForLongPress是一个Runnable,看run方法就行// 再结合下面的run方法可以知道,如果设置了tooltip或longClick,按下去之后,不管是否有move,只要时间到了,就会触发设置的事件postDelayed(mPendingCheckForLongPress, delay);}
}// CheckForLongPress的run
public void run() {if ((mOriginalPressedState == isPressed()) && (mParent != null)&& mOriginalWindowAttachCount == mWindowAttachCount) {recordGestureClassification(mClassification);// 调用longClick,具体代码我就不贴出来了,只要知道有这件事就行了// 不过调用了,不代表一定就会执行longClick,具体要看是否设置了longClickListener或者tooltip// 当然了,有人可能会思考。在longClick响应之前就松开手指,会怎么办?这个就需要看ACTION_UP的代码,ACTION_UP有一行代码会将callback移除掉,以保证不会触发longClickif (performLongClick(mX, mY)) {// 可以看到longClick有handle,就会将mHasPerformedLongPress置为true,这很重要mHasPerformedLongPress = true;}}
}
回到源码分析
相关文章:

android事件分发机制源码分析
没什么用的前言责任链设计模式流程图源码分析 没什么用的前言 事件分发机制是面试中一道必问的题目,而我的应对方式则是,在网络上找一些博客看看,然后做一些笔记,最后在面试时将我自己记住的内容说出来。这种方式本身没有太大的…...

今天,小灰37岁了!
人们常常说,35岁是互联网人的中年危机。现在,小灰已经跨过了中年危机,倒不是因为小灰财务自由了,而是因为今天是小灰37岁的生日。年轻时候,小灰总觉得30岁是一个很遥远的年龄,而现在,小灰距离40…...

基于.NET 7 + iView 的前后端分离的通用后台管理系统开源框架
更多开源项目请查看:一个专注推荐.Net开源项目的榜单 今天给大家推荐一套前后端分离通用后台管理系统开源框架。 项目简介 这是基于.Net 7 Vue.js开发的、前后端分离框架,前端UI框架采用iView,该项目只有基础功能模块,不包含具…...
新一代通信协议—— RSocket
一、简介 RSocket 是一种二进制字节流传输协议,位于 OSI 模型中的5~6层,底层可以依赖 TCP、WebSocket、Aeron 协议。最初由 Netflix 开发,支持 Reactive Streams。其开发背后的动机是用开销更少的协议取代超文本传输协议(HTTP),H…...
【编程实践】这个代码命名规范是真优雅呀!代码如诗!!(多读优秀的开源代码,多实践,你也可以一样优秀!)
目录 管理类命名 传播类命名 回调类命名 监控类命名 内存管理类命名 过滤检测类命名 结构类命名 常见设计模式命名 解析类命名 网络类命名 CRUD命名 其他 End 管理类命名 写代码,少不了对统一资源的管理,清晰的启动过程可以有效的组织代码…...

Linux->进程终止和等待
目录 1. 进程终止场景 1.1 进程退出码 1.2 进程常见退出方式 2. 进程等待 2.1 进程等待的必要性 2.2 进程等待的方式 wait()方式 waitpid()方式 options参数 status参数 1. 进程终止场景 代码运行完毕,结果正确 代码运行完毕,结果不正确 代码异…...

超店有数分享:tiktok数据分析工具推荐,助你成功出海!
现阶段的跨境电商人都纷纷入局tiktok,这是风口也是发展趋势。Tiktok的下载量已经超过了35亿,每月都有10亿用户活跃,在154国家/地区使用。Tiktok用户每天在平均花1小时左右进行浏览,打开率也很高。如今,tiktok也越来越成…...
2022 第十四届蓝桥杯模拟赛第三期(题解与标程)
第十四届蓝桥杯模拟赛第三期1. 最小的十六进制问题描述答案提交参考答案2. Excel的列问题描述答案提交参考答案3. 相等日期问题描述答案提交参考答案4. 多少种取法问题描述答案提交参考答案5. 最大连通分块问题描述答案提交参考答案6. 哪一天问题描述输入格式输出格式样例输入样…...

「TCG 规范解读」PC 平台相关规范(1)
可信计算组织(Ttrusted Computing Group,TCG)是一个非盈利的工业标准组织,它的宗旨是加强在相异计算机平台上的计算环境的安全性。TCG于2003年春成立,并采纳了由可信计算平台联盟(the Trusted Computing Platform Alli…...

HNU工训中心:直流电路测量分析实验报告
工训中心的牛马实验 实验目的 1.熟悉直流电路的测量和分析方法。 2.熟悉直流电源、电压表、电流表的使用法及其特性。 实验仪器和器材 1.实验仪器 直流稳压电源型号:IT6302 台式多用表型号:UT805A 2.实验(箱)器材 电路实验箱 元器件:电阻…...

tensorflow2.4--1.框架介绍
前言 虽然1.x版本tensorflow有很多项目都基于此构建,然而随着2.x版本的推出,很多架构已经发生了改变,代码发生了改变,同时很多模组已经废弃不用或者更新,tensorflow1.x已经不能再兼容最新的项目,与时俱进是必要的,因此…...
c++11 关键字 final 使用
写在最前。。。 请支持原创~~ 1. 功能 用以指定一个 virtual function 不能被派生类重写;或者指定一个 class 不能被继承;2. 语法 对于类中成员函数有两种情况: 只声明时,final 紧跟参数的右括号,如果是纯虚函数&a…...

力扣(LeetCode)426. 将二叉搜索树转化为排序的双向链表(2023.02.28)
将一个 二叉搜索树 就地转化为一个 已排序的双向循环链表 。 对于双向循环列表,你可以将左右孩子指针作为双向循环链表的前驱和后继指针,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。 特别地,我们希望可以…...
华为OD机试真题Python实现【玩牌高手】真题+解题思路+代码(20222023)
玩牌高手 题目 给定一个长度为N的整数数组,表示一个选手在N轮内选择的牌面分数, 选手基于规则选牌,请计算所有轮结束后其可以获得的最高总分数。 选择规则如下: 在每轮里选手可以选择获取该轮牌面,则其总分数加上该轮牌面分数为其新的总分数选手也可不选择本轮牌面,直接…...

“速通“ 老生常谈的HashMap [实现原理源码解读]
👳我亲爱的各位大佬们好😘😘😘 ♨️本篇文章记录的为 HashMap 实现原理&&源码解读 相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉…...

Linux系统介绍及熟悉Linux基础操作
一、什么是Liunx Linux,全称GNU/Linux,是一种免费使用和自由传播的类UNIX操作系统,其内核由林纳斯本纳第克特托瓦兹(Linus Benedict Torvalds)于1991年10月5日首次发布,它主要受到Minix和Unix思想的启发&am…...

mysql数据库limit的四种用法
文章目录前言一、语法二、参数说明三、常用示例-4种用法总结前言 mysql数据库中limit子句可以被用于强制select语句返回指定的记录数。limit接受一个或两个数字参数。参数必须是一个整数常量。如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,…...

嵌入式 linux 系统开发网络的设置
目录 一、前言 二、linux网络静态地址设置 前言 为什么要对linux系统下的ubuntu进行网络设置呢? 因为我们在嵌入式开发中,我们要保证windows系统、linux系统、开发板的ip要处于同一个网段,而默认ubuntu下的linux系统的ip是动态分配的&#…...

算法设计与分析——十大经典排序算法一(1--5)
目录 算法设计与分析——十大经典排序算法 第1关:冒泡排序 参考代码 第2关:选择排序 参考代码 第3关:插入排序 参考代码 第4关:希尔排序 参考代码 第5关:归并排序 参考代码 作者有言 一个不知名大学生&#x…...
六.慕课的冲击:知识何以有力量?
6.1知识就是力量?【单选题】关于技术进步,以下说法错误的是( )。A、技术进步可以不依靠知识积累B、知识的力量推动技术进步C、技术黑箱换句话说即是天上掉馅饼D、专利保护产生的垄断利润,构成创新动力我的答案:A【判断题】罗伯特索洛认为,技…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...

抖音增长新引擎:品融电商,一站式全案代运营领跑者
抖音增长新引擎:品融电商,一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中,品牌如何破浪前行?自建团队成本高、效果难控;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...

对WWDC 2025 Keynote 内容的预测
借助我们以往对苹果公司发展路径的深入研究经验,以及大语言模型的分析能力,我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际,我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测,聊作存档。等到明…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
Java数值运算常见陷阱与规避方法
整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...

Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
【SpringBoot自动化部署】
SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一,能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时,需要添加Git仓库地址和凭证,设置构建触发器(如GitHub…...