Android Touch事件的分发过程

     一.不知道你是否在涉及到Android触屏事件的时候有过如下的疑问:

        1.View的onTouchEvent()方法返回true和false有什么区别? SDK给出的解释很简单:"返回true代表该事件已经被处理过了,返回false则相反",这句话完全没有解释清楚问题。

        2.View的onTouchEvent()方法在处理ACTION_DOWN的时候返回true,在处理ACTION_MOVE的时候返回false,代表着是处理了还是没处理?返回super.onTouchEvent()又是什么含义?

        3.重写onTouchEvent()方法和通过setOnTouchListener()设置一个触屏监听有什么区别,看起来好像很类似。

        4.View的dispatchTouchEvent(),onTouchEvent(),setOnClickListener(),ViewGroup的onInterceptTouchEvent()把我绕晕了,这些方法怎么使用怎么重写?

        5.假设一个ViewGroup有两个子view,这两个view有一部分是重叠的,点击该重叠部分,事件由哪个View来处理?

        6.Activity的onTouchEvent()和dispatchEvent()何时调用,怎么重写?

        7.最重要的一点疑问是:触屏事件从顶层ViewGroup一直向下是怎么传递的?

        如果你有类似的疑问,相信我的这篇博客能给你答案。


        二.首先需要明确的几点是:

        1.View一般是为了显示某些内容而存在的,它也通常用来处理用户的触屏等交互事件,而ViewGroup则是做为View的容器而存在的,虽然在代码上它是View的子类,但它通常只是做为容器用来组织它的子视图布局方式,默认情况下甚至ViewGroup的onDraw方法都不会被调用到,因为在ViewGroup的构造函数中设置了WILL_NOT_DRAW这个标志。

        2.我们知道android里边View层次是一种树型结构,需要明确的是一个ViewGroup它的直接子视图才算是树结构中的儿子,再往下一层就不算了,类似于进程间的父子关系。举例,FrameLayout有两个子视图,分别是LinearLayout和TextView,而LinearLayout又有三个子视图ImageView,那么调用FrameLayout的getChildCount()方法只会返回2,而不是5。因此以下内容中"子视图"这个术语代表着一个ViewGroup的直接子视图,它即可能是一个View类,也可能是一个ViewGroup类。

        3.Activity视图的最顶层View是DecorView,它是在PhoneWindow类中通过generateDecor()方法生成的,它继承自FrameLayout,是View层次的根视图。

        4.对于触屏来说有三个主要的事件:down,move,up

        那么一个触屏事件到底是怎么在View层次中上向下传递的?(这里只考虑事件已经到达DecorView时的情形,事实上是ViewRootImpl类接收到底层InputDispatch传递过来的事件,这里就不写了),ViewRootImpl在deliverPointerEvent()方法中通过调用mView.dispatchPointerEvent(event);将触屏事件传递给了DecorView,DecorView通过dispatchTouchEvent()继续向下传递给子视图,如果子视图也是一个ViewGroup,它又会调用自己的dispatchTouchEvent()方法向下传递,如果子视图是一个View,那么子视图的onTouchEvent()方法就会被调用,如果子视图处理了该事件,那么事件传递就中止。整个过程像是一个递归过程,理解了一个ViewGroup怎么通过dispatchTouchEvent()传递给它的子视图这一层也就理解了整个过程。

        这里就不分析ViewGroup的dispatchTouchEvent()方法的代码了,直接给出我总结出来的结论,有兴趣的读者可以分析看看。


        三.总结

        以下情景假设一个ViewGroup有三个子视图,按index顺序为v1,v2,v3。v1也是一个ViewGroup,v2和v3都是普通的view,而且它们有一点重叠的部分。

        1.ViewGroup的dispatchTouchEvent()向下分发事件给它的子视图,那么会先分发给v3调用它的onTouchEvent方法,如果v3不处理该事件,会继续分发给v2,如果v2不处理事件,会继续分发给v1,由于v1是一个ViewGroup,则会调用它的dispatchTouchEvent()分发给它的子视图。

        2.v3不处理该事件的含义是:在down事件到达时,onTouchEvent()方法返回false,如果在接收到down事件时返回true,则表示处理了该事件,那么不管你在接收到move和up事件的时候返回的是什么都没有关系。因此思想是:只要你愿意处理down事件,那么你必须处理接下来的其他事件。

        3.v3能接收触屏事件的前提是它的显示矩形框必须在触屏的范围之内,这里显而易见的道理,而且v3必须是VISIBLE的,或者它正处于动画之中,否则事件会传递给v2,事实上所有子视图都需要做这种判断。

        4.如果v1,v2,v3都决定不处理触屏事件,那么事件最终由ViewGroup自己来处理,它的onTouchEvent()方法会被调用。

        5.如果事件传递到了v1,v1是否处理取决于它的子视图,如果它的子视图有一个处理了该事件,那么就代表v1处理了事件,如果它的所有子视图都没有处理事件而且v1本身的onTouchEvent()的方法在处理down事件的时候返回false,那么才代表v1没有处理事件。

        6.如果通过setOnTouchListener()设置了一个有效的监听到view中,那么事件到达时会直接调用这个监听方法,只有该监听方法返回了false才会调用onTouchEvent()。

        到这里,onTouchEvent()返回值的含义应该很明确了,那么super.onTouchEvent()返回值是什么呢?看代码也比较简单,如果一个view不是clickable的或者不是longClickable的,那么super.onTouchEvent()直接返回false,否则就进行onClick和onLongClick处理并返回true。可调用setClickable(true),setLongClickable(true)来改变view的状态,调用setOnClickListener()和setOnLongClickListener()也是一样的效果。

        由上面结论,如果两视图是兄弟关系,它们又互有重叠部分,点击该重叠部分,先处理该事件的是下标比较大的那个视图,如果这个视图不想处理事件,才让另外一个处理。

     四.onInterceptTouchEvent()


        ViewGroup可以调用它的onInterceptTouchEvent()方法去拦截子视图的事件,这个方法默认返回的是false表示不拦截,如果在onInterceptTouchEvent()接收到down事件时返回了true,那么接下来的down,move和up事件都会被ViewGroup自己的onTouchEvent()方法所接收,所有的子视图都接收不到事件,而ViewGroup自己的onInterceptTouchEvent()方法也只有down事件会被传递过去,因为都由父View来处理,所以该方法再接收到move和up事件就没有意义了,所以只会有down事件会被传递。注释中的说法:There are no touch targets and this action is not an initial down,so this view group continues to intercept touches.

        如果一个子视图决定处理全部三个事件,那么每次事件到来时都会先调用父view的onInterceptTouchEvent()方法,如果在某个事件上返回了true,那么就会拦截到该事件以及随后的事件到ViewGroup自己的onTouchEvent()方法处理,子视图会接收到ACTION_CANCEL事件。

        举例:如果子视图处理了down事件,但是ViewGroup在move到来时拦截住了move事件,那么子视图就收不到接下来的move和up事件,会收到ACTION_CANCEL事件,而ViewGroup则会接收move和up事件,onInterceptTouchEvent()方法也只会接收到down和move事件。

        拦截方法有时非常有用,例如ScrollView它会先把down事件交给子视图处理,如果是点击事件,就交给子视图,如果判断出来正在拖动子视图,那么会拦截住move事件,交由自己处理,调用overScrollBy()产生滚动。

        当然如果自己重写了ViewGroup的dispatchTouchEvent()方法就自己掌控了事件的分发过程,和上面的流程就不一定一样了。


     五.Activity onTouchEvent()

