每个人都是独一无二的,都可以发光和发热,即使当前的自己依旧弱小,但是要相信,自己也会去有撑起一片天的
是时候。
自我的反思:
相信很多人都熟透了,说不就是三个方法吗?dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent这三个方法吗,这不是so easy吗?但是如果我问你,你真的看过事件分发的源代码吗?你可以用事件分发下一个下拉刷新的组件吗?你根据下拉刷新有什么自己的思考吗?你会怎么回答我呢?还是会说自己真的会事件分发吗?我会在事件分发中讲解,同时抛出一些问题,以及和我们日常开发中有关系的case。(在这里,大家也可以看看任志刚的艺术探索,挺好的一本书)
- dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent三者是什么关系,调动顺序是什么?
- 三个方法个子的适用场景是什么,我应该什么时候重写什么方法?
方法简介
-
dispatchTouchEvent:件分发中,如果事件能够发送到当前View,那么这个方法一定回调用。在这个方法的内部,会用到onInterceptTouchEvent和onTouchEvent,综合判断当前事件是否会被消费。
-
onInterceptTouchEvent:示是否拦截此事件,这个方法只会被调用一次(相信大家早已知道,但是为什么只会调用一次呢?而且这个方法对我们有什么用呢?稍后解析)
-
onTouchEvent:个方法是用来处理事件的,比如我们需要在这里处理手指的滑动,以及各种触摸位置的监听等等。。。(再提出一个问题,既然这个地方是来进行事件处理的,那我在上面两个方法中处理不行吗?)而且一个事件序列,如果你需要消费,那么这个事件序列都会给你,如果不消费,那么就无法收到此事件序列了。(啥叫一个事件序列呢,就是你的手指从按下到抬起),但是我有什么方法,我接受到了这个事件,后续的事件我不想要了,怎么处理呢???
源码分析
事件分发的过程概述以及关注要点
事件的分发会经历以下几个阶段 activity->window->view,其中我们需要关注的也就是window->view这一步而已,目前我没有遇到activity->window这个阶段做一些特殊操作的。问题又来了,流程是什么呢?这里我说一下我个人的观点。
- window和view的关系是什么,为什么需要window这个层级,以及window和view之间的大小关系事件传递和分发我们需要解决什么问题呢?(这里我都会在分析过程中一一说明)
- view的事件分发,其实就是ViewGroup和View的之间事件传递而已,就是上文中介绍的那三个方法。在分析源码的过程中会提到,同时我也会给大家解释一下Ptr下拉刷新这个库的核心代码。
activity->window的分发
仔细观察一下其实可以发现,activity和window都是继承了window这个抽象类实现了这个抽象类的接口,来实现事件的传递(这个地方,其实很值的我们学习的,你可以简单理解为代理模式,activity的事件处理交给了内部代理类PhoneWindow,因为他们继承同一个类,那我们日后在写代码的过程中是不是可以效仿一下,而不是类A交给类B来做的事情,用不同的方法名,恶心不?)
Window类(建议自己去翻看一下这个类的说明):来说,就是这个window类是顶级window的抽象类,并且提供了标准的UI设置方式,这个UI一般和键盘有关的用的比较多,比如我们常设置的键盘属性等。它的实现类只有PhoneWindow,该例应用作添加到窗口管理器的顶级视图(Google翻译的)。
家不理解最后一句话,我说一下自己的理解:其实你也可以这么理解,其实我们的界面显示是没有window的,window大家得意简单理解就是一个约束,我的view就是放在window中的(方便大家理解)。为什么这么说呢,我们可以约束窗口的大小,如果窗口就这么大了,我的view也显示不出来不是(有裁剪属性,可以让View显示出来,暂不考虑),你的点击事件是window->view的,window设置了位置和大小,当你触摸到了window上,事件才会发到你的view上,不知道这么说大家能理解吗?可以理解一个约束,不要搞得太高深。(后面我也会和大家介绍一个WindowManger,专门介绍window的,方便大家理解,不需要过多关注)
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
分析一
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
分析二:
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
分析三:
return onTouchEvent(ev);
}
先看一下注释: 大概意义就是说,当你的手机触摸到屏幕,事件开始分发的时候,这个方法就是入口。
- 分析一:当activity接收到down事件的时候,会触发onUserInteraction方法,这个是提供给activity实现类重写的,如果你要监听用户开始触摸屏幕操作,你可以实现它。
- 分析二:会把事件传递给window的dispatchTouchEvent来处理,如果返回true,证明window先发了,那么就返回true。这个就验证了activity->window这个流程。
- 分析三:如果上面都不执行,意味着window没有消费事件,那么就自己处理,走自己的onTouchEvent方法,很显然我们需要核心关注的事件处理是在window里面。
上面我们说过,window的实现类只有一个,那就是PhoneWindow,我们到PhoneWindow来看看。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// 分析一
return mDecor.superDispatchTouchEvent(event);
}
分析一:很显然,传递给了mDecor这个View,这个有验证了我们上面所说的window->view这个流程。 简介:mDecor就是一个继承FrameLayout的自定义View,里面核心就是两个View,一个是顶部状态栏,一个就是我们重写的setContentView,设置的View,其实这个我们也能很好的明白,这个就是我们的手机显示的全部,我也没有见过显示window这个View啊?对不,更证明了window其实就是一个约束,以及Android用来分层级的,即window的叠加,一个页面打开另外一个页面,其实你就可以理解window叠加,自己的window负责处理自己的事件。(不知道你懂没有,后面还会介绍的)
那么我们就需要看看mDecor的事件分发了,是吧。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
// 分析一
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
分析一:这个返回值用到了Window内部类CallBack,这个CallBack是用来拦截事件用的,不是我们需要关系的核心事件分发流程,我们需要关心的依旧是super.dispatchTouchEvent(ev),由于FrameLayout没有实现这个方法,我们需要到ViewGroup中查看。但是ViewGroup的代码太多,调重要的讲,换句话说,基本也不太可能把每一行代码什么意思讲的都很清除,我们只要理解并梳理好核心逻辑即可。
// Check for interception.
final boolean intercepted;
// 分析一
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;
}
分析一:当前事件是down事件,或者mFirstTouchTarget!=null的时候,会判断是否拦截事件。第一,当前是down事件,即触摸刚开始的时候会进行判断,其次就是后面这个判断,这个判断是什么意思呢,就是第一个处理事件的TouchTarget,TouchTarget是一个包装着当前view以及多点触控信息,以及next view 信息的类,后面会介绍,暂且知道,这是第一个处理事件的View类即可。
分析二:这个标志位相信大家很熟悉了,就是希望父控件不要拦截事件,分发给自己处理,如果设置了这个标志位,以为这父控件不会处理,从而会把事件分发下来,至于自己处理还是不处理,就需要调用自己的onInterceptTouchEvent方法,进行判断了,很好理解。
分析三:如果当前不是down事件,或者还没有子View拦截消费事件,那么就需要父控件拦截住,交给父控件自己处理,如果父控件的onTouchListener返回true,或者当前控件是ENABLED等返回true的情况下,同时自己的onTouchEvent也返回true(自己消费了事件)那么消息就自己消化了,否则,会向上传递(后面会有分析)。
这里我们可以得到结论:如果有父控件有子View消费了down事件,后面的事件还会走到这个判断,actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null,是会返回true的,因为mFirstTouchTarget不为空了,于是intercepted这个父控件的变量永远都是为false了,表明如果这个控件不处理这个事件,那么这个view个事件序列了,这里相信大家也就很清楚了。
接下来我们就分析,这个mFirstTouchTarget这个变量,官方注释是First touch target in the linked list of touch targets,很好理解父控件可能有很多子View,一定是一个链表的,但是这个View是第一个处理事件的View。
// Check for cancelation.
// 分析一
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;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessibility 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;
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
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;
// 刚开始的时候就是空的,我们假设这个父控件是有子View的,不然也没有啥分析的必要了,
// 没有子View,就会回到我之前分析的,自己处理,处理不了往上抛
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 = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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);
// 偏向于获取焦点的View,如果找到并且比对恰好,就在这个View执行正常的
// 事件分发,这个有可能会多次两次遍历PS:我也不知道goole为啥这样干,直接
// 分发不好吗😭,难道有焦点大概率会消费事件,如果是这样,确实会提高性能,
// 减少遍历,这个也只是我的猜测,不太懂。
// 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 (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 由于mFirstTouchTarget还是空的,还没有找到消费事件的View呢,
// 所以newTouchTarget也是空的,这个方法是在能够响应事件的View中,找到
// 目标View,因为子View不是所有都能响应事件的,childcount需要遍历一次,
// 然后判断有么哟在响应事件的linklist中,有就返回,否则返回null,假设
// newTouchTarget找到了不为空,那么这个循环也就没有必要进行下去了,直接 // break
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);
// 分析三
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();
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();
}
// 这个时候就很尴尬,子view中么有找到,就指向之前的mFirstTouchTarget
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;
}
}
}
简单总结:其实这个循环核心就是找到mFirstTouchTarget,看看谁可以消费事件。
分析一:这里我强调一下,大家注意下这个cancel事件,使用场景是啥呢,比如一个button,一个click就是点击到放开对不,如果在,我点击了一个item,然后不松手滑动,触发了下拉刷新,这个时候应该就是下拉刷新的,这个item就不应该响应onClick事件,大家可以理解吧,cancel有这个功能,后面我给大家讲解下拉刷新用的到,而且这个知识点值得学习。
分析二:这个定义子View的列表集合,挺巧的,大家一个点进去看看,我给大家讲一下,就不粘贴代码了。子View定制事件顺序,啥意思呢?如果我们有一个ViewGroup,子View有三个,我们可以通过ViewGroup的getChildDrawingOrder来定义绘制顺序(从代码看,是和事件传递保持一致的),而且你仔细看代码会发现,其中的View.getZ属性是优先于getChildDrawingOrder的(你重写getChildDrawingOrder获得了一个顺序,但是还会根据View.getZ重新排列一下)。有人会问,这个有什么用呢,有用啊,假设View的绘制顺序是从左到右,你完全可以重写这个方法,实现从右往左绘制,实现一些花里胡哨的操作。
分析三:这个就是事件传递给子View的核心代码了。
分析之前,我们看看这个dispatchTransformedTouchEvent方法的注释说明:过滤一些不合法的触控ids,重新设置一下action,啥意思呢,就是MotionEvent的action会改变一下(比如,如果当前事件被cancel了,可能就会把这个action改变成cancel,很好理解),同时如果这个view为null,交给父亲处理,换言之,就是null的view不处理事件。
dispatchTransformedTouchEvent:这个方法就不做过多介绍了,没有什么东西,我们需要核心关注的就是一个变量handled,这个变量表明这个事件是否被处理,handled = super.dispatchTouchEvent(event),所以我们关心的重点就又是dispatchTouchEvent这个方法,接下来,就是进入View的源代码进行分析。
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
// 有没有感觉这个位置都很有意思,一看就就知道是Google留下来的口子,干嘛用的呢?
// (其实我点出来,没有其他的想法,就是要告诉大家,其实很多时候,
// 我们需要的代码,其实可能别人已经给我封装好了,或者暴露了口子,还是要多看多学习的)
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 分析一
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 分析二
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// 同样调用个,看这个方法的名字,是摁住正在滑动的列表,但是可能会有人有问题啦?
// 如果同样是停止滑动,抬起手的一瞬间不是应该惯性滑动吗?
// 那我的惯性还要不要了?那我怎么知道是按下调用的还是抬起手指调用的?
// 其实我理解,这个stopNestedScroll,在触摸点击到离开,你的列表列表拖动就停止了,从手势上来说是没
// 有问题的,我离开了触摸就是没有了滑动,至于惯性是为了好的用户体验,怎么实现的,你可以自行翻看源码
// 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;
}
分析一:onTouchListener,如果我们对View设置了onTouchListener,返回值我们这是了true,那么这个View是有可能消费这个事件的。
分析二:onTouchEvent,这个方法就是我们常说的时间分发的三个重要方法之一,我们经常在这里处理我们的手势操作,处理down,up的等事件。
我们可以看到,onTouchListener的优先级应该是比onTouchEvent高的
其实都没有什么特别难以理解的,大家知道这个简单流程即可,我更想给大家介绍的是onTouchEvent,这个里面有很多值得我们学习的东西。
首先提几个问题:
- 如何区分当前是滑动还是点击事件?
- 是如何实现click和longClick的?
- 如何取消当前View的响应事件,想想我之前说的cancel???
温馨提示:大家可以从down事件开始看,我也不知道老外的代码为啥up在上
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// View可以点击的依据是什么
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 不可点击直接setPressed=false,大家可以看到,pressed这个功能,Google是直接集成在View中的
if ((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.
return clickable;
}
// 如果touchDelegate能够消费,就消费了(说说delegate的好处吧,为啥delegate?这不就是设计模式 // 了吗,哈哈哈哈)
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果不可以点击,悬停啥的也就没有必要走进去了
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
// 手指抬起,如果不可点击,来一次clear,整体和cancel差不多
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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, x, y);
}
// 没有消费过长按事件,当前可能处理的是click
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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();
}
// 开始处理onClick
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 now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
// 即使不可以点击,但是可以悬停也是要判断长按的,显示悬停效果
if (!clickable) {
// 这个方法会被多次调用,判断是不是longclick
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
// 判断当前View是不是在可滑动的容器当中
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;
// mPendingCheckForTap是一个runnable,主要就是延时作用,有人说,既然延时,为什么我还需要创建一个runnable。原因是,在mPendingCheckForTap内部,做了一个逻辑,需要判断当前是不是长按
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
// 很明显是通过handler,messagequeue来时实现的,小伙伴可以自己看细节
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
// 同样判断是不是长按
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
// 稍微关注一下,如果是cancel事件,可以理解为clear操作,重置标志位,以及移除messagequeue的runnable的消息
思考一下,如果我们父View,在给子View分发事件的时候,串改一下,是不是子View响应的一些事件就会cancel呢?
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
// 这里很有意思哈,当前到底是滑动呢,还是长按呢?down的时候,我延时一下,move的时候又遇到了这个问题。
// ambiguousGesture=true,表示当前确实根据当下的事件,判断不出来,模棱两可,而且当队列中还有longClick没有消费,就会走进来
if (ambiguousGesture && hasPendingLongPressCallback()) {
final float ambiguousMultiplier =
ViewConfiguration.getAmbiguousGestureMultiplier();
// 很简单,就是看当前的点击落点有么有在View范围
if (!pointInView(x, y, touchSlop)) {
// The default action here is to cancel long press. But instead, we
// just extend the timeout here, in case the classification
// stays ambiguous.
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* ambiguousMultiplier);
// Subtract the time already spent
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= ambiguousMultiplier;
}
// 手指划出View外,移除longclick,很显然
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
// 当前是不是长按
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
// 如果当前是长按,并且队列中有长按的runnable的消息,移除掉,立即发送一个delay=0的消息,立马执行longClick的操作
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
removeLongPressCallback();
checkForLongClick(
0 /* send immediately */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
}
return true;
}
return false;
}
提示
- 不要问我为什么有的行数代码没有注释,你不可能把每一行代码都扣的很细,升入代码的细节,你先要理解核心流程,后面有兴趣可以通读。
- 我在我认为必要的代码上面留下了注释,有可能是说明,也有可能是提示,或者我感觉比较有意思的点,让大家学习,所以,最好你还是可以跟着我的注释,自己走一遍。
让我们来回答一下上面的问题
-
问题1:滑动还是点击,需要用户在点击的时候,做一个延时操作,而且longClick如果执行了,我让mHasPerformedLongPress=true,表明经历longClick事件,那么在up事件onClick就不会执行了,在move的时候同样会有模棱两可的时候,当前滑动和长按会冲突,这个时候会有mTouchSlop这边变量,最小滑动距离,来判断当前更加接近哪一个行为。在View中冲突主要是滑动和长按,在down和move都有了很有意思的处理。
-
问题2:click和longClick都是通过post实现的,内部会有一个messageQueue,通过handler来实现。如果我让你自己实现一个View,你自己来实现onClick事件你会怎么处理呢?很多人说,这个View收到了down和up时间就可以了啊,是的,很简单,但是和longClick冲突你怎么解决呢?longClick你又会怎么实现呢?要知道,没有move会发生longClick,有move也会发生longClick的是不?有了longClick,但是我要取消咋办?你没有放在消息队列,如果父View发送了cancel,你怎么取消呢?放在queue里面处理起来有什么优势呢?会不会更好一些,大家可以自己去看看。
-
问题3:cancel,简单理解,就是一次clear的重置操作,会移除tap和longclick的runnable,后面我给大家分析一个库会用到。
好了,到这里,事件分发我给大家基本分析完了,然后我会给大家总结一下。
事件分发总结
-
事件分发的顺序是,activity---window---view
-
ViewGroup接受到事件后,会调用ViewGroup的dispatchTouchEvent分发,默认是不拦截事件的。内部,首先子View有没有设置不让父ViewGroup不拦截的标志位,设置了,intercepted=false,否则,查看自己的onIntercepted方法,是不是需要拦截,需要就是true,不需要就是false,如果intercepted=true,就自己处理,处理不了给上一级。同时也可以发现,onIntercept方法不是每一次都会调用的。
-
如果父View不拦截,事件分发下去了,那么就会遍历自己的子View,查看谁可以消费事件,如果可以消费,就给它消费,消费不了,就会给父ViewGroup。
-
dispatchTouchEvent是每一次都会调用的,onIntercept是不会每次调用的,onTouchEvent是最终消费实际的行为,如果要控制事件分发流程,核心是前两个方法入手。
-
每个方法的调用时机是什么?dispatchTouchEvent是每次时间分发都会用到的,我们可以在这个方法收敛,控制所有的事件分发,是事件分发的口子,在这个方法基本上你可以做任何你想做的,包括onIntercept和onTouchEvent。onIntercept方法,主要用来复写拦截逻辑,判断是不是需要拦截。onTouchEvent是具体消费事件的行为,常常做一些手势操作等。建议:尽量不要在dispatchTouchEvent做拦截和手势,把后面两个方法的职责放在一个地方(不是不可以做哈),都杂糅在一起,有点恶心。
最后最后,大家可以巩固一下,或者自己打印一下日志信息,加强学习。