Android触控事件分析

96
zyq_neuq
2016.12.22 15:26 字数 4158

Android触控事件分析(基于Android4.1),此文是早期写的,虽然现在android已经到了7.0了,但核心逻辑差别不大。

阅读建议:

第一节阅读之后建议看一下设置-开发者选项中的指针位置绘制的源代码。

第二节和第三节建议参照源代码来进行阅读,只需理解处理流程和设计思路就行。

一、 触控事件上层(MotionEvent和状态机)

Android是个好东西,很人性化,透明度很高,对于我们想修改的都留下了接口。

一般当我们需要处理触摸事件时有两种方式:

Ø 委托式 : 将事件委托给监听器来进行处理。即定义一个View.onTouchListener()子类的监听器,由其onTouch()方法来处理。

Ø 回调式 : 通过重写View类自己的onTouchEvent()方法来处理,在执行时会回调该方法,在其中执行自定义的代码。

关键看一下参数中的MotionEvent。

1. MotionEvent 类

有人说要学好Android触控,必须先学好MotionEvent,那么下面我们就来简单学习下该类。


Paste_Image.png

InputEvent就不多说了,此为抽象类,是所有输入设备对象的抽象集。有空可以看一下NativeFramework中该类起的作用。

问题: 每个触控事件触发的回调参数均为一个MotionEvent对象,而目前都是支持多点触控,每个点又有那么多特性,一个对象是如何表示的呢?

先来看一下下面几个定义和方法。


Paste_Image.png

Paste_Image.png

Paste_Image.png

注:nativeGetAction(mNativePtr)就是Action的类型,即原始的。 该mNativePtr是JNI中的一个指针,无需考虑。Android2.2即将该方法描述为mAction。此处也这样叫。

仔细查看定义和注释有什么新发现没?

当mAction为0x00到0xff之间时getAction()返回值和getActionMasked()是一样的。这是什么原因,是因为此时只有单点触控。 那么有多点触控时会怎么样,那肯定是mAction值大于0xff了。我们知道多点触控,肯定带有每个点的索引信息了,那么Android是如何设计的呢。mAction的低八位用来表示动作类型,即0x00~0xff,高八位用来表示触控点索引信息。

所以看


Paste_Image.png

即为mAction & 0xff00 >> 8 ,就是mAction的高八位值。

下面在看一下ACTION动作事件定义:在MotionEvent.java中

ACTION_DOWN = 0;

ACTION_UP = 1;

ACTION_MOVE = 2;

ACTION_CANCEL =3 ;

ACTION_POINTER_DOWN = 5; //A non-primary pointer has gone down.

ACTION_POINTER_UP = 6;

ACTION_SCROLL = 8; //the most event contains relative vertical and/or horizontal scroll offset.

术语:
主触点,副触点:发送触屏事件的时候,除了此触屏事件所对应的触点之外,如果当前触点多于一个或者等于一个,则此事件为副触点事件,发送此事件的触点叫做副触点。否则为主触点事件,发送此事件的触点为主触点。-----摘自《手势识别详细设计.doc》

另一个注意点:getX(),getY(),getRawX(),getRawY().getX()表示获取View的触摸坐标。 GetRawX()表示相对屏幕上的位置。

2. MotionEvent对象事件处理

我们常见的触摸事件除了按下,弹起,移动之外还有很多, 诸如长按,双击,Scroll,Fling等,他们是怎么判断的,还有这些长按,双击等事件的时间能否自由设置。下面我们来分析下如何产生这些事件及如何自定义处理时间。

前面说过应用处理触摸事件的两种方式,其实Android源码中的view或特殊控件都是使用这两种方式来进行处理的。

下面看一下View.java对onTouchEvent()的处理。

此处只分析大概处理流程,不抠具体细节。

case MotionEvent.ACTION_DOWN:

    // a short period in case this is a scroll.

    if (isInScrollingContainer) {

        mPrivateFlags |= PFLAG_PREPRESSED;

        if (mPendingCheckForTap == null) {

            mPendingCheckForTap = new CheckForTap();

        }

        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

    } else {

        setPressed(true);

        checkForLongClick(0);

    }

break;

事件响应是先有按下才会有后续事件。因此先查看ACTION_DOWN。

  • 在此case中判断如果是在scrollingContainer中则等待180ms执行检查是否为Tap事件。因为可能按下之后可能会有scroll操作,如果有将丢弃长按检测。

  • 而如果不在container中,则立即执行长按检测。

     private final class CheckForTap implements Runnable {
    
         public void run() {
    
             mPrivateFlags &= ~PFLAG_PREPRESSED;
    
             setPressed(true);
    
             checkForLongClick(ViewConfiguration.getTapTimeout());
    
         }
    
    }

    在其中执行了setPressed()操作。 其后执行checkForLongClick().

    postDelayed(mPendingCheckForLongPress,
    
          ViewConfiguration.getLongPressTimeout() - delayOffset);

    即等待500ms-180ms 来执行longPress操作。

