1. Touch事件和绘制事件的异同之处

Touch事件和绘制事件非常相似,都是由ViewRoot派发下来的,可是不同之处在绘制事件是由应用中的某个View发起请求,一层一层上传到ViewRoot。再有ViewRoot下发绘制,传递canvas给全部子View让其绘制自身,绘制好后,再通知WMS进行画到屏幕上。而Touch事件是由硬件捕获到触摸后由系统传递给应用的ViewRoot,再由ViewRoot往下一层一层传递。

他们的处理过程都是自上而下的分发,可是绘制多了一层自下往上的请求。

事件存在消耗。事件的处理方法都会返回一个boolean值。假设该值为true,则本次事件下发将会终止。

2. MotionEvent

2.1 MotionEvent对象的产生

系统有一个线程在循环收集屏幕硬件信息。当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装成一个MotionEvent对象,然后把该对象存放到一个消息队列中。

系统的还有一个线程循环的读取消息队列中的MotionEvent。然后交给WMS去派发,WMS把该事件派发给当前处于活动的Activity,即处于活动栈最顶端的Activity。

这就是一个先进先出的消费者和生产者的模板。一个线程不停的创建MotionEvent对象放入队列中。还有一个线程不断的从队列中取出MotionEvent对象进行分发。

当用户的手指从接触屏幕到离开屏幕,是一个完整的触摸事件。在该事件中。系统会不断收集事件信息封装成MotionEvent对象。

收集的间隔时间取决于硬件设备,比如屏幕的灵敏度以及cpu的计算能力。眼下的手机一般在20毫秒左右。

MotionEventCompat.getActionMasked()

2.2 MotionEvent对象具体解释

MotionEvent对象包括了触摸事件的时间、位置、面积、压力、以及本次事件的Dwon发生的时间。

MotionEvent经常使用的Action分为5种:Down 、Up、Move、Cancel、OutSide

MotionEvent中我们经常使用的方法就是获取点击的坐标,由于这是与我们操作息息相关的。获取坐标有两种方式:

  • getX和getY用于获取以该View左上角为坐标原点的坐标
  • getRowX和getRowY用于获取以屏幕左上角为坐标原点的坐标

2.3 5种Touch事件

  • Down:一次触摸事件的第一个MotionEvent对象,即手指初次接触屏幕。
  • Up:通常为一次触摸事件的最后一个MotionEvent对象,即手指离开屏幕。
  • Move:通常多次发生在一次触摸事件之中。表示触摸点发生了移动,我们通常把手指放到屏幕上,实际也会触发该事件。由于人手总是在轻微抖动的。
  • Cancel:经常使用于取消某个触摸事件,通常是由程序逻辑来指定该事件,用于取消某次触摸事件。
  • OutSide:当触摸点发生在响应事件的View之外时,传递的事件,通常由程序逻辑来指定。

在上面5种事件中。Down为最重要的事件,由于这是一个触摸事件的起始点。程序的非常多逻辑推断。都须要依据该事件做处理,比如分发拦截。一次触摸事件必须要有Down事件,这也是MotionEvent对象中都包括了本次触摸事件的Down事件发生的时间点这个属性。其次是Move和Up,通过这3个事件的逻辑处理,就构建出来滑动,点击,长按,双击等多种效果。

2.4 创建一个MotionEvent对象

  1. public static MotionEvent obtain(
  2. long downTime, //当用户最初按下開始一连串的位置事件。这必须得到SystemClock.uptimeMillis()
  3. long eventTime, //当这个特定的事件是生成的。这必须得到SystemClock.uptimeMillis()
  4. int action, //该次事件的Action
  5. float x, //该次事件的x坐标
  6. float y, //该次事件的y坐标
  7. float pressure, //该次事件的压力,通常感觉标准压力,从0-1取值
  8. float size, //点击的区域大小,通常依据特定标准范围从0-1取值
  9. int metaState, //一个修饰性的状态。好像一直都是0
  10. float xPrecision, //x坐标的准确度
  11. float yPrecision, //y坐标的准确度
  12. int deviceId, //触屏设备id,假设是0,说明这个事件不是来自物理设备
  13. int edgeFlags //系统默认都是返回0,程序在传递时,能够通过逻辑推断加入方向位置
  14. )

或者一个更简单的方式:

  1. public static MotionEvent obtain(
  2. long downTime,
  3. long eventTime,
  4. int action,
  5. float x,
  6. float y,
  7. int metaState)

也能够通过一个MotionEvent来创建一个新的

  1. public static MotionEvent obtain(MotionEvent event)

通过以上的方式。我们知道。我们也能够通过代码来构建一个虚假的MotionEvent。并分发下去。

  1. view.dispatchTouchEvent(
  2. MotionEvent.obtain(SystemClock.uptimeMillis(),
  3. SystemClock.uptimeMillis(),
  4. MotionEvent.ACTION_DOWN,100,100,0));

然后通过延迟以此往下派发Move和Up时间,形成一个完整的触摸操作。

3. dispatchTouchEvent触摸事件分发

之前我们知道触摸事件是被包装成MotionEvent进行传递的,而该对象是继承了Parcelable接口,正由于如此,才干够从系统中传递到我们的应用中。系统通过跨进程通知ViewRoot,ViewRoot会调用DecorView的dispatchTouchEvent下发。

这里有一个和其它事件传递不同的地方。DecorView会优先传递给Activity。而不是它的子View。

而Activity假设不处理又会回传给DecorView。DecorView才会再将事件传给子View。

