一 前言
最近复习到了事件分发这知识点,之前对这个知识点只能说懂一些,能做到简单描述而已。既然最近复习到了,那我就写下来加深印象。接下来好好看,因为是刚学习到的,写的不是很深,看完基本都能懂这是怎么回事的。
二 点击和触摸
首先要明白一点,点击(click)和触摸(touch)是两个不同的事件,之前一直以为两者都一样,之后才知道。简单说下:
①点击是:手指放下屏幕开始直到手指离开屏幕后,click事件才被触发。你可以想一下,有时候你不小心点下屏幕,但是误点不想,你就往其他地方移动,这样手指离开了,事件没被触发,相当于click事件没被触发。
②触摸是:当你手指接触屏幕开始,这个事件就开始触发了,与click不同的是,click一个完整的操作才触发。
总结一点:触摸事件(touch)优先于点击事件(click)触发
三 事件分发流程
事件分发的流程很复杂,反正就是从硬件到软件。在Android里面,为了能简单描述,我只说Activity开始里面的流程。
布局.png
自己不会画图,在网上找了这张图。相信大家都能看懂这张图吧。Activity不用说的,你看到的页面。ViewGroup就是Activity的XML布局里面的父布局,你可以理解成你写的LinearLayout、ConstraintLayout...。View更清楚啦,ViewGroup里面的TextView、ImageView、Button...
好了,话不多说进入正题。刚才说从Activity开始的,我们假设点击一个ConstraintLayout里面包裹的Button时,弹出一条吐司。这个事件的传递顺序是:Activity -> ViewGroup -> View,即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View
大部分之前的做法就是在Activity里面注册这个Button的点击事件,然后再onClick里面写Toast操作,这个大家基本都会的吧。但你只知道是这么写的,但里面的流程怎么走的就不知道了吧?
本来想跟大家分析下源码的,为了简洁点,我只挑重点:
有这么三个东西:
dispatchTouchEvent():事件开始分发,结果表示事件有没有被消费
onInterceptTouchEvent():拦截事件,拦截后就停止往下分发了
onTouchEvent():事件分发的结果,表示有没有被消费,结果回调给dispatchTouchEvent,可以理解成onTouchEvent返回什么,dispatchTouchEvent的结果就是什么
①Activity里面有dispatchTouchEvent()和onTouchEvent()
②ViewGroup里面有dispatchTouchEvent()和onInterceptTouchEvent()
③View里面有dispatchTouchEvent()和onTouchEvent()
接下来我会对这三个部分的事件分发都分析一波😏
四 Activity的事件分发
每次事件分发,都是从Activity里面的dispatchTouchEvent()开始分发的
//Activit.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
分析:第一个判断是,事件是不是DOWN事件(按下),一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true。onUserInteraction()是个空方法,一般情况不做处理,所以走到了第二个判断,getWindow().superDispatchTouchEvent(ev),至于它是啥,等等再讲,若getWindow().superDispatchTouchEvent(ev)的返回true,则Activity.dispatchTouchEvent()就返回true,则方法结束,true表示这个事件被消费,否则,继续往下走,第三个方法onTouchEvent()。里面有个mWindow.shouldCloseOnTouch(this, event)表示:对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等。举个栗子:写过弹窗(dialog)的就是知道,弹窗不是全屏的时候,你的点击弹窗外的地方时,默认情况下弹窗消失,此时mWindow.shouldCloseOnTouch(this, event)返回true,弹窗finish()。一般情况下,Activity.onTouchEvent()返回false,回调,Activity.dispatchTouchEvent()返回false,整个事件没有被消费,分发结束。目前,你只看到了Activity的事件分发,别急~接着往下讲ViewGroup和View的事件分发。
噢对了,刚才还getWindow().superDispatchTouchEvent(ev),还没解释,它是什么呢?看代码就知道,它是Window里面的一个方法,Window是个抽象类,而PhoneWindow就是它的实现类,(这里插入一下其他知识点)每个activity都对应一个窗口window,这个窗口是PhoneWindow的实例,PhoneWindow对应的布局是DecorView,它是一个FrameLayout也是最顶层的View,FrameLayout又是继承ViewGroup,所以最终是进入了ViewGroup的dispatchTouchEvent(),简单点说,getWindow().superDispatchTouchEvent(ev)就是进入ViewGroup判断事件分发。
总结:当一个点击事件发生时,从Activity的事件分发开始
activity事件分发.png
以上就是Activity的事件分发,应该都通俗易懂吧🤭,接下来看ViewGroup的事件分发,就是刚才提及的getWindow().superDispatchTouchEvent(ev)
五 ViewGroup的事件分发
从上面Activity事件分发机制可知,ViewGroup事件分发机制从dispatchTouchEvent()开始,它的dispatchTouchEvent()比Activity的代码量多,为了大家都能听懂所以我只挑重点
//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
... // 仅贴出关键代码
// ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
// 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
// b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 通过for循环,遍历了当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
// 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
// 若是,则进入条件判断内部
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 条件判断的内部调用了该View的dispatchTouchEvent()
// 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
// 调用子View的dispatchTouchEvent后是有返回值的
// 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
// 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
}
}
}
}
}
// 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
// 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
// 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())
// 此处需与上面区别:子View的dispatchTouchEvent()
}
...
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
关键代码比Activity里面多多了😓,我刚开始看的时候一见这么多都不想接着看了,但是都学到这里了就一起看完吧。
分析:
①
首先判断,disallowIntercept || !onInterceptTouchEvent(ev),disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改,所以不做任何操作情况下就是false了,此时进不进入这个判断就要看onInterceptTouchEvent(ev)。刚刚上面有讲到,onInterceptTouchEvent(ev)只在ViewGroup里面出现,这是什么东西呢?看英文就知道“拦截触摸事件”,若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部,若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断。
②
假设onInterceptTouchEvent()不拦截,就是false,取反为true就进入内部,内部又是什么呢?接这看。里面很多字段,大家看得懂和看不懂都没关系,只要知道for循环那里就是遍历当前ViewGroup下的所有子View。好的,取到每个子View后要干嘛呢?对每个子View判断frame.contains(scrolledXInt, scrolledYInt),那两个参数就是点击时的坐标表示,这个判断就是判断当前遍历的View是不是被你点击时选中的,如果是!进入最后一层判断child.dispatchTouchEvent(ev),这个又是什么呢?child表示是ViewGroup的子View,即View.dispatchTouchEvent(),也就是说这里实现了点击事件从ViewGroup到子View的传递,后面会讲到View里面的处理,如child.dispatchTouchEvent(ev)返回true,表明事件被子View消费了,整个ViewGroup.dispatchTouchEvent()返回true,回调到Activity就是Activity.dispatchTouchEvent()返回true,结束事件分发。child.dispatchTouchEvent(ev)返回false相反,返回流程也是一样的,表明事件没被子View消费,结束事件分发。
③
假设onInterceptTouchEvent()拦截了,就是true,取反为false就不进入内部,直接走到了判断条件target == null,target不做处理时为空,进入后返回super.dispatchTouchEvent(ev),这个又是什么呢?再提一个知识点,ViewGroup是继承View的,所以说到底它就是一个View,只不过它里面可以包含很多子View,所以刚才的super.dispatchTouchEvent(ev),就是父类的dispatchTouchEvent(),即View.dispatchTouchEvent()。返回结果跟第②点一样的逻辑,结果都回调给Activity。
④
onInterceptTouchEvent(MotionEvent ev)的默认返回值是false,我们可以复写onInterceptTouchEvent(),从而让其返回true,实现拦截效果,即事件不往子View里面传递,给本身去处理,父类View.dispatchTouchEvent()。
注:②、③最终都是走到View里面的dispatchTouchEvent(),但表现不一样,②是子View的,③是父类的。
至此,假如你能都懂了,那你就对这个事件分发理解了三分之二了😆,想想还是有点激动的。接着往下看最后的View里面事件分发怎么处理的吧。
六 View的事件分发
从上面ViewGroup事件分发机制知道,View事件分发机制从dispatchTouchEvent()开始,你可以理解成,事件分发都会走到这里,事件有没有消费最终都在这里回调出去。
//View.java
public boolean dispatchTouchEvent(MotionEvent event) {
// 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
// 1. mOnTouchListener != null
// 2. (mViewFlags & ENABLED_MASK) == ENABLED
// 3. mOnTouchListener.onTouch(this, event)
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
分析:
①
mOnTouchListener是什么呢?View里面有这个setOnTouchListener方法,类似于注册点击事件一样,我们在给控件注册Touch事件时,就已经给mOnTouchListener赋值了,所以注册后mOnTouchListener不为空。
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
// 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
}
②
(mViewFlags & ENABLED_MASK) == ENABLED,这个很好理解,该条件是判断当前点击的控件是否enable,由于很多View默认enable,故该条件恒定为true。(你也可以给控件把enable属性设置为false,这样就跳出内部了)
③
mOnTouchListener.onTouch(this, event),回调控件注册Touch事件时的onTouch(),已Button为例
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束;若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)
④
View的onTouchEvent(event),重点来了,以上条件中有一个不成立时都走这个方法,源码也是有点多,同样我也只讲重点,先贴出来吧
public boolean onTouchEvent(MotionEvent event) {
···//仅贴出关键代码
// 若该控件可点击,则进入switch判断中
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
// a. 若当前的事件 = 抬起View(主要分析)
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
...// 经过种种判断,此处省略
// 执行performClick()
performClick();
break;
// b. 若当前的事件 = 按下View
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
// c. 若当前的事件 = 结束事件(非人为原因)
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
// d. 若当前的事件 = 滑动View
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
// 若该控件可点击,就一定返回true
return true;
}
// 若该控件不可点击,就一定返回false
return false;
}
首先判断 ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE),这个意思该控件是否能点击,若能,则进入switch判断,若不能直接跳出,最外层返回false,也就是说控件不可点击时,onTouchEvent直接返回false,表明事件没被消费。继续看switch判断,其实就是一些动作判断,什么DOWN啊,UP啊,MOVE啊...里面每个动作的代码可以先不看,你最后看下,不管什么动作进来了,它最后处理完都会返回true,总结一点就是控件可点击时,一定返回true。接下来重点看下UP动作,执行了performClick()。
public boolean performClick() {
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
只要我们通过setOnClickListener()为控件View注册1个点击事件,那么就会给mOnClickListener变量赋值(即不为空),则会往下回调onClick(),onClick()就是你注册点击事件时里面要做的东西,就像开头里面弹吐司的操作。之后 performClick()返回true。
注:onTouch()的执行先于onClick(),也就验证开头说的总结
View事件分发.pngView的事件分发是比其他两个复杂一点,但也不是很难懂,跟着源码走,所有逻辑都能缕清了。
七 总结
Android 的事件分发说难不难,说简单不简单,最重要的是要先从布局开始,然后从源码入手,跟着源码一步步走下去,思路就很清楚啦!结合刚才三个图,用一幅图总结下:
事件分发.png
若您已经看到此处,那么恭喜你,你已经能非常熟悉掌握Android的事件分发机制了
本人也是刚学不久的,写的不好的地方麻烦大伙帮忙指出,大牛轻拍 = =
网友评论