在其中执行performLongClick().在该函数中处理长按需要做的事情,如

     handled = li.mOnLongClickListener.onLongClick(View.this);

    handled = showContextMenu();

performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);

长按监听器中流程,显示contextMenu, 处理长按震动反馈。

注意: 此处有两个时间数据: tapTimeout 和 longPressTimeout.

我们可以查看ViewConfiguration类:

private static final int TAP_TIMEOUT = 180;

    private static final int DOUBLE_TAP_TIMEOUT = 300;

private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500;

所以这就回答了上面那个问题,时间是可以自定义的,但最好采用google提供的, 这是经过大量积累得来的数据。而此处的longTimeout是设置辅助功能界面中’触摸和按住延迟’选项可设置的,如果没有设置那就是用默认的,500ms。

case MotionEvent.ACTION_MOVE:

如果有move操作即移除Tap和LongPress回调。

case MotionEvent.ACTION_UP:

如果不是长按操作,则执行performClick();--播放声音,处理点击。

其实所有事件的处理都是一个状态机过程,大家如有兴趣可以看下面几个文件对onTouchEvent的处理。

ListView.java  即AbsListView.java

ScrollView.java

Android还提供了部分单点和多点检测, GestureDetector, ScaleGestureDetector.

NUBIA 还自定义了很多手势检测,ZTEGesture*.

此处看一下GestureDetector为我们提供了哪些触控事件:

OnGestureListener:

onDown()

onShowPress();//当用户按触屏幕,并且在抬起或移动手指之前触发

onSingleTap();

onScroll();//用户按下手指并且匀速移动手指后,在抬起手指前调用.--dragging

onLongPress();

onFling();//在用户按下并且加速移动手指后,在抬起手指前调用. --- flick.

OnDoubleTapListener:

onSingleTapConfirmed();

onDoubleTap();//双击(double-tap)事件发生时调用

onDoubleTapEvent();//在任何双击手势发生时调用,包括按下(down),移动,或抬起(up)MotionEvent.

用户可继承适配器SimpleOnGestureDetector来处理需要处理的事件。

下图为两点双击事件状态机 -----摘自《手势识别详细设计.doc》


Paste_Image.png

3. MotionEvent 底层事件获取

看过了上层的处理,我们来看下面数据是如何获取的。
JNI只是对应了一个转调用,而事实上Android在NativeFramework中有一个真正的MotionEvent对象,用来存储触摸事件的信息,包括PointerCoords和PointerProperties。


Paste_Image.png

在该文件中有对MotionEvent类的定义, 而我们发现该类也只是一个转发作用,我们查看构造函数,发现对上层提供接口的所有数据均来自此类的初始化函数,那么到底在什么地方创建的该对象,怎么赋值的。想知道,估计你还得看第二节。

二、触控事件分发机制

在阅读本节之前可以先看看一篇博文,http://blog.csdn.net/myarrow/article/details/7091061, 讲解了一部分,是基于Android4.0的。与4.1有些差异,但不是很大。

该从什么地方说起呢?好难啊。 先想一下吧, 任何触摸事件表现形式均在顶层view上,而该view是依赖于activity的。话说在onResume时会将view显示出来,那就从这里开始。

在执行时会调用ActivityThread的handleResumeActivity(),


Paste_Image.png

看到没,获取window的DecorView,即整个window的顶层View。调用流程为:WindowManager.addView(), 在实现类WindowManagerImpl中实现addView(),最后一行通过root.setView()。在ViewRootImpl中实现setView(),在其中调用windowSession.add()。 windowSession为客户端,而服务器端为Session.java,在Session中转而调用WindowManagerService的addWindow()来实现add方法。

下面我们来分析一下WindowManagerService中addWindow都做了哪些事情。其实这里实现了事件信息传递和交互的通道,内部采用socketpair,通过InputChannel来实现。

有兴趣可以看一下openInputChannelPair(), 在其中创建socketpair,一个匿名的已连接套接字,一个为发送端,一个为接收端,可以进行双工通讯。(参考UNIX网络编程,注意和PIPE的区别,下面第三节中有使用到PIPE)


Paste_Image.png