dispatchTouchEvent就是触摸事件传递的对外接口,不管是DecorView传给Activity,还是ViewGroup传递给子View,都是直接调用对方的dispatchTouchEvent方法,并传递MotionEvent參数。

我们首先来看看Activity中的dispatchTouchEvent逻辑:

  1. public boolean dispatchTouchEvent(MotionEvent ev) {
  2. if (ev.getAction() == MotionEvent.ACTION_DOWN) {
  3. onUserInteraction();
  4. //这是一个空实现的方法,以便子类实现。该方法在Key事件和touch事件的dispatch方法中都被调用,
  5. // 就是方便用户在事件被传递之前做一下自己的处理。
  6. }
  7. //这才是事件真正的分发
  8. if (getWindow().superDispatchTouchEvent(ev)) {
  9. //superDispatchTouchEvent是一个抽象方法,可是getWindow()获取的对象实际是FrameWork层的
  10. // PhoneWindow,该对象实现了这种方法,内部是直接调用DecorView的superDispatchTouchEvent
  11. // 是直接调用dispatchTouchEvent,这样就传递到子View中了
  12. return true;
  13. }
  14. //假设上面事件没有被消费掉。那么就调用Activity的onTouchEvent事件。
  15. return onTouchEvent(ev);
  16. }
  17. //PhoneWindow的superDispatchTouchEvent方法直接调用了mDecor的superDispatchTouchEvent
  18. public boolean superDispatchTouchEvent(MotionEvent event) {
  19. return mDecor.superDispatchTouchEvent(event);
  20. }
  21. //mDecor即为Activity真正的根View。我们通过setContentView所加入的内容就是加入在该View上,
  22. // 它实际上就是一个FrameLayout
  23. public boolean superDispatchTouchEvent(MotionEvent event) {
  24. return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent
  25. }

至此我们已经至少明确了以下几点:

1、我们能够重载Activity的onUserInteraction方法,在Down事件触发传递前,实现我们的一些需求。实际上源代码中有非常多这种方法,再某个方法体的第一行提供一个空实现的回调方法,在某个方法的最后一行提供一个空实现的回调方法,以便子类去实现自己的逻辑。比如AsyncTask就有相似的方式。这些技巧都能非常好的提高我们代码的扩展性。

2、Activity会间接的调用根View的dispatchTouchEvent,并通过if推断返回值,假设为true,即向上层返回true。也就是调用Activity的dispatchTouchEvent的WMS。即操作系统。

3、假设if推断为false。即根View和根View下的全部子View均为消费掉该事件,那么以下的代码就有运行机会,即Activity的onTouchEvent,并把该方法的返回值作为结果返回给上层。

3.1 View的dispatchTouchEvent

View中的处理相当简单明了,由于不涉及到子View,所以仅仅在自身内部进行分发。首先推断是否设置了触摸监听,而且能够响应事件,就交由监听的onTouch处理。假设上述条件不成立,或者监听的onTouch事件没有消费掉该事件。则交由onTouchEvent进行处理,并把返回结果交给上层。

  1. public boolean dispatchTouchEvent(MotionEvent event) {
  2. if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
  3. mOnTouchListener.onTouch(this, event)) {
  4. //推断mOnTouchListener是否存在,而且控件可点的情况下。运行onTouch,假设onTouch返回true。就消耗该事件
  5. return true;
  6. }
  7. //假设以上条件都不成立。则把事件交给onTouchEvent来处理
  8. return onTouchEvent(event);
  9. }

3.2 ViewGroup的dispatchTouchEvent

3.3 Down事件

  • 通过onInterceptTouchEvent方法推断是否要拦截事件,默认fasle
  • 依据scroll换算后的坐标找出所接受的子View。

    有动画的子View将不接受触摸事件。

  • 找到能接受的子View后把event中的坐标转换成子View的坐标
  • 调用子View的dispatchTouchEvent把事件传递给子View。
  • 假设子View消费了该事件,则把target记录为子View。方便后面的Move和Up事件的传递。
  • 假设子View没有消费,则继续寻找下一个子View。
  • 假设没找到,或者找到的子View都不消费,就会调用View的dispatchTouchEvent的逻辑,也就是推断是否有触摸监听,有的话交给监听的onTouch处理,没有的话交给自己的onTouchEvent处理

