1、案例

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

MyButton

[java] view plain copy
  1. packagecom.example.zhy_event03;
  2. importandroid.content.Context;
  3. importandroid.util.AttributeSet;
  4. importandroid.util.Log;
  5. importandroid.view.MotionEvent;
  6. importandroid.widget.Button;
  7. publicclassMyButtonextendsButton
  8. {
  9. privatestaticfinalStringTAG=MyButton.class.getSimpleName();
  10. publicMyButton(Contextcontext,AttributeSetattrs)
  11. {
  12. super(context,attrs);
  13. }
  14. @Override
  15. publicbooleanonTouchEvent(MotionEventevent)
  16. {
  17. intaction=event.getAction();
  18. switch(action)
  19. {
  20. caseMotionEvent.ACTION_DOWN:
  21. Log.e(TAG,"onTouchEventACTION_DOWN");
  22. break;
  23. caseMotionEvent.ACTION_MOVE:
  24. Log.e(TAG,"onTouchEventACTION_MOVE");
  25. break;
  26. caseMotionEvent.ACTION_UP:
  27. Log.e(TAG,"onTouchEventACTION_UP");
  28. break;
  29. default:
  30. break;
  31. }
  32. returnsuper.onTouchEvent(event);
  33. }
  34. @Override
  35. publicbooleandispatchTouchEvent(MotionEventevent)
  36. {
  37. intaction=event.getAction();
  38. switch(action)
  39. {
  40. caseMotionEvent.ACTION_DOWN:
  41. Log.e(TAG,"dispatchTouchEventACTION_DOWN");
  42. break;
  43. caseMotionEvent.ACTION_MOVE:
  44. Log.e(TAG,"dispatchTouchEventACTION_MOVE");
  45. break;
  46. caseMotionEvent.ACTION_UP:
  47. Log.e(TAG,"dispatchTouchEventACTION_UP");
  48. break;
  49. default:
  50. break;
  51. }
  52. returnsuper.dispatchTouchEvent(event);
  53. }
  54. }

在onTouchEvent和dispatchTouchEvent中打印了日志~

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

布局文件:

[html] view plain copy
  1. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. tools:context=".MainActivity">
  6. <com.example.zhy_event03.MyButton
  7. android:id="@+id/id_btn"
  8. android:layout_width="wrap_content"
  9. android:layout_height="wrap_content"
  10. android:text="clickme"/>
  11. </LinearLayout>

最后看一眼MainActivity的代码

[java] view plain copy
  1. packagecom.example.zhy_event03;
  2. importandroid.app.Activity;
  3. importandroid.os.Bundle;
  4. importandroid.util.Log;
  5. importandroid.view.MotionEvent;
  6. importandroid.view.View;
  7. importandroid.view.View.OnTouchListener;
  8. importandroid.widget.Button;
  9. publicclassMainActivityextendsActivity
  10. {
  11. protectedstaticfinalStringTAG="MyButton";
  12. privateButtonmButton;
  13. @Override
  14. protectedvoidonCreate(BundlesavedInstanceState)
  15. {
  16. super.onCreate(savedInstanceState);
  17. setContentView(R.layout.activity_main);
  18. mButton=(Button)findViewById(R.id.id_btn);
  19. mButton.setOnTouchListener(newOnTouchListener()
  20. {
  21. @Override
  22. publicbooleanonTouch(Viewv,MotionEventevent)
  23. {
  24. intaction=event.getAction();
  25. switch(action)
  26. {
  27. caseMotionEvent.ACTION_DOWN:
  28. Log.e(TAG,"onTouchACTION_DOWN");
  29. break;
  30. caseMotionEvent.ACTION_MOVE:
  31. Log.e(TAG,"onTouchACTION_MOVE");
  32. break;
  33. caseMotionEvent.ACTION_UP:
  34. Log.e(TAG,"onTouchACTION_UP");
  35. break;
  36. default:
  37. break;
  38. }
  39. returnfalse;
  40. }
  41. });
  42. }
  43. }

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

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

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

