前言
View事件分发机制是Android面试中备受关注的问题,今天我们就来扒开其表象,一起探究下他的本质。
TouchEvent的产生与封装
当屏幕被触摸, 硬件设备会产生触摸事件传入内核层以及Framework层, 最后经过一系列事件处理到达ViewRootImpl的processPointEvent, 至于这中间的过程不是本次讨论的重点, 有想了解的小伙伴可参考从根源上看屏幕点击事件是如何传递到View中的, 当事件到达ViewRootImpl的processPointEvent方法之后调用mView.dispatchPointEvent(ev)开始对事件进行分发过程, 而此时的mView就是DecroView, dispatchPointEvent(ev)方法是其子类View的方法, 在View.dispatchPointEvent(ev)方法中对事件进行判断是否TouchEvent, 并将其封装成TouchEvent继续进行下一步的分发。 DecroView重写了dispatchTouchEvent(ev)方法, 并且在这里会获取到当前Window.CallBack(Window.Callback 是个接口,而 Activity 和 Dialog 都实现了这个接口)继而事件正式进入Activity, 这里如果不了解可以看下下面的Activity、Window与DecorView的之间的关系。 从点击屏幕到事件分发到Activity的过程如下:
//ViewRootImpl.java
private int processPointerEvent(QueuedInputEvent q) {
boolean handled = mView.dispatchPointerEvent(event);//mView是DecroView
}
//View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
//DecroView.java
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();//Window.Callback 是个接口,而 Activity 和 Dialog 都实现了这个接口
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
关于Activity、Window、DecroView之间的关系
三者关系图如下:
Activity与Window关联
在Activity.attach()方法中会创建PhoneWindow, 并通过setCallBack()方法将自己传入Window中, 这样Activity和Window实现了互相持有。
//Activity.java
final void attach(){
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setCallback(this);//将Activity传入Window中
}
Window与DecroView的关联
setContentView()经过一系列调用进入AppCompatDelegateImpl.setContentView(resId)中有一个ensureSubDecro()方法, 如果DecorView还没有创建就会进入createSubDecor()方法, 在这里会进行DecorView的创建以及与Window的绑定工作。
//MainActivity.java
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)
}
//AppCompatActivity.java
@Override
public void setContentView(@LayoutRes int layoutResID) {
initViewTreeOwners();
getDelegate().setContentView(layoutResID);
}
//AppCompatDelegateImpl.java
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
}
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
}
}
private ViewGroup createSubDecor() {
......
mWindow.getDecorView();
......
mWindow.setContentView(subDecor);
}
//PhoneWindow.java
@Override
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);
}else{
mDecor.setWindow(this);
}
}
protected DecorView generateDecor(int featureId) {
...
return new DecorView(context, featureId, this, getAttributes());//把Window传入DecorView中
}
TouchEvent在Activity中的分发过程
注意: 本次事件分发过程中的View布局为: xml文件下一个占满布局的OuterViewGroup, OuterViewGroup里面有一个占50%的InnerViewGroup, 要求在X轴滑动距离大于Y轴的时间InnerViewGroup响应, 否则OuterViewGroup响应。
通过前面的一系列过程,Touch事件已经到达MainActivity.dispatchTouchEvent(ev)方法中,里面直接调用super.disptachTouchEvent()既进入其父类Activity的dispatchTouchEvent()方法中, 在这里就可以看出: 如果getWindow().superDispatchTouchEvent(ev)返回true那么其直接放回true, 并且onTouchEvent()方法就不会被执行, 否则就会进入onTouchEvent()方法, 并返回其执行结果。
TouchEvent在Activity中的分发过程如下:
//MainActivity.java
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
return super.dispatchTouchEvent(ev)
}
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
TouchEvent在ViewGroup中的分发过程
DOWN事件的分发过程
上面的getWindow()方法返回Window, 它唯一的实现类就是PhoneWindow, 进入PhoneWindow的superDispatchTouchEvent()方法只是调用了mDecor.superDispatchTouchEvent(), 而DecorView的superDispatchTouchEvent()中只是调用了其父类ViewGroup.java的dispatchTouchEvent(ev)方法。
当ACTION_DOWN进来时候, 先去判断一下拦截有没有被disllow, 如果没有会进入onInterceptTouchEvent(ev)判断是否拦截, DecorView重写的onInterceptTouchEvent(ev)方法会被执行, 这里会对点击的区域做出判断是否越界, 默认情况下会返回false既不拦截, 此时为DOWN事件, canceled以及intercept都为false, 并且newTouchTarget == null, 此时会进入子View的倒序遍历分过过程(这里会判断子View是否可以触摸等判断), 会将事件通过dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法分发给子View(此时的child为OuterViewGroup), 进而继续调用OuterViewGroup的dispatchTouchEvent(ev)方法, 至此事件分发到OuterViewGroup中。
OuterViewGroup.dispatchTouchEvent(ev)方法只是调用其父类ViewGroup的分发方法, 其分发过程和上面基本一样, 只是重写了onInterceptTouchEvent(ev)方法, 在Down事件的时候记录下初始坐标的x,y位置并调用父类的拦截方法, 由于ViewGroup.onInterceptTouchEvent()默认返回false, 所以Down事件直接返回false, 后续来了move事件, 根据和初始位置的坐标距离计算在X,Y方向上的距离大小, 如果Y方向移动距离大,那么直接返回true,既拦截, 否则不拦截。
当OuterViewGroup收到Down事件的时候其onInterceptTouchEvent返回false, 那么其Down事件会分发到其子View(InnerViewGroup)中, 由于InnerViewGroup的onInterceptTouchEvent以及dispatchTouchEvent方法都是调用其父类的方法而已, 所以最终还是进入ViewGroup.dispatchTouchEvent(ev)方法, 由于其没有子View, 所以mFirstTouchTarget == null, 最终事件会通过dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)(此时的child == null)方法将事件分发到其父类View的dispatchTouchEvent(ev)方法中。
注意: 针对OuterViewGroup其mFirstTouchTarget = newTouchTarget是包含InnerViewGroup信息的target, , InnerViewGroup的newTouchTarget, 以及mFirstTouchTarget 此时都为null
MOVE事件的分发过程
当MOVE事件到来之后, 根据上面的分析我们知道其会进入OuterViewGroup的dispatchTouchEvent(ev)方法, 此时:
- 如果Y轴的位移距离大于X轴, 那么onInterceptTouchEvent(ev)就会返回true既直接拦截, 那么事件不会被分发到其子View中, 而此时mFirstTouchTarget == newTouchTarget !=null, cancelChild = true;所以会执行dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)), 所以其子View(InnerViewGroup)会收到canceled消息,并且Target信息会被清除, 而后续的Move事件到来事件由于mFirstTouchTarget==null所以其会通过dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)(其中child==null) 执行到其父类View的dispatchTouchEvent(ev)方法, 在这里的执行过程在后文会进行分析
- 如果Y轴的位移距离不大于X轴,那么onInterceptTouchEvent(ev)就会返回false既不拦截, 那么事件就会被分发到其子View中, 接下来的分析过程和Down基本相同了就。
//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
//首先Down事件进来会进入此循环
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//如果拦截没有被disllow就会进入
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
}
if (!canceled && !intercepted) {
//ACTION_DOWN事件进来时候newTouchTarget == null, mFirstTouchTarget = null
if (newTouchTarget == null && childrenCount != 0) {
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
newTouchTarget = getTouchTarget(child);//这是个单链表的遍历查找
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//只有子View消费了此事件, mFirstTouchTarget才会被赋值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
}
}
}
//
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;
}
}
//没有子View或者没有子View消费此事件, 那么事件就会调用此ViewGroup处理过程
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else{
// 如果前面有子View消费了事件,但是此次父View又拦截了本次事件, 那么就给子View发送一个cancel事件, 并清除target,下次再来事件直接就分发给父View了
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//如果此时的父View拦截了事件, 那么cancelChild是true, 此时子View就会收到一个cancel事件, 如果子View消费了down事件,但是父View此时拦截了接下来的move事件, 那么子View会收到cancel事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
//OuterViewGroup.java
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
/**外部拦截法: 判断当前的事件我是否处理, 如果处理直接拦截,否则不拦截分发到子View中去**/
if (event.action == MotionEvent.ACTION_DOWN) {
startX = event.x
startY = event.y
}
if (event.action == MotionEvent.ACTION_MOVE) {
val pointX = event.x - startX
val pointY = event.y - startY
Log.d(TAG, "$TAG onInterceptTouchEvent: x = $pointX, y = $pointY")
if (Math.abs(pointX) < Math.abs(pointY)) {
Log.d(TAG, "$TAG onInterceptTouchEvent: true")
return true
}
}
return super.onInterceptTouchEvent(event)
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
Log.d(TAG, "$TAG dispatchTouchEvent: ${event.action}")
return super.dispatchTouchEvent(event)
}
//InnerViewGroup.java
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return super.onInterceptTouchEvent(event)
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return super.dispatchTouchEvent(event)
}
TouchEvent在View中的分发过程
当事件进入View.dispatchTouchEvent()中, 会先判断其是否设置了OnTouchListener()如果设置了,那么就会执行他的onTouch()方法, 如果onTouch方法返回true,那么onTouchEvent()方法就不会被执行, 否则就会执行onTouchEvent()方法。由于InnerViewGroup重写了onTouchEvent()方法并直接返回true, 故分发放回回返回true, 由于其返回true故OuterViewGroup以及MainActivity的onTouch()方法就不会被执行。如果InnerViewGroup没有重写onTouchEvent那么就会执行其父类View的OnTouchEvent方法, 在performClick()方法中会对点击事件进行判断, 如果设置了OnClickListener()那么就会执行其onClick()方法。
//View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
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;
}
}
public boolean onTouchEvent(MotionEvent event) {
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
public boolean performClick() {
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
}
结语
上述为自己的一些理解,如有不同意见欢迎批评与指正!