上次已经从源码角度分析了View的事件分发,如果对View的事件分发不熟悉请先阅读:
View事件分发
今天要说的是ViewGroup事件分发。
1.ViewGroup是什么
从源码中可以看到,她是一个abstract的类,父类是View,头部注释:
* <p>
* A <code>ViewGroup</code> is a special view that can contain other views
* (called children.) The view group is the base class for layouts and views
* containers. This class also defines the
* {@link android.view.ViewGroup.LayoutParams} class which serves as the base
* class for layouts parameters.
* </p>
注释告诉我们,ViewGroup是一个特殊的View。哪里特殊呢?
第一:她可以包含多个子View和子ViewGroup,是所有布局的直接或间接父类。
第二:她定义了布局参数类:ViewGroup.LayoutParams。这两个特殊点也就是和View的区别。
常用的LinearLayout、RelativeLayout都是继承自ViewGroup。
2.深入
上次先分析了View的onTouch和onClick方法,这次同样用这两个方法来分析ViewGroup。
为了方便我们打log,我们不能直接用ViewGroup或者LinearLayout,我们先自定义一个布局TestDispatchEventLayout,继承LinearLayout,如下:
public class TestDispatchEventLayout extends LinearLayout {
public TestDispatchEventLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
}
然后在Activity的布局中使用我们自定义的布局,并且在其中加两个Button(为什么用Button,因为Button是默认可点击的,对onTouchEvent事件有影响,具体参考上一篇文章)如图所示:
<?xml version="1.0" encoding="utf-8"?>
<com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button2" />
</com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout>
在Activity中给TestDispatchEventLayout和两个Button分别注册监听:
layout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ALog.d("layout onTouch");
return false;
}
});
button1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ALog.d("button1 onTouch action = " + event.getAction());
return false;
}
});
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ALog.e("button1 onClick");
}
});
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ALog.e("button2 onClick");
}
});
我们给button1加了一个onTouch监听,来回顾一下上一次说的View点击事件:事件先传递给onTouch,如果onTouch返回false(事件没有被onTouch消费掉),则事件继续传递给onClick。
运行程序,点击Button1,打印结果如下:
button1 onTouch action = 0
button1 onTouch action = 2
button1 onTouch action = 1
button1 onClick
如果把onTouch返回true,理论上应该只有onTouch的打印,没有onClick的打印,重新运行后点击Button1,打印结果如下:
button1 onTouch action = 0
button1 onTouch action = 2
button1 onTouch action = 1
打印正确。把onTouch返回改回false。
分别点击Button2和空白区域,打印结果如下:
button2 onClick
layout onTouch
结合以上所有打印结果,我们可以发现几个现象:
1、点击按钮的时候,layout的onTouch没有执行。
2、点击空白区域的时候,layout的onTouch执行。
暂时得出假设:onTouch先传递子View,再传递给ViewGroup,并且子View会把事件消费掉。
验证假设:
首先,要知道ViewGroup中的一个方法:onInterceptTouchEvent,看源码:
/**
* 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) {
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;
}
注释告诉我们这个方法用来拦截所有的触摸屏幕动作事件。并且允许你在事件被分配给你的子view或者子viewgroup时进行监视,你可以在任何时候去拦截掉事件。
打个比方:父控件把up事件拦截,子控件会处理到down和move,但是没有up触发不了onClick。
既然这个方法是有返回值的,那么我们重写onInterceptTouchEvent方法,改变返回值来看一下效果,先改为返回false,如下:
public class TestDispatchEventLayout extends LinearLayout {
public TestDispatchEventLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
}
运行程序,分别点击Button1、Button2和空白区域。打印如下:
button1 onTouch action = 0
button1 onTouch action = 1
button1 onClick
button2 onClick
layout onTouch
再把onInterceptTouchEvent返回值改为true,运行程序,分别点击Button1、Button2和空白区域。打印如下:
layout onTouch
layout onTouch
layout onTouch
可以看出,不管我们点击哪里,都只触发layout的onInterceptTouchEvent,按钮的onTouch和onClick被屏蔽了。这说明事件应该是先传递给ViewGroup,再传递给View,和我们上面的假设相反,说明上面的假设是错误的。
重新假设:事件先传递给ViewGroup,再传递给View。如果ViewGroup不拦截事件,事件将由View消费。
验证假设:
上次文章中我们说了只要我们触摸了控件,一定会调用该控件的dispatchTouchEvent方法,那么既然ViewGroup也是集成View的,它肯定也会执行dispatchTouchEvent方法。让我们来看一下ViewGroup中dispatchTouchEvent源码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
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.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
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.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
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;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
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);
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;
}
}
}
// 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.
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;
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;
}
可以看出这个方法是重写父类View中的dispatchTouchEvent。方法很长只看关键部分:
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();
}
首先,当事件是ACTION_DOWN的时候,ViewGroup的dispatchTouchEvent中会清除所有的上一个手势的状态。
然后是事件拦截:
// 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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
先判断是否是ACTION_DOWN事件或者有首次触摸目标,
false:表示没有首次触摸目标,或者这个事件不是初始的DOWN事件,那么只能说明DOWN已经被自己消费了,所以ViewGroup继续拦截事件,于是intercepted = true;
true:继续判断是否允许拦截(ViewGroup中disallowIntercept默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改),不允许intercepted = false; 允许拦截intercepted = onInterceptTouchEvent(ev);
可以看到ViewGroup的onInterceptTouchEvent方法在这里被调用了,也就是说我们之前在onInterceptTouchEvent返回true,这里的intercepted就会被赋值为true。
继续往下看:if (!canceled && !intercepted),如果条件满足:不是取消并且不拦截,进入到条件内部,
继续判断:当事件是按下或者多点按下或者鼠标在View上时,会倒序遍历所有的子View,(为什么要倒序,因为安卓的布局机制是后加进来的view会显示在上层,可以想象一下圆筒包装的薯片,先装进去的薯片在底层,后装进去的薯片在上层。如果点击的位置有很多层view,那么最上层的薯片来响应事件。)
然后通过getTouchTarget拿到的触摸目标对应的子View,如果不为空,则跳出循环遍历。否则继续向下执行:if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
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);
}
// 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方法,如果child是ViewGroup递归调用dispatchTouchEvent,如果child是View,默认会调用其onTouchEvent()。
如果dispatchTransformedTouchEvent返回true说明子View消费掉了该事件,同时进入该if判断,执行下面操作:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
给newTouchTarget赋值,mFirstTouchTarget = newTouchTarget,alreadyDispatchedToNewTouchTarget设置为true,并且跳出循环。
如果dispatchTransformedTouchEvent返回false,说明没有子View消费该事件,mFirstTouchTarget为空,递归后就会让intercepted为true,事件将被ViewGroup拦截,不会再传递给子View,那么该子View就无法继续处理ACTION_MOVE事件和ACTION_UP事件。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null)
可以看到,如果一开始事件不是ACTION_DOWN,就不会经过上面的流程,直接执行下面的流程。上面对ACTION_DOWN处理后mFirstTouchTarget可能是空可能不为空,所以下面的流程会有if (mFirstTouchTarget == null),判断了mFirstTouchTarget值是否为null的情况。
当mFirstTouchTarget==null时,也就是事件未被子View消费,则调用super.dispatchTouchEvent处理事件,和普通View一样了。
所以之前点击Button的时候只打印了Button的onTouch和onClick,却没有打印layout的onTouch。因为Button的onTouchEvent返回的是true,所以dispatchTransformedTouchEvent返回true,因此mFirstTouchTarget !=null,导致intercepted为false,ViewGroup不拦截事件,事件由子View Button消费。
当mFirstTouchTarget !=null 时,也就是说找到了消费事件的子View,后续的事件可以继续传递给该子View处理。
到此ViewGroup的dispatchTouchEvent方法分析完毕。
3.浅出
基于上面的分析,如果我们把TestDispatchEventLayout的继承改为RelativeLayout,并且onInterceptTouchEvent返回false,然后Activity的布局改为Btn1在Btn2的上层:
<com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button2" />
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1" />
</com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout>
其他不变,理论上的结果应该只打印Btn1的事件。 运行程序,点击按钮,打印结果如下:
button1 onTouch action = 0
button1 onTouch action = 1
button1 onClick
果然没有button2的打印了。
总结:
1、事件派发由ViewGroup,再传递到View;
2、ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,决定事件是否传递给子View;
3、子View中如果将传递的事件消费掉,ViewGroup中将不会再处理该事件。
这里在捎带提一下View中的dispatchTouchEvent,简单记作:enable决定执行onTouch,onTouch为false决定执行onTouchEvent,onTouchEvent决定dispatchTouchEvent返回值
最后来做几个实验:
1、首先自定义TestDispatchEventLayout 继承 RelativeLayout 代码如下所示:
public class TestDispatchEventLayout extends RelativeLayout {
public TestDispatchEventLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
}
2、接着,在自定义布局下面加上2个ImageView,id分别为button1和button2(取名button方便改为button,但是记住实际是ImageView,也就是说默认不是可点击的):
<com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimaryDark"
android:text="Button2" />
<ImageView
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@mipmap/ic_launcher"
android:text="Button1" />
</com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout>
3、然后给layout和2个控件都加上setOnTouchListener监听,并且打印,如下:
ImageView button1;
ImageView button2;
TestDispatchEventLayout layout;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_layout_inflate_view);
button1 = (ImageView) findViewById(R.id.button1);
button2 = (ImageView) findViewById(R.id.button2);
layout = (TestDispatchEventLayout) findViewById(R.id.main_layout);
ALog.i("layout.getChildCount() = " + layout.getChildCount());
layout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ALog.i("layout onTouch action = " + event.getAction());
return false;
}
});
button1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ALog.d("button1 onTouch action = " + event.getAction());
return false;
}
});
button2.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ALog.e("button2 onTouch action = " + event.getAction());
return false;
}
});
}
因为是继承RelativeLayout,所以button1层级是堆叠在button2之上,如图:
现在我们点击ImageView,会打印什么?
按上面的理论分析:Down事件先到达layout的dispatchTouchEvent,因为没有设置intercepet为true,所以layout不拦截事件,传递给她的子控件,最上层子控件是button1,所以button1拿到down事件进入dispatchTouchEvent,因为我们注册了button1的onTouch,但是没有返回true,所以button1会打印一次onTouch--Down,然后执行button1的onTouchEvent,又因为button1实际是ImageView,默认不可点击,所以onTouchEvent直接返回false,事件继续向下传递给button2,button2同理只会打印一次onTouch--Down,继续把事件传递给layout,layout会打印onTouch--Down,因为没有把onTouch返回true,而且默认也是不可点击,所以也没法继续执行后续的onTouch--Up的事件,事件到此结束。
运行后打印结果为:
layout.getChildCount() = 2
button1 onTouch action = 0
button2 onTouch action = 0
layout onTouch action = 0
理论分析正确。
用图来表示:
如果我们把中间的那个ImageView注册onClick事件(setOnClickListener中会把控件改为可点击状态),也就是button2,会怎么打印?
理论分析:Down事件先到达layout的dispatchTouchEvent,因为没有设置intercepet为true,所以layout不拦截事件,传递给她的子控件,最上层子控件是button1,所以button1拿到down事件进入dispatchTouchEvent,因为我们注册了button1的onTouch,但是没有返回true,所以button1会打印一次onTouch--Down,然后执行button1的onTouchEvent,又因为button1实际是ImageView,默认不可点击,所以onTouchEvent直接返回false,事件继续向下传递给button2,button2会打印一次onTouch--Down,然后执行button2的onTouchEvent,这次button2是可点击的,onTouchEvent返回true,事件被button2消费,于是执行后续的Move、Up事件,所以会继续打印button2的onTouch--Move,onTouch--Up,onClick,事件到此结束,不会再传递给layout。
运行后打印结果:
layout.getChildCount() = 2
button1 onTouch action = 0
button2 onTouch action = 0
button2 onTouch action = 1
button2 onClick
理论分析正确。
现在我们来把onTouchEvent事件也打印出来,在上面的基础上改动如下:
创建一个自定义ImageView:
public class MyImageView extends ImageView {
public MyImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
ALog.e("onTouchEvent action = " + event.getAction());
return super.onTouchEvent(event);
}
}
在onTouchEvent方法中加入打印。
之前的布局最上层的ImageView也就是button1改成用MyImageView:
<?xml version="1.0" encoding="utf-8"?>
<com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimaryDark"
android:text="Button2" />
<com.example.xuchun.myapplication.view.MyImageView
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@mipmap/ic_launcher"
android:text="Button1"
/>
</com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout>
activity中的代码改动如下:
ALog.i("layout.getChildCount() = " + layout.getChildCount());
layout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ALog.i("layout onTouch action = " + event.getAction());
return false;
}
});
button1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ALog.d("button1 onTouch action = " + event.getAction());
return false;
}
});
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ALog.d("button1 onClick");
}
});
button2.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ALog.e("button2 onTouch action = " + event.getAction());
return false;
}
});
这时候点击button1会怎么打印?
理论分析:Down事件先到达layout的dispatchTouchEvent,因为没有设置intercepet为true,所以layout不拦截事件,传递给她的子控件,最上层子控件是button1,所以button1拿到down事件进入dispatchTouchEvent,因为我们注册了button1的onTouch,但是没有返回true,所以button1会打印一次onTouch--Down,然后执行button1的onTouchEvent,因为注册了onClick,所以button1是可点击状态,于是onTouchEvent会返回true,消费事件,后续的事件继续执行:onTouch--Move,onTouchEvent--Move,onTouch--Up,onTouchEvent--Up,onClick,事件传递结束,不会再传给button2和layout。
运行程序,打印如下:
button1 onTouch action = 0
onTouchEvent action = 0
button1 onTouch action = 2
onTouchEvent action = 2
button1 onTouch action = 1
onTouchEvent action = 1
button1 onClick