[html] view plain copy
  1. 08-3106:09:39.030:E/MyButton(879):dispatchTouchEventACTION_DOWN
  2. 08-3106:09:39.030:E/MyButton(879):onTouchACTION_DOWN
  3. 08-3106:09:39.049:E/MyButton(879):onTouchEventACTION_DOWN
  4. 08-3106:09:39.138:E/MyButton(879):dispatchTouchEventACTION_MOVE
  5. 08-3106:09:39.138:E/MyButton(879):onTouchACTION_MOVE
  6. 08-3106:09:39.147:E/MyButton(879):onTouchEventACTION_MOVE
  7. 08-3106:09:39.232:E/MyButton(879):dispatchTouchEventACTION_UP
  8. 08-3106:09:39.248:E/MyButton(879):onTouchACTION_UP
  9. 08-3106:09:39.248:E/MyButton(879):onTouchEventACTION_UP

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

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

1、dispatchTouchEvent

2、setOnTouchListener的onTouch

3、onTouchEvent

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

2、dispatchTouchEvent

首先进入View的dispatchTouchEvent

[java] view plain copy
  1. /**
  2. *Passthetouchscreenmotioneventdowntothetargetview,orthis
  3. *viewifitisthetarget.
  4. *
  5. *@parameventThemotioneventtobedispatched.
  6. *@returnTrueiftheeventwashandledbytheview,falseotherwise.
  7. */
  8. publicbooleandispatchTouchEvent(MotionEventevent){
  9. if(!onFilterTouchEventForSecurity(event)){
  10. returnfalse;
  11. }
  12. if(mOnTouchListener!=null&&(mViewFlags&ENABLED_MASK)==ENABLED&&
  13. mOnTouchListener.onTouch(this,event)){
  14. returntrue;
  15. }
  16. returnonTouchEvent(event);
  17. }

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

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

[java] view plain copy
  1. /**
  2. *Registeracallbacktobeinvokedwhenatoucheventissenttothisview.
  3. *@paramlthetouchlistenertoattachtothisview
  4. */
  5. publicvoidsetOnTouchListener(OnTouchListenerl){
  6. mOnTouchListener=l;
  7. }
其实就是我们在Activity中设置的setOnTouchListener。

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

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

3、View的onTouchEvent:

接下来是View的onTouchEvent:

