Android View 事件分发机制 源码解析

一直想写事件分发机制的文章,不管咋样,也得自己研究下事件分发的源码,写出心得~

首先我们先写个简单的例子来测试View的事件转发的流程~

1、案例

为了更好的研究View的事件转发,我们自定以一个MyButton继承Button,然后把跟事件传播有关的方法进行复写,然后添加上日志~

MyButton

?
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
package com.example.zhy_event03;
 
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
 
public class MyButton extends Button
{
     private static final String TAG = MyButton. class .getSimpleName();
 
     public MyButton(Context context, AttributeSet attrs)
     {
         super (context, attrs);
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event)
     {
         int action = event.getAction();
 
         switch (action)
         {
         case MotionEvent.ACTION_DOWN:
             Log.e(TAG, "onTouchEvent ACTION_DOWN" );
             break ;
         case MotionEvent.ACTION_MOVE:
             Log.e(TAG, "onTouchEvent ACTION_MOVE" );
             break ;
         case MotionEvent.ACTION_UP:
             Log.e(TAG, "onTouchEvent ACTION_UP" );
             break ;
         default :
             break ;
         }
         return super .onTouchEvent(event);
     }
     
     @Override
     public boolean dispatchTouchEvent(MotionEvent event)
     {
         int action = event.getAction();
 
         switch (action)
         {
         case MotionEvent.ACTION_DOWN:
             Log.e(TAG, "dispatchTouchEvent ACTION_DOWN" );
             break ;
         case MotionEvent.ACTION_MOVE:
             Log.e(TAG, "dispatchTouchEvent ACTION_MOVE" );
             break ;
         case MotionEvent.ACTION_UP:
             Log.e(TAG, "dispatchTouchEvent ACTION_UP" );
             break ;
 
         default :
             break ;
         }
         return super .dispatchTouchEvent(event);
     }
 
     
}

在onTouchEvent和dispatchTouchEvent中打印了日志~

然后把我们自定义的按钮加到主布局文件中;

布局文件:

?
1
2
3
4
5
<linearlayout xmlns:android= "http://schemas.android.com/apk/res/android" xmlns:tools= "http://schemas.android.com/tools" android:layout_width= "match_parent" android:layout_height= "match_parent" tools:context= ".MainActivity" >
 
     <com.example.zhy_event03.mybutton android:id= "@+id/id_btn" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:text= "click me" >
 
</com.example.zhy_event03.mybutton></linearlayout>

最后看一眼MainActivity的代码

?
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
package com.example.zhy_event03;
 
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.Button;
 
public class MainActivity extends Activity
{
     protected static final String TAG = "MyButton" ;
     private Button mButton ;
     @Override
     protected void onCreate(Bundle savedInstanceState)
     {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         
         mButton = (Button) findViewById(R.id.id_btn);
         mButton.setOnTouchListener( new OnTouchListener()
         {
             @Override
             public boolean onTouch(View v, MotionEvent event)
             {
                 int action = event.getAction();
 
                 switch (action)
                 {
                 case MotionEvent.ACTION_DOWN:
                     Log.e(TAG, "onTouch ACTION_DOWN" );
                     break ;
                 case MotionEvent.ACTION_MOVE:
                     Log.e(TAG, "onTouch ACTION_MOVE" );
                     break ;
                 case MotionEvent.ACTION_UP:
                     Log.e(TAG, "onTouch ACTION_UP" );
                     break ;
                 default :
                     break ;
                 }
                 
                 return false ;
             }
         });
     }
 
     
}

在MainActivity中,我们还给MyButton设置了OnTouchListener这个监听~

好了,跟View事件相关一般就这三个地方了,一个onTouchEvent,一个dispatchTouchEvent,一个setOnTouchListener;

下面我们运行,然后点击按钮,查看日志输出:

