转载请注明本文出自远古大钟的博客(http://blog.csdn.net/duo2005duo),谢谢支持!
Touch概述
Touch操作即是用手触摸或者用鼠标操作屏幕所造成的事件触发。这些事件最基本的包括按下Down,移动Move,取消Cancel和离开触摸屏Up四种事件。一个完整的Touch过程一般是由Down->(Move)->Up/Cancel这四个事件组成,值得注意的是,一个完整的触摸事件必须由Down开始,再到Up/Cancel结束,中间的Move可以有可以没有,当然Touch事件不止这四个事件,但这四个事件是最基本,开发中必须考虑到的。
当用户开启应用触摸屏幕,系统服务就通过IPC(Binder)通知应用的主线程Looper中,最终传递到我们应用中Activity,View和ViewGroup中。
需要对Touch机制清晰才可以解决以下一些类似问题:
1. touch监听没被调用到
2. 双层滑动模块嵌套后发生滑动不了的现象
3. 设置了onClickListener后,点击View没有反馈
4. 点击两下View才调用onClickListener的bug
宏观
以上是Touch事件的传递顺序,一个Touch事件要传递到View中,必须经过Activity向下分发,如果在ViewGroup在子View中找到可以处理这个事件的View,则向下再传递下去,否则ViewGroup会尝试处理这个事件。下面详细介绍View,ViewGroup,Activity这三个类收到Touch事件的处理已经它们如何分发Touch事件。
View的Touch逻辑
Android中View对于Touch的处理逻辑主要集中在以下三个个位置中
//最主要的触摸事件的分发逻辑,向接收Touch事件的子View(包括自己)派发事件,对于View而非ViewGroup来说,这里只会对自己分发。
boolean dispatchTouchEvent(MotionEvent event);
//当前View处理触摸事件的可选方法,在dispatchTouchEvent()中被调用
void setOnTouchListener(OnTouchListener l);
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
//当前View处理触摸事件的默认方法,在dispatchTouchEvent()中被调用,如果已经设置了OnTouchListener,并且OnTouchListener已经消费了这个Touch事件,返回true,则不会触发这个方法。
boolean onTouchEvent(MottionEvent event);
以上几个方法返回值为true代表事件被处理,如果返回false,则代表事件没有被处理。
View之dispatchTouchEvent
让我们看一下dispatchTouchEvent
的逻辑,删除部分不重要代码,源码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
//1.停止嵌套滑动
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
//2.安全监测
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//3.如果当前View使能(setEnabled(true)),则调用Touch监听器
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//4.如果Touch监听器返回false或者没有调用Touch监听器,则返回调用onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
//停止嵌套滑动
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
从上面的源码中可以看出View的dispatchTouchEvent()
主要的逻辑如下:
1.停止嵌套滑动(5.0以后添加的)
2.做了安全监测,如果View开启了安全检测(setFilterTouchesWhenObscured(true)
)并且当前View所在的Window被其他Window遮盖的话,则不会调用再处理Touch事件
3.如果当前View使能,才会调用OnTouchListener
4.不管View使能与否,只要OnTouchListener没有处理事件,就会让onTouchEvent()
来处理事件,View不使能的情况下会调用View自己的onTouchEvent()
感悟:在dispatchTouchEvent中,会优先调用listener,后再调用onTouchEvent,View的dispatchTouchEvent()
的作用不能理解成向其他View传递Touch,它的作用应用理解为对组合(listener)与继承(onTouchEvent)的派发。我们知道,最常用的两种复用技术无非就是组合与继承,谷歌为了方便开发者,将这两种技术用dispatchTouchEvent()
结合起来。此外,还有一个enable的属性,这个属性改变了dispatchTouchEvent中的调用组合与继承的逻辑。
View之onTouchEvent
前面我们已经知道如果事件没有被OnTouchListener处理的话,将会被onTouchEvent()
处理。 onTouchEvent()
在源码中主要是处理
press :按下时候View状态的改变,比如View的背景的drawable会变成press 状态
click/tap: 快速点击
longClick:长按
focus:跟press类似,也是View状态的改变
touchDelegate:分发这个点击事件给其他的View,这个点击事件传到其他View前会改变这个事件的点击坐标,如果在指定的Rect里面,则是View的中点坐标,否则在View之外
让我们看一下OnTouchEvent
的逻辑,源码如下:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//1.View不使能的情况下(setEnabled(false)),依然可能消费事件
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
//2.用TouchDelegate将自己的区域变成其他View中心点的操作
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//3.从这里跟1结合可以知道,只要View是Clickable或者LongClickable,就一定消费事件
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
//只有在press的情况下,才可能click,longClick也做了同样的判断
if ((mPrivateFlags & PRESSED) != 0) {
//4.如果我们在当前View还没获取焦点,并且能在touch下foucus,那么第一次点击只会将这个View的状态改成focus,而不会触发click
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
//5.已经有longClick执行过了,就不再执行click了
if (!mHasPerformedLongPress) {
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
if (!focusTaken) {
performClick();
}
}
//6.取消press
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (!post(mUnsetPressedState){
mUnsetPressedState.run();
}
}
break;
case MotionEvent.ACTION_DOWN:
//7.press,定时检测并且执行longclick
mPrivateFlags |= PRESSED;
refreshDrawableState();
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick();
}
break;
case MotionEvent.ACTION_CANCEL:
//8.清理状态
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
//9.如果移动到View外,则不press,如果移动到View内,则press
int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press checks
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
} else {
// Inside button
if ((mPrivateFlags & PRESSED) == 0) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
View的onTouch方法归纳为一下几点:
1.不管View使能与否,只要clickable或者longclickable,就一定消费事件(返回true)
2.如果View不使能,并且clickable或者longclick,就只会消费事件但不做其他任何操作
3.如果View使能,先看看TouchDelegate消费与否,如果不消费再给自己消费
4.处理包括focus,press,click,longclick
感悟:谷歌选择继承的方式作为默认的实现,这个实现非常重要,并不可少。因为它关联着focus,click,press,longClick,同时还关联着TouchDelegate,错误地改写他或者添加Listener的后果都将可能导致以上5种机制失效。另外,对于Click与onLongClick,有专门对应的属性focus,press进行关联。longClick的Touch过程中必须保持一致是press状态。
ViewGroup的Touch逻辑
而ViewGroup继承与View,并覆盖了
dispatchTouchEvent(MotionEvent event);
而且比View多了一个处理Touch的位置:
viewgroup.onInterceptTouchTouchEvent(MotionEvent event);
这个方法的返回值主要用于是否阻止向子View派发触摸事件,默认返回false,不阻止。
精简后的源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
//1.只有在非拦截的情况的下寻找target
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 防止onInterceptTouchEvent()的时候改变Action
ev.setAction(MotionEvent.ACTION_DOWN);
// 遍历子View,第一个消费这个事件的子View的为Target
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
//当然只遍历可见的,并且没有在进行动画的。
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
//up或者cancel的时候清空DisallowIntercept
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// 如果没有target,则把自己当成View,向自己派发事件
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
return super.dispatchTouchEvent(ev);
}
// 如果有Target,拦截了,则对Target发送Cancel,并且清空Target
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
//up 或者 cancel清空Target
if (isUpOrCancel) {
mMotionTarget = null;
}
//如果有Target,并且没有拦截,则向Target派发事件,这个事件会转化成Target的坐标系
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
综上,ViewGroup的主要的任务是找一个Target,并且用这个target传递事件,主要逻辑如下
1.在Down并且不拦截的时候会多出一个寻找Target的过程,在这个过程中遍历子View,如果子View的dispatchTouch为true,则这个子View就是当前ViewGroup的Target。找Target是处理Down事件时候特有的,其他事件不会触发找Target。
2.如果没有Target,则发送把自己当做一个View去处理这个事件(super.dispatchTouch()
)
3.如果有Target并且拦截,则发送Cancel给子View
4.如果有Target并且不拦截,则调用Target的dispatchTouch
5.可以利用requestDisallowInterceptTouchEvent(boolean)来强制viewparent不拦截事件。但是作用域限于一个Touch的过程(Down->Up/Cancel)
感悟:我们发现,在ViewGroup的分发过程中,相对原先的View分发过程中,多了对子View分发功能,同时,添加了向下屏蔽分发的功能(onInterceptTouchEvent)和对应禁止屏蔽的功能(disallowIntercept )。Enable属性也不作用于ViewGroup类的分发过程。 此外,原本事件的分发应该是一个树的广度遍历,谷歌为了加速遍历过程,在Touch过程一次广度遍历(Down)中加入了一个指针(Target),使得在本次Touch过程的接下来的流程中只需树高度个数的target即可。
对于dispatchTouchEvent返回值,分发过程只有Down过程用到,那么是不是返回值在其他阶段就没有作用了呢?
不是的,在Activity的dispatchTouch逻辑中会用到。
Activity的Touch逻辑
ViewGroup收到的事件是由Activity发送出去的。Activity的Touch逻辑非常简单,源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//这个最终传递到setContentView对应的View中
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果ContentView没有对时间进行处理,统一由Activity的onTouchEvent()来处理
return onTouchEvent(ev);
}
Activity的Touch逻辑归纳如下:
Activity将事件经过一些步骤发送给ContentView,如果ContentView没消费,就交给Activity自己处理。
问题解决
文章开头提出了几个问题,可以用这篇文章的分析解决:
touch监听器没被调用到?
看View.dispatchTouchEvent()
,ViewGroup.dispatchTouchEvent()
a)如果是View非使能,直接用setEnabled(true)
b)如果是事件被这个View的viewparent拦截了。可以修改这个viewparent的onInterceptTouchTouchEvent()
,或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()
双层滑动模块嵌套后发生滑动不了的现象?
看ViewGroup.dispatchTouchEvent()
如果是事件被这个View的viewparent拦截了。可以修改这个viewparent的onInterceptTouchTouchEvent()
,或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()
设置了onClickListener后,点击View没有反应?
看View.onTouchEvent()
a)如果是View非使能,直接用setEnabled(true)
b)可能覆盖了onTouchEvent(),需要在覆盖的方法调用super.onTouchEvent()
或者手动调用performClick()点击两下View才调用onClickListener的bug?
看View.onTouchEvent()
这个其实是安卓的设计,当某个View调用了setFocusableInTouchMode(true)
后,第一次点击会引起这个View的focus,第二次点击才会调用onClickListener,只需要设置setFocusableInTouchMode(false)
即可。