接下来我们来研究ViewGroup的dispatchTouchEvent。这是略微复杂的分发逻辑。

  1. public boolean dispatchTouchEvent(MotionEvent ev) {
  2. final int action = ev.getAction();//获取事件
  3. final float xf = ev.getX();//获取触摸坐标
  4. final float yf = ev.getY();
  5. final float scrolledXFloat = xf + mScrollX;//获取当前须要偏移的偏移量量
  6. final float scrolledYFloat = yf + mScrollY;
  7. final Rect frame = mTempRect; //当前ViewGroup的视图矩阵
  8. boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//是否禁止拦截
  9. if (action == MotionEvent.ACTION_DOWN) {//假设事件是按下事件
  10. if (mMotionTarget != null) { //推断接受事件的target是否为空
  11. //不为空肯定是不正常的,由于一个事件是由DOWN開始的。而DOWN还没有被消费,所以目标也不是不可能被确定,
  12. //造成这个的原因可能是在上一次up事件或者cancel事件的时候,没有把目标赋值为空
  13. mMotionTarget = null; //在此处拯救
  14. }
  15. //不同意拦截,或者onInterceptTouchEvent返回false,也就是不拦截。
  16. 注意,这个推断都是在DOWN事件中推断
  17. if (disallowIntercept || !onInterceptTouchEvent(ev)) {
  18. //从新设置一下事件为DOWN事件,事实上没有必要,这仅仅是一种保护错误,防止被篡改了
  19. ev.setAction(MotionEvent.ACTION_DOWN);
  20. //開始寻找能响应该事件的子View
  21. final int scrolledXInt = (int) scrolledXFloat;
  22. final int scrolledYInt = (int) scrolledYFloat;
  23. final View[] children = mChildren;
  24. final int count = mChildrenCount;
  25. for (int i = count - 1; i >= 0; i--) {
  26. final View child = children[i];
  27. if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
  28. || child.getAnimation() != null) {//假设child可见,或者有动画,获取该child的矩阵
  29. child.getHitRect(frame);
  30. if (frame.contains(scrolledXInt, scrolledYInt)) {
  31. // 设置系统坐标
  32. final float xc = scrolledXFloat - child.mLeft;
  33. final float yc = scrolledYFloat - child.mTop;
  34. ev.setLocation(xc, yc);
  35. if (child.dispatchTouchEvent(ev)) {//调用child的dispatchTouchEvent
  36. //假设消费了,目标就确定了,以便接下来的事件都传递给child
  37. mMotionTarget = child;
  38. return true; //事件消费了,返回true
  39. }
  40. }
  41. }
  42. }
  43. //能到这里来。证明全部的子View都没消费掉Down事件,那么留给以下的逻辑进行处理
  44. }
  45. }
  46. //推断是不是up或者cancel事件
  47. boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
  48. (action == MotionEvent.ACTION_CANCEL);
  49. if (isUpOrCancel) {
  50. //假设是取消,把禁止拦截这个标志位给取消
  51. mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
  52. }
  53. final View target = mMotionTarget;
  54. if (target == null) {
  55. //推断该值是否为空。假设为空,则没找到能响应的子View,那么直接调用super的dispatchTouchEvent,也就是View的dispatchTouchEvent
  56. ev.setLocation(xf, yf);
  57. return super.dispatchTouchEvent(ev);
  58. }
  59. //能走到这里来,说明已经有target,那也说明。这里不是DOWN事件。由于DOWN事件假设有target,已经在前面返回了,运行不到这里
  60. if (!disallowIntercept && onInterceptTouchEvent(ev)) {//假设有目标,又非要拦截。则给目标发送一个cancel事件
  61. final float xc = scrolledXFloat - (float) target.mLeft;
  62. final float yc = scrolledYFloat - (float) target.mTop;
  63. ev.setAction(MotionEvent.ACTION_CANCEL);//该为cancel
  64. ev.setLocation(xc, yc);
  65. if (!target.dispatchTouchEvent(ev)) {
  66. //调用子View的dispatchTouchEvent,就算它没有消费这个cancel事件,我们也无能为力了。
  67. }
  68. //清除目标
  69. mMotionTarget = null;
  70. //有目标。又拦截,自身也享受不了了。由于一个事件应该由一个View去完毕
  71. return true;//直接返回true,以完毕这次事件,好让系统開始派发下一次
  72. }
  73. if (isUpOrCancel) {//取消或者UP的话。把目标赋值为空。以便下一次DOWN能又一次找,此处就算不赋值。下一次DOWN也会先把它赋值为空
  74. mMotionTarget = null;
  75. }
  76. //又不拦截,又有目标,那么就直接调用目标的dispatchTouchEvent
  77. final float xc = scrolledXFloat - (float) target.mLeft;
  78. final float yc = scrolledYFloat - (float) target.mTop;
  79. ev.setLocation(xc, yc);
  80. return target.dispatchTouchEvent(ev);
  81. //也就是说,假设是DOWN事件,拦截了,那么每次一次MOVE或者UP都不会再推断是否拦截,直接调用super的dispatchTouchEvent
  82. //假设DOWN没拦截。就是有其它View处理了DOWN事件,那么接下来的MOVE或者UP事件拦截了。那么给目标View发送一个cancel事件,告诉它touch被取消了,而且自身也不会处理。直接返回true
  83. //这是为了不违背一个Touch事件仅仅能由一个View处理的原则。
  84. }

3.4 Move和Up事件

推断事件是否被取消或者事件是否要拦截住,是的话,给Down事件找到的target发送一个取消事件。假设不取消,也不拦截,而且Down已经找到了target,则直接交给target处理。不再遍历子View寻找合适的View了。

这种处理事件是正确的,我们用手机经常能够体会到,当我手指按在一个拖动条上之后,在拖动的时候手指就算移出了拖动条,依旧会把事件分发给拖动条控制它的拖动。

4. onInterceptTouchEvent

ViewGroup的方法。事件拦截,return true表示拦截触摸事件,事件就不往下传递

子View能够调用getParent().requestDisallowInterceptTouchEvent( true ) 请求父控件不拦截touch事件

5. View的onTouchEvent

从View的dispatchTouchEvent能够看出。事件终于的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序猿来编写逻辑,那么我们来看看onTouchEvent事件。View仅仅能响应click和longclick,不具备滑动等特性。

Down时,设置按压状态,发送一个延迟500毫秒的长按事件。

Move时。推断是否移出了View,移出后移除按压状态,长按事件。

