触摸事件的传递过程
触摸事件的传递过程
事件传递过程涉及的几个API如下:
- dispatchTouchEvent : 分发事件。如果返回true,表示事件分发下去后被处理了;返回false,则表示分发下去后没有被任何view处理。
- onInterceptTouchEvent :拦截事件。如果返回true,则表示拦截事件。如果返回false,则表示不拦截。这里拦截的是本来要传给子View的事件,所以这个方法是ViewGroup独有的。
- onTouchEvent : 处理事件。如果返回true,则表示处理事件,如果返回false 则表示不处理事件。
事件传递首先从父容器ViewGroup开始,父容器调用dispatchTouchEvent 分发事件,在该方法内部会调用onInterceptTouchEvent 判断是否为拦截事件,如果返回为true,表示对事件进行拦截,事件会直接传递给VIewGroup的onTouchEvent方法。如果返回为false(默认就是false),那么事件就会传递给View的dispatchTouchEvent 方法,View中没有onInterceptTouchEvent 方法,会直接调用onTouchEvent的方法处理事件。 如果View的onTouchEvent 的方法返回true,那么表示事件被处理,事件传递结束。
如果返回false 表示不处理,事件又会传递给父容器ViewGroup的onTouchEvent 方法处理。
事件传递细节
事件传递和Activity的关系
触摸事件是由Activity传递给DecorView的。
Activity中dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//获取到Activity的PhoneWindow,将事件传递给其中的DecorView,
//DecorView再将事件分发给他的子View
if (getWindow().superDispatchTouchEvent(ev)) {
//如果事件分发下去有子View进行消费,就直接返回true
return true;
}
//如果DecorView将事件分发下去后没有任何View处理,就会交给Activity的onTouchEvent 处理
return onTouchEvent(ev);
}
这就进入了ViewGroup的方法中了。
ACTION_DOWN 没有被处理后
ACTION_DOWN 没有被处理后的其他事件会被DecorView拦截。判断逻辑:
// 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;
}
这段代码用于检查是否拦截子View事件的。当actionMasked !=MotionEvent.ACTION_DOWN 并且mFirstTouchTarget == null 是才会拦截事件。
mFirstTouchTarget 是TouchTarget 对象,用来保存处理触摸事件View 以及触摸点的id值,当ACTION_DOWN 事件分发下去后,如果有子View处理事件,也就是其onTouchEvent返回了true,那么ViewGroup 就会处理这个事件的子View用mFirstTouchTarget 保存。如果没有子View处理,那么mFirstTouchTarget 就不会被赋值。
结论:如果 ACTION_DOWN 分发下去没有被处理,会造成.mFirstTouchTarget ==null 后续事件ACTION_MOVE 和 ACTION_UP 就满足了actionMasked !=MotionEvent.ACTION_DOWN ,将导致这些事件被拦截掉。ViewGroup自己处理对应的事件。
点击位置没有子View时的处理
在dispatchTouchEvent时,会去检查子View的可见性及子View所处的位置。
检查子view的可见性
/**
* Returns whether this view can receive pointer events.
*
* @return {@code true} if this view can receive pointer events.
* @hide
*/
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
检查子View所在的位置
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
@UnsupportedAppUsage
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempLocationF();
point[0] = x;
point[1] = y;
//将触摸事件位置转换成子View为坐标系的位置
transformPointToViewLocal(point, child);
//判断转换后的位置(X,Y) = (point[0],point[1]) 是否在子View的内部。
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
/**
* @hide
*/
@UnsupportedAppUsage
public void transformPointToViewLocal(float[] point, View child) {
point[0] += mScrollX - child.mLeft;
point[1] += mScrollY - child.mTop;
if (!child.hasIdentityMatrix()) {
child.getInverseMatrix().mapPoints(point);
}
}
/**
* Utility method to determine whether the given point, in local coordinates,
* is inside the view, where the area of the view is expanded by the slop factor.
* This method is called while processing touch-move events to determine if the event
* is still within the view.
* 此方法在View类中
* @hide
*/
@UnsupportedAppUsage
public boolean pointInView(float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
点击位置有多个子View重叠的处理
ViewGroup会将所有的子View存入到一个数组中,默认是按照添加进这个ViewGroup的顺序排序,在ViewGroup的dispatchTouchEvent 的方法中,会调用buildTouchDispatchChildList() 方法对子View进行排序,在默认的排序基础上,先根据子View的z值大小,再根据绘制顺序进行调整,Z值越大,绘制顺序越靠前的子View会排在数组的最后面。Z值是主要因素,Z值一致时,才会根据绘制顺序排序。在事件传递时,会从数组的后面往前遍历所有的子View,直到有子View消费事件。
总结下来基本就最后加入ViewGroup的子View最先获取到事件。
拦截子View事件的处理
触摸事件坐标的转换
因为在一个触摸事件的坐标在ViewGroup和子View的中的坐标可能不一样,怎么进行坐标的转换的?
在ViewGroup的dispatchTouchEvent 方法中会调用dispatchTransformedTouchEvent 方法来转换坐标并将事件传递给子View。
onTouch 和 onTouchEvent 的关系
处理触摸事件通常有两种做法:一是给控件设置触摸事件的监听器,二是覆写onTouchEvent() 方法,那么监听器中的onTouch方法和View本身的onTouchEvent 方法 都是处理事件,他们之间的有什么区别和联系呢?
View在事件分发时,在监听器不为空的情况下,会先调用触摸事件监听器的onTouch 方法,如果onTouch 返回true,表示事件在监听器中消费了,result 会被标记成true,那么后面就不会调用onTouchEvent方法,反之则会调用。