从android源码角度分析touch机制

来源:互联网 发布:js中树形菜单制作方法 编辑:程序博客网 时间:2024/05/14 13:37

Touch概述

Touch操作即是用手触摸或者用鼠标操作屏幕所造成的事件触发。这些事件最基本的包括按下Down,移动Move,取消Cancel和离开触摸屏Up四种事件。一个完整的Touch过程一般是由Down->(Move)->Up/Cancel这四个事件组成,值得注意的是,一个完整的触摸事件必须由Down开始,再到Up/Cancel结束,中间的Move可以有可以没有,当然Touch事件不止这四个事件,但这四个事件是最基本,开发中必须考虑到的。 
当用户开启应用触摸屏幕,系统服务就通过IPC(Binder)通知应用的主线程Looper中,最终传递到我们应用中Activity,View和ViewGroup中。 
需要对Touch机制清晰才可以解决以下一些类似问题: 
1. touch监听没被调用到 
2. 双层滑动模块嵌套后发生滑动不了的现象 
3. 设置了onClickListener后,点击View没有反馈 
4. 点击两下View才调用onClickListener的bug

宏观

这里写图片描述
以上是Touch事件的传递顺序,一个Touch事件要传递到View中,必须经过Activity向下分发,如果在ViewGroup在子View中找到可以处理这个事件的View,则向下再传递下去,否则ViewGroup会尝试处理这个事件。下面详细介绍View,ViewGroup,Activity这三个类收到Touch事件的处理已经它们如何分发Touch事件。

View的Touch逻辑

Android中View对于Touch的处理逻辑主要集中在以下三个个位置中

//最主要的触摸事件的分发逻辑,向接收Touch事件的子View(包括自己)派发事件,对于View而非ViewGroup来说,这里只会对自己分发。boolean dispatchTouchEvent(MotionEvent event);//当前View处理触摸事件的可选方法,在dispatchTouchEvent()中被调用void setOnTouchListener(OnTouchListener l);public interface OnTouchListener {    boolean onTouch(View v, MotionEvent event);}//当前View处理触摸事件的默认方法,在dispatchTouchEvent()中被调用,如果已经设置了OnTouchListener,并且OnTouchListener已经消费了这个Touch事件,返回true,则不会触发这个方法。boolean onTouchEvent(MottionEvent event);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

以上几个方法返回值为true代表事件被处理,如果返回false,则代表事件没有被处理。

View之dispatchTouchEvent

让我们看一下dispatchTouchEvent的逻辑,删除部分不重要代码,源码如下:

