Android Touch事件

1. 五造轮子

1.1. 一造:直接传给目标Viewbash

咱们先实现一个最简单的需求:Activity 中有一堆层层嵌套的 View,有且只有最里边那个 View 会消费事件 (黄色高亮 View 表明能够消费事件,蓝色 View 表明不消费事件)微信

思考方案:框架

  1. 首先事件从哪儿来,确定得从父亲那来,由于子View被包裹在里面,没有直接与外界通讯的办法,而实际中Activity链接着根ViewDecorView,它是通往外界的桥梁,能接收到屏幕硬件发送过来的触摸事件
  2. 因此事件是从Activity开始,通过一层一层 ViewGroup ,传到最里边的 View
  3. 这时只须要一个从外向里传递事件的passEvent(ev)方法,父亲一层层往里调,能把事件传递过去,就完成了需求

示意图ide

open class MView {
    open fun passEvent(ev: MotionEvent) {
        // do sth
    }
}
    
class MViewGroup(private val child: MView) : MView() {
    override fun passEvent(ev: MotionEvent) {
        child.passEvent(ev)
    }
}
复制代码
  1. 暂时把Activity当成MViewGroup处理也没有问题
  2. 为何是MViewGroup继承MView而不是反过来,由于 MView 是不须要 child 字段的

1.2. 二造:从里向外传给目标View布局

而后咱们增长一条需求,让状况复杂一点:Activity中有一堆层层嵌套的View,有好几个叠着的View能处理事件post

同时须要增长一条设计原则:用户的一次操做,只能被一个View真正处理(消费)ui

  1. 要求这条原则是为了让操做的反馈符合用户直觉
  2. 很容易理解,正常状况下人只会想一次就作一件事
    1. 好比一个列表条目,列表能够点击进入详情,列表上还有个编辑按钮,点击能够编辑条目
      1. 这是一个上下两个View都能点击的场景,但用户点一个地方,确定只想去作一件事,要么进入详情,要么是编辑条目,若是你点编辑结果跳了两个页面,那确定是不合适的
    2. 再好比在一个可点击Item组成的列表里(好比微信的消息界面),Item能够点击进入某个聊天,列表还能滑动上下查看
      1. 若是你让Item和列表都处理事件,那在你滑动的时候,你可能得跳一堆你不想去的聊天页面

若是使用第一次试造的框架,要遵照这条原则,就须要在每个能够处理事件的View层级,判断出本身要处理事件后,不继续调用child的passEvent()方法了,保证只有本身处理了事件。 但若是真这样实现了,在大部分场景下会显得怪怪的,由于处理事件的顺序不对:this

  1. 好比仍是上面的列表,当用户点击按钮想编辑条目的时候,点击事件先传到条目,若是你在条目中判断须要事件,而后把事件消费了不传给子View,用户就永远点不开编辑条目了
  2. 并且换个角度看更加明显,用户确定但愿点哪,哪儿最靠上、离手指最近的东西被触发

因此实现新增需求的一个关键是:找到那个适合处理事件的View,而咱们经过对业务场景进行分析,获得答案是:那个最里面的View适合处理事件spa

这就不能是等parent不处理事件了才把事件传给child,应该反过来,你须要事件的处理顺序是从里向外:里边的child不要事件了,才调用parent的passEvent()方法把事件传出来。 因而得加一条向外的通道,只能在这条向外的通道上处理事件,前面向里的通道什么都不干,只管把事件往里传。 因此这时你有了两条通道,改个名字吧,向里传递事件是passIn()方法,向外传递并处理事件是passOut()方法。设计

示意图

open class MView {
    var parent: MView? = null

    open fun passIn(ev: MotionEvent) {
        passOut(ev)
    }

    open fun passOut(ev: MotionEvent) {
        parent?.passOut(ev)
    }
}

class MViewGroup(private val child: MView) : MView() {
    init {
        child.parent = this // 示意写法
    }

    override fun passIn(ev: MotionEvent) {
        child.passIn(ev)
    }
}
复制代码

这段代码没有问题,很是简单,可是它对需求意图的表达不够清晰,增长了框架的使用难度

  1. 如前所述,咱们但愿passIn()的时候只传递事件,但愿在passOut()的时候每一个View决定是否要处理事件,并进行处理,并且在处理事件后,再也不调用parent的passOut()方法把事件传出来
  2. 你会发现,这其中包含了两类职责:
    1. 一类是事件传递控制逻辑,另外一类是事件处理钩子
    2. 其中事件传递控制逻辑基本不会变化,但事件处理的钩子中可能作任何事情
  3. 咱们须要把不一样职责的代码分开,更须要把变化的和不变的分开,减小框架使用者的关注点

因而咱们用一个叫作dispatch()的方法单独放事件传递的控制逻辑,用一个叫作onTouch()的方法做为事件处理的钩子,并且钩子有一个返回值,表示钩子中是否处理了事件:

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}

class MViewGroup(private val child: MView) : MView() {
    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = child.dispatch(ev)
        if (!handled) handled = onTouch(ev)

        return handled
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}
复制代码

这样写完,整个行为其实没有变化,但你会发现:

  1. 控制逻辑集中在dispatch()中,一目了然
  2. onTouch()单纯是一个钩子,框架使用者只须要关心这个钩子和它的返回值,不用太关心控制流程
  3. 另外,连parent也不须要了

1.3. 三造:区分事件类型

上文的实现看上去已经初具雏形了,但其实连开始提的那条原则都没实现完,由于原则要求一次操做只能有一个 View 进行处理,而咱们实现的是一个触摸事件只能有一个View进行处理。 这里就涉及到一次触摸操做和一个触摸事件的区别:

  1. 假设尚未触摸事件的概念,咱们要怎么区分一次触摸操做呢?
    1. 把触摸操做细分一下,大概有按下动做、抬起动做、与屏幕接触时的移动和停留动做
    2. 很容易想到,要区分两次触摸操做,能够经过按下和抬起动做进行区分,按下动做开始了一次触摸操做,抬起动做结束了一次触摸,按下和抬起中间的移动和停留都属于这一次触摸操做,至于移动和停留是否要区分,目前没有看到区分的必要,能够都做为触摸中来处理
  2. 因而在一次触摸操做中就有了三种动做的类型:DOWN/UP/ING,其中ING有点不够专业,改个名字叫MOVE吧
  3. 而每一个触摸动做会在软件系统中产生一个一样类型的触摸事件
  4. 因此最后,一次触摸操做就是由一组从DOWN事件开始、中间是多个MOVE事件、最后结束于UP事件的事件流组成

