滑动有一系列事件,经常用到的事件如下:
1)、ACTION_DOWN:手指接触屏幕
2)、ACTION_MOVE:手指在屏幕滑动
3)、ACTION_UP:手指离开屏幕
一次完整的滑动事件由ACTION_DOWN开始,ACTION_UP结束。经历的事件有以下两种情况:
1)、ACTION_DOWN->ACTION_UP
2)、ACTION_DOWN->一个或者多个ACTION_MOVE->ACTION_UP
通过MotionEvent可以获得当前控件的位置
1)、获取点击位置在当前View中的位置。getX()、getY()获取相对当前View左上角的x、y坐标
2)、获取点击位置在屏幕中的位置。getRawX()、getRawY()获取相对屏幕左上角的x、y坐标
先看一个例子。其中ViewGroupA是ViewGroupB的父容器,ViewGroupB是ViewC的父容器,所以方法都调用对应的super。触摸ViewC时发生的事件传递如下:
因为这些控件都没有消耗事件,所以ACTION_MOVE都不会传递到这些控件,只传递到Activity。
1)、public boolean dispatchTouchEvent(MotionEvent ev) 是否分发或传递事件
用来进行事件的分发。会调用的自己的onInterceptTouchEvent、子View的dispatchTouchEvent、自己onTouchEvent,上面的调用根据情况有的不会调用,后面具体分析。如果该方法直接返回false,则后面的子控件的dispatchTouchEvent就不会执行了,开始向上调用onTouchEvent。ViewGroupB中直接返回false,事件分发不会继续向下走了,开始向上调用onTouchEvent。
2)、public boolean onInterceptTouchEvent(MotionEvent ev) 是否拦截该事件,默认不做拦截
在dispatchTouchEvent中调用,用来判断是否拦截某个事件,如果拦截同一事件序列中该方法不会再执行了。只有ViewGroup中才有该方法。
a、返回false/super.onInterceptTouchEvent:不做此次拦截,事件将会正常向下分发,分发至下级的dispatchTouchEvent方法再次判断是否分发事件。
b、返回true:表示ViewGroup容器拦截后续事件,会执行该控件的onTouchEvent()方法然后停止向下分发,转而通过onTouchEvent()向上传递,直到最终被消费。
在ViewGroupB的onInterceptTouchEvent返回true,日志如下:
在ViewGroupB拦截事件后会从ViewGroupB的onTouchEvent向上一直传递到Activity的onTouchEvent。因为所有onTouchEvent都没有消耗事件,所以ACTION_MOVE只在Actiivty中传递,不会在View和ViewGroup中传递。这里我们得出一个结论:ACTION_DOWN时没有控件消耗事件(onTouchEvent返回true),那后面的ACTION_MOVE和ACTION_UP的都不会传递到这些控件(即不回调其dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent),只会传递到Activity(回调其dispatchTouchEvent、onTouchEvent)。
在ViewGroupB的onInterceptTouchEvent和onTouchEvent都返回true,日志如下:
注意上面虽然是两张图片,但是都是一个触摸事件的日志,因为中间有很多ACTION_MOVE的日志,所以把ACTION_UP分开截图。在ViewGroupB拦截事件后会调用ViewGroupB的onTouchEvent,因为ViewGroupB的onTouchEvent返回true,消耗了事件,不会再向上调用父控件的onTouchEvent。表示由ViewGroupB消耗事件,ACTION_MOVE时任然会调用ViewGroupB上层父控件的dispatchTouchEvent和onInterceptTouchEvent,即ViewGroupB上面的父控件任然可以拦截事件,但是ViewGroupB的子控件就不会收到这些事件了。如果ViewGroupB上面的父控件不拦截事件,ACTION_MOVE就交给ViewGroupB的onTouchEvent处理。
在上面条件的基础上更改为在ACTION_MOVE时ViewGroupA拦截事件,这时ViewGroupB会收到ACTION_CANCEL,下一次ACTION_MOVE时就会执行ViewGroupA的onTouchEvent,即事件交给ViewGroupA的onTouchEvent消耗。我们在ViewGroupA的onTouchEvent中返回了true,如果返回false,还会向上执行Activity的onTouchEvent。日志如下:
3)、public boolean onTouchEvent(MotionEvent event) 是否消费掉此次事件
在dispatchTouchEvent中调用,用来处理点击事件。返回false表示不消耗该事件,同一事件序列中该View无法再次接受到事件。
a、返回false/super.onTouchEvent():不消费掉此次事件,事件将会层层向上传递,直到被消费。
b、返回true:立即消费掉事件,事件将不会向上传递。
在ViewGroupB的onTouchEvent返回true,日志如下:
上面所有控件都没有拦截事件,所以在第一方框中看到最终dispatchTouchEvent分发到ViewC中,然后向上执行onTouchEvent。ViewGroupB的onTouchEvent返回true消耗事件后,ACTION_MOVE时不会调用ViewGroupB和其子控件的dispatchTouchEvent和onInterceptTouchEvent,会调用ViewGroupB父控件的dispatchTouchEvent和onInterceptTouchEvent。这里可以得出个结论:即使所有控件没有拦截事件,某个控件的onTouchEvent返回true消耗事件后,这个控件和其子控件的dispatchTouchEvent和onInterceptTouchEvent不会再调用,会调用其父控件的dispatchTouchEvent和onInterceptTouchEvent。即其父控件还可以拦截事件。
使用伪代码表示上面三个方法的关系
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
if(consume == false){
consume = onTouchEvent(ev);
}
}
return true;
}
事件传递机制规则如下:点击事件产生后首先会传递给根ViewGroup,它的dispatchTouchEvent会调用,如果它的onInterceptTouchEvent返回true表示它要拦截当前事件,后面的事件就交给该ViewGroup处理,它的onTouchEvent会被调用。如果它的onInterceptTouchEvent返回fasle表示它不拦截当前事件,那么当前事件会传递给子View,然后子View的dispatchTouchEvent会被调用,这样就把事件分发到子View中了。如果所有子View的dispatchTouchEvent都返回false,就会调用当前ViewGroup的onTouchEvent方法。
总结:
1)、事件分发是从Activity开始,然后传递到Window,然后传递到DecorView,然后再传递到我们定义的界面。
2)、整个流程呈U形,U形左半部分是从Activity开始,事件从父控件向下分发到子控件,调用各个控件的dispatchTouchEvent和onInterceptTouchEvent。U形的转折点就是从Activity开始事件向下分发到最后一个View或者ViewGroup的onTouchEvent。U形右半部分从最后一个View或者ViewGroup的onTouchEvent从子控件到父控件向上依次调用各个控件的onTouchEvent。
3)、无论有没有onInterceptTouchEvent拦截事件,ACTION_DOWN时只要没有控件消耗事件(onTouchEvent都返回false),那ACTION_MOVE和ACTION_UP的都不会传递到这些控件(即不回调其dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent),只会传递到Activity(回调其dispatchTouchEvent、onTouchEvent)。所以决定ACTION_MOVE和ACTION_UP是否传递到某个控件不是onInterceptTouchEvent,而是onTouchEvent是否消耗事件。
4)、如果所有控件没有拦截事件,某个控件的onTouchEvent返回true消耗事件后,这个控件和其子控件的dispatchTouchEvent和onInterceptTouchEvent不会再调用,会调用其父控件的dispatchTouchEvent和onInterceptTouchEvent。即其父控件还可以拦截事件。
5)、如果某个控件拦截了事件,那么事件分发就结束,即子控件的dispatchTouchEvent和onInterceptTouchEvent不会再调用。会调用这个控件的onTouchEvent,如果这个控件也不消费事件就会继续向上回调父控件的onTouchEvent。
(如果ACTION_DOWN时ViewGroup拦截了事件,但是onTouchEvent返回false,会依次调用父控件的onTouchEvent,如果都是false,后续事件只调用Activity的onTouchEvent。)
6)、View(包括ViewGroup)的onTouchEvent中ACTION_DOWN时返回true,但ACTION_MOVE返回false,这时不会调用父控件的onTouchEvent。后续的事件还是调用该View(包括ViewGroup)的onTouchEvent。
7)、要理解事件分发机制的三个方法。
dispatchTouchEvent是事件分发最终调用的方法,dispatchTouchEvent中包含了onInterceptTouchEvent和onTouchEvent,从Activity开始传播到ViewGroup都是调用dispatchTouchEvent进行分发。
onInterceptTouchEvent的作用是父控件是否拦截这个事件,事件都是从父控件传递到子控件。注意这个方法只决定拦截,是否处理这个事件(即消耗)不由它控制,拦截后事件就不会传递到下层的子控件。
onTouchEvent表示是否消耗这个事件,即是否处理事件。消耗后下个ACTION_MOVE才会传递到这个控件。传递的方式就从该控件的最上层父控件开始一级一级调用dispatchTouchEvent和onInterceptTouchEvent到该控件,如果父控件没有拦截事件就会执行该控件的onTouchEvent,如果拦截了事件dispatchTouchEvent和onInterceptTouchEvent方法就从拦截事件的父控件结束,并调用拦截了事件的父控件的onTouchEvent。
当我们点击屏幕时事件是先传递到activity,然后传递到window,然后是DecorView(DecorView是setContentView中view的父容器),然后DecorView传到我们定义的布局中。
activity中dispatchTouchEvent代码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
getWindow().superDispatchTouchEvent(ev)将事件传到Window中处理。然而Window是一个抽象类,其唯一实现是PhoneWindow,所以我们去PhoneWindow中找superDispatchTouchEvent方法,代码如下:
public boolean superDispatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}
这样就把事件传到DecorView,DecorView其实是继承FrameLayout,也是一个ViewGroup,所以事件就传递到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();
}
先复位状态,做一些初始化操作。比较重要把mFirstTouchTarget 设置为null、清空标志FLAG_DISALLOW_INTERCEPT。
// 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是否拦截事件。有两种情况可以调用是否拦截事件的方法:1、点击事件是ACTION_DOWN,即该次事件序列的开始。2、mFirstTouchTarget != null。当事件由ViewGroup的子控件成功处理时mFirstTouchTarget 会赋值指向该子控件即ViewGroup不拦截事件并将事件交给子控件处理时mFirstTouchTarget != null(所以父ViewGroup开始没有拦截事件,后面也有拦截事件的机会)。满足上面两个条件之一后还需要需要满足子控件没有请求父控件不拦截事件(即子控件没有调用父控件的requestDisallowInterceptTouchEvent,那么FLAG_DISALLOW_INTERCEPT标志也没有置位)
if (!canceled && !intercepted){
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
......
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
......
resetCancelNextUpFlag(child);
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();
newTouchTarget = addTouchTarget(child, idBitsToAssign); // 为mFirstTouchTarget赋值为消耗事件的子控件
alreadyDispatchedToNewTouchTarget = true;
break; // 返回true,那么就会跳出for循环
}
}
}
如果没有拦截,即!intercepted为true就会依次调用子控件的dispatchTouchEvent,在dispatchTransformedTouchEvent中调用的,代码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Perform any necessary transformations and dispatch.
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;
}
从而就完成了事件的分发。
如果dispatchTouchEvent返回true,就满足if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 的条件,就会执行break 那么就会跳出for循环并调用addTouchTarget()。在addTouchTarget中为mFirstTouchTarget赋值为消耗事件的子控件,addTouchTarget代码如下:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果所有的dispatchTouchEvent都返回false,那么mFirstTouchTarget也没有赋值为null,就会调用如下代码:
// 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);
}
因为此时dispatchTransformedTouchEvent的child传的是null,那么就会调用View中定义的dispatchTouchEvent,然后就会调用onTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
}
super.dispatchTouchEvent(event);调用的是View中的dispatchTouchEvent(),View中的dispatchTouchEvent()详细在下节详解,View中的dispatchTouchEvent()最终会调用onTouchEvent()。如果onTouchEvent()返回false,又会执行上一层ViewGroup的下面代码:
// 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);
}
后面的逻辑和上面描述一样。就可以实现第二节例子中ViewGroupB到ViewGroupA中onTouchEvent一层一层向上调用,如下图。
但是ViewC的onTouchEvent什么时候调用的呢?其实View的onTouchEvent调用很简单,就在View的dispatchTouchEvent()中调用,具体逻辑见下一节。
至此ViewGroup的dispatchTouchEvent方法结束。
public boolean dispatchTouchEvent(MotionEvent event) {
......
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;
}
onFilterTouchEventForSecurity是过滤窗口被遮挡时的触摸事件。如果设置了mOnTouchListener就会调用mOnTouchListener.onTouch,如果该方法返回true,消耗了事件就不会调用了onTouchEvent。也就是说mOnTouchListener.onTouch没有消耗事件时才会调用onTouchEvent,这样设计的优点就是外部可以方便处理触摸事件(外部通过实现mOnTouchListener)
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
......
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
......
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
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();
}
}
}
......
}
mIgnoreNextUpEvent = false;
break;
......
}
return true; // 当View的CLICKABLE或者LONG_CLICKABLE为true时onTouchEvent就会返回true消耗事件
}
return false;
}
当View的CLICKABLE或者LONG_CLICKABLE为true时onTouchEvent就会返回true消耗事件。在ACTION_UP时调用performClick。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
......
return result;
}
在performClick中调用mOnClickListener.onClick处理点击事件。
自定义View的onTouchEvent和mOnClickListener.onClick处理点击事件冲突问题:
我们自定义View的触摸滑动事件会重写onTouchEvent,一般不会调用父类的onTouchEvent,即不能调用mOnClickListener.onClick处理点击事件。ScrollView就是这样,可以滑动,但是不能响应点击事件。
如果想自定义View时既可以滑动也可以响应点击事件,可以在MotionEvent.ACTION_UP事件时调用performClick()。但是这样滑动结束时(手抬起)也会回调onClick(),因此需要区分是滑动还是点击再决定是否调用performClick()。可以通过如下方式判断,通过标志isMove判断是不是滑动。
var isMove = false
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
Log.d(TAG, "ACTION_DOWN")
downX = event.x
downY = event.y
isMove = false
}
MotionEvent.ACTION_MOVE -> {
Log.d(TAG, "ACTION_MOVE")
isMove = true // 回调ACTION_MOVE事件就是滑动,不是点击
val moveX: Float = event.x - downX
val moveY: Float = event.y - downY
moveView(moveX.toInt(), moveY.toInt())
}
MotionEvent.ACTION_UP -> // 点击事件的处理
if (!isMove) {
performClick()
} else {
isMove = false
}
}
return true
}
注意:OnTouchListener 优先级比 OnTouchEvent 高,onClickListener 优先级最低。