View 事件简介
View 事件,既 MotionEvent,是用户触摸屏幕的一系列事件。同一事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以 ACTION_DOWN 开始,中间含有一系列的 ACTION_MOVE,最终以 ACTION_UP 结束。
View 事件分发简述
当用户点击屏幕的时候,TouchEvent 最先传递给Activity.dispatchTouchEvent(MotionEvent)
,然后再调用DecorView.superDispatchTouchEvent(MotionEvent)
,接着直接调用super.dispatchTouchEvent(MotionEvent)
,即ViewGroup.dispatchTouchEvent(MotionEvent)
。
ViewGroup 和 View 分发事件,有三个很重要的方法:
-
public boolean dispatchTouchEvent(MotionEvent ev)
:用来进行事件的分发,如果事件能够传递给当前 View,那么该方法一定会调用,返回值受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 的影响,表示是否消耗当前事件; -
public boolean onInterceptTouchEvent(MotionEvent ev)
:表示在上述方法内部调用,判断当前 View 是否拦截某个事件。如果当前 View 拦截了某个事件,那么同一个事件序列当中,此方法不会被再次调用;反之,如果是下级 View 拦截了事件,并且下级 View 没有调用public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
,那么当前 View 的onInterceptTouchEvent(MotionEvent ev)
会被继续调用,即当前 View 依旧拥有拦截后续事件的能力; -
public boolean onTouchEvent(MotionEvent event)
:在 dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
上述三个方法的关系大致可以用下面的伪代码来表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume=false;
if(onInterceptTouchEvent(ev)){
consume=onTouchEvent(ev);
}else{
cousume=child.dispatchTouchEvent(ev);
}
returen consume;
}
dispatchTouchEvent搭建了事件分发的框架,一般不需要重写,自定义 View 时通常重写的是onInterceptTouchEvent和onTouchEvent。事件的分发有点类似有序树的查找算法,ViewGroup 就是结点,View 就是叶子。遍历到 View 的时候,就要返回了。
源码分析
ViewGroup
dispatchTouchEvent
// Handle an initial down.
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 事件的时候,会重置一下状态(比如说重置FLAG_DISALLOW_INTERCEPT状态位,允许拦截事件),清除之前的 TouchTarget。
TouchTarget :Describes a touched view and the ids of the pointers that it has captured.
// 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;
}
此段代码是判断当前 ViewGroup 是否要拦截事件,首先介绍几个变量的含义:
- mFirstTouchTarget:touch target list 的第一项,表示上一个处理事件的 child;
- disallowIntercept:是否允许当前 ViewGroup 拦截事件,默认是允许,其 child 可以设置为不允许;
由上述代码可知,如果 MotionEvent 不是 ACTION_DOWN 且 child 没有处理上一个事件,则 ViewGroup 会拦截下事件;否则会调用 onInterceptTouchEvent 来判断是否需要拦截事件(除非 child 不允许 view parent 拦截事件)。
if (!canceled && !intercepted) {
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
.....
//当 ViewGroup 不拦截事件,且事件为 DOWN 类型,那么就要遍历其 child,寻找处理事件的 child。
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
......
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//能够接受事件的child:事件坐标在 View 内;View 可见或者没有在执行动画
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;
}
//
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
......
//用 child 获得新的 TouchTarget,并将其添加到 Touch Target list 的第一个;
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
当 ViewGroup 不拦截事件时,且事件类型为 DOWN 时,ViewGroup 会遍历其 child,寻找能接收事件的 child,然后调用dispatchTransformedTouchEvent
将事件传递给 child 进行处理。具体逻辑处理可以看代码中的注释。
// 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 {
// 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 (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
......
}
}
}
如果没有 child 要处理事件,那么就ViewGroup 自身尝试处理事件;如果有,那么遍历 Touch Target list ,每一个child 都尝试处理事件。
下面让我们看一下dispatchTransformedTouchEvent
的代码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
......
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
//根据 pointerId、child 的偏移量对 MotionEvent 进行转换
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
//如果 child 为null,那么调用 super.dispatchTouchEvent,即 View 的 dispatchTouchEvent 方法,
//看能否消耗该事件,否则,事件传递给 child 进行处理
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
......
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
逻辑不复杂,主要就是对 MotionEvent 进行根据 pointerId 、child 的偏移量进行转换,然后如果 child 不为null,那么调用 child 的 dispatchTouchEvent,将事件传递给 child 进行处理,如果 child 为null,那么调用 super.dispatchTouchEvent,即viewgroup尝试处理该事件。
上面就是 ViewGroup 分发事件基本框架代码,大致可以总结如下:
- 如果事件是 DOWN 类型,先判断当前 ViewGroup 是否拦截该事件(通常 ViewGroup 是不会拦截 DOWN 事件,否则child 完全接受不到事件了);如果不拦截事件,那么遍历其 child,寻找处理该事件的 child。如果没有 child 处理事件,那么 ViewGroup 自行尝试处理该事件,否则 child 处理该事件,并将 mFirstTouchTarget 设置为该 child;
- 事件不是 DOWN,且之前的事件没有 child 进行处理(mFirstTouchTarget 为 null),那么ViewGroup 尝试处理该事件;如果之前的事件有 child 进行处理了,那么先判断 ViewGroup是否需要拦截该事件,不需要,则直接交由之前处理事件的 child 直接处理。
View
dispatchTouchEvent
不同于 ViewGroup,View 不能再向下分发事件,要么自身处理事件,要么不处理返回给 parent处理,相当于树中的叶子,所以 View 没有 onInterceptTouchEvent 方法,它的 dispatchTouchEvent 也是用来判断并处理事件的。下面看看其源码:
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
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;
}
}
......
return result;
}
由上可知,首先会判断 View 是否设置了 OnTouchListener,如果是,则将事件交给它处理,否则才会调用 VIew 的 onTouchEvent 方法。可见 OnTouchListener 的优先级高于 View 的 onTouchEvent,这是方便我们在外部设置处理事件的方法。
接下来看看 View 的 onTouchEvent 方法:
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);
}
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) {
// 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;
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);
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;
由上可知,View 的 onTouchEvent 的处理逻辑大致如下:
- 只要 View 时 clickable 的,总是返回 true,即总是消耗事件;
- 如果 View 时 disabled 的,那么直接返回 TRUE,照样消耗事件,但是不执行相应的动作;
- 如果 View 设置的 Delegate,那么直接把事件交给 Delegate处理(利用这点可以将事件交给其他 View 来处理);
- 如果 View 自己处理事件,那么根据事件的类型处理方法如下:
- DOWN:设置状态为 pressed,同时设置一个延时任务,用以判断 longClick;
- MOVE:判断事件是否超过了 View 的边界,如果是,重置状态,取消 longClick 的延时任务等;
- UP:如果状态为 pressed,那么执行 performClick,并且重置状态;
- CANCEL:重置状态
实战分析
对于自定义 View,如何正确地分发、处理事件非常重要,下面就大致说说 ViewGroup 和 View 分别是如何重写相关方法,以实现需求。
View
对于 View,因为不需要分发事件,所以 View 一般只需要重写 onTouchEvent,然后根据事件类型分情况处理:
- Donw:一定要返回 TRUE,否则同一序列的后续事件都不会交给这个 View 处理了;
- Move:通常是我们处理的关键,根据它来进行相应的逻辑处理,比如说移动 View,绘画之类的;
- Up:重置状态,资源回收等处理;
ViewGroup
对于 ViewGroup,首先是要重写 onInterceptTouchEvent:
- Down:返回 FALSE,这样 child 才有可能接收到事件并进行处理;
- Move:根据需要进行判断返回 TRUE or FALSE,返回 TRUE,说明 ViewGroup 要拦截事件,交由其 onTouchEvent 进行处理,FALSE 则继续给 child 进行处理;
- Up:返回 FALSE,这样child 才有可能接收到 Up 事件,进行相应的处理;
onTouchEvent:
- 返回TRUE,表示 ViewGroup 消耗了事件,后续事件都会交由它处理;
- 返回 FALSE,表示 ViewGroup 不再消耗事件,但是,如果ViewGroup dispatchTouchEvent(DownEvent)的时候返回了 TRUE,即消耗了 DOWN 事件,那么就算他不消耗后续事件,后续事件依然会传递给它,而不会交给 parent 来处理,最终这些未处理的事件会在 Activity 中处理;如果没有消耗过 DOWN 事件,那么事件会直接交给 parent 来处理(因为 mFirstTouchTarget 为 null);