因而设计原则更确切地说就是:一次触摸产生的事件流,只能被一个View消费

在上次试造的基础上把一个事件变成一个组事件流,其实很是简单:处理DOWN事件时跟前面处理一个事件时同样,但须要同时记住DOWN事件的消费对象,后续的MOVE/UP事件直接交给它就好了

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}

class MViewGroup(private val child: MView) : MView() {
    private var isChildNeedEvent = false

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false
        
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
        
            handled = child.dispatch(ev)
            if (handled) isChildNeedEvent = true

            if (!handled) handled = onTouch(ev)
        } else {
            if (isChildNeedEvent) handled = child.dispatch(ev)
            if (!handled) handled = onTouch(ev)
        }
        
        if (ev.actionMasked == MotionEvent.ACTION_UP) {
            clearStatus()
        }
            
        return handled
    }
    
    private fun clearStatus() {
        isChildNeedEvent = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}
复制代码

代码好像增长了不少,其实只多作了两件事:

  1. 增长了一个isChildNeedEvent状态,对是子View是否处理了DOWN事件进行记录,并在其余触摸事件时使用这个状态
  2. 在收到DOWN事件的最开始和收到UP事件的最后,重置状态

此时框架使用者仍是只须要关心onTouch()钩子,在须要处理事件时进行处理并返回true,其余事情框架都作好了。

1.4. 四造:增长外部事件拦截

上面的框架已经能完成基本的事件分发工做了,但下面这个需求,你尝试一下用如今框架能实现吗? 需求:在可滑动View中有一个可点击View,须要让用户即便按下的位置是可点击View,再进行滑动时,也能够滑动外面的的可滑动View。

这个需求其实很是常见,好比全部「条目可点击的滑动列表」就是这样的(微信/QQ聊天列表)。

假如使用上面的框架:

  1. 可滑动View会先把事件传到里边的可点击View
  2. 可点击View一看来事件了,我又能点击,那舍我其谁啊
  3. 而后外面的可滑动View就永远没法处理事件,也就没法滑动

因此直接使用如今的模型去实现的「条目可点击的滑动列表」会永远滑动不了。

那怎么办呢?

  1. 难道要让里面的可点击View去感知一下(层层往上找),本身是否是被一个能消费事件的View包裹?是的话本身就不消费事件了?
    1. 这确定是不行的,先不说子View层层反向遍历父亲是否是个好实现,至少不能外面是能够滑动的,里边View的点击事件就所有失效
  2. 或者咱们调整dispatch()方法在传入事件过程当中的人设,让它不是只能往里传递事件,而是在本身能消费事件的时候把事件给本身
    1. 这确定也是不行的,跟第一个办法的主要问题同样

直接想实现以为处处是矛盾,找不到突破口,那就从头开始吧,从什么样的触摸反馈是用户以为天然的出发,看看这种符合直觉的反馈方案是否存在,找出来它是什么,再考虑咱们要怎么实现:

  1. 当用户面对一个滑动View里有一个可点击View,当他摸在可点击View上时,他是要作什么?
  2. 显然,只有两个可能性,要么用户想点这个可点击View,要么用户想滑动这个可滑动View
  3. 那么,当用户刚用手指接触的时候,也就是DOWN事件刚来的时候,能判断用户想干什么吗?很抱歉,不能
  4. 因此,客观条件下,你就是不可能在DOWN事件传过来的时候,判断出用户到底想作什么,因而两个View其实都不能肯定本身是否要消费事件

你先忘记前面说的原则,你想一想,不考虑其余因素,也不是只能用DOWN事件,只要你能判断用户的想法就行,你有什么办法?

  1. 办法确定是有的,你能够多等一会,看用户接下来的行为能匹配哪一种操做模式
    1. 点击操做的模式是这样:用户先DOWN,而后MOVE很小一段,也不会MOVE出这个子View,关键是比较短的时间就UP
    2. 滑动操做的模式是这样:用户先DOWN,而后开始MOVE,这时候可能会MOVE出这个子View,也可能不,但关键是比较长的时间也没有在UP,一直是在MOVE
  2. 因此你的结论是,只有DOWN不行,还得看接下来的事件流,得走着瞧
  3. 再多考虑个长按的状况,总结就是:
    1. 若是在某个时间内UP,就是点击里边的View
    2. 若是比较长的时间UP,但没怎么MOVE,就是长按里边的View
    3. 若是在比较短的时间MOVE比较长的距离,就是滑动外面的View

看上去这个目标 View 断定方案很不错,安排得明明白白,但咱们现有的事件处理框架实现不了这样的断定方案,至少存在如下两个冲突:

  1. 由于子View和父View都没法在DOWN的时候判断当前事件流是否是该给本身,因此一开始它们都只能返回false。但为了能对后续事件作判断,你但愿事件继续流过它们,按照当前框架的逻辑,你又不能返回false。
  2. 假设事件会流过它们,当事件流了一下子后,父 View 判断出这符合本身的消费模式啊,因而想把事件给本身消费,但此时子 View 可能已经在消费事件了,而目前的框架是作不到阻止子 View 继续消费事件的