获取InputChannel, 一个置为Input,一个置为output。RegisterInputChannel中调用nativeRegisterInputChannel。而这个outInputChannel哪里来的呢, 往回看,发现是在ViewRootImpl中创建,然后做了这样的处理。


Paste_Image.png

Paste_Image.png

这里有待继续分析…..涉及到View的知识,可以参考下面的InputConsumer来分析。

到这里我们就来看看到底事件是如何分发了:

看看InputManagerService.java , 在WindowManagerService中创建InputManagerService类对象,并start。参考JNI流程分析,在native中执行


Paste_Image.png

并执行InputManager的start方法。

看一下InputManager 类


Paste_Image.png

此处仔细看, 对应的一个reader和readerThread, dispatcher和dispatcherThread。

看一下构造函数和start吧。


Paste_Image.png

Paste_Image.png

Paste_Image.png

创建读取线程和分发线程,并使其run起来。

此时再看这个


Paste_Image.png

实质是调用:


Paste_Image.png

Paste_Image.png

此处的inputChannel 就是上面定义的inputchannel[0]。创建Connection,并将其和fd和对应的connection加入数组中。

Connection:


Paste_Image.png

Connection就是一个对应关系表, 将fd, InputChannel对应起来。

看看上面InputManager和start方法:


Paste_Image.png

这种设计模式叫?忘了,不管了。关注一下在创建InputReader 时 将dispatcher传入。

即InputReader的成员变量mQueuedListener为dispatcher的执行者,具体代码分析flush函数,关注Args,例如MotionArgs, flush执行后,将调用dispatcher->notifyMotion();

看一下start函数后的结果。

  1. InputReader :调用InputReaderThread::ThradLoop(),调用InputReader::loopOnce(),在其中有重要的三步,我们分析一下:

Paste_Image.png

Paste_Image.png

Paste_Image.png

第一步要想理解,估计你得看第三节了,主要是从驱动设备节点中读取事件信息。

第三部估计你已经清楚了,如果只关注Motion的话,那么就是调用InputDispatcher->notifyMotion().一般为用来进行强制刷新的作用吧?

下面我们来仔细分析一下第二步:

看一下读取的mEventBuffer为一个数组, 在其中调用processEventsDeviceLocked();

---à InputDevice::Process();-à mapper->process();此时注意mapper的初始化,我们只关注多点触控,即MultiTouchInputMapper -> process()。对每一个RawEvent执行process操作。


Paste_Image.png

由于是逐一处理RawEvent,所以在TouchInputMapper::process()中最终会执行sync操作。

那么我们先来看看MultiTouchMotionAccumulator.process(); 此处用了一个Slot,将所有的RawEvent数据信息放入Slot数组中。

看sync, TouchInputMapper::sync(),在其中调用syncTouch().看syncTouch的实现者,只有MultiTouchInputMapper::syncTouch().在其中将slot中的值又逐个赋给mCurrentRawPointerData。而在sync之中,又有


Paste_Image.png

将raw变成cooked的(命名很有意思,把生的变成熟的),并执行dispatchTouch,

并在其中dispatchMotion()并传入相关数据。

而在此处将执行getListener()->notifyMotion(&args).即InputDispatcher->notifyMotion().

2. InputDispatcher :

调用InputDispatcher::dispatchOnce() -à mLooper->pollOnce();

此时我们就需要关注两个队列了。mInBoundQueue, 和 outBoundQueue.

先看mInBoundQueue:

上面InputReader的结果是调用InputDispatcher::notifyMotion(),看一下里面做了什么事


Paste_Image.png

入队列。

在看InputDispatcher::dispatchOnce() 中的代码,调用dispatchOnceInnerLocked并执行

在没有pendingEvent时出队列、


Paste_Image.png

并执行dispatchMotionLocked().-> dispatchEventLocked()


Paste_Image.png

在该函数中做了一些split操作, 最后执行Enqueue,注意入的队列已经变了。

看outBoundQueue:

在上面enuque操作中执行enqueueDispatchEntryLocked,


Paste_Image.png

此时我们看mLooper->pollOnce(), 他和上层的looper是不一样的。转调用pollInner,内部实现为epoll调用。 (和select类似的调用,不懂可以研究一下linux系统编程,这里主要用于对该fd的监测,即相关的Connection,outboundQueue。)。

此时回过头来看看前面的registerInputChannel()。在add connection到mConnectionByFd之后


Paste_Image.png

我们关注一下hanleReceiveCallback函数在InputDispatcher中, 调用

finishDispatchCycleLocked(),

-> onDispatchCycleFinishLocked()

-> doDispatchCycleFinishedLockedInterruptible()