事实上DecorView的dispatchTouchEvent会先调用Activity的dispatchTouchEvent,代码如下:

    @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            // Stylus events with side button pressed are filtered and other
            // events are processed normally.
            if (mEnableGestures
                    && MotionEvent.BUTTON_SECONDARY == ev.getButtonState()) {
                mStylusFilter.onTouchEvent(ev);
                return false;
            }
            final Callback cb = getCallback();
            return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)
                    : super.dispatchTouchEvent(ev);
        }

Callback cb实际上是一个Activity引用,它是在Activity的attach方法被调用的时候通过mWindow.setCallback(this)将自身的引用给设置到Window类的mCallback成员变量中去的,这样在PhoneWindow这个子类中通过getCallback()方法得到的其实是Window所关联的Activity对象。因此这里会先调用Activity的dispatchTouchEvent方法(暂时忽略其他判断条件),Activity的dispatchTouchEvent()方法代码如下,很简单:

   /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     * 
     * @param ev The touch screen event.
     * 
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

它的getWindow()实际上引用的是PhoneWindow对象,又调用到了DecorView的superDispatchTouchEvent方法,而这个方法的作用就是上面所说的在View层次中进行事件的分发,如果有任何一个子视图处理了该事件,那么Activity的dispatchTouchEvent方法就返回true,否则会调用Activity的onTouchEvent方法。