因此要解决上述的冲突,就确定要对上一版的事件处理框架进行修改,并且看上去一不当心就会大改

  1. 首先看第二个冲突,解决它的一个直接方案是:调整 dispatch() 方法在传入事件过程当中的人设,让它不是只传递事件了,还能够在往里传递事件前进行拦截,可以看状况拦截下事件并交给本身的 onTouch() 处理

  2. 基于这个解决方案,大概有如下两个改动相对小的方案调整思路:

    1. 思路一:
      1. 当事件走到可滑动父View的时候,它先拦截并处理事件,并且还把事件给攒着
      2. 当通过了几个事件
        1. 若是判断出符合本身的消费模式,那就直接开始本身消费了,也不用继续攒事件了
        2. 若是判断出不是本身的消费模式,再把全部攒着的事件一股脑给子 View,触发里边的点击操做
    2. 思路二:
      1. 全部的 View 只要可能消费事件,就在onTouch()里对DOWN事件返回true,无论是否识别出当前属于本身的消费模式
      2. 当事件走到到可滑动父 View 的时候,它先把事件往里传,里边可能会处理事件,可能不会,可滑动父 View 都暂时不关心
      3. 而后看子 View 是否处理事件
        1. 假如子 View 不处理事件,那啥问题没有,父 View 直接处理事件就行了
        2. 假如子 View 处理事件,可滑动父View就会绷紧神经暗中观察乘机而动,观察事件是否是符合本身的消费模式,一旦发现符合,它就把事件流拦截下来,即便子View也在处理事件,它也不往里disptach事件了,而是直接给本身的onTouch()
  3. 两个思路总结一下:

    1. 思路一:外面的父 View 先拦事件,若是判断拦错了,再把事件往里发
    2. 思路二:外面的父 View 先不拦事件,在判断应该拦的时候,忽然把事件拦下来
  4. 这两个思路都要对当前框架作改变,看似差很少,但其实仍是有比较明显的优劣的

    1. 思路一问题比较明显:
      1. 父 View 把事件拦下来了,而后发现拦错了再给子 View,但其实子 View 又并不必定能消费事件,这不就是白作一步吗。等到子View不处理事件,又把事件们还给父View,父View还得继续处理事件。整个过程不只繁琐,并且会让开发者感受到别扭
      2. 因此这个思路不太行,还得是把事件先给子View
    2. 思路二就相对正常多了,只有一个问题(下一节再讲,你能够猜一猜,这里我先当没发现),并且框架要作的改变也不多:
      1. 增长一个拦截方法onIntercept()在父 View 往里dispatch事件前,开发者能够覆写这个方法,加入本身的事件模式分析代码,而且能够在肯定要拦截的时候进行拦截
        1. 把分析拦截逻辑抽成一个方法很是合理:何时拦,何时不拦,内里的逻辑不少,但对外暴露的 API 能够很小,很是适合抽出去
      2. 在肯定本身要拦截事件的时候,即便里边在一开始消费了事件,也不把事件往里传了,而是直接给本身的onTouch()

示意图:

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}

class MViewGroup(private val child: MView) : MView() {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false

        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
            
            if (onIntercept(ev)) {
                isSelfNeedEvent = true
                handled = onTouch(ev)
            } else {
                handled = child.dispatch(ev)
                if (handled) isChildNeedEvent = true

                if (!handled) {
                    handled = onTouch(ev)
                    if (handled) isSelfNeedEvent = true
                }
            }
        } else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                if (onIntercept(ev)) {
                    isSelfNeedEvent = true
                    handled = onTouch(ev)
                } else {
                    handled = child.dispatch(ev)
                }
            }
        }

        if (ev.actionMasked == MotionEvent.ACTION_UP) {
            clearStatus()
        }
        
        return handled
    }

    private fun clearStatus() {
        isChildNeedEvent = false
        isSelfNeedEvent = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }

    open fun onIntercept(ev: MotionEvent): Boolean {
        return false
    }
}
复制代码

写的过程当中增长了一些对细节的处理:

  1. 不只是在DOWN事件的dispatch()前须要拦截,在后续事件中,也须要加入拦截,不然没法实现中途拦截的目标
  2. 在某一个事件判断拦截以后,还须要在后续事件中再判断一次是否要拦截吗?
    1. 彻底不须要,咱们但愿的就是在一次触摸中,尽量只有1个对象去消费事件,决定是你了,那就不要变
    2. 因此增长一个isSelfNeedEvent记录本身是否拦截过事件,若是拦截过,后续事件直接就交给本身处理
  3. 在后续事件时,子 View 没有处理事件,外面也不会再处理了,一样由于只能有一个 View 处理(Actvity会处理这样的事件,后面会提到)

这一下代码是否是看上去瞬间复杂了,但其实只是增长了一个事件拦截机制,对比上一次试造的轮子,会更容易理解。(要是 Markdown 支持代码块内自定义着色就行了)

并且对于框架的使用者来讲,关注点仍是很是少

  1. 重写onIntercept()方法,判断何时须要拦截事件,须要拦截时返回true
  2. 重写onTouch()方法,若是处理了事件,返回true

1.5. 五造:增长内部事件拦截

上面的处理思路虽然实现了需求,但可能会致使一个问题:里边的子 View 接收了一半的事件,可能都已经开始处理并作了一些事情,父 View 突然就不把后续事件给它了,会不会违背用户操做的直觉?甚至出现更奇怪的现象?

