开启成长之旅!这是我参加「日新计划 12 月更文挑战」的第24天,点击查看活动详情
1.点击事情的传递规则
dispatchTouchEvent
-
- 用来进行事情的分发,假如事情能够传递给当时View那么这个办法必定会被调用,回来成果受当时View的
onTouchEvent
和下级View的onInterceptTouchEvent
影响,标明是否耗费当时事情。
- 用来进行事情的分发,假如事情能够传递给当时View那么这个办法必定会被调用,回来成果受当时View的
onInterceptTouchEvent
-
- 用来判断是否阻拦事情,假如当时View阻拦了某个事情那么在同一个事情序列中不会再调用这个办法,回来成果标明是否阻拦当时事情。
onTouchEvent
-
- 用来处理事情,假如不耗费那么在同一个事情序列中将不会再接收到事情,回来成果标明是否耗费了当时事情
- View的
OnTouchListener
、onTouchEvent
、onClickListener
三个办法中优先级联系
-
-
onTouchListener
>onTouchEvent
>onClickListener
-
- View被点击后事情的传递进程
-
- View被点击后传递的顺序为
Activity
→Window
→View
,也便是说事情总是先传递给Activity
,然后Activity
再传递给Window
,然后Window
再传递给尖端View
,尖端View
接收到事情后就会按照事情分发机制去分发事情。
- View被点击后传递的顺序为
- 事情分发机制的一些结论
-
-
View
没有onInterceptTouchEvent
办法,一旦有点击事情传递给它,那么它的onTouchEvent
必定会被调用 -
View
的onTouchEvent
默许都会耗费事情的,除非它是不行点击的例如clickable
和longClickable
一起为false,View
中的longClickable
默许为false,clickable
要分情况,例如Button
的clickable
为true,TextView
的clickable
为false -
View
的ebable
属性不影响onTouchEvent
的默许回来值. - 事情的传递是由外向内的,即事情的传递总是先传递给父元素,再由父元素传递给子view,能够用
requestDisallowInterceptTouchEvent
办法干预父元素的事情分发进程。
-
2.事情分发的源码解析
- Activity对点击事情的分发进程
当一个点击操作产生时,事情最早传递给当时的Activity,由Activity的dispatchTouchEvent
进行事情的分发
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
依据dispatchTouchEvent
源码可知事情开端先交给Activity所隶属的Window进行事情分发,假如回来true事情循环完毕,回来false意味着无人处理,一切View的onTouchEvent
都回来了false,那么Activity的onTouchEvent
就会被调用
- Window对事情的分发进程
Window
类能够控制尖端View
的外观和行为策略,它的仅有实现是android.policy.PhoneWindow
。那么从PhoneWindow
源码中能够得知它把事情传递给了DecorView
- 尖端View对点击事情的分发进程
尖端View
一般是ViewGroup
,抵达ViewGroup
之后首先会调用dispatchTouchEvent
,假如回来true那么事情将不再进行分发而是ViewGroup
自己处理,这是假如ViewGroup
中设置了onTouchListener
那么onTouch
就会被调用否则就会调用onTouchEvent
,当设置了onTouchListener
时onTouchEvent
就会被屏蔽,也便是说调用了onTouch
就不会调用onTouchEvent
,在onTouchEvent
中假如设置了onClickListener
那么onClick
会被调用。ViewGroup
假如回来false那么事情将向下分发,传递给它的子View
,然后子View
调用dispatchTouchEvent
对事情进行分发,到这儿尖端View到子View的事情分发就完结了,子View再向下分发的进程跟之前一样,如此循环就完结了整个事情的分发。
-
ViewGroup
对点击事情的分发进程
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;
}
代码actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
的意思是判断是否要阻拦事情,其间actionMasked == MotionEvent.ACTION_DOWN
很好理解,而mFirstTouchTarget != null
的意思从后边的代码就能够知道,当ViewGroup
不阻拦事情而是交给子元素处理时mFirstTouchTarget
就会被赋值,这个条件就会建立,假如ViewGroup
阻拦了事情mFirstTouchTarget
将不会被赋值,这样上面的条件就不会建立,那么假如event
是ACTION_UP
和ACTION_MOVE
时if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
这个条件便是false,这就标明事情将由ViewGroup
自己处理,后续的onInterceptTouchEvent
将不会再被调用而且同一序列中的其他事情都会默许交给它来处理。
存在一个特殊情况,在子View
中假如通过requestDisallowInterceptTouchEvent
装备了FLAG_DISALLOW_INTERCEPT
的符号位那么ViewGroup
将无法阻拦除ACTION_DOWN
以外的事情,因为在ViewGroup
中遇到ACTION_DOWN
这个事情时会重置这个符号位,这就导致了子View
中设置的这个符号位无效。
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
上面的代码中能够得知当事情是ACTION_DOWN
时在cancelAndClearTouchTarget
办法中ViewGroup
会做重置状态的操作,在resetTouctState
中会重置FLAG_DISALLOW_INTERCEPT
,所以就验证了上面的结论requestDisallowInterceptTouchEvent
的装备不会影响ViewGroup
对ACTION_DOWN
的处理。
//这儿主要是针对ViewGroup不阻拦事情的代码,或许说是ViewGroup向子View分发事情的源码
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
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);
//这儿调用的是子元素的dispatchTouchEvent
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();
//子元素的dispatchTouchEvent回来true那么暂时就不用考虑子元素的内部是怎样分发的
//此刻mFirstTouchTarget就会被从头赋值并跳出循环
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
上面的这块的代码主要是ViewGroup
不阻拦事情的时分,这儿首先遍历了ViewGroup
中的子元素,然后判断子元素是都能够接受到点击事情,判断子元素能否接受到点击事情的要素主要有两个:1.子元素是否正在播放动画;2.当点击子元素的时分点击的坐标是否正好在子元素的区域内,这两个条件满足后事情就会向下传递。接下来便是调用dispatchTransfmoredTouchEvent
办法来传递事情,而且从这个办法的源码中能够得知它在向子元素分发事情的时分调用的便是子元素的dispatchTouchEvent
办法,这样事情就交给了子元素处理,然后完结了一轮事情分发。详细的能够看它的源码,源码中有这么一段
//dispatchTransformedTouchEvent部分源码
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
//这个child在父元素的dispatchTouchEvent办法中不阻拦那一段代码会有赋值,因而它不为null
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
假如子元素的dispatchTouchEvent
回来true那么就暂时不用考虑子元素是如何进行事情分发的,此刻mFirstTouchTarget
就会被从头赋值并跳出ViewGroup
遍历子元素的循环。假如子元素的dispatchTouchEvent
回来false时ViewGroup
就会持续向下分发事情,条件是下面还有子元素。
从下面的源码中能够了解到mFirstTouchTarget
的赋值是在addTouchTarget
内部完结的,mFirstTouchTarget
的赋值将直接影响ViewGroup
对事情的阻拦策略,假如为null那么ViewGroup
就默许阻拦下同一事情序列中的一切点击事情。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
假如遍历一切的子元素后事情都没有被处理那么ViewGroup
会怎样样呢?
首先剖析遍历完一切元素都没有被处理的情况有哪些:
-
-
ViewGroup
没有子元素,这个交由他自己处理 - 子元素处理了事情可是
dispatchTouchEvent
回来了false,这一块是因为子元素在onTouchEvent
中回来了false,这种情况下ViewGroup
也会自己处理事情。
-
- View对点击事情的处理进程
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
上面这段代码中dispatchTransformedTouchEvent
的第三个参数的child
,可是现在为null,从这个办法中能够得知当child==null
时会调用super.dispatchTouchEvent
,这儿就转到了View
的dispatchTouchEvent
办法,也便是说点击事情开端交由View
来处理了。
先来看一下View#distatchTouchEvent
源码
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();
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;
//这儿先判断是否装备了OnTouchListener,再判断onTouch是否回来true
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);
}
// 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
是不包括ViewGroup
的因而它是一个独自的元素,也就不存在向下分发事情的情况,所以事情就只能由自己来处理了,在处理的进程中首先判断是否装备了onTouchListener
然后再判断onTouch
是否为true,假如为true那么事情就不会由onTouchEvent
处理而是交给onTouch,这样做的好处是便利在外界处理点击事情
下面再来剖析onTouchEvent
的实现,先看一下View
处于不行用的状态时点击事情的处理进程
View#onTouchEvent
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
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;
}
上面的代码中的注释写的很清楚,不行用状态下的View
依旧会耗费点击事情只不过是没有任何响应的。
再来剖析一下onTouchEvent
对点击事情的处理进程是怎样样的
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
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);
}
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();
}
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;
...
}
return true;
}
先来剖析下在什么情况下会进入switch
语句开端耗费点击事情,当clickable == true
或许TOOLTIP == true
时点击事情会有响应,clickable == true
的可能有三点:CLICKABLE = true
或许LONG_CLICKABLE = true
或许CONTEXT_CLICKABLE = true
,这个CONTEXT_CLICKABLE
能够理解为一个能够点击的视图,TOOLTIP
又是什么呢,从它的注释能够理解为是一个长按或许悬停的操作,这个能够理解为指针在浏览器的某个按钮上放着然后会弹出一个提示。剖析完在什么情况下会开端耗费点击事情后再来剖析下详细是怎样实现点击的逻辑的。
在ACTION_UP
中最终会通过performClickInternal()
办法调用performClick()
,然后在performClick
办法中最终会调用View
的onClick
办法,这儿还要了解的一点是LONG_CLICKABLE
默许为false,CLICKABLE
是否为true则要依据详细的View
来决议,例如Button
默许为true,TextView
默许为false,当调用了setOnClickListener
时CLICKABLE
就会主动置为true,当调用了setOnLongClickListener
会主动把LONG_CLICKABLE
置为true。
//View#performInternal
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
//View#performClick
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}