Up时,取消按压,并推断它能否够通过触摸获取焦点。是的话设置焦点。推断长按事件是否运行了,假设还没运行。就删除,并运行点击事件。

  1. public boolean onTouchEvent(MotionEvent event) {
  2. final int viewFlags = mViewFlags;
  3. //先推断标示位是否为disable,也就是无法处理事件。
  4. if ((viewFlags & ENABLED_MASK) == DISABLED) {
  5. if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
  6. setPressed(false);
  7. }//假设是UP事件,而且状态为按压,取消按压。
  8. //系统源代码解释:尽管是disable,可是还是能够消费掉触摸事件,仅仅是不触发不论什么click或者longclick事件。
  9. //依据是否可点击,可长按来决定是否消费点击事件。
  10. return (((viewFlags & CLICKABLE) == CLICKABLE ||
  11. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
  12. }
  13. if (mTouchDelegate != null) {
  14. //先检查触摸的代理对象是否存在,假设存在。就交由代理对象处理。
  15. // 触摸代理对象是能够进行设置的。一般用于当我们手指在某个View上。而让另外一个View响应事件。另外一个View就是该View的事件代理对象。
  16. if (mTouchDelegate.onTouchEvent(event)) {//假设代理对象消费了,则返回true消费该事件
  17. return true;
  18. }
  19. }
  20. if (((viewFlags & CLICKABLE) == CLICKABLE ||
  21. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
  22. //假设是可点击或者长按的标识位运行以下的逻辑。这些标志位能够设置,也能够设置了相应的listener后自己主动加入
  23. //由于作为一个View,它仅仅能单纯的接受处理点击事件,像滑动之类的复杂事件普通View是不具备的。
  24. switch (event.getAction()) {
  25. case MotionEvent.ACTION_UP://处理Up事件
  26. boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;//是否包括暂时按压状态
  27. if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {//假设本身处于被按压状态或者暂时按压状态
  28. //暂时按压状态会在以下的Move事件中说明
  29. boolean focusTaken = false;
  30. if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
  31. //假设它能够获取焦点,而且能够通过触摸来获取焦点,而且如今不是焦点。则请求获取焦点,由于一个被按压的View理论上应该获取焦点
  32. focusTaken = requestFocus();
  33. }
  34. if (prepressed) {
  35. //假设是暂时按压,则设置为按压状态,PFLAG_PREPRESSED是一个非常短暂的状态。用于在某些时候短时间内表示Pressed状态,但不须要绘制
  36. setPressed(true);//设置为按压状态。是由于暂时按压不会绘制。这个时候强制绘制一次。确保用户能够看见按压状态
  37. }
  38. if (!mHasPerformedLongPress) {
  39. //是否运行了长按事件,还没有的话,这个时候能够移除长按的回调了。由于UP都已经触发,说明从按下到UP的时间不足以触发longPress
  40. //至于longPress,会在Down事件中说明
  41. removeLongPressCallback();
  42. if (!focusTaken) {//假设是焦点状态。就不会触摸click,这是为什么呢?由于焦点状态通常是交给按键处理的,
  43. //pressed状态才是交给触摸处理,假设它是焦点。那么它的click事件应该由按键来触发
  44. if (mPerformClick == null) { //封装一个Runnable对象。这个对象中实际就调用了performClick();
  45. mPerformClick = new PerformClick();
  46. }
  47. if (!post(mPerformClick)) {//向消息队列发生该runnabel。假设发送不成功。则直接运行该方法。
  48. performClick();//这种方法内部会调用clickListner
  49. }
  50. //为什么不直接运行呢?假设这个时候直接运行,UP事件还没运行完,发送post。能够保障在这个代码块运行完毕之后才运行
  51. }
  52. }
  53. if (mUnsetPressedState == null) {//仍旧是创建一个Runnabel对象,运行setPressed(false)
  54. mUnsetPressedState = new UnsetPressedState();
  55. }
  56. if (prepressed) {
  57. //假设是暂时按压状态。之前的Down和move都还未触发按压状态,仅仅在up时设置了,这个状态才刚刚绘制。为了保证用户能看到,发生一个64秒的延迟消息,来取消按压状态。 postDelayed(mUnsetPressedState,
  58. ViewConfiguration.getPressedStateDuration());
  59. //这是一个64毫秒的短暂时间。这是为了让这个按压状态持续一小段时间,以便手指离开时候,还能看见View的按压状态
  60. } else if (!post(mUnsetPressedState)) {//假设不是暂时按压,则直接发送,发送失败,则直接运行
  61. mUnsetPressedState.run();
  62. }
  63. removeTapCallback();
  64. //移除这个callBack,这个callBack内部就是把暂时按压状态设置成按压状态。由于这个已经不是必需了,手指已经up了
  65. }
  66. break;
  67. case MotionEvent.ACTION_DOWN:
  68. mHasPerformedLongPress = false;
  69. //按下事件把长按事件运行的变量设置为false,代表还没运行长按。由于才按下,表示新的一个长按事件能够開始计算了
  70. if (performButtonActionOnTouchDown(event)) {
  71. //先把这个事件交由该方法。该方法内部会推断是否为上下文的菜单按钮,或者是否为鼠标右键,假设是就弹出上下文菜单。
  72. //如今有些手机的上下文菜单按钮也是在屏幕触屏上的
  73. break;
  74. }
  75. //这种方法会一直往上找父View,推断自身是否在一个能够滚动的容器中
  76. boolean isInScrollingContainer = isInScrollingContainer();
  77. //假设是在一个滚动的容器中,那么按压事件将会被推迟一段时间,假设这段时间内,发生了Move。那么按压状态讲不会被显示,直接滚动父视图
  78. if (isInScrollingContainer) {
  79. mPrivateFlags |= PFLAG_PREPRESSED; //先加入暂时的按压状态,该状态表示按压,但不会绘制
  80. if (mPendingCheckForTap == null) {
  81. mPendingCheckForTap = new CheckForTap();
  82. //创建一个runnable对象,这个runnable内部会取消暂时按压状态,设置为按压状态,并启动长按的延迟事件
  83. }
  84. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
  85. //向消息机制发生一个64毫秒的延迟时间,该事件会取消暂时按压状态,设置为直接按压,并启动长按时间的计时
  86. } else {
  87. //假设不在一个滚动的容器中,则直接设置按压状态,并启动长按计时
  88. setPressed(true);
  89. checkForLongClick(0);
  90. //长按事件就是向消息机制发送一个runnable对象,封装的就是我们在lisner中的代码。延迟500毫秒运行。也就是说长按事件在我们按下的时候发送,在up的时候检查一下运行了吗?假设没运行。就取消,并运行click
  91. }
  92. break;
  93. case MotionEvent.ACTION_CANCEL: //假设是取消事件,那就好办了,把我们之前发送的几个延迟runnable对象给取消掉
  94. setPressed(false); //设置为非按压状态
  95. removeTapCallback(); //取消mPendingCheckForTap。也就是不用再把暂时按压设置为按压了
  96. removeLongPressCallback(); //取消长按事件的延迟回调
  97. break;
  98. case MotionEvent.ACTION_MOVE: //move事件
  99. final int x = (int) event.getX(); //取触摸点坐标
  100. final int y = (int) event.getY();
  101. // 用于推断是否在View中,为什么还要推断呢?
  102. //这是由于父View是在Down事件中推断是否在该View中的。假设在。以后的Move和up都会传递过来。不再进行范围推断
  103. if (!pointInView(x, y, mTouchSlop)) {
  104. //mTouchSlop是一个常量。数值为8,也就是说。就算你的落点超出了View的8像素位置。也算在View中。
  105. //是由于人的手指触摸点比較大,有可能你感觉点在某个控件的边缘。可是实际落点已经超出这个View,所以这里给了8像素的范围
  106. removeTapCallback();//假设在范围外,就移除这些runnable回调
  107. if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
  108. //假设是按压状态,就取消长按。设置为非按压状态,为什么这个时候取消呢,由于在Down的时候,我们能够知道,仅仅有是按压状态,才会设置长按
  109. removeLongPressCallback();
  110. setPressed(false);
  111. }
  112. }
  113. break;
  114. }
  115. return true; //至此,能够返回true。消费该事件
  116. }
  117. return false; //假设不可点击。也不可长按,则返回false,由于View仅仅具备消费点击事件
  118. }

