距离上篇文章有半个月,感觉有些荒度,不过这篇文章写下来确实是用了半个月,确实是小白,所以看了好多文章才算是搞懂了事件分发。这篇文章更像是总结,把别人的东西比较清晰易懂的点自己记录下来,再通过自己的实践来了解事件分发的过程,废话不多说,带你看看小白是怎么一步步了解事件分发的。
1. 什么是事件
要了解事件分发,那我们先说说什么是事件,其实这里的事件指的就是点击事件,当用户触摸屏幕的时候,将会产生点击事件(Touch
事件)
Touch
事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent
对象
MotionEvent事件类型
事件类型 | 具体动作 |
---|---|
MotionEvent.ACTION_DOWN | 按下View(所有事件的开始) |
MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
MotionEvent.ACTION_MOVE | 滑动View |
MotionEvent.ACTION_CANCEL | 结束事件(非人为原因) |
事件序列:其实就是从手指触摸屏幕到离开屏幕所发生的一系列事件
2. 什么是事件分发
我们要讲的事件分发其实就是将点击事件传递到某个具体的View
,这个传递的过程就叫做事件分发
3. 事件在哪些对象间进行传递、顺序是什么
Activity
的UI
界面由Activity
、ViewGroup
、View
及其派生类组成
事件分发在这三个对象之间进行传递。
当点击事件发生后,事件先传到Activity
,再传到ViewGroup
,最终传到View
4. 事件分发有啥用?
默认情况下事件分发会按照由Activity
到ViewGroup
再到View
的顺序进行分发,当我们不想View
进行处理,让ViewGroup
处理,那就可以进行拦截,这些知识可以用于解决滑动冲突。
例如:外部滑动和内部滑动方向不一致,当ScrollView
嵌套Fragment
,且Fragemnt
内部有个竖向的ListView
,当用户左右滑动时,要让外部的View
拦截单击事件,当用户上下滑动时,要让内部的View
拦截点击事件。怎么拦截,在哪里拦截,就用到了我们这篇文章所讲的内容了。
5. 事件分发涉及到的函数及相应的作用
方法 | 作用 |
---|---|
dispatchTouchEvent | 进行事件分发 |
onInterceptTouchEvent | 事件拦截 |
onTouchEvent | 事件消耗(就是交给当前View处理) |
- dispatchTouchEvent: 用来进行事件分发,若事件能够传递到当前
View
,则此方法一定会被调用。 - onInterceptTouchEvent: 在
dispatchTouchEvent
方法内被调用,用来判断是否拦截某个事件。若当前View
拦截了某个事件,则该方法不会再被调用,返回结果表示是否拦截当前事件,该方法只在ViewGroup
中存在。 - onTouchEvent: 用来处理点击事件,返回结果表示是否消耗当前事件,若不消耗,则在同一事件序列中,当前
View
无法再次接收到事件。
这三个方法可用以下伪代码表示
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
对应的根ViewGroup
,当一个点击事件产生时,Activity
会传递给它,这时它的dispatchTouchEvent
就会被调用,若该ViewGroup
的onInterceptTouchEvent
返回true
,代表拦截该事件,但是否消耗该事件,还要看它的onTouchEvent
的返回值,如果不拦截,则代表将事件分发下去给子View
,接着子View
的dispatchTouchEvent
方法会被调用,如此反复直到事件被最终处理。
6. Activity的事件分发
当一个点击事件发生时,事件最先传到Activity
的dispatchTouchEvent()
进行事件分发。这里主要要弄明白Activity
是怎么将事件分发到ViewGroup
中的
6.1 Demo演示
我们先看一个案例
(1) 自定义一个MyViewGroup
,继承自ViewGroup
,重写dispatchTouchEvent()
方法
public class MyViewGroup extends ViewGroup{
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "dispatchTouchEvent: ");
//这里我们暂时先返回false
return false;
}
}
(2) 在Activity
的布局中,使用该布局作为最外层布局
<com.ld.eventdispatchdemo.activitydispatch.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/myViewGroup"
android:orientation="vertical"
tools:context=".activitydispatch.ActivityDispatchActivity">
</com.ld.eventdispatchdemo.activitydispatch.MyViewGroup>
(3) 重写该Activity
的dispatchTouchEvent()
和onTouchEvent()
方法,打印log日志
public class Activity extends AppCompatActivity{
...
private static final String TAG = "Activit_activitydispatch";
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(ev.getAction()==MotionEvent.ACTION_DOWN){
Log.i(TAG, "dispatchTouchEvent: ");
}
//这里是仿照源码的格式写的
if(getWindow().superDispatchTouchEvent(ev)){
Log.i(TAG, "dispatchTouchEvent: 这里被调用");
return true;
}
return onTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
}
当MyViewGroup
的dispatchTouchEvent
返回false
时,打印的log日志为:
Activit_activitydispatch: dispatchTouchEvent:
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: onTouchEvent:
Activit_activitydispatch: onTouchEvent:
Activit_activitydispatch: onTouchEvent:
Activit_activitydispatch: onTouchEvent:
Activit_activitydispatch: onTouchEvent:
当MyViewGroup
的dispatchTouchEvent
返回true
时,打印的log日志为:
Activit_activitydispatch: dispatchTouchEvent:
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: dispatchTouchEvent: 这里被调用
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: dispatchTouchEvent: 这里被调用
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: dispatchTouchEvent: 这里被调用
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: dispatchTouchEvent: 这里被调用
仔细观察可以看到,当MyViewGroup的dispatchTouchEvent返回false时,Activity的onTouchEvent会被调用,返回true时,不会被调用,这是什么原因呢?
你可能会有疑问,我的Activity
的dispatchTouchEvent()
方法内为何要这样写呢?别急,看完下面的源码你就知道了。
6.2 源码解析
目的:
1、研究MyViewGroup
的dispatchTouchEvent
返回false
时,Activity
的onTouchEvent
会被调用,返回true
时,不会被调用的原因
Activity
中的dispatchTouchEvent()
源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//该方法为空方法,不用管它
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
有没有很熟悉,为了方便观察打日志所以上面我们重写了Activity
的dispatchTouchEvent
方法,但内容和源码基本一致。
可以从源码看到,当getWindow().superDispatchTouchEvent(ev)==true
时,那么此时return ture
,自然就不会调用底下的onTouchEvent()
方法,即Activity
的onTouchEvent()
。
getWindow()
返回Window
对象,Window
是抽象类,而PhoneWindow
是Window
的唯一实现类,所以getWindow().superDispatchTouchEvent(ev)
其实就是调用的PhoneWindow
内的superDispatchTouchEvent(ev)
方法。
看看PhoneWindow
类源码:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow
将事件直接传递给了DecorView
,接下来看看DecorView
是什么
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
}
mDecor
是getWindow().getDecorView()
返回的View
,通过setContentView
设置的View
是该View
的子View
。
DecorView
继承自FrameLayout(ViewGroup)
,所以mDecor.superDispatchTouchEvent(event)
其实调用的就是ViewGroup
的dispatchTouchEvent()
方法,所以到这里你就懂了吧,
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
其实就相当于下面这个
if(viewgroup.DispatchTouchEvent(ev)){
return true;
}
所以说当我们的MyLayout
的DispatchTouchEvent()
返回true
时,Activity
的onTouchEvent
就不会被调用。
6.3 事件时怎么从Activity分发到ViewGroup中的
从上面Activity
的dispatchTouchEvent
源码可知道,默认状态下,它内部一定会调用该方法,而if()
条件中的内容其实就是调用ViewGroup
的dispatchTouchEvent()
方法,也就是在这里完成了Activity
到ViewGroup
的事件分发。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
6.4 小结:Activity分发的流程图
7. ViewGroup的事件分发
上面讲了Activity
在dispatchTouchEvent
内将事件传递到了ViewGroup
的dispatchTouchEvent()
方法中,那么ViewGroup
又是如何将事件进一步向下分发的呢?
7.1 Demo演示
(1) 自定义MyLayout
,继承自LinearLayout
,重写onInterceptTouchEvent()
方法,并返回true
,重写dispatchTouchEvent()
、onTouchEvent()
方法,打印log日志
public class MyLayout extends LinearLayout {
private static final String TAG = "MyLayout_ViewGroupDispatch";
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(TAG, "onInterceptTouchEvent: ");
//此处暂时返回true观察现象
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
}
(2) 在Activity
的布局中,使用该布局作为最外层布局,并在该布局内添加一个按钮
<com.ld.eventdispatchdemo.viewgroupdispatch.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/myLayout"
tools:context=".viewgroupdispatch.ViewGroupActivity">
<Button
android:id="@+id/btn1"
android:text="Button1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.ld.eventdispatchdemo.viewgroupdispatch.MyLayout>
(3) 在Activity
内为按钮添加点击事件
public class ViewGroupActivity extends AppCompatActivity {
private Button btn1;
private static final String TAG = "Activity_ViewGroupDispatch";
@Override
protected void onCreate(Bundle savedInstanceState) {
btn1 = findViewById(R.id.btn1);
btn1.setOnClickListener(new View.OnClickListener() {
@SuppressLint("LongLogTag")
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 点击了按钮1");
}
});
}
}
当MyLayout
的onInterceptTouchEvent()
返回true
时,分别点击空白处、点击按钮,log日志如下:
//点击空白处
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: onTouchEvent:
//点击按钮
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: onTouchEvent:
当MyLayout
的onInterceptTouchEvent()
返回false
时,分别点击空白处、点击按钮,log日志如下:
//点击空白处
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: onTouchEvent:
//点击按钮
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
Activity_ViewGroupDispatch: onClick: 点击了按钮1
可以看到当ViewGroup(MyLayout)
的onInterceptTouchEvent()
返回true
时,并没有触发按钮的点击事件,并且自身的onTouchEvent()
方法被调用,当返回false
时,按钮的点击事件触发,但自身的onTouchEvent()
方法未被调用。
而且在默认状态下,onInterceptTouchEvent()
一定会被调用。
以上现象是什么原因呢?接下来我们看看ViewGroup的dispatchTouchEvent()
方法的源码
7.2 源码解析
目的:
1、研究当ViewGroup(MyLayout)
的onInterceptTouchEvent()
返回true
时,并没有触发按钮的点击事件,并且自身的onTouchEvent()
方法被调用,当返回false
时,按钮的点击事件触发,但自身的onTouchEvent()
方法未被调用的原因
2、分享事件是怎么从ViewGroup
分发到View
中的
ViewGroup
的dispatchTouchEvent()
源码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
......
//一大堆代码
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
我们想要了解事件是如何分发,其实是主要看ViewGroup
的dispatchTouchEvent()
方法什么时候返回true
,什么时候返回false
。看源码可以知道,ViewGroup
的dispatchTouchEvent()
方法返回的是handle
的值,所以我们只需要观察该方法内改变handle
值的语句。
首先初始化了handle
的值,默认为false
然后你可以看到dispatchTouchEvent()
的大部分内容都在if (onFilterTouchEventForSecurity(ev)) {}
这个条件判断内,也就是说如果onFilterTouchEventForSecurity(ev)
方法返回true
的话,那么就进入该if
判断内。若返回false
,则dispatchTouchEvent()
返回初始值为false
的handled
,表示不分发事件。
查看下onFilterTouchEventForSecurity(ev)
方法
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
FILTER_TOUCHES_WHEN_OBSCURED
是android:filterTouchesWhenObscured
属性所对应的。android:filterTouchesWhenObscured
是true
的话,则表示其他视图在该视图之上,导致该视图被隐藏时,该视图就不再响应触摸事件。MotionEvent.FLAG_WINDOW_IS_OBSCURED
为true
的话,则表示该视图的窗口是被隐藏的
而我们并没有在XML
中为控件设置android:filterTouchesWhenObscured
属性,所以它==0,没有进入if()
方法,所以onFilterTouchEventForSecurity()
方法返回true
,那么if (onFilterTouchEventForSecurity(ev))
判断必定会进入如下的判断中。
接下来我们看看if(onFilterTouchEventForSecurity(ev))
判断下的内容
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
......
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
可以看到,若为ACTION_DOWN
事件,就会触发 cancelAndClearTouchTargets(ev)
和resetTouchState()
方法,在resetTouchState()
方法中,有一个clearTouchTargets()
方法,而在 clearTouchTargets()
方法内会将mFirstTouchTarget
设置为null
。我们暂时先记住这个mFristTouchTarget
已经置为null
了。
我们再看if (onFilterTouchEventForSecurity(ev)){}
该判断内的其他代码
if (onFilterTouchEventForSecurity(ev)) {
......
//记录是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//判断是否设置了FLAG_DISALLOW_INTERCEPT这个标记位,设置了为true,否则为false
//disallowIntercept代表禁止拦截判断
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;
}
......
}
前面我们知道了mFirstTouchTarget
为null
,所以说只要是ACTION_DOWN
事件,就会进入到if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {}
该方法块内,因为我们没有设置FLAG_DISALLOW_INTERCEPT
属性,所以它为false
,所以进入到了if (!disallowIntercept) {}
方法块内,调用了onInterceptTouchEvent()
方法。这里就解释了为何默认情况下dispatchTouchEvent()
后会调用onInterceptTouchEvent()
方法。
接下来我们看下if (onFilterTouchEventForSecurity(ev)) {}
方法块其余部分源码
if (onFilterTouchEventForSecurity(ev)) {
......
if (!canceled && !intercepted) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判断子元素是否能够接受点击事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//调用子元素的dispatchTouchEvent方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
在上一个方法块中,intercepted=onInterceptTouchEvent()
的返回值,当不拦截的时候,intercepted==false
,进入到if (!canceled && !intercepted) {}
方法块内。可以看到我们通过for
循环遍历了所有的子元素,然后判断子元素是否能够接收到点击事件。判断子元素是否能够接收点击事件由两点决定:1、canViewReceivePointerEvents(child)
判断点击事件坐标是否落在子元素的区域内,2、isTransformedTouchPointInView(x, y, child, null)
判断子元素是否在播放动画。
所以说当onInterceptTouchEvent()
返回false
时,触发了点击事件,返回true
时没有触发。
若满足这两个条件,则事件传递给它处理。有一个为ture
则不会进入到该if (!canceled && !intercepted) {}
方法块内,而是执行下面的代码。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}
这个方法块,dispatchTransformedTouchEvent()
其实是调用子元素的dispatchTouchEvent()
方法,dispatchTransformedTouchEvent()
源码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
......
if (child == null) {
//child为null,调用父类的dispatchTouchEvent方法,ViewGroup父类为View,所以是调用View的dispatchTouchEvent方法。
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
//child不为null
handled = child.dispatchTouchEvent(transformedEvent);
}
......
}
可以看到当child
不为null
时,调用child.dispatchTouchEvent(transformedEvent)
,完成了从ViewGroup
到View
的事件分发。
7.3 事件怎么从ViewGroup分发到View中的
在上面的源码中,ViewGroup
的dispatchTouchEvent
方法内,当onInterceptTouchEvent
返回false
时,会调用dispatchTransformedTouchEvent()
方法,而该方法内会调用View
的dispatchTouchEvent
,在这里实现了事件从ViewGroup
到View
的事件分发。
7.4 小结:ViewGroup分发的流程图
8. View的事件分发
8.1 Demo演示
(1) 自定义一个MyButton
,继承自Button
,重写dispatchTouchEvent()
和onTouchEvent()
方法并打印日志
public class MyButton extends AppCompatButton {
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouchEvent: ACTION_DOWN");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
(2) 在Activity
布局中,放入该控件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".viewdiapatch.ViewDispatchActivity">
<com.ld.eventdispatchdemo.viewdiapatch.MyButton
android:id="@+id/btn_click"
android:text="view的点击事件分发"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"/>
</LinearLayout>
(3) 在Activity
内为按钮添加点击事件和touch
事件
public class ViewDispatchActivity extends AppCompatActivity {
private static final String TAG = "Activity_viewDispatch";
private Button btnClick;
@Override
protected void onCreate(Bundle savedInstanceState) {
btnClick = findViewById(R.id.btn_click);
btnClick.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: ");
}
});
btnClick.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouch: ");
break;
}
//这里暂时先返回false查看日志
return false; // 返回false,onTouchEvent会被调用
}
});
}
}
当按钮的onTouch()
方法的返回值为false
时,打印的log日志为:
MyButton_viewDispatch: dispatchTouchEvent:
Activity_viewDispatch: onTouch:
MyButton_viewDispatch: onTouchEvent: ACTION_DOWN
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
Activity_viewDispatch: onClick:
当按钮的onTouch()
方法的返回值为true
时,打印的log日志为:
MyButton_viewDispatch: dispatchTouchEvent:
Activity_viewDispatch: onTouch:
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
可以看到当View
的onTouch()
方法返回false
时,View
的onTouchEvent()
方法和onClick()
方法会被调用,当返回true
时,这两个方法都不会被调用。这是什么原因呢?
8.2 源码解析
目的:
1、想得知为何View
的onTouch()
返回false
时,它的onTouchEvent()
和onClick()
方法会被调用,而返回false
时都不会被调用。
我们看View
的dispatchTouchEvent()
方法源码
public boolean dispatchTouchEvent(MotionEvent event) {
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
}
(mViewFlags & ENABLED_MASK) == ENABLED
代表控件enable
,li.mOnTouchListener
代表其设置的OnTouchListener
,当我们为View
通过setOnTouchListener()
方法设置touch
监听事件时,li.mOnTouchListener
就不为空。li.mOnTouchListener.onTouch(this, event)
代表onTouch()
方法的返回值。
所以说当我们设置了onTouch
监听事件并返回false
时,源码这里的result=false
,所以if(!result&&onTouchEvent(event))
内的onTouchEvent
方法会被调用。
当onTouch()
返回true
时,if(!result&&onTouchEvent(event))内!result==false
,所以后面的onTouchEvent()
方法不会被调用。
onTouchEvent()
不被调用的时候,onClick()
也不会被调用,他俩可能有关系,我们看下onTouchEvent()
的源码
public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
......
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
break;
return true;
}
return false;
}
由其中的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
可以看到,只要View
的clickable
和longclickable
有一个为true
,那么clickable
就会为true
。然后会进入到switch
语句中,在经过各种判断后会执行到performClickInternal()
方法,而该方法源码为以下内容
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
可以看到调用了performClick()
方法,接下来看它的源码
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
可以看到li.mOnClickListener.onClick(this);
,调用了click
方法,所以说onCLick()
方法在onTouchEvent()
方法内被调用,onTouchEvent
不被执行,那么onCLick
一定不会执行。
8.3 小结:View分发的流程图
9. 一个U形图解释
10. 总结
其实如果只是想逻辑的话也很好理解。
dispatchTouchEvent
代表分发事件,onInterceptTouchEvent()
代表拦截事件,onTouchEvent()
代表消耗事件,由自己处理。
默认状态下事件是按照从Activity
到ViewGroup
再到View
的顺序进行分发的,分发下去处不处理是另一回事,分发完成后,不处理则向上一层回调,调用上一层的onTouchEvent
进行处理事件,若onTouchEvent
返回true
,则表示在该层消耗了事件,若返回false
,表示事件还没被处理,需要再向上回调一层,调用上一层的onTouchEvent
方法。
以上就是全部内容,第一次分析源码,有错误的地方还望指出,参考文章大神写得也很详细,一定要看看。
多说一句,看源码确实很头大,尤其在看不懂却还要看到这么多文字就更耐不下心来了,但是当我看到一篇文章下的一个评论时,确实是激励了我,不懂就多读,没有解决不了的问题!