这个问题确实比较麻烦,分两类状况讨论

  1. 里边的 View 接收了一半事件,但尚未真正开始反馈交互,或者在进行能够被取消的反馈

    1. 好比对于一个可点击的View,View的默认实现是只要被touch了,就会有pressed状态,若是你设置了对应的background,你的 View 就会有高亮效果
    2. 这种高亮即便被中断也没事,不会让用户感受到奇怪,不信你本身试试微信的聊天列表
    3. 但一个值得注意的点是,若是你只是直接不发送MOVE事件了,这会有问题,就这个按下高亮的例子,若是你只是不传MOVE事件了,那谁来告诉里边的子View取消高亮呢?因此你须要在中断的时候也传一个结束事件
      1. 可是,你能直接传一个UP事件吗?也是不行的,由于这样就匹配了里边点击的模式了,会直接触发一个点击事件,这显然不是咱们想要的
      2. 因而外面须要给一个新的事件,这个事件的类型就叫取消事件好了CANCEL
    4. 总结一下,对于这种简单的可被取消状况,你能够这样去处理:
      1. 在肯定要拦截的时候,在把真正的事件转发给本身的onTouch()的同时,另外生成一个新的事件发给本身的子View,事件类型是CANCEL,它将是子View收到的最后一个事件
      2. 子View能够在收到这个事件后,对当前的一些行为进行取消
  2. 里边的View接收了一半事件,已经开始反馈交互了,这种反馈最好不要去取消它,或者说取消了会显得很怪

    1. 这个时候,事情会复杂一些,并且这个场景发生的远比你想象中的多,形式也多种多样,不处理好的后果也比只是让用户感受上奇怪要严重得多,可能会有的功能会实现不了,下面举两个例子

      1. 在ViewPager里有三个page,page里是ScrollView,ViewPager能够横向滑动,page里的ScrollView能够竖向滑动
        1. 若是按前面逻辑,当ViewPager把事件给里边ScrollView以后,它也会偷偷观察,若是你一直是竖向滑动,那没话说,ViewPager不会触发拦截事件
        2. 但若是你竖着滑着滑着,手抖了,开始横滑(或者只是斜滑),ViewPager就会开始紧张,想「组织终于决定是我了吗?真的假的,那我可就不客气了」,因而在你斜滑必定距离以后,突然发现,你划不动ScrollView了,而ViewPager开始动
        3. 缘由就是ScrollView的竖滑被取消了,ViewPager把事件拦下来,开始横滑
        4. 这个体验仍是比较怪的,会有种过于灵敏的感受,会让用户只能当心翼翼地滑动
      2. 在一个ScrollView里有一些按钮,按钮有长按事件,长按再拖动就能够移动按钮
        1. (更常见的例子是一个列表,里边的条目能够长按拖动)
        2. 一样按前面的逻辑,当你长按后准备拖动按钮时,你怎么保证不让ScrollView把事件拦下来呢?
    2. 因此这类问题是必定要解决的,但要怎么解决呢

      1. 仍是先从业务出发,从用户的角度看,当里边已经开始作一些特殊处理了,外面应不该该把事件抢走?
        1. 不该该对吧,OK,解决方针就是不该该让外边的View抢事件
      2. 因此接下来的问题是:谁先判断出外边的View不应抢事件,里边的子View仍是外边的父View?而后怎么不让外边的View抢?
        1. 首先,确定是里边的View作出判断:这个事件,真的,外边的View你最好别抢,要不用户不开心了
        2. 而后里边就得告知外边,你别抢了,告知能够有几个方式
          1. 外边抢以前问一下里边,我能不能抢
          2. 里边在肯定这个事件不能被抢以后,从dispatch方法返回一个特别的值给外边(以前只是true和false,如今要加一个)
          3. 里边经过别的方式通知外边,你不要抢
        3. 讲道理,我以为三个方式都行,但第三个方式最为简单直接,并且对框架没有过大的改动,Android也使用了这个方式,父View给子View提供了一个方法requestDisallowInterceptTouchEvent(),子View调用它改变父View的一个状态,同时父View每次在准备拦截前都会判断这个状态(固然这个状态只对当前事件流有效)
        4. 而后,这个状况还得再注意一点,它应该是向上递归的,也就是,在复杂的状况中,有可能有多个上级在暗中观察,当里边的View决定要处理事件并且不许备交出去的时候,外面全部的暗中观察的父View都应该把脑壳转回去

因此,连同上一次试造,总结一下

  1. 对于多个可消费事件的View进行嵌套的状况,怎么断定事件的归属会变得很是麻烦,没法马上在DOWN事件时就肯定,只能在后续的事件流中进一步判断
  2. 因而在没判断归属的时候,先由里边的子View消费事件,外面暗中观察,同时两方一块对事件类型作进一步匹配,并准备在匹配成功后对事件流的归属进行抢拍
  3. 抢拍是先抢先得
    1. 父亲先抢到,发个CANCEL事件给儿子就完了
    2. 儿子先抢到,就得大喊大叫,撒泼耍赖,爸爸们行行好吧,最后得以安心处理事件

另外有几个值得一提的地方:

  1. 这种先抢先得的方式感受上有点乱来是吧,但目前也没有想到更好的办法了,通常都是开发者本身根据实际用户体验调整,让父亲或儿子在最适合的时机准确及时地抢到应得的事件
  2. 父View在拦截下事件后,把接下来的事件传给本身的onTouch()后,onTouch()只会收到后半部分的事件,这样会不会有问题呢?
    1. 确实直接给后半部分会有问题,因此通常状况是,在没拦截的时候就作好若是要处理事件的一些准备工做,以便以后拦截事件了,只使用后半部分事件也能实现符合用户直觉的反馈
interface ViewParent {
    fun requestDisallowInterceptTouchEvent(isDisallowIntercept: Boolean)
}

open class MView {
    var parent: ViewParent? = null

    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}

open class MViewGroup(private val child: MView) : MView(), ViewParent {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false
    private var isDisallowIntercept = false

    init {
        child.parent = this
    }

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false
        
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
            
            // add isDisallowIntercept
            if (!isDisallowIntercept && onIntercept(ev)) {
                isSelfNeedEvent = true
                handled = onTouch(ev)
            } else {
                handled = child.dispatch(ev)
                if (handled) isChildNeedEvent = true

                if (!handled) {
                    handled = onTouch(ev)
                    if (handled) isSelfNeedEvent = true
                }
            }
        } else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                // add isDisallowIntercept
                if (!isDisallowIntercept && onIntercept(ev)) {
                    isSelfNeedEvent = true

                    // add cancel
                    val cancel = MotionEvent.obtain(ev)
                    cancel.action = MotionEvent.ACTION_CANCEL
                    handled = child.dispatch(cancel)
                    cancel.recycle()
                } else {
                    handled = child.dispatch(ev)
                }
            }
        }
        
        if (ev.actionMasked == MotionEvent.ACTION_UP 
            || ev.actionMasked == MotionEvent.ACTION_CANCEL) {
            clearStatus()
        }
        
        return handled
    }
    
    private fun clearStatus() {
        isChildNeedEvent = false
        isSelfNeedEvent = false
        isDisallowIntercept = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }

    open fun onIntercept(ev: MotionEvent): Boolean {
        return false
    }

    override fun requestDisallowInterceptTouchEvent(isDisallowIntercept: Boolean) {
        this.isDisallowIntercept = isDisallowIntercept
        parent?.requestDisallowInterceptTouchEvent(isDisallowIntercept)
    }
}
复制代码

此次改动主要是增长了发出CANCEL事件和requestDisallowInterceptTouchEvent机制

  1. 在发出CANCEL事件时有一个细节:没有在给 child 分发CANCEL事件的同时继续把原事件分发给本身的onTouch
    1. 这是源码中的写法,不是我故意的,多是为了让一个事件也只能有一个View处理,避免出现bug
  2. 实现requestDisallowInterceptTouchEvent机制时,增长了ViewParent接口
    1. 不使用这种写法也行,但使用它从代码整洁的角度看会更优雅,好比避免反向依赖,并且这也是源码的写法,因而直接搬来了