从上面的代码我们总结一下View对触摸事件的处理:

1、是否为diabale。假设是。直接依据是否设置了click和longclick来返回。

2、是否设置了触摸代理对象,假设有。把事件传递给触摸代理对象。交由其处理,假设消费了,直接返回

3、是否为click或者longclick的,假设是,返回true。不是返回false。

而View对click和longclick的处理例如以下:

Down:

  • 推断能否够触摸上下文菜单。
  • 是否在能够滑动的容器中,假设是先设置暂时按压,再发送一个延迟消息把暂时按压改为按压。并发送一个延迟500毫秒的事件去运行长按代码
  • 假设不在滚动容器中,直接设置按压状态,并发送一个延迟500毫秒的事件去运行长按代码。

Move:

  • 取触摸点坐标推断是否在View中(额外添加了8像素的范围)
  • 假设在,不用做不论什么事。

  • 假设不在,取消暂时按压到按压回调。取消长按延迟回调,设置为非按压状态

Up

  • 推断是否为按压或者暂时按压状态
  • 假设不是,不做不论什么处理
  • 假设是先推断其能否够获取焦点。然后请求焦点。
  • 假设是暂时按压状态。设置暂时按压状态为按压状态。保证界面被绘制成按压状态,让用户能够看见。

  • 假设长按回调还未触发。取消长按回调。假设不是焦点状态。触发click事件。

  • 假设是暂时按压状态,发送一个延迟取消按压状态的,保证按压状态持续一段时间。让用户可见。

  • 假设不是暂时按压状态,直接发送消息取消按压状态。发送失败。直接取消按压状态。

  • 取消把暂时按压设置按压的回调。

从中我们知道View的onTouchEvent主要处理了click和longclick事件,当按下时,向消息机制发送一个延迟500毫秒的长按回调事件。当移动时候推断是否移出了View的范围,超出则取消事件。当离开时,推断长按事件是否触发了,假设没触发且不是焦点,就触发click事件。

在这里最绕的就是暂时按压和按压状态,暂时按压是为了处理滑动容器的。让处于滑动容器中,按下时。我们先设置的是暂时按压,持续64毫秒,是为了推断接下来的时间内是否发生了move事件。假设发生了,将不会再出发按压状态,这样不会让用户看到listView滚动时,item还处于按压状态。在离开时,我们再次推断是否处于暂时按压,假设是在64毫秒内触发了down和up。说明按压状态还没来得急绘制。则强制设置为按压状态。保证用户能看到,并在取消回调的方法上加上64毫秒的延迟

6. onTouch与onClick

  1. ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
  2. iv_image.setOnTouchListener(new OnTouchListener() {
  3. @Override
  4. public boolean onTouch(View v, MotionEvent event) {
  5. System.out.println("iv_image---onTouch--" + event.getAction());
  6. return false;
  7. }
  8. });

