MotionEvent事件分发笔记

一、相关函数

Activity里,有两个回调函数 :


public boolean dispatchTouchEvent(MotionEvent ev);    
public boolean onTouchEvent(MotionEvent ev); 

ViewGroup里,有三个回调函数 :

public boolean dispatchTouchEvent(MotionEvent ev);    
public boolean onInterceptTouchEvent(MotionEvent ev);    
public boolean onTouchEvent(MotionEvent ev);  

View里,有两个回调函数 :

public boolean dispatchTouchEvent(MotionEvent ev);    
public boolean onTouchEvent(MotionEvent ev); 

最重要的一个函数是dispatchTouchEvent,

  • 它用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用。返回结果受当前View的onTouchEvent方法和下级View的diapatchTouchEvent方法的影响。表示是否消耗该事件。

一个事件序列,包括 ACTION_DOWN、ACTION_MOVE、ACTION_UP 都通过dispatchTouchEvent向下传递

二、dispatchTouchEvent

2.1、 Activity.dispatchTouchEvent

Activty中的源码如下:

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction(); // 空方法
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            /*
            PhoneWindow类中实现:
            @Override
            public boolean superDispatchTouchEvent(MotionEvent event) {
                return mDecor.superDispatchTouchEvent(event);
            }
            DecorView里的实现:
            public boolean superDispatchTouchEvent(MotionEvent event) {
                return super.dispatchTouchEvent(event);
            }
            调用的是父类FrameLayout的方法
             */
            return true;
        }
        return onTouchEvent(ev);

2.2、View.dispatchTouchEvent

View.java

 public boolean dispatchTouchEvent(MotionEvent event) {
            //mOnTouchListener 是否处理?
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //mOnTouchListener 消费事件,则onTouchEvent尝试消费
            if (!result && onTouchEvent(event)) {
                result = true;
            }

            return result
 }

View.dispatchTouchEvent()中

  • 优先由onTouch()处理MotionEvent(),onTouch()处理
  • mOnTouchListener.onTouch()不处理,则由onTouchEvent()处理

2.3、ViewGroup.dispatchTouchEvent

2.3.1、mFirstTouchTarget

ViewGroup.java

class ViewGroup{
  // First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;  
}
   

TouchTarget 关键属性如下:

private static final class TouchTarget {
        // The touched child view.
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        public TouchTarget next;
}
  • View child:被点击的子控件,即消耗事件的目标控件。
  • int pointerIdBits:目标捕获的所有指针的指针ID的组合位掩码"。
    为了区分多点触控时不同的触控点,每一个触控点都会携带一个pointerId。而pointerIdBits即是所有被目标控件消耗的触控点的pointerId的组合。即pointerIdBits包含一个或以上的pointerId数据。
  • TouchTarget next:
    记录下一个TouchTarget对象,由此组成链表

mFirstTouchTarget 是ViewGroup中的一个私有变量,mFirstTouchTarget是"触摸目标"链表的头部。

mFirstTouchTarget 是ViewGroup中所有child中, 消费了触摸事件的TouchTarget,仅限于ViewGroup children中消费了触摸事件的View,而不包括ViewGroup本身。

2.3.2、dispatchToucheEevnt底层逻辑

底层逻辑一:根据 mFirstTouchTarget 指向,分发事件
  • mFirstTouchTarget != null,说明ViewGroup的某一个子View已经处理了ToucheEvent,则直接将TouchEvent交由mFirstTouchTarget指向的子View处理
  • mFirstTouchTarget == null,说明没有子View消费TouchEvent,进而由ViewGrop自身处理
