[关闭]
@TryLoveCatch 2022-04-26T08:34:21.000000Z 字数 11042 阅读 1672

Android知识体系之事件分发

Android知识体系


事件分发

事件分发,牵扯到多个层面的以下几个方法:

基本流程

其他那一块,我们之后再说,先说说上面三个维度的,我们用给一张图来说明下:

  1. 图也是分为三个维度的,从上到下,ActivityViewGroupView
  2. 图中仅仅是针对down事件的分析。

基于方法分析

  1. dispatchTouchEvent(),返回true,就代表消费了;返回false,就代表不往下传递了,往上层抛;返回super.dispatchTouchEvent(ev),往下传递。
  2. onTouchEvent(),返回true,就代表消费了;返回falsesuper.onTouchEvent(event),就代表自己不处理,往上层抛。
  3. onInterceptTouchEvent(),返回true,就代表拦截,调用自己的onTouchEvent();返回falsesuper.onInterceptTouchEvent(event),就代表不拦截,往下传递。
  4. 通过1、2、3,我们可以将返回值,分为三种:truefalsesuper,我们的代码,也会根据具体的情况,来返回三个中的一个。所以三个不同的返回值,接下来的处理流程,我们需要了解。
  5. ActivitydispatchTouchEvent无论返回true还是false都会被消费,不在往下传递了,也不会往上抛。
  6. ViewGroupdispatchTouchEvent,只有在返回super.dispatchTouchEvent(ev)的时候,才会调用onInterceptTouchEvent()

基于三个维度分析

  1. 对于ActivitydispatchTouchEvent()每次都会调用,返回truefalse,事件被消费,到此终止。返回super,事件可以往下传递。
  2. Activity自己的onTouchEvent()想要执行,有两种途径(当然前提都是自己的dispatchTouchEvent()返回super):1、ViewGroupdispatchTouchEvent()返回false。2、时间一直没有被消费,一直往上抛,最后通过ViewGrouponTouchEvent()抛给它。
  3. 对于ViewGroup,事件传递过来,dispatchTouchEvent()每次都会调用,返回true,事件被消费,到此终止;返回false,抛给上层的onTouchEvent();返回super,必然后调用onInterceptTouchEvent(),来判断是否拦截事件,返回 true,拦截事件,执行自己的onTouchEvent();返回false或者super,继续向下传递。
  4. ViewGrouponTouchEvent()执行的时候,返回 true,自己消费;返回false或者super,抛给上层的onTouchEvent()
  5. View,自己是事件传递过程中的最后一块了,dispatchTouchEvent()每次都会调用,返回true,事件被消费,到此终止;返回false,抛给上层的onTouchEvent();返回super,则调用自己的onTouchEvent()
  6. ViewonTouchEvent()执行的时候,返回 true,自己消费;返回false或者super,抛给上层的onTouchEvent()

move&up

下面上图,图比较简单,就不做说明了。
红线为down的过程
蓝线为moveup的过程


图一


图二


图三


图四


图五

重点说下图五down的路线是这样:

  1. Activity dispatchTouchEvent()
  2. ViewGroup1 dispatchTouchEvent()
  3. ViewGroup1 onInterceptTouchEvent()
  4. ViewGroup2 dispatchTouchEvent()
  5. ViewGroup2 onInterceptTouchEvent()
  6. ViewGroup2 onTouchEvent()
  7. ViewGroup1 onTouchEvent()

moveup的路线有所不同,它们是直接到达目的地的:

  1. Activity dispatchTouchEvent()
  2. ViewGroup1 dispatchTouchEvent()
  3. ViewGroup1 onInterceptTouchEvent()
  4. ViewGroup1 onTouchEvent()

所以,moveup不会走和down同样的路线,它们会直接到达目的地,所谓目的地,就是消费了down的方法,它俩会经过>=1(1或多个)个dispatchTouchEvent()、比dispatchTouchEvent()少一个的onInterceptTouchEvent()<=1(0或者1个)个onTouchEvent()

OnTouchListener onTouchEvent OnClickListener

测试一

先说OnTouchListener,代码如下:

  1. mView.setOnTouchListener(new View.OnTouchListener() {
  2. @Override
  3. public boolean onTouch(View v, MotionEvent event) {
  4. Log.e("hhhh", "activity View OnTouchListener " + Utils.toString(event));
  5. return false;
  6. }
  7. });

我们来看一下打印:

  1. activity dispatchTouchEvent down
  2. viewGroup dispatchTouchEvent down
  3. viewGroup onInterceptTouchEvent down
  4. view dispatchTouchEvent down
  5. activity View OnTouchListener down
  6. view onTouchEvent down
  7. viewGroup onTouchEvent down
  8. activity onTouchEvent down
  9. activity dispatchTouchEvent up
  10. activity onTouchEvent up