点击ImageView的时候仅仅会打印一次。由于onTouch()返回false,仅仅传递down事件,不会传递up事件

  1. System.out: iv_image---onTouch--0
  1. // ImageView天生不能被点击,没有点击事件
  2. ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
  3. iv_image.setOnTouchListener(new OnTouchListener() {
  4. @Override
  5. public boolean onTouch(View v, MotionEvent event) {
  6. System.out.println("iv_image---onTouch--" + event.getAction());
  7. return true; // 把返回值改为true
  8. }
  9. });

把onTouch()方法返回值改为true,点击ImageView会打印两次(down and up)

  1. System.out: iv_image---onTouch--0
  2. System.out: iv_image---onTouch--1
  1. ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
  2. iv_image.setOnTouchListener(new OnTouchListener() {
  3. @Override
  4. public boolean onTouch(View v, MotionEvent event) {
  5. System.out.println("iv_image---onTouch--" + event.getAction());
  6. return true;
  7. }
  8. });
  9. //加入click事件
  10. iv_image.setOnClickListener(new OnClickListener() {
  11. @Override
  12. public void onClick(View v) {
  13. System.out.println("iv_image---onClick");
  14. }
  15. });

还是打印两次,onTouch()返回true,click事件并不会得到运行

  1. ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
  2. iv_image.setOnTouchListener(new OnTouchListener() {
  3. @Override
  4. public boolean onTouch(View v, MotionEvent event) {
  5. System.out.println("iv_image---onTouch--" + event.getAction());
  6. return false;
  7. }
  8. });
  9. iv_image.setOnClickListener(new OnClickListener() {
  10. @Override
  11. public void onClick(View v) {
  12. System.out.println("iv_image---onClick");
  13. }
  14. });

打印三次。两次touch事件(down and up)和一次click事件

  1. Button button = (Button) findViewById(R.id.button);
  2. button.setOnTouchListener(new OnTouchListener() {
  3. @Override
  4. public boolean onTouch(View v, MotionEvent event) {
  5. System.out.println("button---onTouch--" + event.getAction());
  6. return false;
  7. }
  8. });

点击Button会打印两次


  1. Button button = (Button) findViewById(R.id.button);
  2. button.setOnTouchListener(new OnTouchListener() {
  3. @Override
  4. public boolean onTouch(View v, MotionEvent event) {
  5. System.out.println("button---onTouch--" + event.getAction());
  6. return true;
  7. }
  8. });
  9. button.setOnClickListener(new OnClickListener() {
  10. @Override
  11. public void onClick(View v) {
  12. System.out.println("button---onClick");
  13. }
  14. });

打印两次,由于onTouch()返回true,不会运行onTouchEvent(),而click事件是在onTouchEvent()中运行,所以也不会运行click事件


  1. Button button = (Button) findViewById(R.id.button);
  2. button.setOnTouchListener(new OnTouchListener() {
  3. @Override
  4. public boolean onTouch(View v, MotionEvent event) {
  5. System.out.println("button---onTouch--" + event.getAction());
  6. return false;
  7. }
  8. });
  9. button.setOnClickListener(new OnClickListener() {
  10. @Override
  11. public void onClick(View v) {
  12. System.out.println("button---onClick");
  13. }
  14. });

打印三次

  1. public boolean dispatchTouchEvent(MotionEvent event) {
  2. if (!onFilterTouchEventForSecurity(event)) {
  3. return false;
  4. }
  5. if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
  6. mOnTouchListener.onTouch(this, event)) {
  7. return true;
  8. }
  9. return onTouchEvent(event);
  10. }

a 推断mOnTouchListener是否为null

b 推断当前的控件是否可用

c 推断view的onTouch。

d 假设以上一个返回为false。

那么就会调用onTouchEvent

首先推断mOnTouchListener不为null。而且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件假设都满足。直接return true ; 也就是以下的onTouchEvent(event)不会被运行了。

假设我们设置了setOnTouchListener,而且return true。那么View自己的onTouchEvent就不会被运行了

onTouch是优先于onClick运行, onClick的调用在onTouchEvent(event)方法中

view的事件分发

  1. 返回true。说明能够响应down事件和up事件
  2. 返回false,仅仅会响应down事件。不会响应up事件。

    在down事件假设能消费(处理)当前事件。

    那么在up的时候也会把事件传递给当前的view,在down事件处理不了当前事件。那么在up的时候。也不会把事件传递给当前的view

Android的事件分发实例分析

7. ScrollView的onTouchEvent