ViewGroup.java
 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
        // 分发给touchTarget
            if (mFirstTouchTarget == null) {
                // 没有子view处理这次事件流,就调用自己的(父类View)的dispatchTouchEvent(),分发给自己
                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) { // 这个循环似乎只会循环一次,因为mFirstTouchTarget是按下事件的target,它的后继似乎是null
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        // 如果已经有子view处理事件并且找到了,方法结果是true
                        handled = true;
                    } else { // 如果事件被拦截,会走这里
                        // 还没有找到相应的子view,就依次调用每个touchTarget的子view或viewGroup父类(View)的dispatchTouchEvent()
                        // 一旦有一个的dispatchTouchEvent()返回true,整体就返回true
                        // 顺便如果事件被拦截,就销毁target链表
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        // 主要由intercepted决定cancelChild是不是true
 
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) { // 如果cancelChild为true,也就是事件被拦截,似乎所有事件都被当成了Action_Cancel处理分发,故而此时子view只会收到cancel事件
                            handled = true;
                        }
                        if (cancelChild) { // 非down的事件被拦截
                            if (predecessor == null) { // predecessor为null
                                mFirstTouchTarget = next; // 之前的mFirstTouchTarget的后继为null,现在mFirstTouchTarget自己变成了null,所以之后的事件,只能走上面的分发给viewGroup自己了
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
    }

底层逻辑二:MotionEvent.ACTION_DOWN 时,计算消费事件的子View: TouchTarget
  • 步骤一:ACTION_DOWN是事件序列的起点,首先清空mFirstTouchTarget
  • 步骤二: ViewGroup事件拦截,此处先省略
  • 步骤三:步骤三: ACTION_DOWN 且ViewGroup没有拦截的情况下,遍历MotionEvent命中的子View,View dispatchTouchEvent() = true,说明消费了事件,调用 addTouchTarget,添加mFirstTouchTarget
  
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    // 步骤一 ACTION_DOWN 时,清空mFirstTouchTarget
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // ACTION_DOWN的时候清空touchTarget链表,因为按下事件是事件序列的开头
        cancelAndClearTouchTargets(ev);
        resetTouchState(); // 这个方法里,mFirstTouchTarget被赋值为null
    }

    //步骤二: 此处省略 ViewGroup事件拦截
    .....
    .....
    //步骤三: ACTION_DOWN 且ViewGroup没有拦截的情况下,遍历点中的子View,尝试子View dispatchTouchEvent(),添加mFirstTouchTarget
     if (!canceled && !intercepted) {// ViewGoup 没有拦截
      
        // Action_Down是事件的起点,仅在ACTION_DOWN时,计算确定TouchTarget
        if (actionMasked == MotionEvent.ACTION_DOWN)
        {
            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
             
                // Find a child that can receive the event.
                // Scan children from front to back.
                final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                final View[] children = mChildren;
                for (int i = childrenCount - 1; i >= 0; i--) {
                    // 从后往前遍历
                    final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                    // 可以当成childIndex = i
                    final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);
              
                    // 寻找之前确定过的处理这次事件流的子view的target,新的ACTION_DOWN事件的话,newTouchTarget为null
                    newTouchTarget = getTouchTarget(child);
                   
                    // 调用子view的dispatchTouchEvent(),若返回为true,则记录newTouchTarget,表示已经有子view处理事件了,退出循环
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        // 将当前子view的touchTarget,此时mFirstTouchTarget被赋值,不再为null
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }

                }
            }   
        }
    }
}
 private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
底层逻辑三:ViewGroup.onInterceptTouchEvent 事件拦截
  
   
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    // 步骤一 ACTION_DOWN 时,清空mFirstTouchTarget
    .... 省略
   

    //步骤二: 此处省略 ViewGroup事件拦截
    
      // Check for interception.
     final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) { 
         // ACTION_DOWN或者此次事件流里之前的事件有子view处理
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) { // 正常是走这儿
            intercepted = onInterceptTouchEvent(ev); // 调用ViewGroup.onInterceptTouchEvent()方法
             ev.setAction(action); // restore action in case it was changed
        } else {
              intercepted = false;
         }
    } else {
                // 没有点到这个ViewGroup的子view(说白了就是空白位置),那这次事件流的剩余事件全部被ViewGroup拦截
                intercepted = true;
    }

    //步骤三: ACTION_DOWN 且ViewGroup没有拦截(intercepted=false)的情况下,遍历点中的子View,尝试子View dispatchTouchEvent(),添加mFirstTouchTarget
    ...省略

}
onInterceptTouchEvent 发生在两种场景
场景一:拦截的是 Motion.ACTION_DOWN

ACTION_DOWN 是事件流的起点,拦截了ACTION_DOWN事件,mFirstTouchTarget为null,整个事件流归ViewGroup处理;

场景二: 拦截的是MotionEvent.ACTION_MOVE(mFirstTouchTarget != null)

拦截的不是down事件,比如move事件,此时mFirstTouchTarget不是null,当前事件归子view管,但子view接收到的却变成了cancel事件,这次事件流之后的事件全归了ViewGroup

三、滑动冲突解决

