Android 事件分发机制

Posted NeilZhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 事件分发机制相关的知识,希望对你有一定的参考价值。

 

       点击事件的分发过程其实是对MotionEvent事件分发过程,当一个MotionEvent产生以后,系统需要把这个事件传递给一个具体的View,而这个传递过程就是分发过程。点击事件的分发由三个重要的方法共同完成:dispatchTouchEvent,onInterceptTOuchEvent,onTouchEvent。

View里,有两个回调函数 :

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

ViewGroup里,有三个回调函数 :

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

在Activity里,有两个回调函数 :

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

android中默认情况下事件传递是由最终的view的接收到,传递过程是从父布局到子布局,也就是从Activity到ViewGroup到View的过程,默认情况,ViewGroup起到的是透传作用。

技术分享图片

         触摸事件是一连串ACTION_DOWN,ACTION_MOVE..MOVE…MOVE、最后ACTION_UP,触摸事件还有ACTION_CANCEL事件。事件都是从ACTION_DOWN开始的,Activity的dispatchTouchEvent()首先接收到ACTION_DOWN,执行super.dispatchTouchEvent(ev),事件向下分发。

       dispatchTouchEvent()返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE

1 不消费ACTION_DOWN,后续收不到ACTION_MOVE、ACTION_UP

技术分享图片

View中没有拦截器,只能调用onTouchEvent选择消费或者不消费该事件。上图中View的onTouchEvent方法返回false,一直将false返回最顶层的ViewGrouop,该事件后续的ACTION_MOVE,ACTION_UP都不会再调用。

注: 如果View是可点击的(Button),onTouchEvent默认返回True,ImageView 则默认返回false。

2 消费ACTION_DOWN

技术分享图片

后续ACTION_MOVE 和 UP 在不被拦截的情况下都会找到这个View

 

3 ACTION_MOVE 和 UP 被上层拦截技术分享图片

技术分享图片

4 全部被上层拦截(包括ACTION_DOWN,ACTION_MOVE,ACTION_UP)

技术分享图片

 

android中的Touch事件都是从ACTION_DOWN开始的:

单手指操作:ACTION_DOWN---ACTION_MOVE----ACTION_UP

多手指操作:ACTION_DOWN---ACTION_POINTER_DOWN---ACTION_MOVE--ACTION_POINTER_UP---ACTION_UP.

参考:http://www.eoeandroid.com/thread-319301-1-1.html?_dsign=495a5374

5 源码解析

1/ dispatchTouchEvent 用来进行事件分发,如果事件能够传递到当前的View,那么此方法一定会被调用,返回结果受当前View的OnTouchEvent和下级View的,onInterceptTOuchEvent方法影响,表示是否消耗当前事件。

/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event)

2/ onInterceptTouchEvent是ViewGroup提供的方法,默认返回false,返回true表示拦截。

 * @param ev The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an
ACTION_CANCEL
 event, and no further
     * messages will be delivered here.
     */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

3/ onTouchEvent  在 dispatchTouchEvent中被调用,用来处理点击事件,返回结果表示是否消耗此事件。如果不消耗,在同一事件序列中,当前View无法再次接受到事件。

6 事件分发与onClick/onTouch 的关系

为同一个button同时设置click和touch的Listener

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d("TAG", "onClick execute");
    }
});
button.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.d("TAG", "onTouch execute, action " + event.getAction());
        
return false;
    }
});

当点击这个Button时的log如下:

技术分享图片

可以看到,onTouch是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你还可能会有多次ACTION_MOVE的执行,如果你手抖了一下)。因此事件传递的顺序是先经过onTouch,再传递到onClick

问题1 : 当把onTouch的方法返回值改成true时,则onClick不再执行

原因:  当事件分发到该Button时,会调用它的onDispatchTouchEvent方法,该方法源码中大致有一个这样的判断:如果onTouch方法返回true时,表示onTouch消耗了改事件直接返回true,onTouchEvent方法则不再被调用(当ACTION_UP事件传递进来时,onClick在该方法中被调用,所以onClick也不会被调用)

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

注: 不同版本的可能上述代码实现不相同,但是思想一致。

问题2:onTouch返回false时,在ACTION_DOWN事件中onDispatchTouchEvent应该返回false,后续的ACTION_UP应该不会被调用,为什么还有log。

原因: 分析onTouchEvent源码,如果view是可点击的,则默认返回true消耗该事件。如果view是不可点击的则返回false,例如ImageView。所以对于BUTTON第一次ACTION_DOWN的onDispatchTouchEvent返回的是true,而对于ImageView返回值为false,它的后续ACTION_MOVE/ACTION_UP不会再被调用。