虽然目前整个框架的代码有点复杂,但对于使用者来讲,依然很是简单,只是在上一版框架的基础上增长了:

  1. 若是View判断本身要消费事件,并且执行的是不但愿被父View打断的操做时,须要马上调用父View的requestDisallowInterceptTouchEvent()方法
  2. 若是在onTouch方法中对事件消费而且作了一些操做,须要注意在收到CANCEL事件时,对操做进行取消

到这里,事件分发的主要逻辑已经讲清楚了,不过还差一段 Activity 中的处理,其实它作的事情相似ViewGroup,只有这几个区别:

  1. 不会对事件进行拦截
  2. 只要有子View没有处理的事件,它都会交给本身的onTouch()
open class MActivity(private val childGroup: MViewGroup) {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false

    open fun dispatch(ev: MotionEvent): Boolean {
        var handled = false

        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()

            handled = childGroup.dispatch(ev)
            if (handled) isChildNeedEvent = true

            if (!handled) {
                handled = onTouch(ev)
                if (handled) isSelfNeedEvent = true
            }
        } else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                handled = childGroup.dispatch(ev)
            }

            if (!handled) handled = onTouch(ev)
        }

        if (ev.actionMasked == MotionEvent.ACTION_UP
            || ev.actionMasked == MotionEvent.ACTION_CANCEL) {
            clearStatus()
        }

        return handled
    }

    private fun clearStatus() {
        isChildNeedEvent = false
        isSelfNeedEvent = false
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}
复制代码

因此回头看,你会发现事件分发其实很简单,它的关键不在于「不一样的事件类型、不一样的View种类、不一样的回调方法、方法不一样的返回值」对事件分发是怎么影响的。

关键在于「它要实现什么功能?对实现效果有什么要求?使用了什么解决方案?」,从这个角度,就能清晰并且简单地把事件分发整个流程梳理清楚。

事件分发要实现的功能是:对用户的触摸操做进行反馈,使之符合用户的直觉。

从用户的直觉出发能获得这么两个要求:

用户的一次操做只有一个View去消费

  1. 让消费事件的View跟用户的意图一致
  2. 第二个要求是最难实现的,若是有多个View均可以消费触摸事件,怎么断定哪一个View更适合消费,而且把事件交给它。

咱们使用了一套简单但有效的先到先得策略,让内外的可消费事件的View拥有近乎平等的竞争消费者的资格:它们都能接收到事件,并在本身断定应该消费事件的时候去发起竞争申请,申请成功后事件就所有由它消费。

场景一

模拟View和ViewGroup都不消费事件的场景:

[down]
|layer:SActivity |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Intercept_BE |type:down
|layer:SViewGroup |on:Intercept_AF |result(super):false |type:down
|layer:SView |on:Dispatch_BE |type:down
|layer:SView |on:Touch_BE |type:down
|layer:SView |on:Touch_AF |result(super):false |type:down
|layer:SView |on:Dispatch_AF |result(super):false |type:down
|layer:SViewGroup |on:Touch_BE |type:down
|layer:SViewGroup |on:Touch_AF |result(super):false |type:down
|layer:SViewGroup |on:Dispatch_AF |result(super):false |type:down
|layer:SActivity |on:Touch_BE |type:down
|layer:SActivity |on:Touch_AF |result(super):false |type:down
|layer:SActivity |on:Dispatch_AF |result(super):false |type:down
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SActivity |on:Touch_BE |type:move
|layer:SActivity |on:Touch_AF |result(super):false |type:move
|layer:SActivity |on:Dispatch_AF |result(super):false |type:move

[move]
...
 
[up]
|layer:SActivity |on:Dispatch_BE |type:up
|layer:SActivity |on:Touch_BE |type:up
|layer:SActivity |on:Touch_AF |result(super):false |type:up
|layer:SActivity |on:Dispatch_AF |result(super):false |type:up
复制代码

这里用BE表明 before,表示该方法开始处理事件的时候,用AF表明after,表示该方法结束处理事件的时候,而且打印处理的结果

从日志中能清楚看到,当View和ViewGroup都不消费DOWN事件时,后续事件将再也不传递给View和ViewGroup

场景二

模拟View和ViewGroup都消费事件,同时ViewGroup在第二个MOVE事件时认为本身须要拦截事件的场景:

[down]
|layer:SActivity |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Intercept |result(false):false |type:down
|layer:SView |on:Dispatch_BE |type:down
|layer:SView |on:Touch |result(true):true |type:down
|layer:SView |on:Dispatch_AF |result(super):true |type:down
|layer:SViewGroup |on:Dispatch_AF |result(super):true |type:down
|layer:SActivity |on:Dispatch_AF |result(super):true |type:down
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Intercept |result(false):false |type:move
|layer:SView |on:Dispatch_BE |type:move
|layer:SView |on:Touch |result(true):true |type:move
|layer:SView |on:Dispatch_AF |result(super):true |type:move
|layer:SViewGroup |on:Dispatch_AF |result(super):true |type:move
|layer:SActivity |on:Dispatch_AF |result(super):true |type:move
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Intercept |result(true):true |type:move
|layer:SView |on:Dispatch_BE |type:cancel
|layer:SView |on:Touch_BE |type:cancel
|layer:SView |on:Touch_AF |result(super):false |type:cancel
|layer:SView |on:Dispatch_AF |result(super):false |type:cancel
|layer:SViewGroup |on:Dispatch_AF |result(super):false |type:move
|layer:SActivity |on:Touch_BE |type:move
|layer:SActivity |on:Touch_AF |result(super):false |type:move
|layer:SActivity |on:Dispatch_AF |result(super):false |type:move
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Touch |result(true):true |type:move
|layer:SViewGroup |on:Dispatch_AF |result(super):true |type:move
|layer:SActivity |on:Dispatch_AF |result(super):true |type:move
 
[up]
|layer:SActivity |on:Dispatch_BE |type:up
|layer:SViewGroup |on:Dispatch_BE |type:up
|layer:SViewGroup |on:Touch |result(true):true |type:up
|layer:SViewGroup |on:Dispatch_AF |result(super):true |type:up
|layer:SActivity |on:Dispatch_AF |result(super):true |type:up
复制代码

从日志中能清楚看到,在ViewGroup拦截事件先后,事件是如何分发的

Android 源码

View的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result = false;

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(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;
            }
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; } 复制代码

定义ListenerInfo局部变量,ListenerInfo是View的静态内部类,用来定义一堆关于View的XXXListener等方法;接着if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句就是重点,首先li对象天然不会为null,li.mOnTouchListener经过下面方法赋值