1、ViewGroup拦截子View的事件

ViewGroup.onInterceptTouchEvent() 返回true,事件就会交由ViewGroup来处理

2、子View禁止父ViewGroup拦截自己的事件

requestDisallowInterceptTouchEvent()禁止父ViewGroup拦截事件

  @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

四、MotionEvent

4.1、事件坐标

每个触摸事件都代表用户在屏幕上的一个动作,
而每个动作必定有其发生的位置。

  • getX()和getY():这两个函数获得的x,y值是相对的坐标值,相对于消费这个事件的视图的左上点的坐标。
  • getRawX()和getRawY():有这两个函数获得的x,y值是绝对坐标,是相对于屏幕的。

4.2、事件类型

  int action = MotionEventCompat.getActionMasked(event);
    switch(action) {
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
            break;
    }

4.3、Pointer

为了可以表示多个触摸点的动作,MotionEvent中引入了Pointer的概念。
一个pointer就代表一个触摸点。

一个MotionEvent对象中可能会存储多个pointer的相关信息,每个pointer都会有一个自己的id和index。

pointer的id在整个事件流中是不会发生变化的,但是index会发生变化。
MotionEvent类中的很多方法都是可以传入一个int值作为参数的,其实传入的就是pointer的index值。比如getX(pointerIndex)和getY(pointerIndex),此时,它们返回的就是index所代表的触摸点相关事件坐标值。

    private final static int INVALID_ID = -1;
    private int mActivePointerId = INVALID_ID;
    private int mSecondaryPointerId = INVALID_ID;
    private float mPrimaryLastX = -1;
    private float mPrimaryLastY = -1;
    private float mSecondaryLastX = -1;
    private float mSecondaryLastY = -1;
    public boolean onTouchEvent(MotionEvent event) {
        int action = MotionEventCompat.getActionMasked(event);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                int index = event.getActionIndex();
                mActivePointerId = event.getPointerId(index);
                mPrimaryLastX = MotionEventCompat.getX(event,index);
                mPrimaryLastY = MotionEventCompat.getY(event,index);
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                index = event.getActionIndex();
                mSecondaryPointerId = event.getPointerId(index);
                mSecondaryLastX = event.getX(index);
                mSecondaryLastY = event.getY(index);
                break;
            case MotionEvent.ACTION_MOVE:
                index = event.findPointerIndex(mActivePointerId);
                int secondaryIndex = MotionEventCompat.findPointerIndex(event,mSecondaryPointerId);
                final float x = MotionEventCompat.getX(event,index);
                final float y = MotionEventCompat.getY(event,index);
                final float secondX = MotionEventCompat.getX(event,secondaryIndex);
                final float secondY = MotionEventCompat.getY(event,secondaryIndex);
                break;
            case MotionEvent.ACTION_POINTER_UP:
                xxxxxx(涉及pointer id的转换,之后的文章会讲解)
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointerId = INVALID_ID;
                mPrimaryLastX =-1;
                mPrimaryLastY = -1;
                break;
        }
        return true;
    }

多点触控相关的事件类型:

  • ACTION_POINTER_DOWN:代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,有新出现了一个触摸点。
  • ACTION_POINTER_UP:代表用户的一个手指离开了触摸屏,但是还有其他手指还在触摸屏上。
    也就是说,在多个触摸点存在的情况下,其中一个触摸点消失了。它与ACTION_UP的区别就是,它是在多个触摸点中的一个触摸点消失时(此时,还有触摸点存在,也就是说用户还有手指触摸屏幕)产生,而ACTION_UP可以说是最后一个触摸点消失时产生。

所以多点触控的事件流 可能是这个样子的:

ACTION_DOWN ->
ACTION_POINTER_DOWN ->
ACTION_POINTER_UP ->
ACTION_UP 

4.4、getAction 和 getActionMasked

MotionEvent对象只包含一个触摸点的事件时,上边两个函数的结果是相同的,但是当包含多个触摸点时,二者的结果不同。

  • getAction获得的int值是由pointer的index值和事件类型值组合而成的(PointerIndex + 事件类型)

  • getActionWithMasked则只返回事件的类型值

getAction() returns 0x0105.
getActionMasked() will return 0x0005
其中0x0100就是pointer的index值。

参考文章

https://blog.csdn.net/qq_37475168/article/details/80520372

Android MotionEvent

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351