前两篇博客从源码的角度对View绘制流程进行了分析,那么当用户需要跟View进行交互的时候,比如点击按钮的时候,按钮是如何得到点击事件的呢?当用户在屏幕上进行点击或触摸的时候,事件是如何传递到各个View的呢?这个就是本篇博客研究的点:View事件分发机制。只有同时掌握View事件分发机和View绘制流程,并辅以一定的练习,才能真正掌握自定义View。下面开始进入正题!注:源码基于API25。
还记得之前说过,在Activity的attach方法里面会新建一个PhoneWindow作为顶层Window,如下所示:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
..........
..........
}
在上面的代码当中有一句:mWindow.setCallback(this),这句话给Window设置了一个Callback回调接口给Activity,来看一下这个回调接口:
/**
* API from a Window back to its caller. This allows the client to
* intercept key dispatching, panels and menus, etc.
*/
public interface Callback {
public boolean dispatchKeyEvent(KeyEvent event);
public boolean dispatchKeyShortcutEvent(KeyEvent event);
/**
* Called to process touch screen events. At the very least your
* implementation must call
* {@link android.view.Window#superDispatchTouchEvent} to do the
* standard touch screen processing.
*
* @param event The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent event);
.............
可以发现这个回调接口里面有很多回调方法,前三个方法都是对事件进行分发,第三个方法dispatchTouchEvent就是触摸事件分发。当android系统发生触摸事件时,会把触摸事件发送给顶层Window(至于是怎么传递给Window的,这里暂时不深入研究,涉及到WindowManager,WindowManagerService等跨进程的调用过程,也涉及到Activity的启动过程分析),这里是PhoneWindow,由于给PhoneWindow设置了回调接口,在Activity当中实现了这个接口,因此,我们查看Activity当中的dispatchTouchEvent方法,如下所示:
/**
* 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);
}
可以看出,如果Action为ACTION_DOWN,首先会调用onUserInteraction方法,这个方法如下所示:
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
*
Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
关于这个方法,注释已经说的很明白了,一般和onUserLeaveHint方法配对使用,主要是用来帮助Activity管理状态栏通知。
调用完onUserInteraction方法之后,就会调用getWindow.superDispatchTouchEvent方法,也就是调用PhoneWindow的superDispatchTouchEvent方法。
如果这个方法返回true,就直接返回true,否则会调用Activity的onTouchEvent方法。
我们可以在Activity重写dispatchTouchEvent方法来对所有的触摸事件进行拦截,防止其分发至window。
当Activity的onTouchEvent被调用的时候,说明Window的superDispatchTouchEvent方法返回false,也就是没有消耗事件,事件最终交给Activity进行处理,因此我们也可以在Activity当中重写onTouchEvent方法来进行事件处理。
来看下PhoneWindow的superDispatchTouchEvent方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可以看出其调用的是顶层视图DecorView的superDispatchTouchEvent方法,如下所示:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
可以看出,DecorView调用了其父类的dispatchTouchEvent方法,跳进去看发现调用的是ViewGroup的dispatchTouchEvent方法。
这个也可以理解,因为DecorView是继承自FrameLayout,而FrameLayout是继承自ViewGroup。
通过以上分析可以看出,View触摸事件的入口是DecorView, 也就是ViewGroup。整个事触摸件的传递过程如下:
-> 顶层PhoneWindow得到触摸事件,调用其dispatchTouchEvent方法
-> Activity当中收到dispatchTouchEvent回调方法,调用mWindow的superDispatchTouchEvent方法
-> 调用PhoneWindow的superDispatchTouchEvent方法
-> 调用DecorView的superDispatchTouchEvent方法
-> 最终调用ViewGroup的dispatchTouchEvent方法
-> View触摸事件分发入口
既然View触摸事件的入口是ViewGroup的dispatchTouchEvent方法,说明这个方法至关重要,接下来看下这个dispatchTouchEvent方法,这个方法比较长,我会在代码中加入注释:
public boolean dispatchTouchEvent(MotionEvent ev) {
................
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//ACTION_DOWN的话就恢复初始状态,清除TouchTarget
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();
}
// 检测事件拦截
final boolean intercepted;//这个标志用于是否拦截事件,如果拦截的话,就交给自身的这个ViewGroup进行处理
//如果是ACTION_DOWN或者mFirstTouchTarget不为空(说明已经有了TouchTarget),就开始判断是否拦截事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//是否不允许拦截,在ViewGroup当中有一个requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法
//这个函数可以用来设置是否拦截,一般用在子View当中,通过调用父View的这个方法来阻止父View拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//允许拦截的话,就调用onInterceptTouchEvent方法,一般我们需要重写这个方法,来根据需求来进行事件拦截
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
//不允许拦截
intercepted = false;
}
} else {
//如果没有TouchTarget且当前action不是初始的ACTION_DOWN,就拦截
//如果当前ViewGroup拦截了ACTION_DOWN,那么剩下的ACTION_UP,ACTION_MOVE事件都是交给它处理,且onInterceptTouchEvent方法
// 不会再次调用,因为此时mFirstTouchTarget==null且action!=ACTION_DOWN
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 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 accessiiblity 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;
//发现没有,这里只是对ACTION_DOWN进行处理,那么ACTION_UP和ACTION_MOVE呢?
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;
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.
//开始遍历子View,找到能够接收事件的子View
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;
}
//判断子view是否能接收pointer事件或者当前的触摸点在子view的边界内,
//如果这两个没有一个满足,就continue,跳到循环的下一步,也就是下一个子view
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
// 如果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);
// 这个函数内部调用了child.dispatchTouchEvent方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 有子View接收这个事件
// 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();
//把child的TouchTarget加入到链表的开头且返回child的TouchTarget
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();
}
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;
}
}
}
//这里有三种情况:一种是当前ViewGroup拦截了事件,一种是没有子View,还有一种是子View的dispatchTouchEvent方法返回了false
//这三种情况下就交给当前ViewGroup进行处理
// 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);
} else {
// 我们发现前面只是对ACTION_DOWN进行了分发,当某个子当某个子View返回true时,会中止Down事件的分发,
// 同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理,如下所示。
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (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;
}
以上对ViewGroup的dispatchTouchEvent方法进行了分析,总结如下:
1、每次当触摸事件为ACTION_DOWN的时候就会清除之前的状态,开始一次新的事件分发
2、如果当前ViewGroup拦截了ACTION_DOWN,那么剩下的ACTION_UP,ACTION_MOVE事件都是交给它处理
3、在自定义继承自ViewGroup的View的时候,通过重写onInterceptTouchEvent对事件进行拦截,事件拦截仅仅是针对于ViewGroup,对于View来说不存在事件拦截的说法
4、ViewGroup当中有一个requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,可以用来设置是否拦截,一般用在子View当中,通过调用父View的这个方法来阻止父View进行事件拦截
5、有三种情况,触摸事件会交给当前的ViewGroup进行处理,此时就把ViewGroup当成普通的View,走的是View事件分发逻辑,调用的是View的dispatchTouchEvent方法:
6、事件分发只是针对ACTION_DOWN进行了分发,当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理!这个就有点类似于:一旦一个活交给你干了,你就得干到底的意思!
以上讲的是ViewGroup的事件分发机制,对于View来说,触摸事件都是由父ViewGroup分发而来,调用的是View的dispatchTouchEvent方法,如下所示:
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;
// 如果满足:mOnTouchListener!=null、View是ENABLED的、mOnTouchListener的onTouch方法返回true,那么result为true
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果上面的result为true,那么就不会调用下面的onTouchEvent方法
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;
}
可以看出:
来看下onTouchEvent方法,如下所示:
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不使能
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//ACTION_UP事件,设置按下状态为false
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
//一个不使能的view,如果是可点击的依然会消耗这个事件,比如button,即使是不使能的,也会返回true消耗事件,只是不对事件作出响应而已。
//因为button默认是可点击的,除非手动设置为不可点击
// 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);
}
// 如果设置了触摸代理,调用触摸代理的onTouchEvent方法,如果返回true,就消耗事件。
// 这个触摸代理常常用在扩大View的点击区域,比如一个图标太小,就可以扩大其点击区域
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果是可点击的
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 如果View还没有获得焦点的话就主动获得焦点
// 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);
}
// 如果长按的动作没有发生且没有忽略下一个ACTION_UP事件
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.
//这个PerformClick是一个Runnable对象
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//把mPerformClick给post到消息队列
if (!post(mPerformClick)) {
// 如果上述Runnable执行失败,就直接调用performClick方法,在这个方法里面调用
// OnClickListener回调接口
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;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
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.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
// 设置为按下状态
setPressed(true, x, y);
// 检测长按,如果长按下成功,会把mHasPerformedLongPress置为true,这样点击事件就得不到响应
// 也就是说长按事件会屏蔽点击事件
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
//复位
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
从以上代码可以看出:
以上从源码的角度对触摸事件的来源、ViewGroup事件分发机制、View事件分发机制进行了解读。
在自定义View的时候,当需要对触摸事件进行处理的时候,一般是重写onTouchEvent方法,拥有子类的View一般还需要重写onInterceptTouchEvent方法进行事件拦截,要想让这两个方法很好的配合使用,就需要熟悉并且理解触摸事件分发机制,再配合之前的View绘制流程,就能自定义出各式各样的View啦!
感谢大家的阅读!有啥问题,欢迎指出,谢谢!