Android事件分发流程源码解析一

Android事件分发流程源码解析一
Android事件分发流程源码解析二及总结
Android滑动冲突解决方案内外部拦截法及原理

一 什么是点击事件(Touch事件)

  • 当用户触摸屏幕时,将产生点击事件(Touch事件),事件相关细节(触摸位置时间等)被封装为MotionEvent对象

MotionEvent事件类型:

  • ACTION_DOWN:初次接触到屏幕时触发
  • ACTION_MOVE:在屏幕上滑动时触发(多次)
  • ACTION_UP:手指离开屏幕时触发
  • ACTION_CANCEL:事件被上层拦截时触发(何时拦截后面做说明)

用户接触屏幕到抬起

在这里插入图片描述

二 事件如何从系统分发到我们自己的View

手指接触屏幕,系统底层最开始交给Activity,我们写的View通过setContentView交给了DecorView,而DecorView又受到Window的管理。Window里面的事件分发方法被Activity的事件分发方法调用

  • 以下是事件分发的流程图
    在这里插入图片描述

2.1 通过源码分析事件是如何从Activity一步步传递到我们通过setContentView的View(该View一般为ViewGroup)

Activity:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {//事件分发并返回结果
            return true;//事件被消费
        }
        return onTouchEvent(ev);//没有View可以处理,调用Activity onTouchEvent方法
    }

activity里面的getWindow().superDispatchTouchEvent(ev),会调用window.superDispatchTouchEvent(ev),而Window的唯一实现类是PhoneWindow,所以会调用PhoneWindow.superDispatchTouchEvent

PhoneWindow:
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到PhoneWindow又调用了DecorView的superDispatchTouchEvent方法,至此Activity事件通过一层层传递到了DecorView

  • DecorView
    在这里插入图片描述

DecorView可以通过 getWindow().getDecorView()可以获得 ,DecorView是整个界面的最顶层View,本质是FrameLayout。一般情况下里面是一个LinearLayout,这个LinearLayout又包含两个FrameLayout,分别显示标题和内容。其中显示内容的FrameLayout,我们的setContentView就是设置内容的View。
其事件分发方法就先传递到DecorView顶级View(为FrameLayout,FrameLayout为ViewGroup),再由DecorView一层层传递到我们设置的View(一般为ViewGroup)

三 事件如何在ViewGroup中传递

3.1 分发、拦截、消费三个方法

  • 事件分发 public boolean dispatchTouchEvent(MotionEvent
    ev),返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent影响

  • 事件拦截 public boolean onInterceptTouchEvent(MotionEvent ev),在dispatchTouchEvent的内部调用

  • 事件消费 public boolean onTouchEvent(MotionEvent event),用来处理点击事件,返回结果表示是否消耗当前事件

三者在View,ViewGroup以及Activity当中的存在关系(事件拦截只在ViewGroup当中存在):
在这里插入图片描述

三者的联系可用下面伪代码表示:

   public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;//事件是否被消费
    if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
        consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
    }else{
        consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
    }
    return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
}

3.2 事件如何在ViewGruop当中传递消费

在开发中,我们的布局文件里面一般是一层层ViewGruop的嵌套,最后在里面放入View,那么系统是如何将事件一层一层传递到View的呢?

试想:如果我们来写是不是将一层层嵌套的ViewGruop和View遍历出来,正常情况首先判断遍历后屏幕最外层的View是否消费事件。如果没消费再交回父级ViewGroup,父级ViewGroup再判断是否需要消费,这样一层一层倒叙的将事件交还回去。拦截的情况,在事件分发的过程当中,ViewGroup可以不将事件分发给子ViewGroup或者View,直接拦截。(注意上图中:拦截方法onInterceptTouchEvent是不是只有ViewGroup才有呢),这样就由该ViewGroup处理事件,如果没有消费再交换给该ViewGroup的父级。

举例:老板分发任务–>总经理–>部分经理–>员工
一个任务由老板分发下去,正常情况一层层分发到员工由员工来做,员工做不了,员工交还给他的上级部门经理做,部门经理做不了就返回给总经理做。拦截的情况就是分发下来的时候,部门经理(或者总经理)直接处理,这样员工就接收不到该事件了, 部门经理处理不了,再交还给其上级处理。

3.3 事件分发源码分析

场景:假设我们的布局文件只是一个ViewGroup(比如LinerLayout)嵌套了一个button,下面通过源码解析从用户手指接触屏幕到抬起屏幕的整个事件分发过程

第一次手指接触屏幕触发ViewGroup的dispatchTouchEvent方法,以及MotionEvent的down事件

1. 步骤一 为intercepted赋值

class:ViewGroup:

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
		......//省略
        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) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
			//**重点 intercepted就是判断该ViewGroup是否需要直接处理事件的标记
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//由于此时动作为ACTION_DOWN进入该if判断
    //disallowIntercept 受到其子类 getParent().requestDisallowInterceptTouchEvent(true)请求父类不要拦截的影响
    //disallowIntercept 这里为true
                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
                    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 (onFilterTouchEventForSecurity(ev)) { },此时为ACTION_DOWN事件进入到 if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {},由于disallowIntercept 为true,所以最后将intercepted设为false,接着往下走