public void setOnTouchListener(OnTouchListener l) {
    getListenerInfo().mOnTouchListener = l;
}
复制代码

li.mOnTouchListener是否是null取决于控件(View)是否设置setOnTouchListener监听。接着经过位与运算肯定控件(View)是否是ENABLED 的,默认控件都是ENABLED 的;接着判断onTouch的返回值是否是true。经过如上判断以后若是都为true则设置默认为false的result为true,那么接下来的if (!result && onTouchEvent(event))就不会执行,最终dispatchTouchEvent也会返回true。而若是if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句有一个为false则if (!result && onTouchEvent(event))就会执行,若是onTouchEvent(event)返回false则dispatchTouchEvent返回false,不然返回true。

控件触摸就会调运dispatchTouchEvent方法,而在dispatchTouchEvent中先执行的是onTouch方法,因此验证了实例结论总结中的onTouch优先于onClick执行道理。若是控件是ENABLE且在onTouch方法里返回了true则dispatchTouchEvent方法也返回true,不会再继续往下执行;反之,onTouch返回false则会继续向下执行onTouchEvent方法,且dispatchTouchEvent的返回值与onTouchEvent返回值相同。

总结结论

在View的触摸屏传递机制中经过分析dispatchTouchEvent方法源码咱们会得出以下基本结论:

  1. 触摸控件(View)首先执行dispatchTouchEvent方法。
  2. 在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。
  3. 若是控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的状况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回同样。
  4. 若是控件不是enable的设置了onTouch方法也不会执行,只能经过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回同样。
  5. 若是控件(View)是enable且onTouch返回true状况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。

View的dispatchTouchEvent方法中的onTouchEvent方法

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress) {
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    boolean isInScrollingContainer = isInScrollingContainer();

                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    if (!pointInView(x, y, mTouchSlop)) {
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }
复制代码

若是一个控件是enable且disclickable则onTouchEvent直接返回false了;反之,若是一个控件是enable且clickable则继续进入过于一个event的switch判断中,而后最终onTouchEvent都返回了true。switch的ACTION_DOWN与ACTION_MOVE都进行了一些必要的设置与置位,接着到手抬起来ACTION_UP时你会发现,首先判断了是否按下过,同时是否是能够获得焦点,而后尝试获取焦点,而后判断若是不是longPressed则经过post在UI Thread中执行一个PerformClick的Runnable,也就是performClick方法。

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }
复制代码
public void setOnClickListener(OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
复制代码

控件只要监听了onClick方法则mOnClickListener就不为null,并且有意思的是若是调运setOnClickListener方法设置监听且控件是disclickable的状况下默认会帮设置为clickable。onClick就在onTouchEvent中执行的,并且是在onTouchEvent的ACTION_UP事件中执行的。

总结结论

  1. onTouchEvent方法中会在ACTION_UP分支中触发onClick的监听。
  2. 当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action。

Android View的触摸屏事件传递机制有以下特征:

  1. 触摸控件(View)首先执行dispatchTouchEvent方法。
  2. 在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。
  3. 若是控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的状况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回同样。
  4. 若是控件不是enable的设置了onTouch方法也不会执行,只能经过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回同样。
  5. 若是控件(View)是enable且onTouch返回true状况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。
  6. 当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action(也就是说dispatchTouchEvent返回true才会进行下一次action派发)。

ViewGroup的dispatchTouchEvent方法

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;
    ......
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // 1)处理初始的ACTION_DOWN
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // 把ACTION_DOWN做为一个Touch手势的始点,清除以前的手势状态。
        cancelAndClearTouchTargets(ev); //清除前一个手势,*关键操做:mFirstTouchTarget重置为null*
        resetTouchState(); //重置Touch状态标识
    }

    // 2)检查是否会被拦截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        // 是ACTION_DOWN的事件,或者mFirstTouchTarget不为null(已经找到可以接收touch事件的目标组件)
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        // 判断禁止拦截的FLAG,由于requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法能够禁止执行是否须要拦截的判断
        if (!disallowIntercept) {
            // 禁止拦截的FLAG为false,说明能够执行拦截判断,则执行此ViewGroup的onInterceptTouchEvent方法
            intercepted = onInterceptTouchEvent(ev); // 此方法默认返回false,若是想修改默认的行为,须要override此方法,修改返回值。
            ev.setAction(action);
        } else {
            // 禁止拦截的FLAG为ture,说明没有必要去执行是否须要拦截了,这个事件是没法拦截的,可以顺利经过,因此设置拦截变量为false
            intercepted = false;
        }
    } else {
        // 当不是ACTION_DOWN事件而且mFirstTouchTarget为null(意味着没有touch的目标组件)时,这个ViewGroup应该继续执行拦截的操做。
        intercepted = true;
    }
    // 经过前面的逻辑处理,获得了是否须要进行拦截的变量值

    final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
    final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    if (!canceled && !intercepted) {
        // 不是ACTION_CANCEL而且拦截变量为false
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 在ACTION_DOWN时去寻找此次DOWN事件新出现的TouchTarget
            final int actionIndex = ev.getActionIndex(); // always 0 for down

            .....

            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
                // 根据触摸的坐标寻找可以接收这个事件的子组件
                final float x = ev.getX(actionIndex);
                final float y = ev.getY(actionIndex);

                final View[] children = mChildren;
                // 逆序遍历全部子组件
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = i;
                    final View child = children[childIndex];
                    // 寻找可接收这个事件而且组件区域内包含点击坐标的子View
                    if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }

                    newTouchTarget = getTouchTarget(child); // 找到了符合条件的子组件,赋值给newTouchTarget

                    ......

                    // 把ACTION_DOWN事件传递给子组件进行处理
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // 若是此子ViewGroup消费了这个touch事件
                        mLastTouchDownTime = ev.getDownTime();
                        mLastTouchDownIndex = childIndex;
                        mLastTouchDownX = ev.getX();
                        mLastTouchDownY = ev.getY();
                        // 则为mFirstTouchTarget赋值为newTouchTarget,此子组件成为新的touch事件的起点
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
                }
            }
            ......
        }
    }

    // 通过前面的ACTION_DOWN的处理,有两种状况。
    if (mFirstTouchTarget == null) {
        // 状况1:(mFirstTouchTarget为null) 没有找到可以消费touch事件的子组件或者是touch事件被拦截了,
        // 那么在ViewGroup的dispatchTransformedTouchEvent方法里面,处理Touch事件则和普通View同样,
        // 本身没法消费,调用super.dispatchOnTouchEvent()把事件回递给父ViewGroup进行处理
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    } else {
        // 状况2:(mFirstTouchTarget!=null) 找到了可以消费touch事件的子组件,那么后续的touch事件均可以传递到子View
        TouchTarget target = mFirstTouchTarget;
        // (这里为了理解简单,省略了一个Target List的概念,有须要的同窗再查看源码)
        while (target != null) {
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                // 若是前面利用ACTION_DOWN事件寻找符合接收条件的子组件的同时消费掉了ACTION_DOWN事件,这里直接返回true
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                // 对于非ACTION_DOWN事件,则继续传递给目标子组件进行处理(注意这里的非ACTION_DOWN事件已经不须要再判断是否拦截)
                if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                    // 若是target子组件进行处理,符合某些条件的话,会传递ACTION_CANCEL给target子组件
                    // 条件是:若是ACTION_DOWN时没有被拦截,然后面的touch事件被拦截,则须要发送ACTION_CANCEL给target子组件
                    handled = true;
                }
                ......
            }
        }
    }

    if (canceled || actionMasked == MotionEvent.ACTION_UP) {
        // 若是是ACTION_CANCEL或者ACTION_UP,重置Touch状态标识,mFirstTouchTarget赋值为null,后面的Touch事件都没法派发给子View
        resetTouchState();
    }
    ......

    return handled;
}
复制代码

