ViewGroup事件分发机制
上篇文章从源码的角度对View的事件分发进行了分析,这篇文章继续对事件分发进行介绍,从源码的角度分析ViewGroup的事件分发,从继承关系看ViewGroup也属于View的一种,但它的内部可以放置View,简单的结论我就不在文章中利用代码进行说明了,默认大家都知道事件是先到ViewGroup,然后再传递到View的。
事件到达Activity时,会调用Activity#dispatchTouchEvent方法,在这个方法,会把事件传递给Window,然后Window把事件传递给DecorView,而DecorView是什么呢?它其实是一个根View,即根布局,我们所设置的布局是它的一个子View。最后再从DecorView传递给我们的根ViewGroup。
所以在Activity传递事件给ViwGroup的流程是这样的:Activity->Window->DecorView->ViewGroup
由于ViewGroup继承自View所以上篇文章所讲的函数,ViewGroup都有,同事查看源码可以知道,事件分发过程中ViewGroup还有个重要函数onInterceptTouchEvent,Touch事件拦截函数。
实例:
public class MyButton extends Button {
public static final String TAG = "===MyButton";
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
System.out.println(TAG + "MyButton====dispatchTouchEvent");
return super.dispatchTouchEvent(event);
//return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
System.out.println(TAG + "MyButton====onTouchEvent");
return super.onTouchEvent(event);
// return true;
}
}
/**
* Created by user on 17-3-26.
* @author lidongxiu
*/
public class MyLanerlayout extends LinearLayout {
public static final String TAG="MyLanerlayout===";
public MyLanerlayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
System.out.println(TAG + "MyLanerlayout====onInterceptTouchEvent");
return false;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
System.out.println(TAG + "MyLanerlayout====dispatchTouchEvent");
return super.dispatchTouchEvent(event);
//return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
System.out.println(TAG + "MyLanerlayout====onTouchEvent");
return super.onTouchEvent(event);
// return true;
}
}
重写ViewGroup的onInterceptTouchEvent方法,可以在ACTION_DOWN,ACTION_MOVE,ACTION_UP时return true,就能够直接拦截事件,之后就不会在传输到子View了。
public class MainActivity extends AppCompatActivity {
public static final String TAG="MainActivity===";
MyButton button1;
MyLanerlayout mylan;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mylan= (MyLanerlayout) findViewById(R.id.mylan);
button1= (MyButton) findViewById(R.id.button1);
mylan.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println(TAG+"MyLanerlayout===onClick()");
}
});
mylan.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println(TAG+"MyLanerlayout===onTouch()"+"eventAction"+"==="+event.getAction());
return false;
}
});
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println(TAG+"button1===onClick()");
}
});
button1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println(TAG+"button1===onTouch()"+"eventAction"+"==="+event.getAction());
return false;
}
});
}
}
执行结果:
点击空白处:
I/System.out(26582): MyLanerlayout===MyLanerlayout====dispatchTouchEvent
I/System.out(26582): MyLanerlayout===MyLanerlayout====onInterceptTouchEvent
I/System.out(26582): MainActivity===MyLanerlayout===onTouch()eventAction===0
I/System.out(26582): MyLanerlayout===MyLanerlayout====onTouchEvent
I/System.out(26582): MyLanerlayout===MyLanerlayout====dispatchTouchEvent
I/System.out(26582): MainActivity===MyLanerlayout===onTouch()eventAction===1
I/System.out(26582): MyLanerlayout===MyLanerlayout====onTouchEvent
I/System.out(26582): MainActivity===MyLanerlayout===onClick()
点击自定义的按钮:
I/System.out(26582): MyLanerlayout===MyLanerlayout====dispatchTouchEvent
I/System.out(26582): MyLanerlayout===MyLanerlayout====onInterceptTouchEvent
I/System.out(26582): ===MyButtonMyButton====dispatchTouchEvent
I/System.out(26582): MainActivity===button1===onTouch()eventAction===0
I/System.out(26582): ===MyButtonMyButton====onTouchEvent
I/System.out(26582): MyLanerlayout===MyLanerlayout====dispatchTouchEvent
I/System.out(26582): MyLanerlayout===MyLanerlayout====onInterceptTouchEvent
I/System.out(26582): ===MyButtonMyButton====dispatchTouchEvent
I/System.out(26582): MainActivity===button1===onTouch()eventAction===1
I/System.out(26582): ===MyButtonMyButton====onTouchEvent
I/System.out(26582): MainActivity===button1===onClick()
所以点击了Button按钮会首先调用调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。所以还是要分析ViewGroup的dispatchTouchEvent方法。
源码:
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;
}
代码比较长,从分段进行源码分析:
1.初始化down事件
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
先判断事件是否为DOWN事件,如果是则初始化,把mFirstTouchTarget置为null。新的完整事件总是从DOWN开始UP结束,所以如果是DOWN事件,那么说明是一个新的事件序列,所以需要初始化之前的状态。这里的mFirstTouchTarget,当ViewGroup的子元素成功处理事件的时候,mFirstTouchTarget会指向子元素。
2检查ViewGroup是否要拦截事件
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是否要拦截事件,定义了一个布尔值intercept来记录是否要进行拦截。
首先执行了这个语句:if(actionMasked == MotionEvent.ACTION_DOWN ||mFirstTouchTarget != null),如果事件是DOWN或者mFirstTouchTatget值不为空的时候,才有可能继续往下执行,否则会直接跳过判断是否拦截。为什么要有这个判断呢?如果子View消耗了ACTION_DOWN事件,然后这里可以由ViewGroup继续判断是否要拦截接下来的ACTION_MOVE事件之类的;但是如果第一次DOWN事件最终不是由子View消耗掉的,那么显然mFirstTouchTarget将为null,所以也就不用判断了,直接把intercept设置为true,此后的事件都是由这个ViewGroup处理。
。
源码中如果disallowIntercept为假,进入判断,disallowIntercept是指是否禁用掉事件拦截的功能,
默认是false,所以不会禁用掉事件拦截,所以要看onInterceptTouchEvent方法,默认返回false,
则intercepted=false,所以默认不拦截所有的事件,重写onInterceptTouchEvent方法,
返回true则intercepted为true,则会拦截后续的事件,后续的事件不会再执行。
if (!canceled && !intercepted) 内部的判定是如果满足条件,只要ViewGroup有孩子,
就交给它的孩子去处理,内部循环遍历了当前ViewGroup下的所有子View,
判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,
然后调用了该View的dispatchTouchEvent,之后的流程就和后面的过程就是上篇文章讲的View的事件传递机制了。
ViewGroup默认不拦截任何事件,但有时特定的ViewGroup会在ACTION_MOVE时拦截掉事件,看源代码会发现还有一个FLAG_DISALLOW_INTERCEPT标志位,这个标志位的作用是禁止ViewGroup拦截除了DOWN之外的事件,一般通过子View的requestDisallowInterceptTouchEvent来设置。重写子类的onTouchEvent()方法,在里面调用getParent().requestDisallowInterceptTouchEvent(true)方法就不会拦截事件了。当传入的参数为true时,表示子组件要自己消费这次事件,告诉父组件不要拦截(抢走)这次的事件。此种情况可以举个例子,如果在滑动控件里面添加一个点击控件,当点击时,里面的控件可以正常处理点击事件,父控件也不会拦截,但是如果想点击在子类控件位置,滑动子控件时,这时父类的控件可以滑动,事件传递到父类控件时可能会被父类消耗掉,以后的事件就不会传递到子类了,在子类设置getParent().requestDisallowInterceptTouchEvent(true)可以告诉父类我要自己消耗事件,不要拦截。
如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN里面直接return true了,那么子View是捕获不到事件的设置getParent().requestDisallowInterceptTouchEvent(true)也没有用,因为不会传递到子View的dispatchTouchEvent。只有ViewGroup的onInterceptTouchEvent(ev) ACTION_DOWN时不拦截,在ACTION_MOVE时拦截才能利用getParent().requestDisallowInterceptTouchEvent(true)让ViewGroup不拦截事件。
3 对ACTION_DWON事件的特殊处理
TouchTarget newTouchTarget = null; // 1boolean alreadyDispatchedToNewTouchTarget = false;if (!canceled && !intercepted) {
(1)
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
(2)
}
}
判断if(!canceled && !intercepted),表示如果不是取消事件以及ViewGroup不进行拦截则进入(1),接着又是一个判断if (actionMasked == MotionEvent.ACTION_DOWN …)这表示事件是否是ACTION_DOWN事件,如果是则进入(2),根据以上两个条件,事件是ACTION_DOWN以及ViewGroup不拦截,那么(2)内部应该是把事件分发给子View。
4子View对事件的处理
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;
}
分发事件给子view进行处理。循环对所有的子View进行遍历,由于ViewGroup不对事件进行拦截,在ViewGroup内部对子View进行遍历,找到能接受事件的子View,从最上层的子View开始往内层遍历。然后根据方法名字我们得知这个判断语句是判断触摸点位置是否在子View的范围内或者子View是否在播放动画,如果均不符合则continue,表示这个子View不符合条件,开始遍历下一个子View。
接着调用了dispatchTransformedTouchEvent()方法,交给子view处理事件,然后得到是否子View 消耗了事件。当传递进来的的child不为null时,就会调用子View的dispatchTouchEvent(event)方法,表示把事件交给子View处理,也即是说,子Viwe符合所有条件的时候,事件就会在这里传递给了子View来处理,完成了ViewGroup到子View的事件传递,当事件处理完毕,就会返回一个布尔值handled,该值表示子View是否消耗了事件。怎样判断一个子View是否消耗了事件呢?如果说子View的onTouchEvent()返回true,那么就是消耗了事件。
如果子View消耗了事件那么最后便会执行addTouchTarget()方法。
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果子View消耗掉了事件,那么mFirstTouchTarget就会指向子View。在执行完后,直接break了,表示跳出了循环,因为已经找到了处理事件的子View,所以无需继续遍历了。
但是如果我们没有点击任何ViewGroup内的控件,只是触摸了自定义的ViewGroup,不会处理任何的子view事件,会去执行ViewGroup的dispatchTouchEvent方法,最终会执行到ViewGroup的onTouch或者onClick事件。
总结:上述过程很复杂,能看懂更好,不能看懂也没问题,代码内部做了很多判断,这里做个简单的总结,Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。当我们点击viewGroup中的控件时,首先执行viewGroup的dispatchTouchEvent方法,内部判断是否通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。如果对传递到子View进行拦截,则会执行自定义ViewGroup的dispatchTouchEvent方法,最终处理自定义ViewGroup的onTouch事件。如果事件没有被拦截,则遍历ViewGroup内部的子View找到可以接收事件的view,调用view的DispatchTouchEvent方法,处理ontouch事件或者点击事件,终止事件的传递。但是如果我们没有点击任何ViewGroup内的控件,只是触摸了自定义的ViewGroup,不会处理任何的子view事件,会去执行ViewGroup的dispatchTouchEvent方法,最终会执行到ViewGroup的onTouch或者onClick事件。
总之就是ViewGroup默认不拦截任何事件,所以事件能正常分发到子View处(如果子View符合条件的话),如果没有合适的子View或者子View不消耗ACTION_DOWN事件,那么接着事件会交由ViewGroup处理,并且同一事件序列之后的事件不会再分发给子View了。如果ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的话,那么最后事件会交由Activity处理。即:逐层分发事件下去,如果都没有处理事件的View,那么事件会逐层向上返回。