参考资料:
1.《Android开发艺术探索》
- http://www.cnblogs.com/sunzn/archive/2013/05/10/3064129.html
总是记不住,写的文字,记录下,方便查阅;
点击事件的传递规则##
MotionEvent就是点击事件,当一个MotionEvent产生了以后,系统需要把她传递给一个具体的View;
这个过程就是事件的分发;
有3个非常重要的方法:
dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent;
dispatchTouchEvent
用来进行事件的分发,如果事件能够传递给当前View,此方法一定被调用;返回结果表示是否消耗此事件,返回结果受当前View的onTouchEvent和下级View的onInterceptTouchEvent影响;
onInterceptTouchEvent
返回结果用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用;
onTouchEvent
在dispatchTouchEvent调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件;
三个方法之间的关系
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = fasle;
if(onInterceptTouchEvent(ev)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
点击事件的传递规则:
对于根ViewGroup来说,点击事件产生后,ViewGroup的dispatchTouchEvent将会调用,如果此ViewGroup的onInterceptTouchEvent返回true,表示要拦截当前事件,这样事件交给ViewGroup来处理,即ViewGroup的onTouchEvent调用;如果返回false,则不拦截事件,事件就会继续传递给他的子元素,然后直接上面的步骤;
当一个点击事件发生时,她的传递过程顺序是:Activity->Window->View, 顶级View收到事件后,就会按照事件的传递机制来进行事件分发,如果View的onTouchEvent返回false,那么父容器的onTouchEvent将会被调用,onTouchEvent返回false,事件就会往上一层回传;类似于Java中的异常处理机制,当前类抛出,调用类就必须catch,如果不catch就得继续往上抛,都不处理的话,那么Java的虚拟机来处理了;
事件机制的一些结论
- 同一个事件序列是指从手指接触到屏幕起,直到手指离开屏幕这个过程中的所产生的一系列事件,事件序列以down开始,中间含有move,最终以up结束;
- 正常情况下,一个事件序列只能被一个View拦截且消耗,一旦一个元素拦截了某此事件,那么同一个事件序列内所有事件将会直接交给他;因此同一个事件序列中的事件不能分别由2个View来处理,但可以通过View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理;
- 某个View一旦决定拦截,那么这一个事件序列都只能由她来处理(前提是:事件序列能够给传递给她),并且她的onInterceptTouchEvent不会再被调用(onInterceptTouchEvent 询问当前view是否拦截事件);
- 某个View一旦开始处理事件,如果不销毁ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其他事件她也收不到了;并且其事件重新交由其父来处理(调用父的onTouchEvent);
- 如果View不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件将消失(在down时,已经找到了targetView,后继事件都会传给该View),此时父View的onTouchEvent不会调用,当前View还可以收到其他后续事件,最终这些消失的点击交由Activity处理 (这是不完整的事件序列);
- ViewGroup默认不拦截任何事件;
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给她,她的onTouchEvent就被调用;
- View的onTouchEvent默认不会消耗事件(返回true),除非他是不可点击的(clickable和longclickable同时为false)。如:Button,TextView;
- View的enable属性不影响onTouchEvent的默认返回值,只有他的clickable或者longclickable有一个为true时,ouTouchEvent就返回true;
- onClick会发生的前提是当前View是可点击的,并且收到了 down和up事件;
- 事件的传递总是由外向内的,事件总是给父,然后再由父去分发给子View,通过requestDisallowInterceptTouchEvent方法,可以让子View中干预父元素的事件分发过程,但 DOWN除外;
事件分发解析##
Activity对点击事件的分发过程####
当一个点击事件发生时,事情会先传递到Activity的dispatchTouchEvent方法中,通过此方法进行事件派发;
// activity 中的 dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
事件会转给getWindow(), getWindow返回的是PhoneWindow,实际上调用的DecorView的superDispatchTouchEvent方法,DecorView是继承自FrameLayout的,所以事件就回到了View上,从源码上可以发现,如果View都不处理事件,Activity onTouchEvent将会执行;
ViewGroup对点击事件的处理####
顶级View一般是一个ViewGroup,事件到达后,会调用dispatchTouchEvent方法,Activity事件也是到达这里,这里如事件拦截(onInterceptTouchEvent返回true),则由ViewGroup处理,如果ViewGroup mTouchListener 有设置,Listener onTouch将调用,并屏蔽onTouchEvent,否则 onTouchEvent 将执行;如果顶级ViewGroup不拦截事件,则事件往下传给事件链上的子View,此时,子View的dispatchTouchEvent将调用,继续重复上面的步骤,如此进行下去,完成事件分发;
ViewGroup - dispatchTouchEvent 代码片段:
// 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 为找到接受事件的子View,如不拦截事件,当后续事件如:MOVE、UP来到时,事件则交给了 子 View 来处理;反之,则交给ViewGroup进行处理;
当ViewGroup不拦截事件时,则事件向下分发给它的子View:
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex); // x,y 坐标
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList 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);
// 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;
}
// 是否能接收事件(可见,并没有在播放动画),x、y 是否在点击区域
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
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();
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对点击事件的处理####
View为一个具体的控件,没有子元素,所以事件无法向下传递,只能自己处理,首先判断,有没有 设置过 OnTouchListener,如则执行 Listener中的onTouch,并根据返回值,来判断是否要执行onTouchEvent;
View 的 dispatchTouchEvent关键代码:
public boolean dispatchTouchEvent(MotionEvent event) {
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;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 执行onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
View onTouchEvent 部分代码
只要view的CLICKABLE和LONG_CLICKABLE有一个为true,则该View为消耗事件,onTouchEvent将返回true,click无视View 的disable
状态;UP 时,将触发performClick方法,
public boolean onTouchEvent(MotionEvent event) {
switch (action) {
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, 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)) {
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();
}
mIgnoreNextUpEvent = false;
break;
}
....
return true;
}
return false;
}
总结##
- 事件从PhoneWindow开始,经由DecorView,实际上是ViewGroup开始进行 dispatchEvent;
- ViewGroup中的 onInterceptTouchEvent 进行事件的拦截判断;
- ViewGroup重写了View的dispatchTouchEvent,onTouchEvent没重写,这个要特别注意;