?
1
2
3
4
5
6
7
8
9
08 - 31 06 : 09 : 39.030 : E/MyButton( 879 ): dispatchTouchEvent ACTION_DOWN
08 - 31 06 : 09 : 39.030 : E/MyButton( 879 ): onTouch ACTION_DOWN
08 - 31 06 : 09 : 39.049 : E/MyButton( 879 ): onTouchEvent ACTION_DOWN
08 - 31 06 : 09 : 39.138 : E/MyButton( 879 ): dispatchTouchEvent ACTION_MOVE
08 - 31 06 : 09 : 39.138 : E/MyButton( 879 ): onTouch ACTION_MOVE
08 - 31 06 : 09 : 39.147 : E/MyButton( 879 ): onTouchEvent ACTION_MOVE
08 - 31 06 : 09 : 39.232 : E/MyButton( 879 ): dispatchTouchEvent ACTION_UP
08 - 31 06 : 09 : 39.248 : E/MyButton( 879 ): onTouch ACTION_UP
08 - 31 06 : 09 : 39.248 : E/MyButton( 879 ): onTouchEvent ACTION_UP

我有意点击的时候蹭了一下,不然不会触发MOVE,手抖可能会打印一堆MOVE的日志~~~

好了,可以看到,不管是DOWN,MOVE,UP都会按照下面的顺序执行:

1、dispatchTouchEvent

2、 setOnTouchListener的onTouch

3、onTouchEvent

下面就跟随日志的脚步开始源码的探索~

2、dispatchTouchEvent

首先进入View的dispatchTouchEvent

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
      * 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) {
         if (!onFilterTouchEventForSecurity(event)) {
             return false ;
         }
 
         if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                 mOnTouchListener.onTouch( this , event)) {
             return true ;
         }
         return onTouchEvent(event);
     }

直接看13行:首先判断mOnTouchListener不为null,并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接return true ; 也就是下面的onTouchEvent(event)不会被执行了;

那么mOnTouchListener是和方神圣,我们来看看:

?
1
2
3
4
5
6
7
/**
   * Register a callback to be invoked when a touch event is sent to this view.
   * @param l the touch listener to attach to this view
   */
  public void setOnTouchListener(OnTouchListener l) {
      mOnTouchListener = l;
  }
其实就是我们在Activity中设置的setOnTouchListener。

也就是说:如果我们设置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不会被执行了,当然了,本例我们return false,我们还得往下探索 ;

已经解决一个常见的问题:View的onTouchListener和onTouchEvent的调用关系,相信大家应该已经明白了~let's go;继续往下。

3、View的onTouchEvent:

接下来是View的onTouchEvent:

