ViewGroup
就是一组View
的集合,它包含很多的子View
和子VewGroup
,是Android中所有布局的父类或间接父类,像LinearLayout
、RelativeLayout
等都是继承自ViewGroup
的。但ViewGroup
实际上也是一个View
,只不过比起View,它多了可以包含子View
和定义布局参数的功能。
本文通过源码(api:10)的形式介绍ViewGroup的事件分发机制。
一.onInterceptTouchEvent
代码片1
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way. Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it). Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
onInterceptTouchEvent()
的注释很长,但源码很短,短得只有1行。
onInterceptTouchEvent()
的作用是是否拦截事件。
a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent()
,从而让其返回true)
b. 返回false = 不拦截(默认)
二.dispatchTouchEvent
1. ACTION_DOWN事件
public boolean dispatchTouchEvent(MotionEvent ev) {
... // 仅贴出关键代码
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// 重点分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false)
// ,可通过调用requestDisallowInterceptTouchEvent()修改
// 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
// b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
// c. 关于onInterceptTouchEvent() ->>在一中onInterceptTouchEvent有分析
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 重点分析2
// 通过for循环,遍历了当前ViewGroup下的所有子View
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);
// 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
// 若是,则进入条件判断内部
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 条件判断的内部调用了该View的dispatchTouchEvent()
// 即 实现了点击事件从ViewGroup到子View的传递(即:View的事件分发机制)
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
// 调用子View的dispatchTouchEvent后是有返回值的
// 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true(由view的事件分发可知),因此会导致条件判断成立
// 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即把ViewGroup的点击事件拦截掉
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
// 重点分析3
// 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
// 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
// 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick()
// ,即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())
// 此处需与上面区别:子View的dispatchTouchEvent()
}
...
}
2. ACTION_MOVE事件
@Override
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;
//...ACTION_DOWN
//...ACTIN_UP or ACTION_CANCEL
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
//....
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
18行:把ACTION_DOWN
时赋值的mMotionTarget
,付给target
;
23行:if (!disallowIntercept && onInterceptTouchEvent(ev))
当前允许拦截且拦截了,才进入IF体,当然了默认是不会拦截的~这里执行了onInterceptTouchEvent(ev)
28-30行:把坐标系统转化为子View
的坐标系统
32行:直接return target.dispatchTouchEvent(ev)
;
可以看到,正常流程下,ACTION_MOVE
在检测完是否拦截以后,直接调用了子View.dispatchTouchEvent
,事件分发下去;
3. ACTION_UP事件
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
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) {...}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if(target ==null ){...}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {...}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
17行:判断当前是否是ACTION_UP
21,28行:分别重置拦截标志位以及将DOWN
赋值的mMotionTarget
置为null
,都UP
了,当然置为null
,下一次DOWN
还会再赋值的~
最后,修改坐标系统,然后调用target.dispatchTouchEvent(ev)
;
正常情况下,即我们上例整个代码的流程我们已经走完了:
1、ACTION_DOWN
中,ViewGroup
捕获到事件,然后判断是否拦截,如果没有拦截,则找到包含当前x,y坐标的子View,赋值给mMotionTarget
,然后调用
mMotionTarget.dispatchTouchEvent(ev)
2、ACTION_MOVE中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)
3、ACTION_UP中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)
当然了,在分发之前都会修改下坐标系统,把当前的x,y分别减去child.left
和 child.top
,然后传给child
;
三.关于拦截
1.如何拦截
上面的总结都是基于:如果没有拦截;那么如何拦截呢?
复写ViewGroup
的onInterceptTouchEvent
方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
//如果你觉得需要拦截
return true ;
case MotionEvent.ACTION_MOVE:
//如果你觉得需要拦截
return true ;
case MotionEvent.ACTION_UP:
//如果你觉得需要拦截
return true ;
}
return false;
}
默认是不拦截的,即返回false;如果你需要拦截,只要return true
就行了,这样该事件就不会往子View
传递了,并且如果你在DOWN
事件 retrun true
,则DOWN
、MOVE
、UP
子View
都不会捕获事件;如果你在MOVE事件 return true
, 则子View在MOVE
和UP
都不会捕获事件。
原因很简单,当onInterceptTouchEvent(ev) return true
的时候,会把mMotionTarget
置为null
;
2.如何不被拦截
如果ViewGroup
的onInterceptTouchEvent(ev)
当ACTION_MOVE
时return true
,即拦截了子View
的MOVE
以及UP
事件;
此时子View
希望依然能够响应MOVE
和UP
时该咋办呢?
Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean)
用于设置是否允许拦截,我们在子View
的dispatchTouchEvent
中直接这么写:
@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
getParent().requestDisallowInterceptTouchEvent(true);
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
getParent().requestDisallowInterceptTouchEvent(true)
; 这样即使ViewGroup
在MOVE
的时候return true
,子View
依然可以捕获到MOVE
以及UP
事件。
从源码也可以解释:
ViewGroup MOVE
和UP
拦截的源码是这样的:
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
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;
}
当我们把disallowIntercept
设置为true
时,!disallowIntercept
直接为false
,于是拦截的方法体就被跳过了~
注:如果ViewGroup
在onInterceptTouchEvent(ev)
ACTION_DOWN
里面直接return true
了,那么子View
是木有办法的捕获事件的~~~
3、如果没有找到合适的子View
我们的实例,直接点击ViewGroup
内的按钮,当然直接很顺利的走完整个流程;
但是有两种特殊情况
1、ACTION_DOWN
的时候,子View.dispatchTouchEvent(ev)
返回的为false
;
如果你仔细看了,你会注意到ViewGroup
的dispatchTouchEvent(ev)
的ACTION_DOWN
代码是这样的
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
只有在child.dispatchTouchEvent(ev)
返回true
了,才会认为找到了能够处理当前事件的View
,即mMotionTarget = child;
但是如果返回false
,那么mMotionTarget
依然是null
mMotionTarget
为null
会咋样呢?
其实ViewGroup
也是View
的子类,如果没有找到能够处理该事件的子View
,或者干脆就没有子View
;
那么,它作为一个View
,就相当于View
的事件转发了~~直接super.dispatchTouchEvent(ev);
源码是这样的:
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);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
我们没有一个能够处理该事件的目标元素,意味着我们需要自己处理~~~就相当于传统的View
~
2、那么什么时候子View.dispatchTouchEvent(ev)
返回的为true
如果你仔细看了上篇博客,你会发现只要子View支持点击或者长按事件一定返回true~~
源码是这样的:
if(((viewFlags &CLICKABLE)==CLICKABLE ||(viewFlags &LONG_CLICKABLE)==LONG_CLICKABLE)){
return true;
}
四、总结
关于代码流程上面已经总结过了~
1、如果ViewGroup
找到了能够处理该事件的View
,则直接交给子View
处理,自己的onTouchEvent
不会被触发;
2、可以通过复写onInterceptTouchEvent(ev)
方法,拦截子View
的事件(即return true
),把事件交给自己处理,则会执行自己对应的onTouchEvent
方法;
3、子View
可以通过调用getParent().requestDisallowInterceptTouchEvent(true)
阻止ViewGroup
对其MOVE
或者UP
事件进行拦截;
现在整个ViewGroup
的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧。
1.Android事件分发是先传递到ViewGroup
,再由ViewGroup
传递到View
的;
2.在ViewGroup中可以通过onInterceptTouchEvent
方法对事件传递进行拦截,onInterceptTouchEvent
方法返回true
代表不允许事件继续向子View
传递,返回false
代表不对事件进行拦截,默认返回false
;
3.子View
中如果将传递的事件消费掉,ViewGroup
中将无法接收到任何事件。