@TryLoveCatch
2022-04-26T16:34:21.000000Z
字数 11042
阅读 1816
Android知识体系
事件分发,牵扯到多个层面的以下几个方法:
boolean dispatchTouchEvent(MotionEvent ev)
boolean onTouchEvent(MotionEvent event)
boolean dispatchTouchEvent(MotionEvent ev)
boolean onInterceptTouchEvent(MotionEvent ev)
boolean onTouchEvent(MotionEvent event)
boolean dispatchTouchEvent(MotionEvent ev)
boolean onTouchEvent(MotionEvent event)
void setOnTouchListener(OnTouchListener l)
void setOnClickListener(@Nullable OnClickListener l)
其他那一块,我们之后再说,先说说上面三个维度的,我们用给一张图来说明下:
- 图也是分为三个维度的,从上到下,
Activity
、ViewGroup
、View
。- 图中仅仅是针对
down
事件的分析。
dispatchTouchEvent()
,返回true
,就代表消费了;返回false
,就代表不往下传递了,往上层抛;返回super.dispatchTouchEvent(ev)
,往下传递。onTouchEvent()
,返回true
,就代表消费了;返回false
或super.onTouchEvent(event)
,就代表自己不处理,往上层抛。onInterceptTouchEvent()
,返回true
,就代表拦截,调用自己的onTouchEvent()
;返回false
或super.onInterceptTouchEvent(event)
,就代表不拦截,往下传递。1、2、3
,我们可以将返回值,分为三种:true
、false
、super
,我们的代码,也会根据具体的情况,来返回三个中的一个。所以三个不同的返回值,接下来的处理流程,我们需要了解。Activity
的dispatchTouchEvent
无论返回true
还是false
都会被消费,不在往下传递了,也不会往上抛。ViewGroup
的dispatchTouchEvent
,只有在返回super.dispatchTouchEvent(ev)
的时候,才会调用onInterceptTouchEvent()
Activity
,dispatchTouchEvent()
每次都会调用,返回true
或false
,事件被消费,到此终止。返回super
,事件可以往下传递。Activity
自己的onTouchEvent()
想要执行,有两种途径(当然前提都是自己的dispatchTouchEvent()
返回super
):1、ViewGroup
的dispatchTouchEvent()
返回false
。2、时间一直没有被消费,一直往上抛,最后通过ViewGroup
的onTouchEvent()
抛给它。ViewGroup
,事件传递过来,dispatchTouchEvent()
每次都会调用,返回true
,事件被消费,到此终止;返回false
,抛给上层的onTouchEvent()
;返回super
,必然后调用onInterceptTouchEvent()
,来判断是否拦截事件,返回 true
,拦截事件,执行自己的onTouchEvent()
;返回false
或者super
,继续向下传递。ViewGroup
的onTouchEvent()
执行的时候,返回 true
,自己消费;返回false
或者super
,抛给上层的onTouchEvent()
。View
,自己是事件传递过程中的最后一块了,dispatchTouchEvent()
每次都会调用,返回true
,事件被消费,到此终止;返回false
,抛给上层的onTouchEvent()
;返回super
,则调用自己的onTouchEvent()
。View
的onTouchEvent()
执行的时候,返回 true
,自己消费;返回false
或者super
,抛给上层的onTouchEvent()
。下面上图,图比较简单,就不做说明了。
红线为down
的过程
蓝线为move
和up
的过程
重点说下图五
,down
的路线是这样:
Activity dispatchTouchEvent()
ViewGroup1 dispatchTouchEvent()
ViewGroup1 onInterceptTouchEvent()
ViewGroup2 dispatchTouchEvent()
ViewGroup2 onInterceptTouchEvent()
ViewGroup2 onTouchEvent()
ViewGroup1 onTouchEvent()
而move
和up
的路线有所不同,它们是直接到达目的地的:
Activity dispatchTouchEvent()
ViewGroup1 dispatchTouchEvent()
ViewGroup1 onInterceptTouchEvent()
ViewGroup1 onTouchEvent()
所以,move
和up
不会走和down
同样的路线,它们会直接到达目的地,所谓目的地,就是消费了down
的方法,它俩会经过>=1
(1或多个)个dispatchTouchEvent()
、比dispatchTouchEvent()
少一个的onInterceptTouchEvent()
和<=1
(0或者1个)个onTouchEvent()
。
先说OnTouchListener
,代码如下:
mView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("hhhh", "activity View OnTouchListener " + Utils.toString(event));
return false;
}
});
我们来看一下打印:
activity dispatchTouchEvent down
viewGroup dispatchTouchEvent down
viewGroup onInterceptTouchEvent down
view dispatchTouchEvent down
activity View OnTouchListener down
view onTouchEvent down
viewGroup onTouchEvent down
activity onTouchEvent down
activity dispatchTouchEvent up
activity onTouchEvent up
在down
的时候,调用了OnTouchListener
,之后move
和up
因为都没有处理,所以直接到达目的地。而且我们注意到,OnTouchListener
是早于onTouchEvent
执行的。
mView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("hhhh", "activity View OnTouchListener " + Utils.toString(event));
return true;
}
});
修改为true
,我们来看一下打印:
activity dispatchTouchEvent down
viewGroup dispatchTouchEvent down
viewGroup onInterceptTouchEvent down
view dispatchTouchEvent down
activity View OnTouchListener down
activity dispatchTouchEvent up
viewGroup dispatchTouchEvent up
viewGroup onInterceptTouchEvent up
view dispatchTouchEvent up
activity View OnTouchListener up
在OnTouchListener
里面消费了事件,这个时候,onTouchEvent
没有执行。
我们再来看看OnClickListener
,测试代码如下:
mView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("hhhh", "activity View OnTouchListener " + Utils.toString(event));
return false;
}
});
mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("hhhh", "activity View onClick");
}
});
日志如下:
activity dispatchTouchEvent down
viewGroup dispatchTouchEvent down
viewGroup onInterceptTouchEvent down
view dispatchTouchEvent down
activity View OnTouchListener down
view onTouchEvent down
activity dispatchTouchEvent move
viewGroup dispatchTouchEvent move
viewGroup onInterceptTouchEvent move
view dispatchTouchEvent move
activity View OnTouchListener move
view onTouchEvent move
activity dispatchTouchEvent up
viewGroup dispatchTouchEvent up
viewGroup onInterceptTouchEvent up
view dispatchTouchEvent up
activity View OnTouchListener up
view onTouchEvent up
activity View onClick
OnClickListener
实在up
之后才调用的。
然而,有一个问题,这个事件View
没有消费啊,但是,在onTouchEvent
这里没有往上抛,而是消费了。
我们如果去掉OnClickListener
,就是上面的`测试二
,打印如下:
activity dispatchTouchEvent down
viewGroup dispatchTouchEvent down
viewGroup onInterceptTouchEvent down
view dispatchTouchEvent down
activity View OnTouchListener down
view onTouchEvent down
viewGroup onTouchEvent down
activity onTouchEvent down
activity dispatchTouchEvent up
activity onTouchEvent up
事件没有消费,而是抛到了activity
里面。
View
的onTouchEvent
代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("hhhh", "view onTouchEvent " + Utils.toString(event));
return super.onTouchEvent(event);
}
我是直接super
的,所以,我们可以得出结论,如果设置了View
的OnClickListener
,并且没有覆盖View
的onTouchEvent
或者调用了super
,那么,onTouchEvent
就会返回true
,消费了这个事件。
mView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("hhhh", "activity View OnTouchListener " + Utils.toString(event));
return true;
}
});
mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("hhhh", "activity View onClick");
}
});
打印如下:
activity dispatchTouchEvent down
viewGroup dispatchTouchEvent down
viewGroup onInterceptTouchEvent down
view dispatchTouchEvent down
activity View OnTouchListener down
activity dispatchTouchEvent up
viewGroup dispatchTouchEvent up
viewGroup onInterceptTouchEvent up
view dispatchTouchEvent up
activity View OnTouchListener up
OnClickListener
没有被调用,有点类似于onTouchEvent
,其实,源码里面,OnClickListener
就是在onTouchEvent
里面被执行的。
dispatchTouchEvent
,会先调用OnTouchListener
,如果返回true
,那么就不会调用onTouchEvent
;另外,针对ViewGroup
,down
事件中,onInterceptTouchEvent
先于OnTouchListener
执行,后面的move&up
事件,不再执行onInterceptTouchEvent
。OnClickListener
是在onTouchEvent
中的up
事件被调用的。如果设置了OnClickListener
,onTouchEvent
就会返回true
,消费事件。OnTouchListener
> onTouchEvent
> OnClickListener
描述一种场景,一个列表,附带下拉回弹的效果。
自定义一个LinearLayout
,来处理下拉回弹效果。
关键代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = y;
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
if ((yMove - yDown) > 0) {
mMove = yMove - yDown;
i += mMove;
layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
}
break;
case MotionEvent.ACTION_UP:
layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
i = 0;
break;
}
return true;
}
这个时候,布局如下:
<MyLinearLayout>
<item1></item1>
<item2></item2>
<item3></item3>
...
</MyLinearLayout>
接下来,我们实现列表的滚动,我们肯定想到了ScrollerView
,布局如下:
<MyLinearLayout>
<ScrollerView>
<LinearLayout>
<item1></item1>
<item2></item2>
<item3></item3>
...
</LinearLayout>
</ScrollerView>
</MyLinearLayout>
然后,我们运行,发现下拉回弹的效果不见了。外部滑动方向与内部滑动方向一致。父布局MyLinearLayout需要响应竖直方向上的向下滑动,实现下拉回弹,子布局ScrollView也需要响应竖直方向上的上下滑动,实现子View的滚动。当内外两层都在同一个方向上可以滑动的时候,就会出现逻辑问题。因为当手指滑动的时候,系统无法知道用户想让哪一层滑动。所以这种场景下的滑动冲突需要我们手动去解决。
外部拦截法是指点击事件先经过父容器的拦截处理,如果父容器需要处理此事件就进行拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。外部拦截法需要重写父容器的onInterceptTouchEvent()方法,在内部做相应的拦截即可。
我们在move
的时候进行判断,如果手指是向上滑动,onInterceptTouchEvent()
返回false
,表示父布局不拦截当前事件,当前事件交给子View
处理,那么我们的子View
就能滚动;如果手指是向下滑动,onInterceptTouchEvent()
返回true
,表示父布局拦截当前事件,当前事件交给父布局处理,那么我们父布局就能实现下拉回弹。
代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = y;
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
if (yMove - yDown < 0) {
isIntercept = false;
} else if (yMove - yDown > 0) {
isIntercept = true;
}
break;
case MotionEvent.ACTION_UP:
isIntercept = false;
break;
}
return isIntercept;
}
运行之后,没有达到我们的目的,还是由问题:手指向上滑动的时候,子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
滚动到开始的位置,父布局就开始拦截事件,形成连续的滑动。
public class MyScrollView extends ScrollView {
public MyScrollView(Context context) {
this(context, null);
}
public MyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
int scrollY = getScrollY();
if (scrollY == 0) {
//允许父View进行事件拦截
getParent().requestDisallowInterceptTouchEvent(false);
} else {
//禁止父View进行事件拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}
return super.onTouchEvent(ev);
}
}
我们看一下ViewGroup
的dispatchTouchEvent
中的一段代码:
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
而requestDisallowInterceptTouchEvent
就是通过mGroupFlags
来控制disallowIntercept
的值。
当requestDisallowInterceptTouchEvent(true)
,disallowIntercept
就为true
,所以intercepted
为false
,而且onInterceptTouchEvent
也不会被调用。所以:
子view
不希望父ViewGroup
拦截事件可以调用mParent.requestDisallowInterceptTouchEvent(true)
。requestDisallowInterceptTouchEvent
这个函数一般不是自己调用的,而是给儿子调用的。requestDisallowInterceptTouchEvent
主要用来解决滑动冲突。父布局:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean isIntercept = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
if (父容器需要当前事件) {
isIntercept = true;
} else {
isIntercept = false;
}
break;
case MotionEvent.ACTION_UP:
isIntercept = false;
break;
}
mLastX = x;
mLastY = y;
return isIntercept;
}
父布局:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;
default:
return true;
}
}
子布局:
public boolean dispatchTouchEvent(MotionEvent event){
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要当前事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
图解 Android 事件分发机制
一个Demo带你彻底掌握View的滑动冲突
重要的函数requestDisallowInterceptTouchEvent
Android滑动冲突解决方法