[java] view plain copy
  1. /**
  2. *Implementthismethodtohandletouchscreenmotionevents.
  3. *
  4. *@parameventThemotionevent.
  5. *@returnTrueiftheeventwashandled,falseotherwise.
  6. */
  7. publicbooleanonTouchEvent(MotionEventevent){
  8. finalintviewFlags=mViewFlags;
  9. if((viewFlags&ENABLED_MASK)==DISABLED){
  10. //Adisabledviewthatisclickablestillconsumesthetouch
  11. //events,itjustdoesn'trespondtothem.
  12. return(((viewFlags&CLICKABLE)==CLICKABLE||
  13. (viewFlags&LONG_CLICKABLE)==LONG_CLICKABLE));
  14. }
  15. if(mTouchDelegate!=null){
  16. if(mTouchDelegate.onTouchEvent(event)){
  17. returntrue;
  18. }
  19. }
  20. if(((viewFlags&CLICKABLE)==CLICKABLE||
  21. (viewFlags&LONG_CLICKABLE)==LONG_CLICKABLE)){
  22. switch(event.getAction()){
  23. caseMotionEvent.ACTION_UP:
  24. booleanprepressed=(mPrivateFlags&PREPRESSED)!=0;
  25. if((mPrivateFlags&PRESSED)!=0||prepressed){
  26. //takefocusifwedon'thaveitalreadyandweshouldin
  27. //touchmode.
  28. booleanfocusTaken=false;
  29. if(isFocusable()&&isFocusableInTouchMode()&&!isFocused()){
  30. focusTaken=requestFocus();
  31. }
  32. if(!mHasPerformedLongPress){
  33. //Thisisatap,soremovethelongpresscheck
  34. removeLongPressCallback();
  35. //Onlyperformtakeclickactionsifwewereinthepressedstate
  36. if(!focusTaken){
  37. //UseaRunnableandpostthisratherthancalling
  38. //performClickdirectly.Thisletsothervisualstate
  39. //oftheviewupdatebeforeclickactionsstart.
  40. if(mPerformClick==null){
  41. mPerformClick=newPerformClick();
  42. }
  43. if(!post(mPerformClick)){
  44. performClick();
  45. }
  46. }
  47. }
  48. if(mUnsetPressedState==null){
  49. mUnsetPressedState=newUnsetPressedState();
  50. }
  51. if(prepressed){
  52. mPrivateFlags|=PRESSED;
  53. refreshDrawableState();
  54. postDelayed(mUnsetPressedState,
  55. ViewConfiguration.getPressedStateDuration());
  56. }elseif(!post(mUnsetPressedState)){
  57. //Ifthepostfailed,unpressrightnow
  58. mUnsetPressedState.run();
  59. }
  60. removeTapCallback();
  61. }
  62. break;
  63. caseMotionEvent.ACTION_DOWN:
  64. if(mPendingCheckForTap==null){
  65. mPendingCheckForTap=newCheckForTap();
  66. }
  67. mPrivateFlags|=PREPRESSED;
  68. mHasPerformedLongPress=false;
  69. postDelayed(mPendingCheckForTap,ViewConfiguration.getTapTimeout());
  70. break;
  71. caseMotionEvent.ACTION_CANCEL:
  72. mPrivateFlags&=~PRESSED;
  73. refreshDrawableState();
  74. removeTapCallback();
  75. break;
  76. caseMotionEvent.ACTION_MOVE:
  77. finalintx=(int)event.getX();
  78. finalinty=(int)event.getY();
  79. //Belenientaboutmovingoutsideofbuttons
  80. intslop=mTouchSlop;
  81. if((x<0-slop)||(x>=getWidth()+slop)||
  82. (y<0-slop)||(y>=getHeight()+slop)){
  83. //Outsidebutton
  84. removeTapCallback();
  85. if((mPrivateFlags&PRESSED)!=0){
  86. //Removeanyfuturelongpress/tapchecks
  87. removeLongPressCallback();
  88. //Needtoswitchfrompressedtonotpressed
  89. mPrivateFlags&=~PRESSED;
  90. refreshDrawableState();
  91. }
  92. }
  93. break;
  94. }
  95. returntrue;
  96. }
  97. returnfalse;
  98. }

代码还是比较长的,

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

[java] view plain copy
  1. privatefinalclassCheckForTapimplementsRunnable{
  2. publicvoidrun(){
  3. mPrivateFlags&=~PREPRESSED;
  4. mPrivateFlags|=PRESSED;
  5. refreshDrawableState();
  6. if((mViewFlags&LONG_CLICKABLE)==LONG_CLICKABLE){
  7. postCheckForLongClick(ViewConfiguration.getTapTimeout());
  8. }
  9. }
  10. }

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

[java] view plain copy
  1. privatevoidpostCheckForLongClick(intdelayOffset){
  2. mHasPerformedLongPress=false;
  3. if(mPendingCheckForLongPress==null){
  4. mPendingCheckForLongPress=newCheckForLongPress();
  5. }
  6. mPendingCheckForLongPress.rememberWindowAttachCount();
  7. postDelayed(mPendingCheckForLongPress,
  8. ViewConfiguration.getLongPressTimeout()-delayOffset);
  9. }