->startDispatchCycleLocked()->

Paste_Image.png

而在publisMotionEvent中执行如下操作:mChannel->sendMessage();


Paste_Image.png

而我们知道该channel即为之前传递的channel[0],此为socketpair的一端,那么输出端肯定是channel[1]。而输出端是在ViewRootImpl中传入的,我们要理解输出端获取和解析数据还有很长的路程。

此处就有两个类:InputPublisher 和 InputConsumer。

刚才看到InputPublisher 是发送端, 那么InputConsumer是接收端,在InputConsumer::consume()函数中执行如下:


Paste_Image.png

下面的涉及到View的一些东西,没有仔细研究,再次简单提及一下。

上面有提到inputChannel[1]即那个outInputChannel, 涉及到InputQueue,又是一个队列.在ViewRootImpl中处理,最后可能是调用InputEventReceiver的consumeBatchedInputEvents(),

这个方法调用JNI代码,调用receiver->consumeEvents(),在其中执行


Paste_Image.png

即上面所说的接受socket的数据。

此时,基本已经明了, 你可以继续分析ViewRootImpl.java, 在该文件中会处理调用上面的consumeBatchedInputEvents, 最终指向的地方就是View.onTouchEvent().将我们需要的数据传递上来,代码可以自己跟踪。下图为网上找到类图,类图继承的箭头方向画错了,但不影响理解,与4.1的变化不大,部分细节自己查看,如4.1没有了gNativeInputManager,而是放在java中,用一个指针来保存等:


Paste_Image.png

Android系统真是复杂, 在input事件的读取和分发上下了大工夫, 这里面不仅仅是触控的,还有各种其他输入的处理。它使用了一个读取器, 然后又有一个分发器, 在分发过程中做了大量的处理,例如丢弃,分离,解决阻塞等等,在分发过程中又使用了两个队列,队列1从reader入,出到队列2,队列2从队列1入,出时以socket形式进行发送,作为生产者,而在window中,socket接收端又以消费者的身份出现,对该事件进行消费,然后进一步解析传递处理,最终传入我们常见的onTouchEvent中。如下图:


Paste_Image.png

流程图: 磁盘: Channel[1]流程图: 磁盘: Channel[0]圆角矩形: outBoundQueue圆角矩形: mInBoundQueue

那么上面我们还遗留了一个问题,就是InputReader中的getEvent()操作,看下第三节吧。

三、 触控事件读取

上一节我们知道了所有数据的来源都来自于getEvent方法的这个参数RawEvent* buffer, 下面我们分析下EventHub.cpp。看看都干了什么事,看构造函数。(这里用到了epoll和pipe操作,可以参考linux编程)。


Paste_Image.png

创建并加入到mEpollFd中。


Paste_Image.png

Paste_Image.png

再看getEvent()操作:


Paste_Image.png

,很明显,将buffer地址赋给了event,下面我们只需要关注event的数据就ok,只要它有了数据,就可以进行上报了。

在关注一下构造函数中将mNeddToScanDevices = true, 所以调用流程如下:

在for循环中第一次时肯定会先走scanDevicesLocked(). ->


Paste_Image.png

看一下DEVICE_PATH = “/dev/input”;

好, 我们知道所有的input事件都是来自于这个目录, 看下你自己的手机:


Paste_Image.png

这么多,总计12个(NUBIA NX50X),在看一下哪个是触摸事件的event


Paste_Image.png

好, 我们知道了/dev/input/event1 是多点触摸屏的。所有触摸数据都是该设备驱动文件进行上报的。(有兴趣也可以看下其他的,例如传感器,power键等,回车可以查看具体上报数据的值)

继续回归代码,看ScanDirLocked中是怎么干的,在里面遍历一遍这个目录,并打开每一个event,执行openDeviceLocked(),在里面对链表mOpeningDevices 赋值。


Paste_Image.png

Paste_Image.png

并将其加入到epoll中


Paste_Image.png

在这些执行完之后将对应的event插入链表的头部,并加入到数组中。

此时在回来看getEvent函数, mOpeningDevices 已经不为NULL了,即进入while循环,对每个event进一步附加时间,顺序等信息。

给下继续看,在epoll_wait执行后mPendingEventItems,和mPendingEventCount将会被赋值,且会进入下面循环:


Paste_Image.png

关注我们前面加入的fd,即读取管道,


Paste_Image.png

读取完成之后就开始读取对应的device的


Paste_Image.png

读取之后将readBuffer格式化,并赋值给event对象。而该event对象也就是返回值。

至此,读取驱动的流程就全部完成了。

开发技能