技术分享图片
  1   public boolean onTouchEvent(MotionEvent event) {
  2         final float x = event.getX();
  3         final float y = event.getY();
  4         final int viewFlags = mViewFlags;
  5         final int action = event.getAction();
  6 
  7         if ((viewFlags & ENABLED_MASK) == DISABLED) {
  8             if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
  9                 setPressed(false);
 10             }
 11             // A disabled view that is clickable still consumes the touch
 12             // events, it just doesn‘t respond to them.
 13             return (((viewFlags & CLICKABLE) == CLICKABLE
 14                     || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
 15                     || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
 16         }
 17         if (mTouchDelegate != null) {
 18             if (mTouchDelegate.onTouchEvent(event)) {
 19                 return true;
 20             }
 21         }
 22 
 23         if (((viewFlags & CLICKABLE) == CLICKABLE ||
 24                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
 25                 (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
 26             switch (action) {
 27                 case MotionEvent.ACTION_UP:
 28                     boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
 29                     if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
 30                         // take focus if we don‘t have it already and we should in
 31                         // touch mode.
 32                         boolean focusTaken = false;
 33                         if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
 34                             focusTaken = requestFocus();
 35                         }
 36 
 37                         if (prepressed) {
 38                             // The button is being released before we actually
 39                             // showed it as pressed.  Make it show the pressed
 40                             // state now (before scheduling the click) to ensure
 41                             // the user sees it.
 42                             setPressed(true, x, y);
 43                        }
 44 
 45                         if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
 46                             // This is a tap, so remove the longpress check
 47                             removeLongPressCallback();
 48 
 49                             // Only perform take click actions if we were in the pressed state
 50                             if (!focusTaken) {
 51                                 // Use a Runnable and post this rather than calling
 52                                 // performClick directly. This lets other visual state
 53                                 // of the view update before click actions start.
 54                                 if (mPerformClick == null) {
 55                                     mPerformClick = new PerformClick();
 56                                 }
 57                                 if (!post(mPerformClick)) {
 58                                     performClick();
 59                                 }
 60                             }
 61                         }
 62 
 63                         if (mUnsetPressedState == null) {
 64                             mUnsetPressedState = new UnsetPressedState();
 65                         }
 66 
 67                         if (prepressed) {
 68                             postDelayed(mUnsetPressedState,
 69                                     ViewConfiguration.getPressedStateDuration());
 70                         } else if (!post(mUnsetPressedState)) {
 71                             // If the post failed, unpress right now
 72                             mUnsetPressedState.run();
 73                         }
 74 
 75                         removeTapCallback();
 76                     }
 77                     mIgnoreNextUpEvent = false;
 78                     break;
 79 
 80                 case MotionEvent.ACTION_DOWN:
 81                     mHasPerformedLongPress = false;
 82 
 83                     if (performButtonActionOnTouchDown(event)) {
 84                         break;
 85                     }
 86 
 87                     // Walk up the hierarchy to determine if we‘re inside a scrolling container.
 88                     boolean isInScrollingContainer = isInScrollingContainer();
 89 
 90                     // For views inside a scrolling container, delay the pressed feedback for
 91                     // a short period in case this is a scroll.
 92                     if (isInScrollingContainer) {
 93                         mPrivateFlags |= PFLAG_PREPRESSED;
 94                         if (mPendingCheckForTap == null) {
 95                             mPendingCheckForTap = new CheckForTap();
 96                         }
 97                         mPendingCheckForTap.x = event.getX();
 98                         mPendingCheckForTap.y = event.getY();
 99                         postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
100                     } else {
101                         // Not inside a scrolling container, so show the feedback right away
102                         setPressed(true, x, y);
103                         checkForLongClick(0, x, y);
104                     }
105                     break;
106 
107                 case MotionEvent.ACTION_CANCEL:
108                     setPressed(false);
109                     removeTapCallback();
110                     removeLongPressCallback();
111                     mInContextButtonPress = false;
112                     mHasPerformedLongPress = false;
113                     mIgnoreNextUpEvent = false;
114                     break;
115 
116                 case MotionEvent.ACTION_MOVE:
117                     drawableHotspotChanged(x, y);
118 
119                     // Be lenient about moving outside of buttons
120                     if (!pointInView(x, y, mTouchSlop)) {
121                         // Outside button
122                         removeTapCallback();
123                         if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
124                             // Remove any future long press/tap checks
125                             removeLongPressCallback();
126 
127                             setPressed(false);
128                         }
129                     }
130                     break;
131             }
132 
133             return true;
134         }
135 
136         return false;
137     }
View Code

参考:http://blog.csdn.net/guolin_blog/article/details/9097463

7 ViewGroup的事件拦截

      一般情况下,如果Layout定义了onTouch事件,同时在Layout中添加了两个按钮。当点击按钮时,onTouch事件不会被调用(ViewGroup中源码可知,调用子View中Button的dispatchTouchEvent返回true,后面的onTouch事件不再被调用)。 但是点击除按钮之外的区域则onTouch事件会被调用。

     当重写Layout中的onIntercreptTouchEvent函数,返回true时,则事件不会被传递到子View,直接调用Viewgroup的onTouchEvent处理事件,不管点击哪里,它的onTouch都会被执行。

http://blog.csdn.net/guolin_blog/article/details/9153747

 

总结:

    1、 一个事件序列,ACTIVON_DOWN,ACTION_MOVE,ACTION_UP。 当DOWN事件时顶层ViewGroup的dispatchTouchEvent返回false时,后续事件都不再执行。

   2、可点击View的onTouchEvent默认返回true,其它返回false

   3、ViewGroup中dispatchTouchEvent和View中的该函数实现不同,View中该函数的onTouch都会执行,如果返回false,再执行onTouchEvnet。 ViewGroup中,如果子View消耗了这次事件则它的onTouch事件不会被执行。 

以上是关于Android 事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章

Android ViewGroup事件分发机制

android 事件分发机制

Android View 事件分发机制

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )

Android事件分发机制五:面试官你坐啊

Android源码分析:View的事件分发机制探析