每日一问:一个ViewGroup中,按住一个View,然后移动,事件传递过程

802 阅读11分钟

解决这个具体的问题需要了解触摸事件的传递过程,如何从屏幕一层一层的传递过来。

前言: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的区域之内)。