?
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
      * Implement this method to handle touch screen motion events.
      *
      * @param event The motion event.
      * @return True if the event was handled, false otherwise.
      */
     public boolean onTouchEvent(MotionEvent event) {
         final int viewFlags = mViewFlags;
 
         if ((viewFlags & ENABLED_MASK) == DISABLED) {
             // A disabled view that is clickable still consumes the touch
             // events, it just doesn't respond to them.
             return (((viewFlags & CLICKABLE) == CLICKABLE ||
                     (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
         }
 
         if (mTouchDelegate != null ) {
             if (mTouchDelegate.onTouchEvent(event)) {
                 return true ;
             }
         }
 
         if (((viewFlags & CLICKABLE) == CLICKABLE ||
                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
             switch (event.getAction()) {
                 case MotionEvent.ACTION_UP:
                     boolean prepressed = (mPrivateFlags & PREPRESSED) != 0 ;
                     if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                         // take focus if we don't have it already and we should in
                         // touch mode.
                         boolean focusTaken = false ;
                         if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                             focusTaken = requestFocus();
                         }
 
                         if (!mHasPerformedLongPress) {
                             // This is a tap, so remove the longpress check
                             removeLongPressCallback();
 
                             // Only perform take click actions if we were in the pressed state
                             if (!focusTaken) {
                                 // Use a Runnable and post this rather than calling
                                 // performClick directly. This lets other visual state
                                 // of the view update before click actions start.
                                 if (mPerformClick == null ) {
                                     mPerformClick = new PerformClick();
                                 }
                                 if (!post(mPerformClick)) {
                                     performClick();
                                 }
                             }
                         }
 
                         if (mUnsetPressedState == null ) {
                             mUnsetPressedState = new UnsetPressedState();
                         }
 
                         if (prepressed) {
                             mPrivateFlags |= PRESSED;
                             refreshDrawableState();
                             postDelayed(mUnsetPressedState,
                                     ViewConfiguration.getPressedStateDuration());
                         } else if (!post(mUnsetPressedState)) {
                             // If the post failed, unpress right now
                             mUnsetPressedState.run();
                         }
                         removeTapCallback();
                     }
                     break ;
 
                 case MotionEvent.ACTION_DOWN:
                     if (mPendingCheckForTap == null ) {
                         mPendingCheckForTap = new CheckForTap();
                     }
                     mPrivateFlags |= PREPRESSED;
                     mHasPerformedLongPress = false ;
                     postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                     break ;
 
                 case MotionEvent.ACTION_CANCEL:
                     mPrivateFlags &= ~PRESSED;
                     refreshDrawableState();
                     removeTapCallback();
                     break ;
 
                 case MotionEvent.ACTION_MOVE:
                     final int x = ( int ) event.getX();
                     final int y = ( int ) event.getY();
 
                     // Be lenient about moving outside of buttons
                     int slop = mTouchSlop;
                     if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                             (y < 0 - slop) || (y >= getHeight() + slop)) {
                         // Outside button
                         removeTapCallback();
                         if ((mPrivateFlags & PRESSED) != 0 ) {
                             // Remove any future long press/tap checks
                             removeLongPressCallback();
 
                             // Need to switch from pressed to not pressed
                             mPrivateFlags &= ~PRESSED;
                             refreshDrawableState();
                         }
                     }
                     break ;
             }
             return true ;
         }
 
         return false ;
     }

代码还是比较长的,

10-15行,如果当前View是Disabled状态且是可点击则会消费掉事件(return true);可以忽略,不是我们的重点;

17-21行,如果设置了mTouchDelegate,则会将事件交给代理者处理,直接return true,如果大家希望自己的View增加它的touch范围,可以尝试使用TouchDelegate,这里也不是重点,可以忽略;

接下来到我们的重点了:

23行的判断:如果我们的View可以点击或者可以长按,则,注意IF的范围,最终一定return true ;

if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//...
return true;
}

接下来就是 switch (event.getAction())了,判断事件类型,DOWN,MOVE,UP等;

我们按照例子执行的顺序,先看 case MotionEvent.ACTION_DOWN (71-78行):

1、MotionEvent.ACTION_DOWN

75行:给mPrivateFlags设置一个PREPRESSED的标识

76行:设置mHasPerformedLongPress=false;表示长按事件还未触发;

77行:发送一个延迟为ViewConfiguration.getTapTimeout()的延迟消息,到达延时时间后会执行CheckForTap()里面的run方法:

1、ViewConfiguration.getTapTimeout()为115毫秒;

2、CheckForTap

?
1
2
3
4
5
6
7
8
9
10
private final class CheckForTap implements Runnable {
       public void run() {
           mPrivateFlags &= ~PREPRESSED;
           mPrivateFlags |= PRESSED;
           refreshDrawableState();
           if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
               postCheckForLongClick(ViewConfiguration.getTapTimeout());
           }
       }
   }

在run方法里面取消mPrivateFlags的PREPRESSED,然后设置PRESSED标识,刷新背景,如果View支持长按事件,则再发一个延时消息,检测长按;

?
1
2
3
4
5
6
7
8
9
10
private void postCheckForLongClick( int delayOffset) {
        mHasPerformedLongPress = false ;
 
        if (mPendingCheckForLongPress == null ) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.rememberWindowAttachCount();
        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout() - delayOffset);
    }

