事件分发机制_图解 android 事件分发机制 - CSDN
精华内容
参与话题
  • 事件分发机制的理解

    2018-04-18 15:49:18
    Android事件分发机制不仅是Android开发体系中的重点也是难点,掌握好了事件分发机制也是我们解决自定义控件、view的滑动冲突等问题的基础。所谓事件的分发,其实就是对MotionEvent事件的分发过程,即当一个...



    Android事件分发机制不仅是Android开发体系中的重点也是难点,掌握好了事件分发机制也是我们解决自定义控件、view的滑动冲突等问题的基础。


    所谓事件的分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一

    个具体的View,而这个传递过程就是分发过程。点击事件的分发过程由下面三个很重要的方法来共同完成。


    1public boolean dispatchTouchEvent(MotionEvent ev)

    用来进行事件的分发。如果事件能够传递给当前的View,那么此事件一定会被调用,返回结果受当前View的onTouchEvent

    和下级view的dispatchTouchEvent影响,表示是否消耗当前事件。

    2public boolean onInterceptTouchEvent(MotionEvent ev)

    在方法1内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列当中,此方法不会

    被再次调用,返回结果表示是否拦截当前事件。

    3public boolean onTouchEvent(MotionEvent ev)

    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,

    当前view无法再次接收事件



    记住下面的图片有助于你理解事件分发



    事件从左上角那个白色箭头开始,由Activity的dispatchTouchEvent做分发
    箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。

    dispatchTouchEvent和 onTouchEvent的框里有个【true---->消费】的字,表示的意思是如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。


    1、如果事件不被中断,整个事件流向是一个类U型图,我们来看下这张图,可能更能理解U型图的意思。


    2、dispatchTouchEvent 和 onTouchEvent 一旦return true,事件就停止传递了(到达终点)对于return true我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁能再收到这个事件了。



    3、dispatchTouchEvent 和 onTouchEvent return false的时候事件都回传给父控件的onTouchEvent处理。

    对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。



    4、dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent
    ViewGroup 和View的这些方法的默认实现就是会让整个事件安装U型完整走完,所以 return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径),中间不做任何改动,不回溯、不终止,每个环节都走到。


    onInterceptTouchEvent()

    Intercept 的意思就拦截,每个ViewGroup每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)如果要自己处理那就在onInterceptTouchEvent方法中 return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的,是不会拦截的,事件会继续往子View的dispatchTouchEvent传递


    首先看下ViewGroup 的dispatchTouchEvent,之前说的return true是终结传递。return false 是回溯到父View的onTouchEvent,然后ViewGroup怎样通过dispatchTouchEvent方法能把事件分发到自己的onTouchEvent处理呢,return true和false 都不行,那么只能通过Interceptor把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent,记住这一点



    总结:
    对于 dispatchTouchEvent,onTouchEvent,return true是终结事件传递。return false 是回溯到父View的onTouchEvent方法。ViewGroup 想把自己分发给自己的onTouchEvent,需要拦截器onInterceptTouchEvent方法return true 把事件拦截下来。ViewGroup 的拦截器onInterceptTouchEvent 默认是不拦截的,所以return super.onInterceptTouchEvent()=return false;View 没有拦截器,为了让View可以把事件分发给自己的onTouchEvent,View的dispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent

    源码分析:

    http://blog.csdn.net/u013277209/article/details/71600419





    展开全文
  • Android事件分发机制解析

    千次阅读 2018-08-19 19:06:50
    看了大神们对android事件分发机制的解析,为了方便自己理解和记忆,特意写一篇博客。 目录 方法执行顺序 各方法简单说明 getParent().requestDisallowInterceptTouchEvent(true) 方法执行顺序 boolean ...

    看了大神们对android事件分发机制的解析,为了方便自己理解和记忆,特意写一篇博客。

    目录

    方法执行顺序

    各方法简单说明

    getParent().requestDisallowInterceptTouchEvent(true)


    方法执行顺序

    boolean dispatchTouchEvent(MotionEvent ev):事件分发,Activity类,View类都有的方法
    boolean onInterceptTouchEvent(MotionEvent ev):事件拦截,只有ViewGroup类才有的方法
    
    boolean onTouch(View v, MotionEvent event):这不是View自带的方法,要通过setOnTouchListener()来添加OnTouchListener然后进入的一个方法
    boolean onTouchEvent(MotionEvent event):事件消费,Activity类,View类都有的方法

    例子布局

    <RelativeLayout
            android:id="@+id/rl_test1"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#00ffff">
    
            <com.ousy.touchtest.MyLinearLayout
                android:id="@+id/ll_test1"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:background="#00ff00"
                android:orientation="vertical">
    
                <com.ousy.touchtest.MyButton
                    android:id="@+id/btn_test1"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="test1"/>
    
            </com.ousy.touchtest.MyLinearLayout>
        </RelativeLayout>

    布局很简单,就一个RelativeLayout里面放个自定义的LinearLayout,LinearLayout里面再放个自定义的Button。自定义了两个控件,因为要重写这两个控件里面的一些方法。 

    大家都知道事件分发机制分为拦截、传递、消费。根据上面的布局,用文字表达,如一个按下的事件MotionEvent.ACTION_DOWN发生的时候,是RelativeLayout先拿到,然后看这个布局R君要不要拦截,不拦截就传递给LinearLayout布局的L君,L君不拦截就传递到B君,B君如果消费掉这次事件(就是处理了这次事件),那这个按下的事件就到此结束了。如果B君不消费,那这个事件就会回到L君,L君消费,就在L君那结束。L君不消费,事件就回到R君那里处理。

    文字表达有点乱,下下面会有流程图。

    Activity里就为各控件添加onTouch方法和打印日志。(以下是Activity部分代码,代码用的是ButterKnife)

        @OnTouch(R.id.btn_test1)
        public boolean onTouch1(View v, MotionEvent event)
        {
            switch (event.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    //btnTest1.getParent().requestDisallowInterceptTouchEvent(true);
                    // 手指按下
                    Log.e("ousyxx", "downTest1");
                    break;
                case MotionEvent.ACTION_MOVE:
                    // 手指移动
                    Log.e("ousyxx", "moveTest1");
                    break;
                case MotionEvent.ACTION_UP:
                    // 手指抬起
                    Log.e("ousyxx", "upTest1");
                    break;
                case MotionEvent.ACTION_CANCEL:
                    // 事件被拦截
                    Log.e("ousyxx", "cancelTest1");
                    break;
            }
    
            return false;
        }
    
        @OnTouch(R.id.ll_test1)
        public boolean onLlTouch(View v, MotionEvent event)
        {
            switch (event.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    // 手指按下
                    Log.e("ousyxx", "downLlTest");
                    break;
                case MotionEvent.ACTION_MOVE:
                    // 手指移动
                    Log.e("ousyxx", "moveLlTest");
                    break;
                case MotionEvent.ACTION_UP:
                    // 手指抬起
                    Log.e("ousyxx", "upLlTest");
                    break;
                case MotionEvent.ACTION_CANCEL:
                    // 事件被拦截
                    Log.e("ousyxx", "cancelLlTest");
                    break;
            }
    
            return false;
        }
    
        @OnTouch(R.id.rl_test1)
        public boolean onRlTouch(View v, MotionEvent event)
        {
            switch (event.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    // 手指按下
                    Log.e("ousyxx", "downRlTest");
                    break;
                case MotionEvent.ACTION_MOVE:
                    // 手指移动
                    Log.e("ousyxx", "moveRlTest");
                    break;
                case MotionEvent.ACTION_UP:
                    // 手指抬起
                    Log.e("ousyxx", "upRlTest");
                    break;
                case MotionEvent.ACTION_CANCEL:
                    // 事件被拦截
                    Log.e("ousyxx", "cancelRlTest");
                    break;
            }
            return false;
        }

    各方法简单说明

    说明之前,大家先看看,假如你没重写过任何方法,就按照上面的布局,你点击那个MyButton控件,大概的事件传递流程如下

    流程图

     

    dispatchTouchEvent:有事件发生的时候,先进入这个方法。

    return false: 事件不会被再进行分发。事件会被传递回上一层的view的onTouch方法、onTouchEvent方法。如果view没有添加onTouchListener,那事件就会直接到onTouchEvent;

    return true:该事件就停在这方法里处理,不会继续传递。

    举个例子1:加入你在Activity类里重写了dispatchTouchEvent这个方法如下,返回super.dispatchTouchEvent(ev),就是执行你没重写原本Activity类自己原本的dispatchTouchEvent方法,当mTouchType为true时就会进入return true,这时这个Activity上就什么事件都不会发生,什么点击滑动都没有,因为事件在该Activity你重写dispatchTouchEvent里已经结束了这次事件。

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev)
        {
            if (mTouchType)
            {
                return true;
            }
    
            return super.dispatchTouchEvent(ev);
        }

    举个例子2:下面是我自定义LinearLayout的代码,在dispatchTouchEvent里直接return false,那么事件就会回到R君那里,流程图大概如下

    public class MyLinearLayout extends LinearLayout
    {
        // 是否拦截
        private boolean mIsIntercept = false;
        public MyLinearLayout(Context context, @Nullable AttributeSet attrs)
        {
            super(context, attrs);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev)
        {
            return false;
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev)
        {
            if (mIsIntercept)
                return true;
    
            return super.onInterceptTouchEvent(ev);
        }
    }

    流程图

    onInterceptTouchEvent:所有ViewGroup类正常的情况下经过上面的方法后都会进入此拦截方法。
    return false:不拦截,事件传递给子view
    return true: 拦截,事件会在该层处理
    上面的代码,我重新写了这个方法,假如上面的代码我没有重写dispatchTouchEvent,而是重写onInterceptTouchEvent,让它return true,流程图就如下
    

    onTouch:控件自己setOnTouchListener添加的方法

    return false: 不消费此事件,事件会传到该控件的onTouchEvent方法

    return true:消费此事件,事件不会传到该控件的onTouchEvent方法,事件就此结束

    onTouchEvent:要不要消费此事件的方法

    return false: 不消费此事件,事件就会返回到自己的父View的onTouch继续处理

    return true:消费此事件,事件就此结束

    举个例子:假如你重写了Button类的onTouchEvent,让它return false,那么流程图大概如下

     

    getParent().requestDisallowInterceptTouchEvent(true)

     这个方法在关于事件分发的开发中可能会见到,所以也记录一下。一般大家可能看到L君的onInterceptTouchEvent的拦截方法的重写是如下这样。可以看到,L君只在ACTION_MOVE处才return true,就是说但你按下B君控件时,按下的事件是进入了B君的onTouch1。而当你按下、滑动的时候,滑动事件就被L君拦截,那么滑动事件就会进入L君的onLlTouch。

    但从打印的日志可以看到,同时也有个事件进入了B君的onTouch1,那就是MotionEvent.ACTION_CANCEL。没错当B君的事件中途被父View拦截后,就会有ACTION_CANCEL事件进入B君的onTouch1。

    @Override
        public boolean onInterceptTouchEvent(MotionEvent ev)
        {
            switch (ev.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    return false;
                case MotionEvent.ACTION_MOVE:
                    return true;
                case MotionEvent.ACTION_UP:
                    return false;
                default:
                    break;
            }
    
            return super.onInterceptTouchEvent(ev);
        }

    这时如果B君不想这移动的事件被拦截的话,就可以使用getParent().requestDisallowInterceptTouchEvent(true)请求父View即L君不要拦截B君接下来的事件。可以看看代码如下:

    @OnTouch(R.id.btn_test1)
        public boolean onTouch1(View v, MotionEvent event)
        {
            switch (event.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    btnTest1.getParent().requestDisallowInterceptTouchEvent(true);
                    // 手指按下
                    Log.e("ousyxx", "downTest1");
                    break;
                case MotionEvent.ACTION_MOVE:
                    // 手指移动
                    Log.e("ousyxx", "moveTest1");
                    break;
                case MotionEvent.ACTION_UP:
                    // 手指抬起
                    Log.e("ousyxx", "upTest1");
                    break;
                case MotionEvent.ACTION_CANCEL:
                    // 事件被拦截
                    Log.e("ousyxx", "cancelTest1");
                    break;
            }
    
            return false;
        }

    如1代码所示,在B君接收到ACTION_DOWN按下事件时执行了请求,那么接下来移动的事件还是会进入到B君的onTouch1了。

    完!

    展开全文
  • / 今日科技快讯 /近日,据国外媒体报道,亚马逊已经有50多个美国仓库有新冠病毒感染病例,这家科技巨头正面临着如何处理工人安全的难题。/ 作者简介 /本篇文章来自七零八...

    /   今日科技快讯   /

    近日,据国外媒体报道,亚马逊已经有50多个美国仓库有新冠病毒感染病例,这家科技巨头正面临着如何处理工人安全的难题。

    /   作者简介   /

    本篇文章来自七零八落问号的投稿,分享了他对Android事件分发机制中,mFirstTouchTarget相关设计的理解。希望对大家有所帮助,同时也感谢作者贡献的精彩文章。

    七零八落问号的博客地址:

    https://www.jianshu.com/u/a101c0b39b29

    /   前言   /

    在ViewGroup事件派分过程中,mFirstTouchTarget起着相当重要的作用。

    但对mFirstTouchTarget的作用是什么,大多数的文章都简单的描述为记录后续事件派分的目标,很少有具体分析这个机制的具体逻辑,更不说其他的一些问题,例如:

    1.为什么要把mFirstTouchTarget设计成链表

    2.记录目标的TouchTarget的pointerIdBits又起到什么作用

    而这个机制又能引申出多点触控相关的问题,例如:

    设ViewGroup VG中,有2个Button:A,B:

    3.按下A,再按下A(多点触控),为什么释放后A的点击事件只会触发一次。

    4.按下A,按下VG(空白区域),为什么先释放A,却无法触发A的点击事件,继续释放VG,又会触发A的点击事件。

    5.按下VG(空白区域),为什么点击A,B无响应。

    本文章在对mFirstTouchTarget进行分析的同时,对ViewGroup和View的机制进行进一步的原理解析(例如resetCancelNextUpFlag具体作用等),相信读者有更多的收获。

    本文不对Accessibility服务相关处理进行分析,并在贴出的源码中移除相关逻辑。本文不对鼠标事件相关处理进行分析,并在贴出的源码中移除相关逻辑。

    /   定义   /

    // First touch target in the linked list of touch targets.
    private TouchTarget mFirstTouchTarget;
    
    

    mFirstTouchTarget是一个TouchTarget对象,通过注释的说明:"触摸目标的链接列表中的第一个触摸目标",可以得出:

    mFirstTouchTarget是"触摸目标"链表的头部。

    引申出一个问题:什么叫做"触摸目标":

    "触摸目标"可以理解为触控点按下时,处理该触控点对应事件的目标控件。


    简单来说,在ViewGroup.dispatchTouchEvent()遇到非拦截事件,且事件类型为ACTION_DOWN或ACTION_POINTER_DOWN,则会触发一个遍历子控件以查找"触摸目标"的流程。

    通过后续的分析,这个概念可以相当容易理解。

    /   TouchTarget的设计   /

    再看看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数据。这个pointerIdBits的运算相关实现,将会在下面提到idBitsToAssign的时候说明。

    简单来说,就是如果记录pointerId为0,2,5时,pointerIdBits即为0010 0101,即:


    0对应0000 0001, 2对应0000 0100, 5对应0010 0000,然后通过或运算合并为0010 0101。

    TouchTarget next


    记录下一个TouchTarget对象,由此组成链表。

    注意到TouchTarget包含obtain和recycle两个方法,用于缓存复用,这个同样在Message中实现,需要缓存复用的时候可以参考借鉴该方式,这也是安卓中常见的操作。

    分析可得, TouchTarget的作用,是记录一个View及其对应分发的触控点列表pointerIdBits,且可以通过next与其他实例形成链表。进一步分析,TouchTarget是对消耗事件的View以链表方式保存,且记录各个View对应的触控点列表,以实现后续的事件派分处理。

    同时可以推理出:

    • 非多点触控:mFirstTouchTarget链表退化成单个TouchTarget对象。

    • 多点触控,目标相同:同样为单个TouchTarget对象,只是pointerIdBits保存了多个pointerId信息。

    • 多点触控,目标不同:mFirstTouchTarget成为链表。

    /   实现   /

    然后进入到ViewGroup.dispatchTouchEvent()方法中看具体实现,去除无关逻辑的代码, 首先是:

    // 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();
    }
    
    

    该部分逻辑主要是重置状态,了解其中的内容我们需要先了解mFirstTouchTarget如何生成,所以暂不分析,只需要知道逻辑为:
    在ACTION_DOWN事件触发时,重置ViewGroup状态,且mFirstTouchTarget会被置空。
    此时,mFirstTouchTarget = null。然后检测ViewGroup是否拦截事件:

    // 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;
    }
    
    

    直接看到用户处理是否拦截的逻辑:

    • 只会在ACTION_DOWN事件时直接触发。

    • 其他的事件会根据是否存在消耗ACTION_DOWN事件的目标控件(即是否有mFirstTouchTarget记录)而决定。

    当不存在消耗ACTION_DOWN事件的目标控件时,后续事件的拦截标记intercepted将会越过用户处理表现为true,可以理解为ViewGroup退化成View,事件处理将交给super.dispatchTouchEvent()进行。

    这里则可以回答文章开篇的问题5:

    当点击VG空白位置时,由于不存在消耗ACTION_DOWN的子控件,导致mFirstTouchTarget为空。任何后续事件的派分,都会由于拦截标记intercepted = true而被拦截,包括多点触控ACTION_POINTER_DOWN事件。

    顺便复习一下拦截处理onInterceptTouchEvent()和requestDisallowInterceptTouchEvent():

    如果子类调用了requestDisallowInterceptTouchEvent(true)时,ViewGroup会越过用户设置的拦截逻辑onInterceptTouchEvent(),表现为优先使子控件处理事件。

    /   核心处理逻辑   /

    然后开始核心处理逻辑,先看2个关键的final局部变量:

    // 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;
    
    

    boolean canceled


    该事件是否需要取消,由resetCancelNextUpFlag(this)或者事件本身决定,基本由事件本身决定。


    resetCancelNextUpFlag内部实际上是对PFLAG_CANCEL_NEXT_UP_EVENT进行操作。


    当控件持有PFLAG_CANCEL_NEXT_UP_EVENT标记时,则清除该标记并返回true,否则返回flase。

    boolean split


    是否支持多点触控,此处默认基本为true。

    这里对涉及到的2个FLAG进行解析:

    View.PFLAG_CANCEL_NEXT_UP_EVENT

    "指示视图是否临时分离"。字面理解为当前View是否与窗口处于分离状态。设置该标签的方法有:

    • performButtonActionOnTouchDown():该方法在View.onTouchEvent()中调用,输入事件为鼠标右键的情况下触发,一般情况无需理会(一般不接入鼠标)。

    • onStartTemporaryDetach():该方法在子控件与父控件"临时分离"时调用。

    一般来说,原生控件中通常在RecycleView/ListView中可能会发生这样的情况。

    即控件执行轻量级临时分离,在触发onStartTemporaryDetach()后,又触发了控件的dispatchTouchEvent()。这时在流程中调用的resetCancelNextUpFlag()方法将会移除控件的PFLAG_CANCEL_NEXT_UP_EVENT标记,并标记当次事件canceled。

    View.FLAG_SPLIT_MOTION_EVENTS

    是否支持拆解MotionEvents拆解,即多点触控。该标记在Api>=11(Android 3.0)的后,ViewGroup在初始化时默认支持(initViewGroup()中)。也可以通过setMotionEventSplittingEnabled()手动管理。

    继续分析,然后是2个变量:

    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    
    

    TouchTarget newTouchTarget

    当事件已经做出派分时,记录派分对应的控件。

    boolean alreadyDispatchedToNewTouchTarget

    记录事件是否已经做出派分。用于过滤已派分的事件,避免事件重复派分。假如事件未被标记为取消或者拦截时,将会进行核心的遍历逻辑,该逻辑中将会尝试查找消耗事件的newTouchTarget:

    if (!canceled && !intercepted) {
        >
    
    

    逻辑中,会先对ACTION_DOWN,ACTION_POINTER_DOWN的情况进行处理(忽略鼠标相关事件):

    >
    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;
        >>
    
    

    int actionIndex

    触控点下标,表明这是第几个触控点。

    int idBitsToAssign

    位分配ID,通过触控点的PointerId计算,又是安卓各种神奇位运算的一个实例。逻辑为1 << ev.getPointerId(actionIndex),即对0000 0001左移,位数为PointerId值,一般情况PointerId从0开始,每次+1。即把PointerId记录通过位进行保存:0对应0000 0001,2对应0000 0100,5对应0010 0000等。

    接下来就是调用removePointersFromTouchTargets检查是否有记录的PointID。

    >>
    // Clean up earlier touch targets for this pointer id in case they
    // have become out of sync.
    removePointersFromTouchTargets(idBitsToAssign);
    
    
    private void removePointersFromTouchTargets(int pointerIdBits) {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if ((target.pointerIdBits & pointerIdBits) != 0) {
                target.pointerIdBits &= ~pointerIdBits;
                if (target.pointerIdBits == 0) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    
    

    假如mFirstTouchTarget不为空,检查mFirstTouchTarget链表,检索是否存在记录了该触控点的TouchTarget,存在时,则移除该触控点记录;移除后,如TouchTarget不存在其他的触控点记录,则从链表中移除。

    当控件的子控件数量大于0时执行【遍历】,以下部分忽略子控件布局排序机制的源码,视为customOrder = false的情况:

    >>
    final int childrenCount = mChildrenCount;
    if (newTouchTarget == null && childrenCount != 0) {
        final float x = ev.getX(actionIndex);
        final float y = ev.getY(actionIndex);
    
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = i;
            final View child = children[childIndex];
    
            if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
                continue;
            }
            >>>
    
    

    先解析这里的continue中断:

    canReceivePointerEvents()

    判断控件是否可以接受事件,当控件可见性为VISIBLE或者正在执行动画时,返回true。

    isTransformedTouchPointInView()

    判断View是否包含事件的坐标,计算过程中通过transformPointToViewLocal()计算当前的真实坐标(其中包括了滚动量mScroll,及View在ViewGroup中的位置数据[LTRB])

    假如当前遍历的View不可接受事件,或点击坐标不在其中,则跳过当前遍历的View。当View可接受事件且点击坐标在该View空间内时,执行下一步:

    >>>
    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;
    }
    
    

    getTouchTarget方法用于查找当前的mFirstTouchTarget是否存在对应View的记录。以上逻辑为如果mFirstTouchTarget已经存在对应View的TouchTarget,则可以直接把idBitsToAssign添加到TouchTarget中,并跳出【遍历】。

    这里则可以回答文章开篇的问题3:


    ACTION_DOWN被A消耗,ACTION_POINTER_DOWN也被A消耗,此时相当于A是2个触控点的目标元素。
    当释放任意一个触控点时,对应的事件是ACTION_POINTER_UP而不是ACTION_UP,导致不产生点击事件。(原理为dispatchTransformedTouchEvent逻辑中,传入的id为A对应的pointerIdBits,此时应为0000 0011,然后会进入if(newPointerIdBits == oldPointerIdBits)的逻辑,该部分逻辑不会通过event.split拆解事件,则为ACTION_POINTER_UP)。假如是ACTION_DOWN的情况,mFirstTouchTarget必然为空,则继续以下流程:

    >>>
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        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;
    }
    
    

    涉及到一个关键方法dispatchTransformedTouchEvent,但由于篇幅问题,此处先直接说明作用.主要作用是调用View.dispatchTouchEvent()以执行View的事件分发流程。当传入参数View child为空时:视为传入View为ViewGroup本身;当传入参数boolean cancel为flase时:将MotionEvent的Action设置为ACTION_CANCEL分发到传入的View。


    上文提及isTransformedTouchPointInView()中进行了坐标偏移处理,同样,该方法中也有相同的操作,只是偏移值直接保存到了MotionEvent中,并在调用完View.dispatchTouchEvent还原。


    该方法中,对MotionEvent进行了拆解,获取对应触摸点的MotionEvent,拆解参考的是传入的位分配ID。
    假如传入的View消耗了该事件,dispatchTransformedTouchEvent将会返回true,然后执行以下逻辑后跳出【遍历】。通过addTouchTarget(),生成一个新的TouchTarget(包裹着消化事件的View),并添加到mFirstTouchTarget头部,并使newTouchTarget指向生成的TouchTarget。alreadyDispatchedToNewTouchTarget标记为true。

    以上完成了【遍历】的逻辑。在>逻辑(即处理ACTION_DOWN、ACTION_POINTER_DOWN)中最后一部分的逻辑为:

    >
    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;
    }
    
    

    假如当前的newTouchTarget等于空(即无法找到消耗ACTION_POINTER_DOWN事件的View),但mFirstTouchTarget不为空,则使newTouchTarget指向mFirstTouchTarget链表最后的元素(一般即为消耗ACTION_DOWN的控件),并把当次ACTION_POINTER_DOWN事件的PointID记录到该元素。

    这里则可以回答文章开篇的问题4。此处原理和问题3一样,只是添加的条件发生变化 - ACTION_DOWN被A消耗,则mFirstTouchTarget的末尾元素为A,后续没有被消耗的ACTION_POINTER_DOWN事件都会传入A中,此时相当于A是2个触控点的目标元素。这时候就处理完>的逻辑,完成ACTION_DOWN,ACTION_POINTER_DOWN引起的目标查找。

    以上部分可能导致的结果有:

    /

    mFirstTouchTarget

    newTouchTarget

    alreadyDispatchedToNewTouchTarget

    ACTION_DOWN 无目标

    null

    null

    false

    ACTION_DOWN 有目标

    TouchTarget(0)

    new TouchTarget(0)

    true

    ACTION_POINTER_DOWN 无目标

    TouchTarget(0)

    TouchTarget(0)

    false

    ACTION_POINTER_DOWN 有目标(已存在)

    不变

    find TouchTarget

    false

    ACTION_POINTER_DOWN 有目标(不存在)

    TouchTarget(n)

    new TouchTarget(n)

    true

    可以总结到如下性质:

    1. 标记位alreadyDispatchedToNewTouchTarget只会在新建TouchTarget时设置true。

    2. ACTION_DOWN无法找到目标时会导致后续所有的派分都直接传到ViewGroup本身。

    3. ACTION_POINTER_DOWN无法找到目标时视为ACTION_DOWN目标接收派分。

    接下来就是派分非ACTION_DOWN类型的事件的处理,该部分处理主要是根据上述的3个对象进行。

    ACTION_DOWN没有派分目标

     if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    }
    
    

    此处dispatchTransformedTouchEvent传入的View参数为null,视为ViewGroup,即派分到自身。

    遍历mFirstTouchTarget进行派分

    // 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;
    }
    
    

    alreadyDispatchedToNewTouchTarget && target == newTouchTarget

    用到了alreadyDispatchedToNewTouchTarget标记,用于过滤新建TouchTarget时已消耗事件的情况,避免重复派分。

    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted

    intercepted标记的作用区域,主要处理ACTION_DOWN类型事件的目标控件,的后续事件派分被拦截的情况(ACTION_DOWN无目标时将会直接导致mFirstTouchTarget == null)。当拦截时,需要对目标控件传入一个ACTION_CANCEl事件以通知目标控件当次事件派分被拦截需要进行取消操作。并在后续处理中将cancelChild的目标控件从mFirstTouchTarget中移除。当不拦截时,派分事件到mFirstTouchTarget链表中的所有目标控件。(由于dispatchTransformedTouchEvent存在触控点ID判断和事件分割,所以实际上只有链表部分的目标控件会收到事件派分)

    以上,完成了后续事件对mFirstTouchTarget的派分。剩余的部分,是对ACTION_UP类型事件进行清理:

    ACTION_UP

    说明这是最后一个触控点抬起,通过resetTouchState()完全清理派分目标和状态。

    ACTION_POINTER_UP

    移除触控点对应的TouchTarget内的pointerIdBits记录,当移除后pointerIdBits = 0(即没有其他触控点记录),则把该TouchTarget从mFirstTouchTarget中移除。

    最后可以回答开篇问题的1和2:

    mFirstTouchTarget设计成链表的作用,是用于记录多点触控情况下,多目标控件的派分逻辑。pointerIdBits的作用,是配合mFirstTouchTarget,使多点触控时,同个目标可以对多个触控点进行合理的处理逻辑。

    阅读该机制的收获其实远不止了解到mFirstTouchTarget的作用,更是能对ViewGroup如何拦截/派分事件进行更深入的理解,当遇到一些较复杂的情况下,也能更加轻松的应对。

    推荐阅读:

    这本《第三行代码》,让大家久等了!

    这份AS快捷键大全,让你的开发效率快10倍

    Android 10存储适配一一我们项目是这么干的!

    欢迎关注我的公众号

    学习技术或投稿

    长按上图,识别图中二维码即可关注

    展开全文
  • 事件分发机制详解

    2017-07-07 11:24:00
    事件分发机制在Android是一个比较重要的知识体系,比较复杂,往往弄得一些初学者一头雾水,中在开发中充分的理解事件分发机制有助于我们解决滑动冲突问题,和一些自定义控件中的疑难问题,提高开发效率。接下里将...

    事件分发机制在Android是一个比较重要的知识体系,比较复杂,往往弄得一些初学者一头雾水,中在开发中充分的理解事件分发机制有助于我们解决滑动冲突问题,和一些自定义控件中的疑难问题,提高开发效率。接下里将详细介绍事件分发机制的整个过程和机理。

    概述

    当一个View被初始化结束后,经过一些列的处理,通过PhoneWindow把该View加载到Activity上面。当我们手指操作屏幕时候,是首先被当前的Activity捕获到的,交给Activity处理,Activity不具有事件的拦截能力,但是具有事件分发和事件响应的能力,若Activity没有子类View处理,则Activity处理;若Activity有子类View处理,则把事件分发给子类View处理。对于View处理事件则需要经过dispatchTouchEvent,onInterceptTouchEvent、onTouchEvent三个阶段,关系图如下:


    事件分发

    事件分发本身也具有消费的能力,当用户触摸屏幕时,被Activity捕获到,他把这个事件通过dispatchTouchEvent(MotionEvent ev)方法会将事件传递给最外层View的dispatchTouchEvent(MotionEvent ev)方法,该方法对事件进行分发。这里分为三种情况:

    1、返回true:表示在本层不再进行分发,且已经进行消费掉,事件传递结束。如果不想让Activity的控件进行消费,可以重写Activity的dispatchTouchEvent方法,强制返回true。

    2、返回false:表示在本层中不再进行事件的分发。如果当前的View的事件直接来自于Activity,则Activity的onTouchEvent进行消费;如果当前View的事件来自于父控件,则交给上层控件的onTouchEvent方法进行消费。

    3、返回super.dispatchTouchEvent(ev):事件将分发给本层的事件拦截onInterceptTouchEvent 方法进行处理。

    事件拦截
    如上文所提到的,第三种情况会调用onInterceptTouchEvent(MotionEvent ev)方法会进行处理。分为三种情况:

    1、返回true:表示将事件进行拦截,并将拦截到的事件交由本层控件 的 onTouchEvent 进行处理。

    2、返回false:表示不拦截该事件,并将该事件交由子View的dispatchTouchEvent方法进行事件分发。

    3、返回super.onInterceptTouchEvent(ev):默认表示不拦截该事件,并将该事件交由子View的dispatchTouchEvent方法进行事件分发。和返回false一样。

    事件响应

    以下三种情况下回执行onTouchEvent(MotionEvent ev)方法:

    1、dispatchTouchEvent返回true

    2、onInterceptTouchEvent返回true

    3、onInterceptTouchEvent返回super.onInterceptTouchEvent(ev)

    在回调onTouchEvent方法时候也分为三种情况:

    1、返回true:表示onTouchEvent处理完事件后消费了此次事件。此时事件终结。

    2、返回false:则表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true,如果到了最顶层View还是返回false,那么认为该事件不消耗,则在同一个事件系列中,当前View无法再次接收到事件,该事件会交由Activity的onTouchEvent进行处理,如果Activity不处理,则这个事件就无响应。

    3、返回super.dispatchTouchEvent(ev):表示不响应。和return false一致。

    注意:

    1、ViewGroup默认返回false,即不拦截任何事件。

    2、不作为容器的View,如TextView,一旦接受到事件,就调用onTouchEvent方法,它们本身没有onInterceptTouchEvent方法。正常情况下,它们都会消耗事件(返回true),除非它们是不可点击的(clickable和longClickable都为false),那么就会交由父容器的onTouchEvent处理。

    点击事件的流程分析

    在系统为我们提供的一些空间中有的默认是可以点击的,如Button,有些是默认不可以点击的,例如:TextView。那么当我们给他设置了点击事件后他是如何执行的呢?OnTouchListener、onTouchEvent、OnClickListener、onTouch、onClick的执行顺序又是什么呢?这里分析一下。通过给view设置触摸事件,点击事件,重写onTouchEvent方法,我们进行日志的打印分析。




    打印的日志如下:


    当按下时候打印onTouch,onTouchEvent;取消的时候打印:onTouch,onTouchEvent,onClick。这里执行了两次事件:按下和取消所以onTouch,onTouchEvent才执行了两次,可以得出:

    点击事件的执行顺序是:OnTouchListener的onTouch方法—->onTouchEvent-->OnClickListener的onClick方法

    当设置onTouch返回值为true时候,打印日志如下:


    可以看出View的onTouchEvent,onClick方法没有被执行,这个再次论证了上面的结论。

    同样的方法我们这里改变onTouchEvent的返回值,来观察日志的打印情况,这里就不再次写出过程了,有兴趣的同学可以下来试试。通过以上分析,我们得出以下结论:

    结论:

    1、点击事件分发过程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent-->OnClickListener的onClick方法。所以三者优先级是onTouch->onTouchEvent->onClick。

    2、平时给调用的setOnClickListener,优先级是最低的,所以onTouchEvent或OnTouchListener的onTouch方法如果返回true,则不响应onClick方法。

    2、如果一个View同时监听了onTouch事件和onClick事件,则在onTouch里面应该返回false,否则点击事件就无法监听到。

    3、View 的onTouchEvent 方法默认都会消费掉事件(返回true),除非它是不可点击的(clickable和longClickable同时为false。


    展开全文
  • 事件分发机制的再整理

    千次阅读 2016-03-18 19:37:29
     三个很重要的方法:dispatchonTouchEvent()(事件分发)、onInterceptTouchEVent()(事件拦截)、onTouchEvent()(事件处理);  传递规则:当一个点击事件发生时候,传递顺序依照Activity->window->顶级的View;...
  • 本文为自己多年来在Android实战开发过程中总结归纳的一些常见问题,现在分享出来希望对...事件分发是Android开发过程中的重点又是难点,一张事件分发流程图,让你彻底搞明白。网上有很多文章写事件分发,感觉都没...
  • 记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了。 还未阅读过的朋友,请先参考 Android事件分发机制完全解析,带你从...
  • 其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识。也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何...
  • 事件分发机制原理及其分析

    千次阅读 2018-11-08 11:12:42
    先给大家讲讲什么是事件分发? 大家知道Android中的视图是由一个个View嵌套构成的层级视图,即一个View里包含有子View,而这个子View里面又可以再添加View。当用户触摸屏幕产生一系列事件时,事件会由高到低,由...
  • 图解android事件分发机制

    千次阅读 2019-02-14 11:21:12
    在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必...
  • Android事件分发机制 详解攻略,您值得拥有

    万次阅读 多人点赞 2019-07-02 12:15:25
    网上有大量关于Android事件分发机制的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化等等 今天,我将全面总结Android的事件分发机制,我能保证这是市面上的最全面、最清晰、最易懂的 ...
  • 事件分发机制的用法和理解

    千次阅读 2018-09-13 09:31:56
    前言 我在昨天的博客里面转载了一篇写的...这篇文章只谈我看了昨天的文章之后,对事件分发机制的理解的用法   正文 其实看过我之前的博客的朋友,对事件分发已经有了一个大概的概念。 事件分发,指的就是手指...
  • 浅谈Android事件分发机制

    万次阅读 2017-08-06 09:44:24
    在Android实际开发过程中经常会遇到View之间的滑动冲突,如ScrollView与Listview、RecyclerView之间的嵌套使用。在很好的解决此类问题之前,我们应深入的了解Android事件响应机制
  • 今天开始写一点关于view的知识,先从最基本的讲吧,android的事件分发机制,其实在我看来,android的事件分发机制在现实生活中经常能看到,所以我觉得还是很好理解的;先看看生活中常见的一种情形吧;比如说,现在你...
  • Android6.0触摸事件分发机制解读

    万次阅读 2016-10-25 13:08:28
    本篇博文是Android触摸事件分发机制系列博文的第一篇,带领大家从全局掌握Android触摸事件分发机制。特别声明的是,本源码解读是基于最新的Android6.0版本。为什么要解读触摸事件分发机制1.掌握View事件分发机制 2....
  • 近日项目中有需求需要使用一个禁止滚动的viewpager,其实代码很简单,重写viewpager下面两个方法就可以了 @Override public boolean onInterceptTouchEvent(MotionEvent event) { return false;...
  • Android中的事件分发机制也就是View与ViewGroup的对事件的分发与处理。在ViewGroup的内部包含了许多View,而ViewGroup继承自View,所以ViewGroup本身也是一个View。对于事件可以通过ViewGroup下发到它的子View并交由...
  • Android View的事件分发机制探索

    千次阅读 2017-08-29 09:19:11
    本文讲述Android framework层的事件分发传递机制,以及开发注意的要点。
  • Android开发——事件分发机制详解

    万次阅读 2017-07-14 14:20:36
    0. 前言 深入学习事件分发机制,是为了解决在Android开发中遇到的滑动冲突问题做准备。事件分发机制描述了用户的手势一系列事件是如何被Android系统传递并消费的。首先对事件分发机制进行概述:如果当一个点击事件...
  • 说起Android滑动冲突,是个很常见的场景,比如SliddingMenu与ListView的嵌套,要解决滑动冲突,不得不提及到View的事件分发机制。 一、Touch事件传递规则分析 首先,我们要知道Touch事件是包装在MotionEvent对象中...
1 2 3 4 5 ... 20
收藏数 73,052
精华内容 29,220
关键字:

事件分发机制