先说个小事情
onXXXXXX()
方法都是对当前View的某个操作进行实际的处理。比如,onDraw()
是对View
的实际绘制,onMeasure()
是对View
进行实际的测量,onLayout()
是进行实际的布局,onTouchEvent()
是对点击事件进行处理,onInterceptTouchEvent()
是对是否拦截事件进行处理。
再说一个小事情
点击事件正常情况下就4个类型,一般处理这4个类型就可以了
-
MotionEvent.ACTION_DOWN
按下 -
MotionEvent.ACTION_UP
抬起 -
MotionEvent.ACTION_MOVE
移动 -
MotionEvent.ACTION_CANCEL
非人为取消,在事件分发过程中产生
现在开始说正事,在我看来点击事件的分发
和处理
其实是两块不同的内容。
点击事件分发主要涉及的函数:
Activity.dispatchTouchEvent()
ViewGroup.dispatchTouchEvent()
View.dispatchTouchEvent()
点击事件分发机制的函数基本上是不会被重写的,因为这个是它内部已经规定好的机制。
点击事件处理主要涉及的函数:
View.onTouchEvent()
点击事件处理机制的函数就经常需要被重写。很多自定义ViewGroup或者自定义View的时候会去重写onTouchEvent()方法。
点击事件分发
分发的顺序是Activity
->ViewGroup
->View
,其中ViewGroup
中的分发逻辑最为复杂。
Activity事件分发机制
Activity的分发机制相对比较简单
//Activity.class
//
public boolean dispatchTouchEvent(MotionEvent ev) {
//这里告诉你就是调用了ViewGroup.dispatchTouchEvent()方法,如果想知道为啥下面会稍微的解释一下
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
听我解释一:
-
getWindow()
获取的是Window
对象是一个抽象类,它的实现类是PhoneWindow
。 -
PhoneWindow
实现superDispatchTouchEvent()
方法,调用了mDecor.superDispatchTouchEvent()
方法。 -
mDecor
是DecorView
的一个实例。 -
DecorView
继承自FrameLayout
,而FrameLayout
继承自ViewGroup
。 - 好了,最后就是
getWindow().superDispatchTouchEvent(ev)
调用的就是ViewGroup.dispatchTouchEvent()
方法。
View事件分发机制
//View.class
//
public boolean dispatchTouchEvent(MotionEvent event) {
......
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
中的事件分发其实很简单,他的分发就是分发给自己的那些方法去处理消费掉; - 第一是给
mOnTouchListener.onTouch()
,如果没有消费掉就再调用View.onTouchEvent()
; - 反正
View
的事件分发机制中的核心就是“我要消费掉这个事件”
ViewGroup事件分发机制
先记住几样东西。
第一、变量mFirstTouchTarget
是接收点击事件的目标View链表。
//ViewGroup.class
//
mFirstTouchTarget
第二、方法cancelAndClearTouchTargets()
向取消View上事件,并且清除。
//ViewGroup.class
//
private void cancelAndClearTouchTargets(MotionEvent event) {
//在mFirstTouchTarget
if (mFirstTouchTarget != null) {
......
//就是各种操作,取消事件,并且清除
}
}
第三、addTouchTarget()
向目标View的链表中增加新的目标View。
//ViewGroup.class
//
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
第四、dispatchTransformedTouchEvent()
分发并转化点击事件,如果有child参数就分发给儿子,如果没有child参数为null就调用super.dispatchTouchEvent()
。
//ViewGroup.class
//
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.class
//
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 判断是否被截获
// 主要判断disallowIntercept参数和onInterceptTouchEvent()方法
// 其中有一个true就是截获事件,具体相关内容
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 {
intercepted = true;
}
//不被取消,不被截获的时候,就继续向子View分发,看这里只处理ACTION_DOWN事件
if (!canceled && !intercepted) {
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
......
for (int i = childrenCount - 1; i >= 0; i--) {
......
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
......
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
......
}
if (preorderedList != null) preorderedList.clear();
}
......
}
}
//把事件分发到目标View
if (mFirstTouchTarget == null) {
//这里如果没有一个接收Touch事件的View,就自己尝试消化掉
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//这里就是向目标View分发事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
......
//根据mFirstTouchTarget链表分发事件
}
}
}
return handled;
}
听我解释一:
- 先后判断
disallowIntercept
变量和onInterceptTouchEvent()
方法,如果是true
就表示被截获,如果是false
就表示没被截获; -
disallowIntercept
表示是否禁用拦截功能,默认是false
。可以通过requestDisallowInterceptTouchEvent()
方法设置为true
。 -
requestDisallowInterceptTouchEvent()
方法是需要用户自己调用的,而且如果设置为true
,那么它的父容器都是true
。 -
onInterceptTouchEvent()
方法默认是不截获。
听我解释二:
- 当
canceled
和intercepted
都为false
的时候,就会先向子View
分发ACTION_DWON事件; - 如果子
View
中的dispatchTransformedTouchEvent()
方法返回true
的时候,就会调用addTouchTarget()
方法; -
addTouchTarget ()
方法中就是在mFirstTouchTarget
中增加接收点击事件的View
。
听我解释三:
- 什么叫被截获,你可以理解
mFirstTouchTarget==null
就是被截获了; - 如果被截获,就会调用
dispatchTransformedTouchEvent()
方法,参数child是null; - 回想一下刚刚要记住的方法“如果没有child参数为null就调用
super.dispatchTouchEvent()
; - 如果没有被截获,就调用
dispatchTransformedTouchEvent()
方法,参数child就是子View
; - 这样就会继续向下分发后续点击事件。
处理机制
真正的事件处理其实只有一个函数就是onTouchEvent()
而且记住只要到DOWN事件的时候返回true
,那么接下来的事件都会在这个onTouchEvent()
里处理。
为啥是这样,请看ViewGroup的
听我解释二
和听我解释三
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:
//抬起,执行点击,长按等效果
......
break;
case MotionEvent.ACTION_DOWN:
//按下,然后进行点击必要的设置,点击状态设置,长按状态设置
......
break;
case MotionEvent.ACTION_CANCEL:
//取消,取消和重置按下时的效果
......
break;
case MotionEvent.ACTION_MOVE:
//移动,取消一些效果,比如长按
break;
}
return true;
}
return false;
}
我们在做自定View的时候如果涉及事件反馈的问题都会重写onEventTouch()
。
看两个例子感受一下
第一个是SeekBar对触摸事件的处理,来看一下:
//AbsSeekBar.class
//
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsUserSeekable || !isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isInScrollingContainer()) {
mTouchDownX = event.getX();
} else {
startDrag(event);
}
break;
case MotionEvent.ACTION_MOVE:
if (mIsDragging) {
trackTouchEvent(event);
} else {
final float x = event.getX();
if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
startDrag(event);
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsDragging) {
trackTouchEvent(event);
onStopTrackingTouch();
setPressed(false);
} else {
// Touch up when we never crossed the touch slop threshold should
// be interpreted as a tap-seek to that location.
onStartTrackingTouch();
trackTouchEvent(event);
onStopTrackingTouch();
}
// ProgressBar doesn't know to repaint the thumb drawable
// in its inactive state when the touch stops (because the
// value has not apparently changed)
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsDragging) {
onStopTrackingTouch();
setPressed(false);
}
invalidate(); // see above explanation
break;
}
return true;
}
第二个是ScrollView对触摸事件的处理,来看一下:
//ScrollView.class
//
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = mScrollY - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
总结一下
- 事件分发和事件处理是两回事,可以分开了讨论;
- 事件分发主要是理解里面的分发逻辑,而事件处理主要是针对自己的需求进行重写
onTouchEvent()
方法; - 分发中最难的是ViewGroup中的分发,记住里面的
mFirstTouchTarget
参数的处理。
其他还不错的文章
HenCoder 3-1 触摸反馈,以及 HenCoder Plus
我叫陆大旭。
一个懂点心理学的无聊程序员大叔。
看完文章无论有没有收获,记得打赏、关注和点赞!
网友评论