?
1
2
3
4
5
6
7
8
9
10
11
12
class CheckForLongPress implements Runnable {
 
         private int mOriginalWindowAttachCount;
 
         public void run() {
             if (isPressed() && (mParent != null )
                     && mOriginalWindowAttachCount == mWindowAttachCount) {
                 if (performLongClick()) {
                     mHasPerformedLongPress = true ;
                 }
             }
         }

可以看到,当用户按下,首先会设置标识为PREPRESSED,如果在115毫秒内抬起了,UP时会移除CheckForTap这个回调(UP时会分析);

如果115后,没有抬起,会将View的标识设置为PRESSED且去掉PREPRESSED标识,然后发出一个检测长按的延迟任务,延时为:ViewConfiguration.getLongPressTimeout() - delayOffset(500ms -115ms),这个115ms刚好时检测额PREPRESSED时间;也就是用户从DOWN触发开始算起,如果500ms内没有抬起则认为触发了长按事件:

1、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture;

2、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false;

好了DOWN就分析完成了;大家回个神,下面回到VIEW的onTouchEvent中的ACTION_MOVE:

2、MotionEvent.ACTION_MOVE

86到105行:

87-88行:拿到当前触摸的x,y坐标;

91行判断当然触摸点有没有移出我们的View,如果移出了:

1、执行removeTapCallback();

2、然后判断是否包含PRESSED标识,如果包含,移除长按的检查:removeLongPressCallback();

3、最后把mPrivateFlags中PRESSED标识去除,刷新背景;

?
1
2
3
4
5
6
private void removeTapCallback() {
        if (mPendingCheckForTap != null ) {
            mPrivateFlags &= ~PREPRESSED;
            removeCallbacks(mPendingCheckForTap);
        }
    }
这个是移除,DOWN触发时设置的PREPRESSED的检测;即当前触发时机在DOWN触发不到115ms时,你就已经移出控件外了;

如果115ms后,你才移出控件外,则你的当前mPrivateFlags一定为PRESSED且发送了长按的检测;

就会走上面的2和3;首先移除removeLongPressCallback()
private void removeLongPressCallback() {
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}

然后把mPrivateFlags中PRESSED标识去除,刷新背景;

好了,MOVE我们也分析完成了,总结一下:只要用户移出了我们的控件:则将mPrivateFlags取出PRESSED标识,且移除所有在DOWN中设置的检测,长按等;

下面再回个神,回到View的onTouchEvent的ACTION_UP:

3、MotionEvent.ACTION_UP

26到69行:

27行:判断mPrivateFlags是否包含PREPRESSED

28行:如果包含PRESSED或者PREPRESSED则进入执行体,也就是无论是115ms内或者之后抬起都会进入执行体。

36行:如果mHasPerformedLongPress没有被执行,进入IF

38行:removeLongPressCallback();移除长按的检测

45-50行:如果mPerformClick如果mPerformClick为null,初始化一个实例,然后立即通过handler添加到消息队列尾部,如果添加失败则直接执行 performClick();添加成功,在mPerformClick的run方法中就是执行performClick();

终于执行了我们的click事件了,下面看一下performClick()方法:

?
1
2
3
4
5
6
7
8
9
10
11
public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
 
        if (mOnClickListener != null ) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnClickListener.onClick( this );
            return true ;
        }
 
        return false ;
    }

if (mOnClickListener != null) { 
mOnClickListener.onClick(this);
return true;
}

久违了~我们的mOnClickListener ;

别激动,还没结束,回到ACTION_UP,

58行:如果prepressed为true,进入IF体:

为mPrivateFlags设置表示为PRESSED,刷新背景,125毫秒后执行mUnsetPressedState

否则:mUnsetPressedState.run();立即执行;也就是不管咋样,最后mUnsetPressedState.run()都会执行;

看看这个UnsetPressedState主要干什么:

private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
}

public void setPressed(boolean pressed) {
if (pressed) {
mPrivateFlags |= PRESSED;
} else {
mPrivateFlags &= ~PRESSED;
}
refreshDrawableState();
dispatchSetPressed(pressed);
}