普通的ViewGroup并没有对onTouchEvent事件做处理,仅仅有能够滚动的才有,我们能够分析一下ScrollView

  • Down时,推断落点是否在子View中,不再就不处理,由于ScrollView仅仅有一个子View。

  • Move时,通过对照本次手指的位置和上一次的位置的距离,计算出Y方向的差值,然后用scorllBy进行滚动视图

  • Up时,通过速度进行fling,这里利用了两个帮助类,一个是计算速度的帮助类VelocityTracker,一个是滚动的帮助类Scroller

  1. public boolean onTouchEvent(MotionEvent ev) {
  2. if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
  3. //假设是down事件。而且触摸到边缘,就不处理EdgeFlags代表是否为边缘,其值是1/2/4/8。代表上下左右
  4. return false;
  5. }
  6. if (mVelocityTracker == null) {
  7. //这是一个追踪触摸事件。并计算速度的帮助类,实现原理就是用三个数组分别记录每次触摸的x/y和时间
  8. mVelocityTracker = VelocityTracker.obtain();
  9. }
  10. mVelocityTracker.addMovement(ev);
  11. final int action = ev.getAction();
  12. switch (action & MotionEvent.ACTION_MASK) {//与上ff,去掉高位有关多点的信息
  13. case MotionEvent.ACTION_DOWN: {//假设是down
  14. final float y = ev.getY();//获取y坐标
  15. if (!(mIsBeingDragged = inChild((int) ev.getX(), (int) y))) {//推断是否開始拖动
  16. //原理就是推断落点是否在child中,ScrollView仅仅能由一个child,假设在。返回true。反之false
  17. //也就是说落点在child中,就是准备開始拖动,不在,就直接返回,这可能是由于设置了padding之类的缘故造成的
  18. return false;
  19. }
  20. if (!mScroller.isFinished()) {//推断滚动是否完毕
  21. mScroller.abortAnimation();//假设没完毕,停止滚动
  22. //相应上一次用户手指离开时候处理fling状态,这次按下手指,直接停止滚动
  23. }
  24. //记录y坐标,以便下次事件来对照
  25. mLastMotionY = y;
  26. mActivePointerId = ev.getPointerId(0);//记住多点的id,下次取值时仅仅取该点的
  27. break;
  28. }
  29. case MotionEvent.ACTION_MOVE:
  30. if (mIsBeingDragged) {//能够看出,假设down的时候落点在child外。则以后就算滑进了child也不处理
  31. //依据上次记录的多点id,找到相应的点,取y值
  32. final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
  33. final float y = ev.getY(activePointerIndex);
  34. final int deltaY = (int) (mLastMotionY - y);//计算位移
  35. mLastMotionY = y;//又一次记录y值
  36. scrollBy(0, deltaY);//滚动指定的距离,这也说明了ScrollView仅仅具备纵向滑动
  37. }
  38. break;
  39. case MotionEvent.ACTION_UP:
  40. if (mIsBeingDragged) {//假设是离开事件
  41. final VelocityTracker velocityTracker = mVelocityTracker;
  42. velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//计算最后1秒钟内的速度,并给定一个最大速度进行限制
  43. //这个最大速度是依据屏幕密度不同而不同的。所以大家也没事别使劲滑动屏幕,由于有这个最大速度限制
  44. //获取y方向的速度
  45. int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
  46. if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) {
  47. //假设有子View,而且计算出来的y的速度比最小速度要大,运行fling状态
  48. //手指滑动的方向和屏幕移动的方向是相反的,所以这里加-
  49. fling(-initialVelocity);
  50. }
  51. mActivePointerId = INVALID_POINTER;//给mActivePointerId又一次赋值为-1,防止下次事件找到了错误的点
  52. mIsBeingDragged = false;//恢复默认值
  53. if (mVelocityTracker != null) {//清空速度计算帮助类
  54. mVelocityTracker.recycle();
  55. mVelocityTracker = null;
  56. }
  57. }
  58. break;
  59. case MotionEvent.ACTION_CANCEL:
  60. if (mIsBeingDragged && getChildCount() > 0) {//推断条件,仅仅有这2个条件成立,才会发生滚动事件。以下的值才会被改变。才须要恢复默认
  61. mActivePointerId = INVALID_POINTER;
  62. mIsBeingDragged = false;
  63. if (mVelocityTracker != null) {
  64. mVelocityTracker.recycle();
  65. mVelocityTracker = null;
  66. }
  67. }
  68. break;
  69. case MotionEvent.ACTION_POINTER_UP://多点触摸时。不是最后一个点离开
  70. onSecondaryPointerUp(ev);
  71. break;
  72. }
  73. return true;
  74. }
  75. //用于应对先按下1点,然后按下2点,1点离开后,2点仍能继续滑动的逻辑
  76. private void onSecondaryPointerUp(MotionEvent ev) {
  77. final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
  78. MotionEvent.ACTION_POINTER_INDEX_SHIFT;//首先对高位进行与操作。然后右移8位,获取其高位代表index的值
  79. final int pointerId = ev.getPointerId(pointerIndex);//取出该点的id
  80. if (pointerId == mActivePointerId) {//假设这个id相应的就是第一个按下的点
  81. //理论上pointerIndex应该是0,所以用第二个按下的点,即1index的点取代
  82. final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
  83. mLastMotionY = ev.getY(newPointerIndex);//取出新点的y坐标
  84. mActivePointerId = ev.getPointerId(newPointerIndex);//记录新点的id
  85. if (mVelocityTracker != null) {//清空之前存入的MotionEvent,也就是说最后的速度仅仅计算该点产生的
  86. mVelocityTracker.clear();
  87. }
  88. }
  89. }

通过以上分析,我们得出以下知识:

  • 在down事件的时候先推断触摸是否处于边缘,假设是,则不处理
  • 在down事件中推断落点是否在子View中。假设不在,不处理
  • 在down事件中推断是否仍在滑动,假设是,先停止
  • 记录第一个按下点的索引值
  • 每次事件都记录住当前的y值
  • 在move事件中通过记录的索引值找到相应的点,获取y坐标
  • 与上一次y坐标进行比对。scrollBy两次的差值
  • 在up事件的时候计算最后一秒钟的速度。而且有最大速度进行限制,当计算的速度大于系统默认的最小速度时,仅仅想fling
  • up和cancel事件还原变量为默认值
  • 假设为多点离开。进行多点离开的处理
  • 该处理方式时:假设离开的是第一个按下的点,那么由第二个按下的点取代其进行y值偏移计算的基点,并清空速度计算的帮助类。又一次记录MotionEvnet