注释上的说法是可以重写Activity的dispatchTouchEvent方法从而在分发事件之前拦截住所有事件。


        六. mScrollX/mScrollY 与触屏坐标偏移

        一个view可以通过scrollBy或者scrollTo对其内容产生滚动,这种滚动只对view的显示产生影响,不会影响到view的矩形框属性,mLeft,mTop等位置属性值不变,而且此view左上角触屏坐标仍然是(0,0)。

        一个ViewGroup也可以通过scrollBy或者scrollTo对其内容产生滚动,由于滚动影响了子视图的显示位置(但是子视图mLeft,mTop等位置属性值仍然不变),因此需要对触屏坐标做调整,让子视图左上角触屏坐标始终是(0,0),这是通过如下代码来实现的:

final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);

handled = child.dispatchTouchEvent(event);

event.offsetLocation(-offsetX, -offsetY);

就保证了子视图显示位置发生变化以后,触屏坐标也相应的跟着变化。这点会对某些ViewGroup计算子视图实际触屏位置有影响,比如ViewGroup的(x,y)处被点击了,要想知道该点有没有落在某个子视图内,需要判断(x + getScrollX() , y + getScrollY()) 这个点有没有处于子视图的矩形范围内,如果处于则表示子视图也被点击,可以处理该事件。

        总结完了,相信开头的所有问题都有了答案,理清了就明白了。





  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
#前言 之前笔者其实已经写过事件分发机制的文章:[快速理解android事件传递拦截机制概念](http://blog.csdn.net/double2hao/article/details/51541061) 但是,现在看来其实更像是一篇知识概括,多出可能未讲清楚,于是打算重写事件分发,用一篇文章大致讲清楚。 首先,形式上笔者最先思考的是使用源码,此者能从原理上讲解分发机制,比起侃侃而谈好得多。但是源码的复杂往往会让新手产生畏惧难以理解,于是笔者最终还是打算使用实例log来让读者理解android事件分发。 #重要函数 笔者此次主要提及最常用的几个函数: (其间区别看源码很容易理解,此处直接给上结果) **onClick():**这个函数是是View提供给我们的OnClickListener这个接口中的函数,在这里可以自定义对点击事件的处理逻辑。会在onTouchEvent()中进行调用。 **onTouch():**这个函数是View提供给我们的OnTouchListener这个接口中的函数,在这里面可以自定义对触摸事件的处理逻辑。 **onTouchEvent():**这个函数是view内部的触摸事件的处理方式,其间包括获取焦点,调用onClick()等等。 **dispatchTouchEvent():**这个是View的事件分发函数,在ViewGroup中进行重写。在View中其间会调用onTouchEvent(),在ViewGroup中其间会调用onInterceptTouchEvent()和onTouchEvent()。 **onInterceptTouchEvent():**这个函数是事件拦截函数,是ViewGroup才有的函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值