解决这个具体的问题需要了解触摸事件的传递过程,如何从屏幕一层一层的传递过来。
前言:Android的很多实现是基于C/S架构的设计思想。Android的Framework层可以理解为server,应用层可以理解为client,Framework为应用层提供服务,此谓之C/S架构。而两端之间的跨进程通信又有各种实现方式。感兴趣可以自己根据源码和一些参考资料看具体的实现。
说回事件的传递,也是从底层传递到上层的,其中有庞大复杂的实现,最好的学习方式就是带着自己的问题一边一边学习源码,理解源码的设计思想,才能更好的理解事件传递流程。输入事件传递流程可以大致的分为三个部分,分别是输入系统部分、WMS处理部分和View处理部分。下面分别对这几个部分进行简单的介绍。
1.事件在底层的采集
输入设备创建结点,写入输入信息:
输入系统由输入子系统和InputManagerService部分组成,其中输入子系统为触发事件的输入设备,Android中这些设备可以为屏幕,物理按键,外接鼠标,外接键盘,手柄等等。当输入设备被检测可用的时候,Linux驱动会在内核空间的dev/input路径下创建对应的结点(Linux的核心是万物皆文件,所谓创建结点,就是在dev/input路径下创建对应的文件,以供设备写入事件信息)。
EventHub获取驱动结点信息:
EventHub监听设备结点的创建删除来判断是否接入/移除输入设备,通过Linux的epoll机制实现监听是否有输入事件信息写入到结点文件中。(我个人的理解是epoll机制有任务唤醒无任务则阻塞,这是Linux实现机制,感兴趣可以深入学习)
InputManager轮询取出EventHub中的事件信息:
InputManager中创建了InputReader和InputDispatcher,其中InputReader会不断的循环读取EventHub中的原始输入事件,将这些原始输入事件进行加工后交由InputDispatcher,InputDispatcher中保存了WMS中的所有的Window信息(WMS会将窗口的信息实时的更新到InputDispatcher中),这样InputDispatcher就可以将输入事件派发给合适的Window。由于InputDispatcher和InputReader都是耗时的,因此单独创建了InputDispatcherThread和InputReaderThread。
WindowManagerService将消息通知到上层的ViewRootImpl:
WMS持有IMS的引用,IMS中的事件分发器InputDispatcher通过共享内存的方式(并非Binder)与上层Client通信,上层告知底层是否消费了事件,底层通知上层有新的事件。
2.事件在上层的分发
ViewRootImpl类中在setView()方法:
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
其中InputState是ViewRootImpl中的内部类,是事件传递链式调用某一阶段,该阶段可以选择完成活动或将其转发到下一个阶段。也就是说每个state中进行不同的处理逻辑,可以理解为一个processer。
在ViewPostImeInputStage 的回调方法中
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
调用 processKeyEvent(q) ##ViewRootIml
--->mView.dispatchPointerEvent(event) ##View
--->disPatchTouchEvent(event);##DecorView
--->mWindow.getCallbak.dispathTouchEvent();##DecorView
--->getWindow().superDispatchTouchEvent(ev) ##Activity
--->mDecor.superDispatchTouchEvent(event);##PhoneWindow
--->dispatchTouchEvent()##ViewGroup { if(interceptTouchEvent()){ onTouchEvent() }else{ childView.dispatchTouchEvent(); } } 上述流程为事件在上层应用层中的传递。
先简单介绍一下activity启动时候ActivityThread,Activity,ViewRootImpl,PhoneWindow和DectorView的初始化流程。有助于帮助理解上述堆栈的调用顺序。
ActivityThread中调用performLaunchActivity(),在这个方法中获取package info和component info并通过反射的方式初始化需要启动的Activity,完成后调用Activity的attach()方法,在Activity的attach()方法中初始化了1.PhoneWindow(Window的唯一实现类)2.PhoneWindow设置callback(该Activity)。之后ActivityThread调用Activity生命周期方法(对应Activity中的生命周期回调函数),首先是onCreate()方法,在onCreate()方法中会调用setContentView()--->getWindow().setContentView(){if(ContentParent == null) {初始化DecorView}},也就是说在onCreate()方法中初始化了通过PhoneWindow初始化了DecorView。然后是onResume(),调用了WindowManager的addView():wm.addView(),因为WindowManager是接口且实现了ViewManager接口,addView()方法的实现在WindowManagerImpl,WindowManagerImpl中持有WindowManagerGlobal对象,业务逻辑由WindowManagerGlobal实现,在addView()中调用了mGlobal.addView(),其中对ViewRootImp进行了初始化,并调用了ViewRootImpl的setView(),其中传入了Activity的DectorView以及Window,之后触发layout,measure,draw等流程
看完事件的传递流程以后回到问题本身,在viewgroup中,按住一个view,然后移动,事件是如何传递的?
按照上述流程可知,在这个问题中,硬件产生触屏事件 -> Native(EventHub、InputReader/Dispatcher、InputManager) -> IMS --采集完成--> ViewRootImpl -> DecorView -> PhoneWindow -> 事件传递到Activity -> PhoneWindow -> DecorView -> 这段流程是不会变化的,那么变化的就是ViewGroup事件传递,抽象流程可理解为下面的伪代码:
dispatchTouchEvent()
{
if(interceptTouchEvent()){
onTouchEvent()
}else{
childView.dispatchTouchEvent();
}
} 那么按住一个View,Down事件触发---会调用viewGroup的dispatchTouchEvent(),调用ViewGroup的interceptTouchEvent()(默认不拦截返回false),如果ViewGroup没有拦截此事件序列,因为被按住的View是ViewGroup的子view,此时会调用此view的dispatchtouchevent(),因为view没有interceptTouchEvent(),所以会直接调用onTouchEvent()。接下来移动,移动的话还是会走之前的流程,当超出子view的区域的时候,ViewGroup的interceptTouchEvent()方法会返回true,意味着父类接手当前事件序列的其他事件。
答案在view的OnTouchEnvent()方法中
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
//先判断标示位是否为disable,也就是无法处理事件。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}//如果是UP事件,并且状态为按压,取消按压。
//系统源码解释:虽然是disable,但是还是可以消费掉触摸事件,只是不触发任何click或者longclick事件。
//根据是否可点击,可长按来决定是否消费点击事件。
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
//先检查触摸的代理对象是否存在,如果存在,就交由代理对象处理。
// 触摸代理对象是可以进行设置的,一般用于当我们手指在某个View上,而让另外一个View响应事件,另外一个View就是该View的事件代理对象。
if (mTouchDelegate.onTouchEvent(event)) {//如果代理对象消费了,则返回true消费该事件
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//如果是可点击或者长按的标识位执行下面的逻辑,这些标志位可以设置,也可以设置了对应的listener后自动添加
//因为作为一个View,它只能单纯的接受处理点击事件,像滑动之类的复杂事件普通View是不具备的。
switch (event.getAction()) {
case MotionEvent.ACTION_UP://处理Up事件
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;//是否包含临时按压状态
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {//如果本身处于被按压状态或者临时按压状态
//临时按压状态会在下面的Move事件中说明
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
//如果它可以获取焦点,并且可以通过触摸来获取焦点,并且现在不是焦点,则请求获取焦点,因为一个被按压的View理论上应该获取焦点
focusTaken = requestFocus();
}
if (prepressed) {
//如果是临时按压,则设置为按压状态,PFLAG_PREPRESSED是一个非常短暂的状态,用于在某些时候短时间内表示Pressed状态,但不需要绘制
setPressed(true);//设置为按压状态,是因为临时按压不会绘制,这个时候强制绘制一次,确保用户能够看见按压状态
}
if (!mHasPerformedLongPress) {
//是否执行了长按事件,还没有的话,这个时候可以移除长按的回调了,因为UP都已经触发,说明从按下到UP的时间不足以触发longPress
//至于longPress,会在Down事件中说明
removeLongPressCallback();
if (!focusTaken) {//如果是焦点状态,就不会触摸click,这是为什么呢?因为焦点状态一般是交给按键处理的,
//pressed状态才是交给触摸处理,如果它是焦点,那么它的click事件应该由按键来触发
if (mPerformClick == null) { //封装一个Runnable对象,这个对象中实际就调用了performClick();
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {//向消息队列发生该runnabel,如果发送不成功,则直接执行该方法。
performClick();//这个方法内部会调用clickListner
}
//为什么不直接执行呢?如果这个时候直接执行,UP事件还没执行完,发送post,可以保障在这个代码块执行完毕之后才执行
}
}
if (mUnsetPressedState == null) {//仍旧是创建一个Runnabel对象,执行setPressed(false)
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
//如果是临时按压状态,之前的Down和move都还未触发按压状态,只在up时设置了,这个状态才刚刚绘制,为了保证用户能看到,发生一个64秒的延迟消息,来取消按压状态。 postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
//这是一个64毫秒的短暂时间,这是为了让这个按压状态持续一小段时间,以便手指离开时候,还能看见View的按压状态
} else if (!post(mUnsetPressedState)) {//如果不是临时按压,则直接发送,发送失败,则直接执行
mUnsetPressedState.run();
}
removeTapCallback();
//移除这个callBack,这个callBack内部就是把临时按压状态设置成按压状态,因为这个已经没必要了,手指已经up了
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
//按下事件把长按事件执行的变量设置为false,代表还没执行长按,因为才按下,表示新的一个长按事件可以开始计算了
if (performButtonActionOnTouchDown(event)) {
//先把这个事件交由该方法,该方法内部会判断是否为上下文的菜单按钮,或者是否为鼠标右键,如果是就弹出上下文菜单。
//现在有些手机的上下文菜单按钮也是在屏幕触屏上的
break;
}
//这个方法会一直往上找父View,判断自身是否在一个可以滚动的容器中
boolean isInScrollingContainer = isInScrollingContainer();
//如果是在一个滚动的容器中,那么按压事件将会被推迟一段时间,如果这段时间内,发生了Move,那么按压状态讲不会被显示,直接滚动父视图
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED; //先添加临时的按压状态,该状态表示按压,但不会绘制
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
//创建一个runnable对象,这个runnable内部会取消临时按压状态,设置为按压状态,并启动长按的延迟事件
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
//向消息机制发生一个64毫秒的延迟时间,该事件会取消临时按压状态,设置为直接按压,并启动长按时间的计时
} else {
//如果不在一个滚动的容器中,则直接设置按压状态,并启动长按计时
setPressed(true);
checkForLongClick(0);
//长按事件就是向消息机制发送一个runnable对象,封装的就是我们在lisner中的代码,延迟500毫秒执行,也就是说长按事件在我们按下的时候发送,在up的时候检查一下执行了吗?如果没执行,就取消,并执行click
}
break;
case MotionEvent.ACTION_CANCEL: //如果是取消事件,那就好办了,把我们之前发送的几个延迟runnable对象给取消掉
setPressed(false); //设置为非按压状态
removeTapCallback(); //取消mPendingCheckForTap,也就是不用再把临时按压设置为按压了
removeLongPressCallback(); //取消长按事件的延迟回调
break;
case MotionEvent.ACTION_MOVE: //move事件
final int x = (int) event.getX(); //取触摸点坐标
final int y = (int) event.getY();
// 用于判断是否在View中,为什么还要判断呢?
//这是因为父View是在Down事件中判断是否在该View中的,如果在,以后的Move和up都会传递过来,不再进行范围判断
if (!pointInView(x, y, mTouchSlop)) {
//mTouchSlop是一个常量,数值为8,也就是说,就算你的落点超出了View的8像素位置,也算在View中。
//是因为人的手指触摸点比较大,有可能你感觉点在某个控件的边缘,但是实际落点已经超出这个View,所以这里给了8像素的范围
removeTapCallback();//如果在范围外,就移除这些runnable回调
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
//如果是按压状态,就取消长按,设置为非按压状态,为什么这个时候取消呢,因为在Down的时候,我们可以知道,只有是按压状态,才会设置长按
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true; //至此,可以返回true,消费该事件
}
return false; //如果不可点击,也不可长按,则返回false,因为View只具备消费点击事件
}
在move event传递到来的时候会进行view的验证(8像素之外也算view的区域之内)。