前言
突然想看看android 下拉刷新源码的实现,其中有scroller的运用,找到了郭霖大神几年前写的一个scroller简化版的viewPager 例子,照着例子敲了一遍,大致意思上在一个viewgroup上摆放一排view,根据滑动的距离调用viewGroup的scrollerby滑动,我在这个viewGroup上随意的放上了TextView,布局如下:
滑动了半天却是没有响应滑动事件.
布局很简单就不贴了,有需要的同学可以看下搜下郭神的这篇文章《Android Scroller完全解析,关于Scroller你所需知道的一切》,随意在button上点击后,log日志如下:
07-14 17:44:07.009 29370-29370/com.tomchen.zoom E/ScrollerViewGroup: dispatchTouchEvent0
07-14 17:44:07.009 29370-29370/com.tomchen.zoom E/ScrollerViewGroup: onInterceptTouchEvent0
07-14 17:44:07.010 29370-29370/com.tomchen.zoom E/TButton: dispatchTouchEvent0
07-14 17:44:07.011 29370-29370/com.tomchen.zoom E/TButton: onTouchEvent0
07-14 17:44:07.011 29370-29370/com.tomchen.zoom E/ScrollerViewGroup: onTouchEvent0
有兴趣的同学可以试试在其他地方点一点,结果也会一致,就是没有调用后续的ACTION_MOVE,ACTION_UP,跳回到viewGroup的onTouchEvent后事件就被掐掉了,为什么呢。
网上事件传递的文章很多,《3副图看清事件传递》...,我都快倒背如流。还有些用了很长的篇幅来讲解,看得我似懂非懂,也有人直接给出了答案,给子元素设置clickable=true。
如果你能说清楚为什么,并且能用其他方式解决这个问题就不用往下看啦。
我的android 安装了 sdk 23的 sourceCode,在android studio 上能直接查看源码 java文件,如果还有人下载sdk比较困难,这里介绍一个不用翻墙的方式下载 最新android studio和sdk的方式,点这里 进入androiddevtools,然后就能在android studio上直接设置断点查看源码啦。
这里先给出结论
- 事件传递Activity-ViewGroup-view,明白 图1 的意思。
- 事件传递的最小单位是view的onTouchEvent,从onTouchEvent开始开始冒泡(如果布局有嵌套会递归弹栈)依次回传ViewGroup的onTouchEvent(如果onTouchEvent()没有返回true)
- View 如果不是clickable ,longClickable,contextClickable (可点击状态),不会消费事件
- 如果给View设置了TouchListener也能处理后续事件,在ACTION_DWON 时返回 true
- ViewGroup也是继承于View,ViewGorup本身clickable=false
- 给View设置clickListener 同时会让 clickable = true
- 如果View 没有消费ACTION_DOWN 事件,之后其他的ACTION_MOVE,ACTION_UP等事件也不会被响应,(包括上层的ViewGroup),ViewGroup 继承于View,所以ViewGroup也同理
先看图
图1
先说ViewGroup层的事件分发源码,去除了很多我认为没有的代码,
@Overridepublic
boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) { //安全检查机制,是否被遮盖
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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.
mFirstTouchTarget = null;
}
// Check for interception. final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { //这里调用调用拦截事件
intercepted = onInterceptTouchEvent(ev);
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) //这里主要调用这个方法
}
}
// 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.
} // Update list of touch targets for pointer up or cancel, if needed.
}
return handled;
}
首先(ACTION_DOWN,ACTION_MOVE,ACTION_MOVE)这一系列事件可以合并为一个流程,并且会和一个对象进行绑定,这个对象是mFirstTouchTarget,这个很重要,当用户按下时,会生成一个新的点击对象,并对老的对象会接受到cancel事件。
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.
mFirstTouchTarget = null;
}
这里开始调用onInterceptTouchEvent,这里注意如果是按下事件,或者mFirstTouchTarget !=null 就会调用onInterceptTouchEvent,刚刚按下事件已经将mFirstTouchTarget置为了空,所以能进如这个条件下的判断。
聪明的你联想下上下文,是不是大致猜测ACTION_DOWN 以后的事件没有被调用是和这个mFirstTouchTarget对象有关呢。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { //这里调用调用拦截事件
intercepted = onInterceptTouchEvent(ev);
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
这行代码的意思是如果是没有取消事件,没有被拦截,并且为按下事件开始分发事件,如果返回true,就把当前点击的View 赋值给mFirstTouchTarget,并设置标识已经分发过一次。
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) ){
alreadyDispatchedToNewTouchTarget = true;
//遍历childView 并把点击view赋值给mFirstTouchTarget
}
}
}
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view. //如果没有响应的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.
if(alreadyDispatchedToNewTouchTarget && target == newTouchTarget){
handled = true;
}
} // Update list of touch targets for pointer up or cancel, if needed.
如果mFirstTouchTarget = null.就不会再传递事件,这时可以在 android studio 可以调试下,直接断点放到mFirstTouchTarget == null 里面,看看是不是跑这里来了,虽然我们只是在界面上放了几个view,但是却会顶层嵌套好几层其他view,AppBarLayout等。
看到这里好像还是不大明白,先看下dispatchTransformedTouchEvent这个分发方法到底做了什么
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
我们的child是几个TextView ,继承View,接着看看super.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)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
return false;
}
这里代码只有几句很简单,如果你设置了onTouchListener,就会先调用mOnTouchListener.onTouch,如果onTouch事件返回true的话,事件将不会往下传递,再记忆下前面的只有在ACTION_DOWN的时候才设置mFirstTouchTarget的值,否则事件不会再往下传递否则onTouch事件也只会调用一次ACTION_DOWN,这里是不是可以作为突破口呢?
接着进入view的onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) ==LONG_CLICKABLE)(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
//这里直接返回true
}
//否则返回false
}
这里也简单,如果View是可点击直接返回true,否则返回false,调用viewGroup的supler(View).dispatchEvent(),
看到这里,你想到几种方案解决上面的问题了呢?
要不现在动手试试?
个人水平有限,如果错误,请指正,大家一起学习进步。