public boolean dispatchTouchEvent(MotionEvent event) {    boolean result = false;    //1.停止嵌套滑动    final int actionMasked = event.getActionMasked();    if (actionMasked == MotionEvent.ACTION_DOWN) {        stopNestedScroll();    }    //2.安全监测    if (onFilterTouchEventForSecurity(event)) {        //noinspection SimplifiableIfStatement        ListenerInfo li = mListenerInfo;        //3.如果当前View使能(setEnabled(true)),则调用Touch监听器            if (li != null && li.mOnTouchListener != null                && (mViewFlags & ENABLED_MASK) == ENABLED                && li.mOnTouchListener.onTouch(this, event)) {            result = true;        }        //4.如果Touch监听器返回false或者没有调用Touch监听器,则返回调用onTouchEvent()        if (!result && onTouchEvent(event)) {            result = true;        }    }    //停止嵌套滑动    if (actionMasked == MotionEvent.ACTION_UP ||            actionMasked == MotionEvent.ACTION_CANCEL ||            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {        stopNestedScroll();    }    return result;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

从上面的源码中可以看出View的dispatchTouchEvent()主要的逻辑如下: 
1.停止嵌套滑动(5.0以后添加的) 
2.做了安全监测,如果View开启了安全检测(setFilterTouchesWhenObscured(true))并且当前View所在的Window被其他Window遮盖的话,则不会调用再处理Touch事件 
3.如果当前View使能,才会调用OnTouchListener 
4.不管使能与否,只要OnTouchListener没有处理事件,就会让onTouchEvent()来处理事件

View之onTouchEvent

前面我们已经知道如果事件没有被OnTouchListener处理的话,将会被onTouchEvent()处理。 
onTouchEvent()在源码中主要是处理

press :按下时候View状态的改变,比如View的背景的drawable会变成press 状态click/tap: 快速点击longClick:长按focus:跟press类似,也是View状态的改变touchDelegate:分发这个点击事件给其他的View,这个点击事件传到其他View前会改变这个事件的点击坐标,如果在指定的Rect里面,则是View的中点坐标,否则在View之外
  • 1
  • 2
  • 3
  • 4
  • 5

让我们看一下OnTouchListener的逻辑,源码如下:

public boolean onTouchEvent(MotionEvent event) {        final int viewFlags = mViewFlags;        if ((viewFlags & ENABLED_MASK) == DISABLED) {            //1.View不使能的情况下(setEnabled(false)),依然可能消费事件            return (((viewFlags & CLICKABLE) == CLICKABLE ||                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));        }        //2.用TouchDelegate将自己的区域变成其他View中心点的操作        if (mTouchDelegate != null) {            if (mTouchDelegate.onTouchEvent(event)) {                return true;            }        }        //3.从这里跟1结合可以知道,只要View是Clickable或者LongClickable,就一定消费事件        if (((viewFlags & CLICKABLE) == CLICKABLE ||                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {            switch (event.getAction()) {                case MotionEvent.ACTION_UP:                    //只有在press的情况下,才可能click,longClick也做了同样的判断                    if ((mPrivateFlags & PRESSED) != 0) {                        //4.如果我们在当前View还没获取焦点,并且能在touch下foucus,那么第一次点击只会将这个View的状态改成focus,而不会触发click                        boolean focusTaken = false;                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {                            focusTaken = requestFocus();                        }                        //5.已经有longClick执行过了,就不再执行click了                        if (!mHasPerformedLongPress) {                            if (mPendingCheckForLongPress != null) {                                removeCallbacks(mPendingCheckForLongPress);                            }                            if (!focusTaken) {                                performClick();                            }                        }                        //6.取消press                        if (mUnsetPressedState == null) {                            mUnsetPressedState = new UnsetPressedState();                        }                        if (!post(mUnsetPressedState){                            mUnsetPressedState.run();                        }                    }                    break;                case MotionEvent.ACTION_DOWN:                    //7.press,定时检测并且执行longclick                    mPrivateFlags |= PRESSED;                    refreshDrawableState();                    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {                        postCheckForLongClick();                    }                    break;                case MotionEvent.ACTION_CANCEL:                    //8.清理状态                    mPrivateFlags &= ~PRESSED;                    refreshDrawableState();                    break;                case MotionEvent.ACTION_MOVE:                    final int x = (int) event.getX();                    final int y = (int) event.getY();                    //9.如果移动到View外,则不press,如果移动到View内,则press                    int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||                            (y < 0 - slop) || (y >= getHeight() + slop)) {                        // Outside button                        if ((mPrivateFlags & PRESSED) != 0) {                            // Remove any future long press checks                            if (mPendingCheckForLongPress != null) {                                removeCallbacks(mPendingCheckForLongPress);                            }                            mPrivateFlags &= ~PRESSED;                            refreshDrawableState();                        }                    } else {                        // Inside button                        if ((mPrivateFlags & PRESSED) == 0) {                            mPrivateFlags |= PRESSED;                            refreshDrawableState();                        }                    }                    break;            }            return true;        }        return false;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93

View的onTouch方法归纳为一下几点: 
1.不管View使能与否,只要clickable或者longclickable,就一定消费事件(返回true) 
2.如果View不使能,并且clickable或者longclick,就只会消费事件但不做其他任何操作 
3.如果View使能,先看看TouchDelegate消费与否,如果不消费再给自己消费 
4.处理包括focus,press,click,longclick

ViewGroup的Touch逻辑

而ViewGroup继承与View,并覆盖了

dispatchTouchEvent(MotionEvent event);
  • 1

而且比View多了一个处理Touch的位置:

viewgroup.onInterceptTouchTouchEvent(MotionEvent event);
  • 1

这个方法的返回值主要用于是否阻止向子View派发触摸事件,默认返回false,不阻止。 
精简后的源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {    final int action = ev.getAction();    final float xf = ev.getX();    final float yf = ev.getY();    final float scrolledXFloat = xf + mScrollX;    final float scrolledYFloat = yf + mScrollY;    final Rect frame = mTempRect;    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;    if (action == MotionEvent.ACTION_DOWN) {        //1.只有在非拦截的情况的下寻找target        if (disallowIntercept || !onInterceptTouchEvent(ev)) {            // 防止onInterceptTouchEvent()的时候改变Action            ev.setAction(MotionEvent.ACTION_DOWN);            // 遍历子View,第一个消费这个事件的子View的为Target            final int scrolledXInt = (int) scrolledXFloat;            final int scrolledYInt = (int) scrolledYFloat;            final View[] children = mChildren;            final int count = mChildrenCount;            for (int i = count - 1; i >= 0; i--) {                final View child = children[i];                //当然只遍历可见的,并且没有在进行动画的。                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE                        || child.getAnimation() != null) {                    child.getHitRect(frame);                    if (frame.contains(scrolledXInt, scrolledYInt)) {                        // offset the event to the view's coordinate system                        final float xc = scrolledXFloat - child.mLeft;                        final float yc = scrolledYFloat - child.mTop;                        ev.setLocation(xc, yc);                        if (child.dispatchTouchEvent(ev))  {                            // Event handled, we have a target now.                            mMotionTarget = child;                            return true;                        }                                            }                }            }        }    }    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||            (action == MotionEvent.ACTION_CANCEL);    //up或者cancel的时候清空DisallowIntercept    if (isUpOrCancel) {        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;    }    // 如果没有target,则把自己当成View,向自己派发事件    final View target = mMotionTarget;    if (target == null) {        // We don't have a target, this means we're handling the        // event as a regular view.        ev.setLocation(xf, yf);        return super.dispatchTouchEvent(ev);    }    // 如果有Target,拦截了,则对Target发送Cancel,并且清空Target    if (!disallowIntercept && onInterceptTouchEvent(ev)) {        final float xc = scrolledXFloat - (float) target.mLeft;        final float yc = scrolledYFloat - (float) target.mTop;        ev.setAction(MotionEvent.ACTION_CANCEL);        ev.setLocation(xc, yc);        if (!target.dispatchTouchEvent(ev)) {            // target didn't handle ACTION_CANCEL. not much we can do            // but they should have.        }        // clear the target        mMotionTarget = null;        // Don't dispatch this event to our own view, because we already        // saw it when intercepting; we just want to give the following        // event to the normal onTouchEvent().        return true;    }    //up 或者 cancel清空Target    if (isUpOrCancel) {        mMotionTarget = null;    }    //如果有Target,并且没有拦截,则向Target派发事件,这个事件会转化成Target的坐标系    final float xc = scrolledXFloat - (float) target.mLeft;    final float yc = scrolledYFloat - (float) target.mTop;    ev.setLocation(xc, yc);    return target.dispatchTouchEvent(ev);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87

综上,ViewGroup的主要的任务是找一个Target,并且用这个target传递事件,主要逻辑如下 
1.在Down并且不拦截的时候会多出一个寻找Target的过程,在这个过程中遍历子View,如果子View的dispatchTouch为true,则这个子View就是当前ViewGroup的Target。找Target是处理Down事件时候特有的,其他事件不会触发找Target。 
2.如果没有Target,则发送把自己当做一个View去处理这个事件(super.dispatchTouch()
3.如果有Target并且拦截,则发送Cancel给子View 
4.如果有Target并且不拦截,则调用Target的dispatchTouch 
5.可以利用requestDisallowInterceptTouchEvent(boolean)来强制viewparent不拦截事件。但是作用域限于一个Touch的过程(Down->Up/Cancel)

Activity的Touch逻辑

ViewGroup收到的事件是由Activity发送出去的。Activity的Touch逻辑非常简单,源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {      if (ev.getAction() == MotionEvent.ACTION_DOWN) {          onUserInteraction();      }      //这个最终传递到setContentView对应的View中      if (getWindow().superDispatchTouchEvent(ev)) {          return true;      }      //如果ContentView没有对时间进行处理,统一由Activity的onTouchEvent()来处理      return onTouchEvent(ev);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Activity的Touch逻辑归纳如下: 
Activity将事件经过一些步骤发送给ContentView,如果ContentView没消费,就交给Activity自己处理。

问题解决

文章开头提出了几个问题,可以用这篇文章的分析解决:

  1. touch监听器没被调用到? 
    View.dispatchTouchEvent(),ViewGroup.dispatchTouchEvent() 
    a)如果是View非使能,直接用setEnabled(true) 
    b)如果是事件被这个View的viewparent拦截了。可以修改这个viewparent的onInterceptTouchTouchEvent(),或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()

  2. 双层滑动模块嵌套后发生滑动不了的现象? 
    ViewGroup.dispatchTouchEvent() 
    如果是事件被这个View的viewparent拦截了。可以修改这个viewparent的onInterceptTouchTouchEvent(),或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()

  3. 设置了onClickListener后,点击View没有反应? 
    View.onTouchEvent() 
    a)如果是View非使能,直接用setEnabled(true) 
    b)可能覆盖了onTouchEvent(),需要在覆盖的方法调用super.onTouchEvent()或者手动调用performClick()

  4. 点击两下View才调用onClickListener的bug? 
    View.onTouchEvent() 
    这个其实是安卓的设计,当某个View调用了setFocusableInTouchMode(true)后,第一次点击会引起这个View的focus,第二次点击才会调用onClickListener,只需要设置setFocusableInTouchMode(false)即可。

0 0
原创粉丝点击