一、View体系#事件分发小插曲
1、MotionEvent
用户在屏幕上进行一些列操作时这些事件都会最终被封装到MotionEvent这个类中。
MotionEvent的动作类型:
- MotionEvent.ACTION_DOWN
手指刚接触屏幕,按下去的那一瞬间产生该事件。 - MotionEvent.ACTION_MOVE
手指在屏幕上移动时候产生该事件 - MotionEvent.ACTION_UP
手指从屏幕上松开的瞬间产生该事件
我们平时操作手机(点击、触摸滑动)是无非就是这三种事件其中的组合:
1、点击事件:ACTION_DOWN -> ACTION_UP
2、触摸滑动事件:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
2、 Activity#setContentView view被处理的顺序
Activity#onCreate#view ->DecorVIew->PhoneWindow->Activity#onResume(回调完,处理View的加载、显示)
3、事件分发的顺序
Activity#dispatchTouchEvent
->PhoneWindow#superDispatchTouchEvent
->DecorView#superDispatchTouchEvent(event)
->setContentView中根布局容器中逐级往下传递
4、从Activity到DecorView分发源码
(1)Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
// 为ACTION_DOWN事件时
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//查看源码发现这个方法是空实现。
}
// 事件交付给了window处理
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
activity会把事件交付给Window处理:
1、如果Window#superDispatchTouchEvent(ev)返回true整个事件循环就结束。
2、返回false意味着事件没人处理,也即所有view的onTouchEvent都返回了false,那么activity自己消费,调用自己的onTouchEvent。
(2)PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow将事件交付给了DecorView分发
(3)DecorView#dispatchTouchEvent
看下DecorView源码,直接调用父类的superDispatchTouchEvent,其实也就是ViewGroup#superDispatchTouchEvent。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
public boolean superDispatchTouchEvent(MotionEvent event) {
// 使用的ViewGroup#dispatchTouchEvent
return super.dispatchTouchEvent(event);
}
}
ps:FrameLayout 未重写superDispatchTouchEvent,其实安卓很多容器如Linearlayout、RelativeLayout等都是继承ViewGroup#superDispatchTouchEvent。也有特殊的容器重写了superDispatchTouchEvent如ScrollView。
4、setContentView 根布局容器中事件分发
二、View&ViewGroup事件分发相关三个重要方法
1、dispatchTouchEvent
(1)作用:用来进行事件的分发。
(2)说明:每个View都有此方法。VIewGroup 继承并重写了此方法。
(3)返回值:返回值受当前View的OnTouchEvent返回值或子View的dispatchTouchEvent返回值影响。true表示消费事件、false表示不消费事件。
2、onInterceptTouchEvent
(1) 作用:事件拦截 。用于判断是否需要拦截事件。
(2)说明:此方法为ViewGroup独有,view没有此方法。在ViewGroup#dispatchTouchEvent中被使用。
(3) 返回值:
true表示拦截事件传递,事件不再向下分发,方法内部调用本view容器的onTouchEvent进行消费事件。
false不拦截事件传递,事件开始向下传递,事件分发到子View容器或子ViewGroup的dispatchTouchEvent中。
3、onTouchEvent
(1)作用:对事件进行消费。
(2)说明:这个方法在dispatchTouchEvent方法中被调用。这个方法实现在View中。ViewGroup 直接继承使用,并未重写这个方法。
(3)返回值:
true表示事件被消费,本次的事件终止。
false表示事件没有被消费,将调用父View的onTouchEvent往上层回传事件。
三、ViewGroup的事件分发源码分析
分析源码前我们首先须知两点
1、一般的控件(TextView、LinearLayout…)是不会重写diapatchEvent、onInterceptTouchEvent、onTouchEvent的方法的,除非有自己的特殊处理时。如ScrollView自带滑动功能,其重写了ViewGroup#diapatchEvent、ViewGroup#onTouchEvent的方法。
2、diapatchEvent、onTouchEvent这两个方法为View所有:
- 因为ViewGroup有子View,所以ViewGroup继承View时重写了diapatchEvent方法。
- 因为ViewGroup有子View,所以事件到达ViewGroup时ViewGroup要决定自己消费还是某个子View消费。这时ViewGroup添加了onInterceptTouchEvent方法。
- onTouchEvent这个方法为View所有,ViewGroup直接继承View的,并未重写这个方法。
1、 ViewGroup#dispatchTouchEvent 源码概览
(1)mInputEventConsistencyVerifier:这个对象为调试对象。mInputEventConsistencyVerifier对象的初始化在VIew中。这个对象的相关逻辑和整个事件分发无关。
View.java
/**
* Consistency verifier for debugging purposes.
* @hide
*/
protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
InputEventConsistencyVerifier.isInstrumentationEnabled() ?
new InputEventConsistencyVerifier(this, 0) : null;
(2)setTargetAccessibilityFocus:安卓中提供的辅助功能的选项,用来帮助有障碍的用户来使用这个系统。这个对事件分发机制几乎没啥影响。
(3)只剩下一个if相关的逻辑了,这里是事件分发的重点,也是本机要关注的核心点。
可以看到ViewGroup #dispatchTouchEvent 的返回值就是handled的值。这个值在if判断中根据不同的情况被更改。
ViewGroup的这个方法中主要做了如下三件事:
1、判断是否拦截事件(默认不拦截,分发给子VIew)
2、在当前viewGroup中找到真正响应事件的view(用户真正操作的View)
3、吧事件分发到View上
2、ViewGroup#dispatchTouchEvent 源码细析
代码较长耐心阅读!!!
boolean handled = false;
//1、安全策略判断
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 2、处理“摁下”事件,也即ACTION_DOWN事件。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//3、检测拦截的逻辑
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//disallowIntercept=false时,可能需要拦截。
//4、onInterceptTouchEvent 拦截判断实现
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
// disallowIntercept = true 时不拦截
intercepted = false;
}
} else {
//没有触摸目标,并且不是action down。viewGroup 拦截事件。
// 也可以换种说法:当前事件不是action down并且没有处理事件子view
// viewGroup 直接拦截事件。
intercepted = true;
}
//辅助功能相关处理 对事件分发机制几乎没啥影响。
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 获取当前事件是否是取消事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//split变量表示当前事件是否分发给多个子视图。
//同一容器中可能存在多个子视图重叠在一起,
//这时split为true代表重叠区域子视图都可获得事件。
//重叠区域子视图的理解:可行想象下父容器为RelativeLayout,
//有3个子Button 重叠在容器左上角区域。
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
//newTouchTarget 初始化为空,下面会用到
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//5、事件分发核心代码:
// 当前事件不是取消事件,并且不需要拦截时寻找View进行分发。
if (!canceled && !intercepted) {
//辅助功能相关处理 对事件分发机制几乎没啥影响。
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//ACTION_POINTER_DOWN代表移动事件
//ACTION_HOVER_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;
//清空之前所有触摸点信息
removePointersFromTouchTargets(idBitsToAssign);
//获取ViewGroup 所有子View数量
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 获取触摸位置坐标(x,y)
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//获得 “所有能够” 接收事件的子view集合
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
//customOrder : 是否自定义了view的绘制顺序,
//为true代表用户自定义了view的绘制顺序
// 自定义View时自定义了绘制顺序影响customOrder值
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//从前到后遍历“所有子view”,查找真正点击的,需要响应事件的view
for (int i = childrenCount - 1; i >= 0; i--) {
// getAndVerifyPreorderedIndex:获取子view真正索引
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//getAndVerifyPreorderedView 找到真正的view
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 检查当前view是否能够接收触摸事件,触摸事件是否在view的范围内
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//前view能够接收触摸事件,触摸事件在view的范围内:开始获得目标view的触摸事件.
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
//newTouchTarget 不为空代表当前的子view已经获得触摸事件,这时便可以结束循环了
break;
}
//前view没获取到触摸事件则:
//调用resetCancelNextUpFlag 检查该view是否设置了暂时不接受事件的标志位,
//设置了就清除掉标志位。下一次就可以接受了.
resetCancelNextUpFlag(child);
//6、dispatchTransformedTouchEvent子类中分发事件:
//方法返回值为true代表事件传递到子View#dispatchTouchEvent
//也即子View#dispatchTouchEvent返回值为true。
// 这时事件循环也会结束,看下面break。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 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();
//内部对mFirstTouchTarget进行赋值
//也即这里走完mFirstTouchTarget !=null
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;
}
}
}// !canceled && !intercepted 的逻辑走完。
//7、继续判断
//(1)mFirstTouchTarget == null代表还没有子view处理事件
//这时拦截结果由dispatchTransformedTouchEvent 结果决定
// 注意此时dispatchTransformedTouchEvent第三个参数child传空
// 代表无child处理
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;
//(2)mFirstTouchTarget != null
//dispatchTransformedTouchEvent已经处理过 handled 设置为true
// 避免重复处理
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;
}
(1)onFilterTouchEventForSecurity#安全策略的判断
1、这个方法的具体实现在View中。判断当前的触摸事件是否符合安全策略。
2、安全策略:常理来说用户只能点击自己能够直接看到的View或者ViewGroup,看不到的是不会去点击的。所以google为事件的分发制定了一个安全策略。如果当前view不在视图的顶部(被其他view遮挡 用户就看不到)、或者当前的view设置了不响应事件,则onFilterTouchEventForSecurity方法返回false。符合安全策略时这个方法返回true。
3、这个安全策略过滤的方法如果直接返回false的话那么ViewGroup的dispatchTouchEvent方法就直接返回false来了。
(2)处理“摁下”事件,也即ACTION_DOWN事件。
ViewGroup中ACTION_DOWN事件会做一些必要初始化操作,为新的事件循环做准备。
1、cancelAndClearTouchTargets(ev):取消并清除所有触摸目标。
2、resetTouchState():重置触摸的状态。
(3)检测拦截的逻辑
1、当前事件action down事件,或者mFirstTouchTarget不为空时,进入检测拦截逻辑。
2、当前存在处理事件的子view时,mFirstTouchTarget就不为空。否则为空。
3、disallowIntercept 标志位,字面意思就是不允许拦截。这个标志位的boolean值受FLAG_DISALLOW_INTERCEPT标志位的影响,这里先打下预防针,滑动冲突的处理就会用到这个,可以先有下概念。true:不允许拦截
false:允许拦截4、这里为啥设置了disallowIntercept 变量的判断?直接调用自身的onInterceptTouchEvent(ev)来判断是否拦截不就行了吗?
其实这里就是为子VIew提供了干扰拦截的能力,子view可以通过设置相应的标志位来影响disallowIntercept 的值,来影响父ViewGroup容器是否拦截
(4)onInterceptTouchEvent 拦截判断实现
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
1、方法返回true,代表拦截事件,方法为false代表不拦截事件。
2、如下四个条件都满足这个方法才返回true:1、ev.isFromSource(xxx)这个方法用来判定指定事件源,这里传入的常量InputDevice.SOURCE_MOUSE代表输入的设备为鼠标。一般情况下我们使用手指触摸,手机使用鼠标几乎很少见。所以这里一般为false。
2、isButtonPressed(MotionEvent.BUTTON_PRIMARY):判断当前是否按下鼠标左键。手机一般都不用鼠标。所以这里一般为false。
3、isOnScrollbarThumb(ev.getX(), ev.getY()):判断当前手指或者鼠标位置是否在滚动条上,是就返回true。
综上:一般情况下viewGroup的onInterceptTouchEvent可以认为默认返回false,默认不拦截事件。
(6)dispatchTransformedTouchEvent子类中分发事件
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// 1、判断是否为down事件
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// 判断是否有子view,没有子view时,直接调用viewGroup父类的dispatchTouchEvent方法(ViewGroup父类就是View)
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
// 存在子view时,事件交付子view的dispatchTouchEvent
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 获得新旧指针位
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
return false;
}
final MotionEvent transformedEvent;
// 2、新旧指针位对比 纠正偏移量
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);
}
// 3、前两个判断都不满足时,这里判断当前的子view是否存在
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
代码虽长但是逻辑明确,主要进行了3个判断:
1、判断是否为ACTION_CANCEL事件
2、新旧指针位对比 纠正偏移量
3、1、2都不满足时,这里判断当前的子view是否存在
方法返回值为true,代表当前的事件传递到了子View的dispatchTouchEvent方法中。
3、ViewGroup 拦截事重点总结
(1)可能拦截事件地方
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//1、重写onInterceptTouchEvent直接返回true时
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
//2、非action_down 且mFirstTouchTarget = 空
intercepted = true;
}
可见想要触发事件拦截如上两种cese都可:
1、重写onInterceptTouchEvent直接返回true注意这个要满足特定的条件disallowIntercept = false,也即子类未干扰父类事件拦截。
2、非action_down 且mFirstTouchTarget = 空
即不是action_down 事件,也没有子view处理,这时ViewGroup直接拦截move、up事件。
(2)拦截后的处理
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 1、直接调用dispatchTransformedTouchEvent 来处理事件,
// 注意这里第三个参数child传参为空,方法内部直接调用父类的
// dispatchTouchEvent处理,最终回传到DecorView的dispatchTouchEvent
//DecorView 重写了dispatchTouchEvent交付给window->Activity处理
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 (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 2、直接调用dispatchTransformedTouchEvent 来处理事件,
// 注意这里第三个参数child传参为空,方法内部直接调用父类的
// dispatchTouchEvent处理,最终回传到DecorView的dispatchTouchEvent
//DecorView 重写了dispatchTouchEvent交付给window->Activity处理
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;
}
}
四、View的事件分发源码分析
1、dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.isTargetAccessibilityFocus()) {
// 1、View不具备可响应事件的焦点直接返回false
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
event.setTargetAccessibilityFocus(false);
}
//定义事件分发的结果, 默认值为fase表示消费事件。
// result的结果就是dispatchTouchEvent的返回值。
boolean result = false;
//2、安全过滤条件判断,符合时进行如下逻辑不符合时
//dispatchTouchEvent的返回值就是result 默认值
if (onFilterTouchEventForSecurity(event)) {
//ListenerInfo 内部封装了所有的事件(点击、长按、触摸等等)
ListenerInfo li = mListenerInfo;
// 2、如下三个条件同时满足时 View拦截事件,消费事件。
//(1)ListenerInfo 非空
//(2)设置了触摸事件(mOnTouchListener )
//(3)触摸事件的onTouch方法返回值为true
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 3、上述2条件不满足result为false,这里调用自己的onTouchEvent
// 处理事件。onTouchEvent 返回值决定着dispatchTouchEvent返回值
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
可以看到,View 类中分发 TouchEvent 还是比较简单的,如果注册了 OnTouchListener 就调用其 onTouch 方法,如果返回 false,还会接着回调 onTouchEvent 方法。onTouchEvent 作为一种兜底方案,它在内部会根据 MotionEvent 的不同类型做相应处理,比如是 ACTION_UP,可能就需要执行 performClick 函数。
2、onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
//获取view可点击状态
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//1、当view处于禁用状态时(DISABLED),onTouchEvent返回值取决于view的
//clickable值 我们也看不到啥现象。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
//我们也看不到啥现象。
return clickable;
}
//2、处理各种action,注意action up中处理的点击事件。
//clickable= true 或者 允许显示TOOLTIP(android8.0上添加新功能)
// 方法最终直接返回true
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!post(mPerformClick)) {
// View的点击事件处理入口
performClickInternal();
}
...
return true;
}
return false;// 3、上述条件都不满足最终返回默认值false
}
3、总结
1、首先在diapatchEvent中进行判断:
- 用户设置了onTouchListener& onTouchListener的onEvent返回true。此时diapatchEvent 返回true。表示此view消耗了事件。
- 用户未设置onTouchListener& 或者用户设置了onTouchListener& onTouchListener的onEvent返回false。这时调用自己onTouchEvent方法
2、onTouchEvent方法:
- 当view处于禁用状态时(DISABLED)onTouchEvent返回值取决于view的clickable值 我们也看不到啥现象。
- view onTouchEvent 默认返回值也是false
五、滑动冲突
场景1:外部滑动方向和内部滑动方向不一致。
场景2:外部滑动方向和内部滑动方向一致。
场景3:上面两种情况的嵌套。
1、场景一
主要是viewpager和fragment配合使用所组成的页面滑动效果。而viewpager可以左右滑动切花页面,我们往往会在fragment中添加ListView或者Recyclerview控件让他上下滑动。
疑问:
这种情况正常啊,平时我们也经常使用,没碰见滑动冲突啊!!!其实viewpager这个控件已经帮我们处理这种滑动冲突场景。
试想:
我们没有使用viewpager,而是使用了HorizontalScrollView等控件,这时必须手动处理滑动冲突了。否则造成结果就是内外两层只能有一层滑动。
2、场景二:
情况比较特殊,当内外两层两个控件在同一方向上可以滑动时,显然存在逻辑问题,这时你的手指在屏幕上滑动时,系统不知道你想让哪个控件滑动。
问题:当你手指在屏幕滑动时,要么是只有一层能滑动,要么是内外两层都滑动的很卡顿。
开发常见:
ListView头部有一个可下拉的刷新头,那么就要判断ListView是否滑动到顶部,到顶部时滑动出现刷新头。
3、场景三:
这种情况是1,2两种情况的嵌套。
常见案例:
最外层是Slidemenu,中间层是viewpager,内层viewpager页面都是listview。
解决:分别解决各层冲突即可
六、滑动冲突解决方案
我们先分析场景1,这也是最简单、最典型的一种滑动冲突,因为他的滑动规则比较简单。不管多复杂的滑动冲突他们之间的区别也就是滑动规则的不同。抛开滑动规则不说我们需要找到一种不依赖具体的滑动规则的通用解决方案。
这里我们根据场景1得到了通用方案,然后场景2,3我们只需要修改具体的滑动逻辑即可。但是要怎么做才能够将事件交给指定的View去处理呢?这里就要用到事件分发机制了。
两种通用的解决方式:
- 外部拦截法
- 内部拦截法
1、外部拦截法伪代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
switch (ev.getAction()){
// down固定代码
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
// move 修改逻辑即可
case MotionEvent.ACTION_MOVE:
if(父容器需要点击事件){// 也就是触发父容器滑动规则时,比如父容器滑动距离较大时
intercepted = true;
}else{
intercepted = false;
}
break;
// up固定代码
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
(1)上面的是外部拦截法的典型写法,面对不同的滑动类型,只需要修改上面的判断条件即可,其他不用修改也不能够修改。
(2)在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,
这是因为一旦父容器拦截了ACTION_DOWN事件,子控件就收不到事件了(down、move、up三者down先生成的,才可能有后续二者)。
那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器进行处理,这个时候就没有办法再传递给子元素了。
(3)其次是ACTION_MOVE事件,这个事件可以根据需求来决定是否拦截,如果父容器需要拦截就返回true,否则返回false。
如果父容器拦截了move事件,子view就收不到move事件。
(4)最后是ACTION_UP事件,这里也直接返回false。这是因为,如果父容器在ACTION_UP时返回true,就会导致子元素无法触发onClick事件(在View的源码中onClick事件是在ACTION_UP中触发的)。
注意ViewGroup默认使用view的onTouchEvent,一般情况下不进行重写。这个方法默认不消费,默认调用父类的onTouchEvent。
2、场景
(1)父容器拦截down -> 后续的move、up都会直接交付父容器处理。因为子View都收不到down不用谈move和up的事情啦。
(2)父容器不拦截down、拦截move、不拦截up事件-> 子控件只能收到 down、up事件。onClick功能正常。move事件无。
(3) 父容器不拦截down、拦截move、拦截up事件 -> 子控件无法进行onClick功能。
拓展
1、如果我只想有view的拖拽事件,而不想要view的点击事件,让你重写这个view的拖拽怎么设计
其实这道题考察大家对view的dispatchTouchEvent和view的onTouchEvent事件的处理流程,上面已经分析了想要view能执行到view的touch事件,
那么必须要求view的dispatchTouchEvent返回true,而dispatchTouchEvent返回true要么是dispatchTouchEvent直接返回true或者view的onTouchEvent返回true。
如果从效率上看,直接将dispatchTouchEvent返回true就ok,而不需要再去关心onTouchEvent方法。
2 、OnClickListener的触发机制
首先说明一下View的OnClick是在onTouchEvent中的action up中触发的。
然而onTouchEvent的回调受一定条件的影响,view的dispatchTouchEvent中View会先判断是否设置了OnTouchListener,如果设置了OnTouchListener并且onTouch方法返回了true,那么onTouchEvent不会被调用。
也即给View设置了触摸事件,触摸事件返回true那么onTouchEvent 就不会回调了。dispatchTouchEvent直接返回true代表这个view把事件消耗了。
如果未设置OnTouchListener,或者设置了OnTouchListener但是onTouch方法返回false这时会调用onTouchEvent 方法。
3、 ScrollView 的点击事件 研究
1、 scrollview滑动时会进行事件拦截的,滑动时代表ScrollView进行事件处理。
拦截事件时交付给自己的onTouchEvent进行处理。由于ScrollView的 onTouchEvent重写了(一般普通的View都未重写如TextView、Button等)把action up 中performOnClick功能给阉割了。
2、scrollView静止时不拦截事件。