8. Layout和Scroll的差别

  • Layout中设置的是自身在父View中的显示区域
  • Scroll是调整自己的显示区域
  • 当父View滚动或者layout变化后,自身在屏幕上的位置会发生变化。

    当自身Scroll滚动后。在屏幕上的显示位置是不变的,变的仅仅是自身的显示内容。
  • Scroll滚动不会影响Layout。仅仅是在draw的时候影响画布偏移和触摸时的坐标计算。

Android的事件分发的更多相关文章

  1. Android View 事件分发机制 源码解析 (上)

    一直想写事件分发机制的文章,不管咋样,也得自己研究下事件分发的源码,写出心得~ 首先我们先写个简单的例子来测试View的事件转发的流程~ 1.案例 为了更好的研究View的事件转发,我们自定以一个My ...

  2. Android之事件分发机制

    本文主要包括以下内容 view的事件分发 viewGroup的事件分发 首先来看两张图 在执行touch事件时 首先执行dispatchTouchEvent方法,执行事件分发. 再执行onInterc ...

  3. android的事件分发机制理解

    android的事件分发机制理解 1.事件触发主要涉及到哪些层面的哪些函数(个人理解的顺序,可能在某一层会一次回调其它函数) activity中的dispatchTouchEvent .layout中 ...

  4. android view事件分发机制

    首先我们先写个简单的例子来测试View的事件转发的流程~ 1.案例 为了更好的研究View的事件转发,我们自定以一个MyButton继承Button,然后把跟事件传播有关的方法进行复写,然后添加上日志 ...

  5. Android Touch事件分发过程

    虽然网络上已经有非常多关于这个话题的优秀文章了,但还是写了这篇文章,主要还是为了加强自己的记忆吧,自己过一遍总比看别人的分析要深刻得多.那就走起吧. 简单演示样例 先看一个演示样例 : 布局文件 : ...

  6. Android View 事件分发机制 源代码解析 (上)

    一直想写事件分发机制的文章,无论咋样,也得自己研究下事件分发的源代码.写出心得~ 首先我们先写个简单的样例来測试View的事件转发的流程~ 1.案例 为了更好的研究View的事件转发,我们自定以一个M ...

  7. 一文读懂 Android TouchEvent 事件分发、拦截、处理过程

    什么是事件?事件是用户触摸手机屏幕,引起的一系列TouchEvent,包括ACTION_DOWN.ACTION_MOVE.ACTION_UP.ACTION_CANCEL等,这些action组合后变成点 ...

  8. Android Touch事件分发机制学习

    Android  事件分发机制 ViewGroup dispatchTouchEvent 返回true dispatchTouchEvent: Activity ACTION_DOWN Myrelat ...

  9. android自定义控件(9)-Android触摸事件分发机制

    触摸事件的传递机制:   首先是最外层的viewgroup接收到事件,然后调用会调用自己的dispatchTouchEvent方法.如果在ACTION_DOWN的时候dispatchTouchEven ...

随机推荐

  1. BZOJ2733: [HNOI2012]永无乡(线段树合并)

    Description 永无乡包含 n 座岛,编号从 1 到 n,每座岛都有自己的独一无二的重要度,按照重要度可 以将这 n 座岛排名,名次用 1 到 n 来表示.某些岛之间由巨大的桥连接,通过桥可以 ...

  2. leaf cell

    leaf cell是否可以理解为设计中与或非门等这些基本的单元?

  3. sql 高性能存储过程分页

    USE [Lyjjr] GO /****** Object: StoredProcedure [dbo].[P_ViewPage] Script Date: 05/29/2015 17:18:56 * ...

  4. 几种基于Java的SQL解析工具的比较与调用

    1.sqlparser http://www.sqlparser.com/ 优点:支持的数据库最多,除了传统数据库外还支持hive和greenplum一类比较新的数据库,调用比较方便,功能不错 缺点: ...

  5. 转载的:Python os 和 os.path模块详解

    os.getcwd()获取当前工作目录,即当前python脚本工作的目录路径 os.chdir("dirname") 改变当前脚本工作目录:相当于shell下cd os.curdi ...

  6. jQuery快速入门知识重点

    1.jquery中attr与prop的区别   attr:是通过setAttribute 和 getAttribute来设置的使用的是DOM属性节点   prop:是通过document.getEle ...

  7. 洛谷 P1808 单词分类_NOI导刊2011提高(01)

    P1808 单词分类_NOI导刊2011提高(01) 题目描述 Oliver为了学好英语决定苦背单词,但很快他发现要直接记住杂乱无章的单词非常困难,他决定对单词进行分类. 两个单词可以分为一类当且仅当 ...

  8. [D3] Creating a D3 Force Layout in React

    Learn how to leverage d3's layout module to create a Force Layout inside of React. We'll take a look ...

  9. amazeui学习笔记--css(常用组件11)--分页Pagination

    amazeui学习笔记--css(常用组件11)--分页Pagination 一.总结 1.分页使用:还是ul包li的形式: 分页组件,<ul> / <ol> 添加 .am-p ...

  10. POJ 3723 Conscription MST

    http://poj.org/problem?id=3723 题目大意: 需要征募女兵N人,男兵M人,没征募一个人需要花费10000美元,但是如果已经征募的人中有一些关系亲密的人,那么可以少花一些钱, ...