ACTION_DOWN时进行一些初始化操做,清除以往的Touch状态而后开始新的手势。在这里你会发现cancelAndClearTouchTargets(ev)方法中有一个很是重要的操做就是将mFirstTouchTarget设置为了null,接着在resetTouchState()方法中重置Touch状态标识。

使用变量intercepted来标记ViewGroup是否拦截Touch事件的传递,该变量相似第一步的mFirstTouchTarget变量,在后续代码中起着很重要的做用。if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这一条判断语句说明当事件为ACTION_DOWN或者mFirstTouchTarget不为null(即已经找到可以接收touch事件的目标组件)时if成立,不然if不成立,而后将intercepted设置为true,也即拦截事件。当事件为ACTION_DOWN或者mFirstTouchTarget不为null时判断disallowIntercept(禁止拦截)标志位,而这个标记在ViewGroup中提供了public的设置方法

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            return;
        }

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

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

在其余地方调用requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,从而禁止执行是否须要拦截的判断。当disallowIntercept为true(禁止拦截判断)时则intercepted直接设置为false,不然调用onInterceptTouchEvent(ev)方法,而后将结果赋值给intercepted。那就来看下ViewGroup与View特有的onInterceptTouchEvent方法,以下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
复制代码

经过标记和action检查cancel,而后将结果赋值给局部boolean变量canceled。

获取一个boolean变量标记split来标记,默认是true,做用是是否把事件分发给多个子View,这个一样在ViewGroup中提供了public的方法设置,以下:

public void setMotionEventSplittingEnabled(boolean split) {
        if (split) {
            mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
        } else {
            mGroupFlags &= ~FLAG_SPLIT_MOTION_EVENTS;
        }
    }
复制代码

if (!canceled && !intercepted)判断代表,事件不是ACTION_CANCEL而且ViewGroup的拦截标志位intercepted为false(不拦截)则会进入其中。

if语句if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE)处理ACTION_DOWN事件,判断了childrenCount个数是否不为0,而后接着拿到了子View的list集合preorderedList;接着经过一个for循环i从childrenCount - 1开始遍历到0,倒序遍历全部的子view,这是由于preorderedList中的顺序是按照addView或者XML布局文件中的顺序来的,后addView添加的子View,会由于Android的UI后刷新机制显示在上层;假如点击的地方有两个子View都包含的点击的坐标,那么后被添加到布局中的那个子view会先响应事件;这样其实也是符合人的思惟方式的,由于后被添加的子view会浮在上层,因此咱们去点击的时候通常都会但愿点击最上层的那个组件先去响应事件。

经过getTouchTarget去查找当前子View是否在mFirstTouchTarget.next这条target链中的某一个targe中,若是在则返回这个target,不然返回null。在这段代码的if判断经过说明找到了接收Touch事件的子View,即newTouchTarget,那么,既然已经找到了,因此执行break跳出for循环。若是没有break则继续向下执行,这里你能够看见一段if判断的代码if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)),调用方法dispatchTransformedTouchEvent()将Touch事件传递给特定的子View。该方法十分重要,在该方法中为一个递归调用,会递归调用dispatchTouchEvent()方法。在dispatchTouchEvent()中若是子View为ViewGroup而且Touch没有被拦截那么递归调用dispatchTouchEvent(),若是子View为View那么就会调用其onTouchEvent()。dispatchTransformedTouchEvent方法若是返回true则表示子View消费掉该事件,同时进入该if判断。知足if语句后重要的操做有:

  • 给newTouchTarget赋值;
  • 给alreadyDispatchedToNewTouchTarget赋值为true;
  • 执行break,由于该for循环遍历子View判断哪一个子View接受Touch事件,既然已经找到了就跳出该外层for循环;

若是if判断中的dispatchTransformedTouchEvent()方法返回false,即子View的onTouchEvent返回false(即Touch事件未被消费),那么就不知足该if条件,也就没法执行addTouchTarget(),从而致使mFirstTouchTarget为null(无法对mFirstTouchTarget赋值,由于上面分析了mFirstTouchTarget一进来是ACTION_DOWN就置位为null了),那么该子View就没法继续处理ACTION_MOVE事件和ACTION_UP事件。

若是if判断中的dispatchTransformedTouchEvent()方法返回true,即子View的onTouchEvent返回true(即Touch事件被消费),那么就知足该if条件,从而mFirstTouchTarget不为null。

if (newTouchTarget == null && mFirstTouchTarget != null)。该if表示通过前面的for循环没有找到子View接收Touch事件而且以前的mFirstTouchTarget不为空则为真,而后newTouchTarget指向了最初的TouchTarget。

对于此处ACTION_DOWN的处理具体体如今dispatchTransformedTouchEvent()方法,该方法返回值具有以下特征:

由于在dispatchTransformedTouchEvent()会调用递归调用dispatchTouchEvent()和onTouchEvent(),因此dispatchTransformedTouchEvent()的返回值其实是由onTouchEvent()决定的。简单地说onTouchEvent()是否消费了Touch事件的返回值决定了dispatchTransformedTouchEvent()的返回值,从而决定mFirstTouchTarget是否为null,进一步决定了ViewGroup是否处理Touch事件,通过上面对于ACTION_DOWN的处理后mFirstTouchTarget可能为null或者不为null。

mFirstTouchTarget为null时,也就是说Touch事件未被消费,即没有找到可以消费touch事件的子组件或Touch事件被拦截了,则调用ViewGroup的dispatchTransformedTouchEvent()方法处理Touch事件(和普通View同样),即子View没有消费Touch事件,那么子View的上层ViewGroup才会调用其onTouchEvent()处理Touch事件。具体就是在调用dispatchTransformedTouchEvent()时第三个参数为null .子view对于Touch事件处理返回true那么其上层的ViewGroup就没法处理Touch事件了,子view对于Touch事件处理返回false那么其上层的ViewGroup才能够处理Touch事件。mFirstTouchTarget不为null时,也就是说找到了能够消费Touch事件的子View且后续Touch事件能够传递到该子View。能够看见在源码的else中对于非ACTION_DOWN事件继续传递给目标子组件进行处理,依然是递归调用dispatchTransformedTouchEvent()方法来实现的处理。

ViewGroup的dispatchTouchEvent中可能执行的onInterceptTouchEvent方法

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
复制代码

若是ViewGroup的onInterceptTouchEvent返回false就不阻止事件继续传递派发,不然阻止传递派发。

ViewGroup的dispatchTouchEvent中执行的dispatchTransformedTouchEvent方法

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } // Calculate the number of pointers to deliver. final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. if (newPointerIdBits == 0) { return false; } // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }
复制代码

在dispatchTouchEvent()中调用dispatchTransformedTouchEvent()将事件分发给子View处理。在此咱们须要重点分析该方法的第三个参数(View child)。在dispatchTouchEvent()中屡次调用了dispatchTransformedTouchEvent()方法,并且有时候第三个参数为null,有时又不是,他们到底有啥区别呢?这段源码中很明显展现告终果。在dispatchTransformedTouchEvent()源码中能够发现屡次对于child是否为null的判断,而且均作出以下相似的操做。其中,当child == null时会将Touch事件传递给该ViewGroup自身的dispatchTouchEvent()处理,即super.dispatchTouchEvent(event)(也就是View的这个方法,由于ViewGroup的父类是View);当child != null时会调用该子view(固然该view多是一个View也多是一个ViewGroup)的dispatchTouchEvent(event)处理,即child.dispatchTouchEvent(event)。 ViewGroup没有重写View的onTouchEvent(MotionEvent event) 方法.

总结结论

Android事件派发是先传递到最顶级的ViewGroup,再由ViewGroup递归传递到View的。 在ViewGroup中能够经过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true表明不容许事件继续向子View传递,返回false表明不对事件进行拦截,默认返回false。 子View中若是将传递的事件消费掉,ViewGroup中将没法接收到任何事件。

Activity的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
复制代码

Activity的attach方法能够发现getWindow()返回的就是PhoneWindow对象(PhoneWindow为抽象Window的实现子类),那就简单了,也就至关于PhoneWindow类的方法,而PhoneWindow类实现于Window抽象类,因此先看下Window类中抽象方法的定义,以下:

public abstract boolean superDispatchTouchEvent(MotionEvent event);
复制代码

PhoneWindow里看下Window抽象方法的实现吧,以下:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
复制代码

在PhoneWindow类里发现,mDecor是DecorView类的实例,同时DecorView是PhoneWindow的内部类。最惊人的发现是DecorView extends FrameLayout implements RootViewSurfaceTaker,看见没有?它是一个真正Activity的root view,它继承了FrameLayout。

DecorView类的superDispatchTouchEvent方法吧,以下:

public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
复制代码

Activity的dispatchTouchEvent方法的if (getWindow().superDispatchTouchEvent(ev))本质执行的是一个ViewGroup的dispatchTouchEvent方法(这个ViewGroup是Activity特有的root view,也就是id为content的FrameLayout布局).

在Activity的触摸屏事件派发中:

  1. 首先会触发Activity的dispatchTouchEvent方法。
  2. dispatchTouchEvent方法中若是是ACTION_DOWN的状况下会接着触发onUserInteraction方法。
  3. 接着在dispatchTouchEvent方法中会经过Activity的root View(id为content的FrameLayout),实质是ViewGroup,经过super.dispatchTouchEvent把touchevent派发给各个activity的子view,也就是咱们再Activity.onCreat方法中setContentView时设置的view。
  4. 若Activity下面的子view拦截了touchevent事件(返回true)则Activity.onTouchEvent方法就不会执行。

Activity的dispatchTouchEvent方法中调运的onUserInteraction方法

public void onUserInteraction() {
    }
复制代码

此方法是activity的方法,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。因此它会用在屏保应用上,由于当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保知足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,本身消失等。

Activity的dispatchTouchEvent方法中调运的onTouchEvent方法

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
复制代码

若是一个屏幕触摸事件没有被这个Activity下的任何View所处理,Activity的onTouchEvent将会调用。这对于处理window边界以外的Touch事件很是有用,由于一般是没有View会接收到它们的。返回值为true代表你已经消费了这个事件,false则表示没有消费,默认实现中返回false。

继续分析吧,重点就一句,mWindow.shouldCloseOnTouch(this, event)中的mWindow实际就是上面分析dispatchTouchEvent方法里的getWindow()对象,因此直接到Window抽象类和PhoneWindow子类查看吧,发现PhoneWindow没有重写Window的shouldCloseOnTouch方法,因此看下Window类的shouldCloseOnTouch实现吧,以下:

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
                && isOutOfBounds(context, event) && peekDecorView() != null) {
            return true;
        }
        return false;
    }
复制代码

判断mCloseOnTouchOutside标记及是否为ACTION_DOWN事件,同时判断event的x、y坐标是否是超出Bounds,而后检查FrameLayout的content的id的DecorView是否为空。其实没啥过重要的,这只是对于处理window边界以外的Touch事件有判断价值而已。

相关文章
相关标签/搜索