2. 步骤二:循环遍历出消费事件的子View

class ViewGroup:
			TouchTarget newTouchTarget = null;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

                // If the event is targeting accessibility 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;
                            }
判断1,View可见并且没有播放动画。2,点击事件的坐标落在View的范围内
            //如果上述两个条件有一项不满足则continue继续循环下一个View
                            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);
                             //dispatchTransformedTouchEvent第三个参数child这里不为null
            //实际调用的是child的dispatchTouchEvent方法
                            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();
                                  //当child处理了点击事件,那么会设置mFirstTouchTarget 在addTouchTarget被赋值
                                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;
                    }
                }
            }

上述源码当中可以看到,对ViewGroup的子View做了倒叙遍历,直到进入if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}判断时,才break跳出循环。并且在里面将newTouchTarget赋值。那么dispatchTransformedTouchEvent该方法何时为true才能进入if判断呢,猜想是不是消费了事件时才为true?

3. 步骤三:循环遍历出消费事件的子View之如和将dispatchTouchEvent方法交给子View
进一步查看dispatchTransformedTouchEvent源码

//此处将dispatchTransformedTouchEvent的源码做省略缩写
   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        //.....省略
        // Perform any necessary transformations and dispatch.
        if (child == null) {
        //如果child为空就调用父类的dispatchTouchEvent
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
         
			//child不为空
            handled = child.dispatchTouchEvent(transformedEvent);
        }


        return handled;
        }

通过上述代码得出在child不为空的情况下调用child.dispatchTouchEvent方法,即View(button)的dispatchTouchEvent方法(我们的例子为ViewGroup嵌套一个Button),这个过程也就是ViewGroup如何一步步将事件分给View的过程。
为了方便看源码,我们暂不查看View的dispatchTouchEvent方法,先假设button事件处理返回为true。

4. 步骤四:如果没有找到子View消费事件,如何将事件交回

通过上述事件传递到了View(button)并且消费了事件,从而进入了if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}判断,并且在该判断当中给mFirstTouchTarget赋值。
但是想想如果button没有消费事件呢,也就是该ViewGroup没有找到子View来消费事件,那么按照我们的设想是不是应该交回事件呢?来看看源码是怎么实现的。

class ViewGroup:
	//....省略
  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循环,handled返回true整个分发方法结束
                while (target != null) {
                    final TouchTarget next = target.next;
                    //alreadyDispatchedToNewTouchTarget 在什么时候赋值了的?步骤二开始为false,找到目标对象中至为true
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    //事件down时进入
                        handled = true;
                    } else {
                    //事件move时进入,又会dispatchTransformedTouchEvent调用child的dispatch方法
                        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;
                }
            }

如果没有消费事件mFirstTouchTarget就为null,那么最终会调用dispatchTransformedTouchEvent方法并且传入child为null,查看步骤三该方法源码,当child为空时调用super.dispatchTouchEvent即自己ViewGroup的dispatchTouchEvent方法。正如我们所述一样,如果子类处理不了就将事件交回。

思考:如果子类干扰了呢?
看上述源码可以看出,步骤一的intercepted值就为true,就不会进入步骤二去遍历寻找子View,也就不会找不到目标对象,mFirstTouchTarget就为空,就会进入步骤四

  • 总结:如果子类没有调用requestDisallowInterceptTouchEvent请求父类不要拦截方法(传false就拦截)来干扰,那么ViewGroup遍历子View,找到能够消费的View,如果子类不能消费事件就交还父级。

至此我们的手指接触屏幕的down事件,在ViewGroup的事件分发方法就走完了,此时找到了事件消费者button,mFirstTouchTarget命中了目标不为空,当我们手指滑动的时候,又会多次进入到ViewGroup的dispatchTouchEvent方法,此时事件为Move

步骤一:intercepted赋值,由于此时mFirstTouchTarget已经赋值,虽然是down事件,依然会进入如图所示,将intercepted置为false
在这里插入图片描述

步骤二:虽然intercepted为false,进入了第一层if,但是此时为Move事件,因此不会进入第二层if,也就是此时不会进入到上述down事件步骤二的for循环中寻找消费事件的子View,此时alreadyDispatchedToNewTouchTarget的值被重置为false
在这里插入图片描述

步骤三:mFirstTouchTarget此时不为空,alreadyDispatchedToNewTouchTarget也为false,此时就会调用dispatchTransformedTouchEvent方法,通过查看,依然在该方法里面依然会调用child.dispatchTouchEvent方法,即move事件交给子类button处理
在这里插入图片描述
上述就是用户从手指按下到滑动事件如何一步步从我们的ViewGroup分发到View的过程,那么后序button(View)如何处理事件: Android事件分发流程源码解析二及总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值