[java] view plain copy
  1. classCheckForLongPressimplementsRunnable{
  2. privateintmOriginalWindowAttachCount;
  3. publicvoidrun(){
  4. if(isPressed()&&(mParent!=null)
  5. &&mOriginalWindowAttachCount==mWindowAttachCount){
  6. if(performLongClick()){
  7. mHasPerformedLongPress=true;
  8. }
  9. }
  10. }

可以看到,当用户按下,首先会设置标识为PREPRESSED

如果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标识去除,刷新背景;

[java] view plain copy
  1. privatevoidremoveTapCallback(){
  2. if(mPendingCheckForTap!=null){
  3. mPrivateFlags&=~PREPRESSED;
  4. removeCallbacks(mPendingCheckForTap);
  5. }
  6. }
这个是移除,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()方法:

[java] view plain copy
  1. publicbooleanperformClick(){
  2. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  3. if(mOnClickListener!=null){
  4. playSoundEffect(SoundEffectConstants.CLICK);
  5. mOnClickListener.onClick(this);
  6. returntrue;
  7. }
  8. returnfalse;
  9. }

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;

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,运行看看:

[java] view plain copy
  1. packagecom.example.zhy_event03;
  2. importandroid.app.Activity;
  3. importandroid.os.Bundle;
  4. importandroid.util.Log;
  5. importandroid.view.MotionEvent;
  6. importandroid.view.View;
  7. importandroid.view.View.OnClickListener;
  8. importandroid.view.View.OnLongClickListener;
  9. importandroid.view.View.OnTouchListener;
  10. importandroid.widget.Button;
  11. importandroid.widget.Toast;
  12. publicclassMainActivityextendsActivity
  13. {
  14. protectedstaticfinalStringTAG="MyButton";
  15. privateButtonmButton;
  16. @Override
  17. protectedvoidonCreate(BundlesavedInstanceState)
  18. {
  19. super.onCreate(savedInstanceState);
  20. setContentView(R.layout.activity_main);
  21. mButton=(Button)findViewById(R.id.id_btn);
  22. mButton.setOnTouchListener(newOnTouchListener()
  23. {
  24. @Override
  25. publicbooleanonTouch(Viewv,MotionEventevent)
  26. {
  27. intaction=event.getAction();
  28. switch(action)
  29. {
  30. caseMotionEvent.ACTION_DOWN:
  31. Log.e(TAG,"onTouchACTION_DOWN");
  32. break;
  33. caseMotionEvent.ACTION_MOVE:
  34. Log.e(TAG,"onTouchACTION_MOVE");
  35. break;
  36. caseMotionEvent.ACTION_UP:
  37. Log.e(TAG,"onTouchACTION_UP");
  38. break;
  39. default:
  40. break;
  41. }
  42. returnfalse;
  43. }
  44. });
  45. mButton.setOnClickListener(newOnClickListener()
  46. {
  47. @Override
  48. publicvoidonClick(Viewv)
  49. {
  50. Toast.makeText(getApplicationContext(),"onclick",Toast.LENGTH_SHORT).show();
  51. }
  52. });
  53. mButton.setOnLongClickListener(newOnLongClickListener()
  54. {
  55. @Override
  56. publicbooleanonLongClick(Viewv)
  57. {
  58. Toast.makeText(getApplicationContext(),"setOnLongClickListener",Toast.LENGTH_SHORT).show();
  59. returnfalse;
  60. }
  61. });
  62. }
  63. }
效果图:


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


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

当然了,View结束,肯定到我们的ViewGroup了,请点击:Android ViewGroup事件分发机制





更多相关文章

  1. Android(安卓)View的点击事件分发机制
  2. Android(安卓)模拟MotionEvent事件 触发输入法
  3. androidsetClickable不起作用没…
  4. Android触摸事件传递机制系列详解
  5. android keydown touch事件传递
  6. Android(安卓)实现事件监听器的五种处理方法
  7. android 初识EventBus
  8. 关于Android的一些理解
  9. Android事件1-开发指南-用户界面-事件处理

随机推荐

  1. Android入门之简单短信发送器
  2. 样式定义Android界面样式
  3. Android之读取手机内存中的文件数据
  4. Android 插拔充电器增加声音
  5. Android(安卓)开发手记三
  6. Android 5中不同效果的Toast
  7. Android实现textview文字滚动显示(跑马灯
  8. adb server didn't act
  9. Android 通过Matrix来对图片进行缩放,旋转
  10. Ubuntu 设置Android adb 环境变量