down的时候,调用了OnTouchListener,之后moveup因为都没有处理,所以直接到达目的地。而且我们注意到,OnTouchListener是早于onTouchEvent执行的。

测试二

  1. mView.setOnTouchListener(new View.OnTouchListener() {
  2. @Override
  3. public boolean onTouch(View v, MotionEvent event) {
  4. Log.e("hhhh", "activity View OnTouchListener " + Utils.toString(event));
  5. return true;
  6. }
  7. });

修改为true,我们来看一下打印:

  1. activity dispatchTouchEvent down
  2. viewGroup dispatchTouchEvent down
  3. viewGroup onInterceptTouchEvent down
  4. view dispatchTouchEvent down
  5. activity View OnTouchListener down
  6. activity dispatchTouchEvent up
  7. viewGroup dispatchTouchEvent up
  8. viewGroup onInterceptTouchEvent up
  9. view dispatchTouchEvent up
  10. activity View OnTouchListener up

OnTouchListener里面消费了事件,这个时候,onTouchEvent没有执行。

测试三

我们再来看看OnClickListener,测试代码如下:

  1. mView.setOnTouchListener(new View.OnTouchListener() {
  2. @Override
  3. public boolean onTouch(View v, MotionEvent event) {
  4. Log.e("hhhh", "activity View OnTouchListener " + Utils.toString(event));
  5. return false;
  6. }
  7. });
  8. mView.setOnClickListener(new View.OnClickListener() {
  9. @Override
  10. public void onClick(View v) {
  11. Log.e("hhhh", "activity View onClick");
  12. }
  13. });

日志如下:

  1. activity dispatchTouchEvent down
  2. viewGroup dispatchTouchEvent down
  3. viewGroup onInterceptTouchEvent down
  4. view dispatchTouchEvent down
  5. activity View OnTouchListener down
  6. view onTouchEvent down
  7. activity dispatchTouchEvent move
  8. viewGroup dispatchTouchEvent move
  9. viewGroup onInterceptTouchEvent move
  10. view dispatchTouchEvent move
  11. activity View OnTouchListener move
  12. view onTouchEvent move
  13. activity dispatchTouchEvent up
  14. viewGroup dispatchTouchEvent up
  15. viewGroup onInterceptTouchEvent up
  16. view dispatchTouchEvent up
  17. activity View OnTouchListener up
  18. view onTouchEvent up
  19. activity View onClick

