学习资料:
- Android开发群英传
- Android开发艺术 探索
- 安卓自定义View进阶-事件分发机制原理
- 郭神的Android事件分发机制完全解析,带你从源码的角度彻底理解
个人理解:
View
的事件体系主要包含两个方面:触控事件和滑动事件
触控事件主要学习:MotionEvent
,事件分发拦截机制
滑动事件主要学习:Velocity
速度追踪,GestureDector
手势检测,Scorller
滑动对象
本篇主要学习触控事件,下篇进行学习滑动事件
1.View的事件分发拦截 <p>
在View的测量方法学习大概了解了UI架构图
。
在Activity
的onCreate()
方法中调用了setContentVie(int myLayoutId)
,一些列方法回调之后,布局文件中的各种控件就添加到了ContentView
,而ContentView
则包含在Activity
的根View
也就是DecorView
。
View
的触控事件的整个过程可以分为: 事件传递,事件拦截,事件处理
View
的测量过程是从外向内,由最外层DecorView
开始,而事件分发也是由最外层DecorView
开始。通常情况下最外层DecorView
并不做管理,而是直接开始考虑Activity
1.1 MotionEvent 触摸事件 <p>
手指接触屏幕的一些列事件就封装在MotionEvent
,典型的事件:
- ACTION_DOWN 手指刚接触屏幕
- ACTION_MOVE 手指在屏幕滑动
- ACTION_UP 手指离开屏幕
手指触摸屏幕的坐标可以通过getX/Y()
和getRawX/Y()
方法拿到
getX()
拿到的是相对于自身左上角的x
坐标,getRawX()
相对于屏幕左上角的x
坐标
1.2 点击事件的分发拦截 <p>
点击事件的分发有3个重要的方法:
public boolean dispachTouchEvent(MotionEvent event)
返回结果表示是否拦截当前事件。返回true
,拦截;false
,不拦截
事件分发的第一步,当事件传递到当前View
一定会调用。返回结果受此View
的onTouchEvent()
方法和下级childView
的dispachTouchEvent
影响。虽然是事件分发第一步,但绝多数情况不推荐直接修改这个方法public boolean onIntercepTouchEvent(MotionEvent event)
返回结果用来判断是否拦截某个事件。
如果当前view
拦截了某个事件,在同一个事件的序列中,此方法便不会被再次调用public boolean onTouchEvent(MotionEvent event)
返回结果表示是否消费了事件。true
,消费了,不用在审核了;false
,不消费,给父容器处理
一段伪码:
public boolean diapatchTouchEvent(MotionEvent event){
boolean consume = false;
//判断是否拦截
if(onIntercetTouchEvent(ev)){ //拦截
consume = onTouchEvent(ev);//消费事件
}else{
consume = child.dispatchTouchEvent(ev);//chileView开始事件分发
}
return consume; //返回事件拦截结果 默认为false
}
对于一个根ViewGroup(A)
,点击事件产生后,事件会先传递给A
,首先会调A
的dispatchTouchEvent()
。
在这个dispatchTouchEvent()
方法内部,调用A.onIenterceptTouchEvvent()
,并对这个方法的返回值使用if()
进行判断:
-
onIenterceptTouchEvvent()方法
返回结果为true
时,就表示A
要拦截当前事件,接着A
的onTouchEvent()
方法就会被调用 -
onIenterceptTouchEvvent()方法
返回结果为false
时,表示A
不拦截当前事件,这时便会childView
调用事件分发的第一步dispatchTouchEvent()
方法,如此反复,直到事件被消费掉
传递顺序:
Acticty -> Window -> ViewGroup -> View
消费顺序:
Acticty <- Window <- ViewGroup <- View
注意:
当一个View(V)
需要修理一个事件时,当V
设置了onTouchListener()
时,onTouchListener()
的onTouch()
就会被回调。事件具体会如何处理,要看onTouch()
的返回值
-
onTouch()
返回true
,可以理解为onTouchListener
消费了事件,便不会传递给onTouchEvent()
-
onTouch()
返回false
,可以理解为onTouchListener
不消费事件,传递给onTouchEvent()
来处理
在onTouchEvent()
方法中,只有V
设置了onClickListener()
时,onClick()
才会被回调
结论1:onTouchListener()优先级比onTouchEvent()高,onClickListener()优先级比onToucnEvent()低
当一个事件由Activity(A)
经过ViewGroup(VG)
传递到了一个View(V)
时,如果V.onTouchEvent()
方法不处理,返回false
时,VG
也不做处理,VG.onTouchEvent()
方法也返回false
,这个事件最终也便交给了A.onTouchEvent()
方法来处理。
截图来自GcsSloop安卓自定义View进阶-事件分发机制原理
2.类生活举例 <p>
下面用一些不恰当的实例来演示事件分发拦截的过程
2.1 情景1 <p>
演示一个日常:老板派发工作,给经理提出需求,经理给组长分发任务,组长再给程序员安排任务
-
Activity
:老板 -
VG_Manager
: 经理 -
VG_GroupLoader
:组长 -
V_Programmer
:程序员
经理代码:
public class VG_Manager extends LinearLayout {
private final String TAG = "英勇青铜5";
private Paint mPaint;
public VG_Manager(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(70f);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText("经理",30f,getHeight()-80f,mPaint);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 经理接到老板发的任务通知");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 经理拦截任务,查看任务通知");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "&&&VG_Manager.onTouchEvent --->经理把自己的任务做了");
return super.onTouchEvent(event);
}
}
组长的代码和经理几乎一摸一样
程序员代码:
public class V_Programmer extends View {
private final String TAG = "英勇青铜5";
private Paint mPaint;
public V_Programmer(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(70f);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText("程序员",30f,getHeight()-50f,mPaint);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "&&&V_Programmer.dispatchTouchEvent ---> 程序员接到组长发的任务通知");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "&&&V_Programmer.onTouchEvent ---> 程序员把自己的任务做完");
return super.onTouchEvent(event);
}
}
差别就在于View
没有onInterceptTouchEvent()
方法
xml代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.szlk.customview.eventl.VG_Manager
android:layout_width="270dp"
android:layout_height="480dp"
android:background="@color/colorPrimary">
<com.szlk.customview.eventl.VG_GroupLoader
android:layout_width="180dp"
android:layout_height="320dp"
android:background="@color/colorAccent">
<com.szlk.customview.eventl.V_Programmer
android:layout_width="90dp"
android:layout_height="160dp"
android:background="@android:color/holo_orange_dark" />
</com.szlk.customview.eventl.VG_GroupLoader>
</com.szlk.customview.eventl.VG_Manager>
</RelativeLayout>
运行效果也很简单
点击程序员
,查看Log
信息
箭头的方向大致就是一个事件的走向
2.2 情景2 <p>
有一天的老板的电脑突然出问题了,老板重启电脑,问题没有解决,老板于是便找来了经理,经理听了老板的描述后,觉得自己能搞定,于是便自己尝试解决问题,没有去找组长
简单修改经理的代码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 被老板喊来修电脑");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 听了老板描述问题后,决定自己先给老板修一下");
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "&&&VG_Manager.onTouchEvent --->经理把电脑修好了");
return super.onTouchEvent(event);
}
主要是将onInterceptTouchEvent()
方法返回值改true
,也就是将事件拦截下来
点击经理
,查看Log
信息:
组长onInterceptTouchEvent()
返回true
和上图就类似了
上面的情况是假设经理会修,如果经理不会修,经理喊来了组长,组长看了老板的电脑后觉得,必须把程序员同学喊来了,于是他们两个都没有拦截事件,把任务安排给了程序员同学,程序员同学到了老板的办公室,给老板直接重装了系统。程序员同学修理好后,觉得并没有必要向组长和经理进行汇报,就把这件事给over
掉了,也就是V_Programmer.onTouchEvent()
返回true
修改代码:
经理代码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 被老板喊来修电脑");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 听了老板描述问题后,喊组长过来");
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "&&&VG_Manager.onTouchEvent --->经理把组长喊来,任务完成");
return false;
}
组长代码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "&&&VG_GroupLoader.dispatchTouchEvent ---> 组长被经理喊来给老板修电脑");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "&&&VG_GroupLoader.onInterceptTouchEvent ---> 组长觉得老板的电脑问题太大,喊来程序员");
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "&&&VG_GroupLoader.onTouchEvent ---> 组长安排给程序员,任务完成");
Log.e(TAG, "&&&VG_GroupLoader.onTouchEvent ---> 默认"+super.onTouchEvent(event));
return false;
}
程序员的代码:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "&&&V_Programmer.dispatchTouchEvent ---> 程序员被喊来给老板修电脑");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "&&&V_Programmer.onTouchEvent ---> 程序员给老板重装系统,修好,奖金15元");
return true;
}
再次点击程序员
,Log
信息
这里我出了点问题,实际打印结果将上面的Log信息完整重复打印了3遍,有时候4遍,我查找了一会也没查出来为啥,有知道我哪里出错的同学,请告诉我 : )
2016.11.09补充
打印3,4
次的原因知道了,因为没有对事件类型进行判断,MotionEvent
中并不是只有DOWN
一个事件,加上类型判断就会只打印一次了
此时的事件流程图
3.一些结论 <p>
这些结论摘自Android开发艺术探索
- 同一个序列从手指落在屏幕开始,以
down
事件开始,中间数量不定的move
事件,最终以up
事件结束,整个过程都是一个事件 - 一般,一个事件只能由一个
View
消费 - 一个
View(V)
对事件进行了拦截,该事件只能由这个V
来消费 - 某个
View(V)
(不是ViewGroup
)一旦开始处理事件,如果它不消费ACTION_DOWN
事件,也就是onTouchEvent()
返回false
,那么同一事件序列的其他事件都不会交给这个V
消费,并且事件将重新交给V
的父容器的onTouchEvent()
进行消费 - 某个
View(V)
不消耗除ACTION_DOWN
以外的其他事件,这个点击事件便会消失,此时父元素的onTouchEvent()
不会被调用,并且V
可以持续收到后续事件,最终这些消失的点击事件会传递给Activity
处理。ps:这条并不理解到底啥意思 -
ViewGroup(VG)
默认不会拦截任何事件,VG.onInterceptTouchEvent()
默认返回false
-
View(V)
没有onIntercepTouchEvent()
,一旦点击事件传递给V
,V.onTouchEvent()
便会消费这个事件 -
View(V)
的onTouchEvent()
默认都会消耗事件,返回true
(ps:这里有疑问,我测试返回为false
)。除非V
是不可不可点击的(clickable
和longClickable
同时为false
)。V
的的longClickable
默认都为false
,clickable
要看控件,Button
的clickable
为true
,TextView
为false
。 -
View(V)
的enable
属性不影响onTouchEvent
的默认返回值。哪怕V
是disable
状态,只要V
的clickable
或者longClickable
有一个返回为true
,V
的onTouchEvent
就返回true
-
onClicck
进行回调前提是View
是可以点击的,并且收到了down
和up
事件 - 事件的传递是由外向内的,事件总是先传递给父容器,然后父容器向下传递。通过
requestDisallowInterceptTouchEvent
方法,可以在childView
中干预父容器事件的分发过程,但ACTION_DOWN
事件除外
这里后面几条并不是很理解。事件分发的源码,也就在Android开发艺术探索
中大体看了看
4.最后 <p>
又是十一,记得去年十一找同学开始学习Android
做一些小的Demo
,经过一年的学习,感觉也算入门了 : )
国庆快乐
本人很菜,有错误,请指出
共勉 : )