把我们的mPrivateFlags中的PRESSED取消,然后刷新背景,把setPress转发下去。

ACTION_UP的最后一行:removeTapCallback(),如果mPendingCheckForTap不为null,移除;

4、总结

好了,代码跨度还是相当大的,下面需要总结下:

1、整个View的事件转发流程是:

View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent

在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。

2、onTouchEvent中的DOWN,MOVE,UP

DOWN时:

a、首先设置标志为PREPRESSED,设置mHasPerformedLongPress=false ;然后发出一个115ms后的mPendingCheckForTap,如果115ms内抬起手指,触发了UP,则不会触发click事件,并且最终执行的是UnsetPressedState对象,setPressed(false)将setPress的传递下去;这种情况很少发生,可能只会在压力测试的时候会发现无法触发click事件;

b、如果115ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-115ms的,检测长按任务消息;

c、如果500ms内(从DOWN触发开始算),则会触发LongClickListener:

此时如果LongClickListener不为null,则会执行回调,同时如果LongClickListener.onClick返回true,才把mHasPerformedLongPress设置为true;否则mHasPerformedLongPress依然为false;

MOVE时:

主要就是检测用户是否划出控件,如果划出了:

115ms内,直接移除mPendingCheckForTap;

115ms后,则将标志中的PRESSED去除,同时移除长按的检查:removeLongPressCallback();

UP时:

a、如果115ms内,触发UP,此时标志为PREPRESSED,则执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;

b、如果是115ms-500ms间,即长按还未发生,则首先移除长按检测,执行onClick回调;

c、如果是500ms以后,那么有两种情况:

i.设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;

ii.没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;

d、最后执行mUnsetPressedState.run(),将setPressed传递下去,然后将PRESSED标识去除;


最后问个问题,然后再运行个例子结束:

1、setOnLongClickListener和setOnClickListener是否只能执行一个

不是的,只要setOnLongClickListener中的onClick返回false,则两个都会执行;返回true则会屏幕setOnClickListener

最后我们给MyButton同时设置setOnClickListener和setOnLongClickListener,运行看看:

?
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
package com.example.zhy_event03;
 
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
import android.widget.Toast;
 
public class MainActivity extends Activity
{
     protected static final String TAG = "MyButton" ;
     private Button mButton ;
     @Override
     protected void onCreate(Bundle savedInstanceState)
     {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         
         mButton = (Button) findViewById(R.id.id_btn);
         mButton.setOnTouchListener( new OnTouchListener()
         {
             @Override
             public boolean onTouch(View v, MotionEvent event)
             {
                 int action = event.getAction();
 
                 switch (action)
                 {
                 case MotionEvent.ACTION_DOWN:
                     Log.e(TAG, "onTouch ACTION_DOWN" );
                     break ;
                 case MotionEvent.ACTION_MOVE:
                     Log.e(TAG, "onTouch ACTION_MOVE" );
                     break ;
                 case MotionEvent.ACTION_UP:
                     Log.e(TAG, "onTouch ACTION_UP" );
                     break ;
                 default :
                     break ;
                 }
                 
                 return false ;
             }
         });
         mButton.setOnClickListener( new OnClickListener()
         {
             @Override
             public void onClick(View v)
             {
                 Toast.makeText(getApplicationContext(), "onclick" ,Toast.LENGTH_SHORT).show();
             }
         });
         
         mButton.setOnLongClickListener( new OnLongClickListener()
         {
             @Override
             public boolean onLongClick(View v)
             {
                 Toast.makeText(getApplicationContext(), "setOnLongClickListener" ,Toast.LENGTH_SHORT).show();
                 return false ;
             }
         });
     }
 
     
}
效果图:


可以看到LongClickListener已经ClickListener都触发了~


最后,本篇博文完成了对View的事件分发机制的整个流程的说明,并且对源码进行了分析;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值