OnClickListener实在up之后才调用的。
然而,有一个问题,这个事件View没有消费啊,但是,在onTouchEvent这里没有往上抛,而是消费了。
我们如果去掉OnClickListener,就是上面的`测试二,打印如下:

  1. activity dispatchTouchEvent down
  2. viewGroup dispatchTouchEvent down
  3. viewGroup onInterceptTouchEvent down
  4. view dispatchTouchEvent down
  5. activity View OnTouchListener down
  6. view onTouchEvent down
  7. viewGroup onTouchEvent down
  8. activity onTouchEvent down
  9. activity dispatchTouchEvent up
  10. activity onTouchEvent up

事件没有消费,而是抛到了activity里面。

ViewonTouchEvent代码如下:

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. Log.e("hhhh", "view onTouchEvent " + Utils.toString(event));
  4. return super.onTouchEvent(event);
  5. }

我是直接super的,所以,我们可以得出结论,如果设置了ViewOnClickListener,并且没有覆盖ViewonTouchEvent或者调用了super,那么,onTouchEvent就会返回true,消费了这个事件。

测试四

  1. mView.setOnTouchListener(new View.OnTouchListener() {
  2. @Override
  3. public boolean onTouch(View v, MotionEvent event) {
  4. Log.e("hhhh", "activity View OnTouchListener " + Utils.toString(event));
  5. return true;
  6. }
  7. });
  8. mView.setOnClickListener(new View.OnClickListener() {
  9. @Override
  10. public void onClick(View v) {
  11. Log.e("hhhh", "activity View onClick");
  12. }
  13. });

打印如下:

  1. activity dispatchTouchEvent down
  2. viewGroup dispatchTouchEvent down
  3. viewGroup onInterceptTouchEvent down
  4. view dispatchTouchEvent down
  5. activity View OnTouchListener down
  6. activity dispatchTouchEvent up
  7. viewGroup dispatchTouchEvent up
  8. viewGroup onInterceptTouchEvent up
  9. view dispatchTouchEvent up
  10. activity View OnTouchListener up

OnClickListener没有被调用,有点类似于onTouchEvent,其实,源码里面,OnClickListener就是在onTouchEvent里面被执行的。

结论

  1. dispatchTouchEvent,会先调用OnTouchListener,如果返回true,那么就不会调用onTouchEvent;另外,针对ViewGroupdown事件中,onInterceptTouchEvent先于OnTouchListener执行,后面的move&up事件,不再执行onInterceptTouchEvent
  2. OnClickListener是在onTouchEvent中的up事件被调用的。如果设置了OnClickListeneronTouchEvent就会返回true,消费事件。
  3. 优先级 OnTouchListener > onTouchEvent > OnClickListener

事件冲突

描述一种场景,一个列表,附带下拉回弹的效果。

下拉回弹

自定义一个LinearLayout,来处理下拉回弹效果。
关键代码如下:

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. int y = (int) event.getY();
  4. switch (event.getAction()) {
  5. case MotionEvent.ACTION_DOWN:
  6. yDown = y;
  7. break;
  8. case MotionEvent.ACTION_MOVE:
  9. yMove = y;
  10. if ((yMove - yDown) > 0) {
  11. mMove = yMove - yDown;
  12. i += mMove;
  13. layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
  14. }
  15. break;
  16. case MotionEvent.ACTION_UP:
  17. layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
  18. i = 0;
  19. break;
  20. }
  21. return true;
  22. }

这个时候,布局如下:

  1. <MyLinearLayout>
  2. <item1></item1>
  3. <item2></item2>
  4. <item3></item3>
  5. ...
  6. </MyLinearLayout>

item滚动

接下来,我们实现列表的滚动,我们肯定想到了ScrollerView,布局如下:

  1. <MyLinearLayout>
  2. <ScrollerView>
  3. <LinearLayout>
  4. <item1></item1>
  5. <item2></item2>
  6. <item3></item3>
  7. ...
  8. </LinearLayout>
  9. </ScrollerView>
  10. </MyLinearLayout>

然后,我们运行,发现下拉回弹的效果不见了。外部滑动方向与内部滑动方向一致。父布局MyLinearLayout需要响应竖直方向上的向下滑动,实现下拉回弹,子布局ScrollView也需要响应竖直方向上的上下滑动,实现子View的滚动。当内外两层都在同一个方向上可以滑动的时候,就会出现逻辑问题。因为当手指滑动的时候,系统无法知道用户想让哪一层滑动。所以这种场景下的滑动冲突需要我们手动去解决。

方法一 外部拦截法

外部拦截法是指点击事件先经过父容器的拦截处理,如果父容器需要处理此事件就进行拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。外部拦截法需要重写父容器的onInterceptTouchEvent()方法,在内部做相应的拦截即可。

我们在move的时候进行判断,如果手指是向上滑动,onInterceptTouchEvent()返回false,表示父布局不拦截当前事件,当前事件交给子View处理,那么我们的子View就能滚动;如果手指是向下滑动,onInterceptTouchEvent()返回true,表示父布局拦截当前事件,当前事件交给父布局处理,那么我们父布局就能实现下拉回弹。
代码如下:

  1. @Override
  2. public boolean onInterceptTouchEvent(MotionEvent event) {
  3. int y = (int) event.getY();
  4. switch (event.getAction()) {
  5. case MotionEvent.ACTION_DOWN:
  6. yDown = y;
  7. break;
  8. case MotionEvent.ACTION_MOVE:
  9. yMove = y;
  10. if (yMove - yDown < 0) {
  11. isIntercept = false;
  12. } else if (yMove - yDown > 0) {
  13. isIntercept = true;
  14. }
  15. break;
  16. case MotionEvent.ACTION_UP:
  17. isIntercept = false;
  18. break;
  19. }
  20. return isIntercept;
  21. }

运行之后,没有达到我们的目的,还是由问题:手指向上滑动的时候,子View开始滚动,然后手指再向下滑动,整个父布局开始向下滑动,松手后便自动回弹。也就是说,刚才滚动的子View已经回不到开始的位置。

仔细分析一下其实这结果是意料之中的,因为只要我手指是向下滑动,onInterceptTouchEvent()便返回true,父布局会拦截当前事件。这里其实又是上面提到的View滑动冲突:理想的结果是当子View滚动后,如果子View没有滚动到开始的位置,父布局就不要拦截滑动事件;如果子View已经滚动到开始的位置,父布局就开始拦截滑动事件。

方法二 内部拦截法

内部拦截法:内部拦截法是指点击事件先经过子View处理,如果子View需要此事件就直接消耗掉,否则就交给父容器进行处理,这样就可以解决滑动冲突的问题。内部拦截法需要配合requestDisallowInterceptTouchEvent()方法,来确定子View是否允许父布局拦截事件。

自定义一个ScrollView,重写onTouchEvent()方法,在MotionEvent.ACTION_MOVE的时候,得到滑动的距离。如果滑动的距离为0,表示子View已经滚动到开始位置,此时调用 getParent().requestDisallowInterceptTouchEvent(false)方法,允许父View进行事件拦截;如果滑动的距离不为0,表示子View没有滚动到开始位置,此时调用 getParent().requestDisallowInterceptTouchEvent(true)方法,禁止父View进行事件拦截。这样只要子View没有滚动到开始的位置,父布局都不会拦截事件,一旦子View滚动到开始的位置,父布局就开始拦截事件,形成连续的滑动。

  1. public class MyScrollView extends ScrollView {
  2. public MyScrollView(Context context) {
  3. this(context, null);
  4. }
  5. public MyScrollView(Context context, AttributeSet attrs) {
  6. this(context, attrs, 0);
  7. }
  8. public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
  9. super(context, attrs, defStyleAttr);
  10. }
  11. @Override
  12. public boolean onTouchEvent(MotionEvent ev) {
  13. switch (ev.getAction()) {
  14. case MotionEvent.ACTION_MOVE:
  15. int scrollY = getScrollY();
  16. if (scrollY == 0) {
  17. //允许父View进行事件拦截
  18. getParent().requestDisallowInterceptTouchEvent(false);
  19. } else {
  20. //禁止父View进行事件拦截
  21. getParent().requestDisallowInterceptTouchEvent(true);
  22. }
  23. break;
  24. }
  25. return super.onTouchEvent(ev);
  26. }
  27. }

requestDisallowInterceptTouchEvent

我们看一下ViewGroupdispatchTouchEvent中的一段代码:

  1. final boolean intercepted;
  2. if (actionMasked == MotionEvent.ACTION_DOWN
  3. || mFirstTouchTarget != null) {
  4. final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
  5. if (!disallowIntercept) {
  6. intercepted = onInterceptTouchEvent(ev);
  7. ev.setAction(action); // restore action in case it was changed
  8. } else {
  9. intercepted = false;
  10. }
  11. } else {
  12. // There are no touch targets and this action is not an initial down
  13. // so this view group continues to intercept touches.
  14. intercepted = true;
  15. }

requestDisallowInterceptTouchEvent就是通过mGroupFlags来控制disallowIntercept的值。
requestDisallowInterceptTouchEvent(true)disallowIntercept就为true,所以interceptedfalse,而且onInterceptTouchEvent也不会被调用。所以:

  1. 子view不希望父ViewGroup拦截事件可以调用mParent.requestDisallowInterceptTouchEvent(true)
  2. requestDisallowInterceptTouchEvent这个函数一般不是自己调用的,而是给儿子调用的。
  3. requestDisallowInterceptTouchEvent主要用来解决滑动冲突。

外部拦截法标准写法

父布局:

  1. @Override
  2. public boolean onInterceptTouchEvent(MotionEvent event) {
  3. boolean isIntercept = false;
  4. int x = (int) event.getX();
  5. int y = (int) event.getY();
  6. switch (event.getAction()) {
  7. case MotionEvent.ACTION_DOWN:
  8. break;
  9. case MotionEvent.ACTION_MOVE:
  10. yMove = y;
  11. if (父容器需要当前事件) {
  12. isIntercept = true;
  13. } else {
  14. isIntercept = false;
  15. }
  16. break;
  17. case MotionEvent.ACTION_UP:
  18. isIntercept = false;
  19. break;
  20. }
  21. mLastX = x;
  22. mLastY = y;
  23. return isIntercept;
  24. }

内部拦截法标准写法

父布局:

  1. @Override
  2. public boolean onInterceptTouchEvent(MotionEvent event) {
  3. switch (event.getAction()) {
  4. case MotionEvent.ACTION_DOWN:
  5. return false;
  6. default:
  7. return true;
  8. }
  9. }

子布局:

  1. public boolean dispatchTouchEvent(MotionEvent event){
  2. int x = (int) event.getX();
  3. int y = (int) event.getY();
  4. switch (event.getAction()) {
  5. case MotionEvent.ACTION_DOWN:
  6. parent.requestDisallowInterceptTouchEvent(true);
  7. break;
  8. case MotionEvent.ACTION_MOVE:
  9. int deltaX = x - mLastX;
  10. int deltaY = y - mLastY;
  11. if (父容器需要当前事件) {
  12. parent.requestDisallowInterceptTouchEvent(false);
  13. }
  14. break;
  15. case MotionEvent.ACTION_UP:
  16. break;
  17. }
  18. mLastX = x;
  19. mLastY = y;
  20. return super.dispatchTouchEvent(event);
  21. }

参考

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
图解 Android 事件分发机制
一个Demo带你彻底掌握View的滑动冲突
重要的函数requestDisallowInterceptTouchEvent
Android滑动冲突解决方法

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注