@universal
2017-11-06T14:03:33.000000Z
字数 7935
阅读 286
android
源码分析
view的事件分发主要是指点击事件,也就是系统对MotionEvent的传递
首先,拓展了解下Android界面的架构图
Activity -->PhoneWindow -->DectorView -->TitleView/ContentView
key point:requestWindowFeature(Window.FEATURE_NO_TITLE)一定要在setContentView()方法之前调用才会生效
(至于原因,去看源码...)
这里所谓的点击事件所对应的对象就是MotionEvent, 典型有三种类型:
正常情况下,一次手指触摸屏幕的行为会产生一系列点击事件。事件序列即:
DOWN -> UP 或者 DOWN -> MOVE-> ... ->MOVE -> UP ;
一个事件序列由DOWN开始,中间含有n个MOVE事件,最终以UP结束。
传递规则
注:在处理事件时,如果一个View设置了OnTouchListener,那么listener中的OnTouch方法的返回值将影响事件的下一步传递,如果为true,则OnTouchEvent不会被调用,如果为false,则调用当前View的OnTouchEvent。在OnTouchEvent中,如果设置了OnClickListener,则OnClick方法会被调用。
从故事开始的地方说起,
当一个MotionEvent产生,事件最先传给activity,由activity进行分发,
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
activity会将事件交给window的dispatchTouchEvent去处理,如果下级View们都不处理,则调用自身的onTouchEvent处理
注:可以在activity中重写此方法,以便在事件发送到窗口之前拦截所有触摸屏事件
事件传给Window
//Window
public abstract boolean superDispatchTouchEvent(MotionEvent event);
//PhoneWindow(实现类)
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
抽象类window会将事件交由它的实现类 PhoneWindow去处理
PhoneWindow直接将事件传递给了DecorView
事件再到DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在上面曾提到,DecorView是继承自FrameLayout,所以直接调用父类的dispatchTouchEvent去处理事件,就这样将事件从activity传到了根View
事件进入正轨,从根ViewGroup开始
(一)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
...
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//mFirstTouchTarget:First touch target in the linked list of touch targets.
// 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);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
先看其中的两个操作check for interception 、 handle an initial down;
(1)check for interception:
从字面意思来看:判断是否拦截事件。在当前事件为ACTION_DOWN时或者mFirstTouchTarget != null时,ViewGroup会判断是否拦截事件。这里的mFirstTouchTarget != null是指有子view要处理此事件。所以一旦ViewGroup决定拦截事件,那么mFirstTouchTarget就为null,而且当事件序列中后续的move和up事件到来时,判断条件不满足,导致ViewGroup中的OnInterceptTouchEvent不会被重复调用。
满足条件后,ViewGroup会判断标记位FLAG_DISALLOW_INTERCEPT的值(一般在子View中设置),如果禁止拦截,Viewgroup将无法拦截除了ACTION_DOWN之外的事件(因为在处理ACTION_DOWN事件的时候,Viewgroup会在下面的"initial down"中resetTouchState重置标志位);反之则调用onInterceptTouchEvent去判断是否拦截事件。
(2)handle an initial down:
在ACTION_DOWN事件到来的时候,清除上一个触摸手势所保存的状态,包括FLAG_DISALLOW_INTERCEPT标志位的重置。所以在面对ACTION_DOWN事件时,ViewGroup总会调用OnInterceptTouchEvent判断是否拦截事件。
(二)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
....
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
....
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
....
if (!canViewReceivePointerEvents(child)||
!isTransformedTouchPointInView(x, y, child, null))
{
ev.setTargetAccessibilityFocus(false);
continue;
}
....
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
....
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
// 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;
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
.....
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
.....
}
如果ViewGroup不拦截事件,则交由分发给子项。具体实现:
先遍历ViewGroup所有子元素,判断子元素是否能收到点击事件,如果能,则事件会传递给它来处理。可以看到dispatchTransformedTouchEvent其实就是调用子view的dispatchTouchEvent。如果子View选择处理事件,则dispatchTouchEvent就会返回true,同时mFirstTouchTarget就会被赋值并结束循环。如果当前子元素不处理,则进入下一个子元素的循环。
如果ViewGroup没有子元素,或者子元素的dispatchTouchEvent返回了false,导致mFirstTouchTarget未被赋值,这时ViewGroup就会自己处理事件。
事件交给了View
(1)View中的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
....
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
....
return result;
}
从代码中可以看出,View会先判断有没有设置OnTouchListener,如果有且onTouch方法返回true,则不再调用下面的onTouchEvent,这样就方便在外部处理MotionEvent。
(2)View中的onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
}
这个地方就应证了上面所说的,view的enable属性不影响view消耗事件,只和CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE属性有关。
public boolean onTouchEvent(MotionEvent event) {
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
....
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
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();
}
}
}
break;
case MotionEvent.ACTION_DOWN:
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
....
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
....
break;
case MotionEvent.ACTION_MOVE:
....
break;
}
return true;
}
}
private final class CheckForLongPress implements Runnable {
@Override
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
}
可以看到,当ACTION_UP事件发生时,如果不是longPress会调用performClick,在这个方法里,如果view设置了OnClickListener,则会调用它的OnClick方法,而且performClick是通过Runnable发布调用的,方便在处理事件之前更新View状态。
当ACTION_DOWN事件发生时,View会检查是否是LongClick事件,如果长按事件设置了回调,则执行回调方法。而且如果回调返回true的话,mHasPerformedLongPress就会设置为true,所以在长按时,当上面UP事件发生,performClick就无法调用,即短按事件就会被屏蔽。
View事件分发的流程大概就是这样,后面还会继续研究滑动冲突的问题。最后,说说源码分析的那点事儿,过程很蓝瘦,但是得学会取舍,一时看不懂先略过,注重大体逻辑,跟着翻译走,没准在后面就能明白前面变量或者方法的意义,水到渠成。