4.3 触屏事件
之前讲的是按键的整体处理流程,并以物理按键为例讲解了物理按键如何被activity处理,事件是对应于界面的,对应触屏事件,点击是如何对应到控件的呢,本章将在这个点上描述,并进行扩展。
4.3.1 事件流程
4.3.1.1 触屏事件流程
同样,为了便于分析问题,我们使用一个调用栈,这里使用拨号盘应用里面点击一个数字按钮的调用栈:
DialActivity$16.onTouch(View, MotionEvent) line: 3499
ImageButton(View).dispatchTouchEvent(MotionEvent) line: 7701
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
SlidingDrawer(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
SlidingDrawer(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
FrameLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
CallKey(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
CallKey(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
FrameLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
FrameLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
PhoneWindow$DecorView(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216
PhoneWindow$DecorView(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
PhoneWindow$DecorView.superDispatchTouchEvent(MotionEvent) line: 2076
PhoneWindow.superDispatchTouchEvent(MotionEvent) line: 1515
DialActivity(Activity).dispatchTouchEvent(MotionEvent) line: 2471
PhoneWindow$DecorView.dispatchTouchEvent(MotionEvent) line: 2024
PhoneWindow$DecorView(View).dispatchPointerEvent(MotionEvent) line: 7886
ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl$QueuedInputEvent) line: 3952
ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl$QueuedInputEvent) line: 3831
ViewRootImpl$ViewPostImeInputStage(ViewRootImpl$InputStage).deliver(ViewRootImpl$QueuedInputEvent) line: 3396
ViewRootImpl$NativePostImeInputStage(ViewRootImpl$InputStage).onDeliverToNext(ViewRootImpl$QueuedInputEvent) line: 3446
ViewRootImpl$NativePostImeInputStage(ViewRootImpl$InputStage).forward(ViewRootImpl$QueuedInputEvent) line: 3415
ViewRootImpl$NativePostImeInputStage(ViewRootImpl$AsyncInputStage).forward(ViewRootImpl$QueuedInputEvent) line: 3522
ViewRootImpl$NativePostImeInputStage(ViewRootImpl$InputStage).apply(ViewRootImpl$QueuedInputEvent, int) line: 3423
ViewRootImpl$NativePostImeInputStage(ViewRootImpl$AsyncInputStage).apply(ViewRootImpl$QueuedInputEvent, int) line: 3579
ViewRootImpl$NativePostImeInputStage(ViewRootImpl$InputStage).deliver(ViewRootImpl$QueuedInputEvent) line: 3396
ViewRootImpl$EarlyPostImeInputStage(ViewRootImpl$InputStage).onDeliverToNext(ViewRootImpl$QueuedInputEvent) line: 3446
ViewRootImpl$EarlyPostImeInputStage(ViewRootImpl$InputStage).forward(ViewRootImpl$QueuedInputEvent) line: 3415
ViewRootImpl$EarlyPostImeInputStage(ViewRootImpl$InputStage).apply(ViewRootImpl$QueuedInputEvent, int) line: 3423
ViewRootImpl$EarlyPostImeInputStage(ViewRootImpl$InputStage).deliver(ViewRootImpl$QueuedInputEvent) line: 3396
ViewRootImpl.deliverInputEvent(ViewRootImpl$QueuedInputEvent) line: 5540
ViewRootImpl.doProcessInputEvents() line: 5520
ViewRootImpl.enqueueInputEvent(InputEvent, InputEventReceiver, int, boolean) line: 5491
ViewRootImpl$WindowInputEventReceiver.onInputEvent(InputEvent) line: 5620
ViewRootImpl$WindowInputEventReceiver(InputEventReceiver).dispatchInputEvent(int, InputEvent) line: 185
MessageQueue.nativePollOnce(int, int) line: not available [native method]
MessageQueue.next() line: 138
Looper.loop() line: 123
ActivityThread.main(String[]) line: 5241
Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]
Method.invoke(Object, Object...) line: 515
ZygoteInit$MethodAndArgsCaller.run() line: 818
ZygoteInit.main(String[]) line: 634
NativeStart.main(String[]) line: not available [native method]
从栈中我们截取有代表性的流程节点如下,
DialActivity$16.onTouch(View, MotionEvent) line: 3499
ImageButton(View).dispatchTouchEvent(MotionEvent) line: 7701
FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
PhoneWindow$DecorView(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917
DialActivity(Activity).dispatchTouchEvent(MotionEvent) line: 2471
PhoneWindow$DecorView.dispatchTouchEvent(MotionEvent) line: 2024
PhoneWindow$DecorView(View).dispatchPointerEvent(MotionEvent) line: 7886
ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl$QueuedInputEvent) line: 3952
在这里我们不再关注底层的实现,从应用开始识别到触屏事件开始分析,其大致流程如下图,
如上图所示,事件经过ViewPostImeInputStage的processPointerEvent,到View. dispatchPointerEvent,根据DecorView重写的dispatchTouchEvent方法,到Activity的实例引用,使用方法Activity.dispatchTouchEvent,将事件先传递到界面上。
再根据管理到的窗口,让PhoneWindow窗口处理事件,处理事件的方法是superDispatchTouchEvent,其内部主要有两个分支,先判断是否要处理OnTouchListener,根据结果判断是否继续处理OnClickListener,如果dispatchTouchEvent内部能正常处理事件,就消耗掉事件,如果不能,就要将事件传递下去。
如果事件能传递下来,则使用Activity.onTouchEvent继续做一些边界判断等处理,最终完成本View对事件的处理。
4.3.1.2 触屏流程代码分析
如前所述,在应用层,事件的处理通道如下,
对于触屏产生的MotionEvent事件,通过JNI传递到应用后,在通道内,会传递给ViewPostImeInputStage,这里会根据事件的类型,使用相应的处理函数,对于物理按键,使用processKeyEvent,这个流程我们前面讨论过;对于触屏类的输入设备,则会使用processPointerEvent,我们现在讨论这个流程;对于轨迹球,使用processTrackballEvent;其他情况使用processGenericMotionEvent。
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
// If delivering a new non-key event, make sure the window is
// now allowed to start updating.
handleDispatchDoneAnimating();
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);
}
}
}
processPointerEvent会调用mView. dispatchPointerEvent(),而mView正是DecorView的实例,这点之前的章节有分析过,故而事件就此传递给PhoneWindow的DecorView,实际上代码实现在view.java类里面,它判断是否是Touch事件,是的话就分发这个事件dispatchTouchEvent,
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
dispatchTouchEvent()的执行代码在哪里实现的,这点比较重要,在不同的环境下,其执行过程不一样,它依赖于类的重写方式和相关变量的初始值。一般看代码都会默认的看到view.java的dispatchTouchEvent(),并依此进行分析,
public boolean dispatchTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
其实不然,在本例中,DecorView间接继承了View,重写了dispatchTouchEvent(其继承关系为DecorView-FrameLayout-ViewGroup-View,只有DecorView重写了dispatchTouchEvent),
DecorView:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Callback cb = getCallback();
return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)
: super.dispatchTouchEvent(ev);
}
可以看到,如果DecorView的callback不为空,才执行View里面的方法,对于在activity界面上的触屏事件,这里对应的callback就是activity的实例,如果Activity的界面子类又重写了dispatchTouchEvent,就执行界面Activity的dispatchTouchEvent,没有就执行基类Activity的dispatchTouchEvent,本例中就是执行Activity的dispatchTouchEvent,
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如上,Activity的dispatchTouchEvent在处理事件时有几个分支,先交付给PhoneWindow.superDispatchTouchEvent(MotionEvent)处理,如果返回true,则代表事件被PhoneWindow 处理完成,消耗掉该事件,否则,还要继续使用onTouchEvent处理。
onTouchEvent处理的情况是:
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
就是说触点落在窗口之外,或未被任何控件接收到的时候。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
在PhoneWindow.superDispatchTouchEvent里,进一步调用了DecorView的superDispatchTouchEvent,它又使用了父类ViewGroup的dispatchTouchEvent,其代码如下,
…
ViewGroup的dispatchTouchEvent过程比较复杂,之后再详细分析其实现过程,我们先看事件的主要处理流程,它会再调用到dispatchTransformedTouchEvent,这个方法会根据传入的view的属性和相关变量,调用view实例类的dispatchTouchEvent方法,因为ViewGroup的嵌套关系,所以产生了很长的调用栈。这些linearLayout、frameLayout、slidingDrawer及自定义的类,都是使用的ViewGroup的dispatchTouchEvent方法。
这个事件在viewGroup树中分发后,最终会到具体的控件上面,本例就是数字按钮对应的ImageButton控件,其继承自imageView—View,实际调用View的dispatchTouchEvent方法,
public boolean dispatchTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
在本例中,由于界面Activity注册了OnTouchListener,所以执行ontouch()方法,完成事件的处理,并消耗掉该事件。Ontouch()方法是用户自定义的,完成用户期望点击控件后的行为,不在本文讨论之内。
如果界面Activity没有注册OnTouchListener监听器,就会执行View的onTouchEvent()方法,从其注释来看,
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}
就是说onTouchEvent这个方法是处理onClick()事件的,这里就是让很多人困惑的onClick和onTouch的地方了,从上面的逻辑看来,就是如果实现了onTouch,在onTouch返回true就消耗掉了事件,onClick得不到执行。简单来理解就是在事件处理流程中,onTouch比onClick先执行,如果onTouch消耗掉事件,onClick就得不到执行。
该执行哪个类的onTouchEvent,依赖于子类是否重写了方法,例如textView有进行重写,它会先执行父类的onTouchEvent,再继续执行自己的处理。
onTouchEvent的实现如下,
public boolean onTouchEvent(MotionEvent event) {
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
…
case MotionEvent.ACTION_DOWN:
…
Return true;
}
Return false;
}
在这个方法中,处理到了TouchDelegate,这个是针对让控件有更大点击区域而改善用户体验的一个辅助类,有兴趣的同学可以自行研究。
接下来会根据控件的状态和按键的状态处理事件,这个方法能处理四个按键行为,DOWN、UP、CANCEL 、MOVE,我们现分析两个常见的按键DOWN、UP。
ACTION_DOWN的处理代码如下,主要是清除长按处理的flag,按钮菜单的处理,滚动容器类的处理,
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// 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 {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
一般流程是直接走到checkForLongClick,在checkForLongClick方法里面,是对长按的处理,和普通短按流程关系不大,所以对于触屏短按事件,如果流程能走到这里,就走完了,该返回true消耗掉这个事件了。(也可以总结出对于触屏事件,Ontouch可以携带事件是否按下的状态,而onClick是没有这种状态的,它只在UP事件中产生)。
长按流程后续会使用专门篇幅讨论,在此不详述。
分析完上面流程后,有个问题需要思考,如果一个控件需要同时支持短按和长按,该如何设计监听函数?其执行流程又是怎样?
MotionEvent.ACTION_UP事件处理主要代码如下,
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress) {
removeLongPressCallback();
…
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
…
它主要完成的工作有设置view的press状态,清除长按相关变量、消息等状态,并Post一个消息出去,完成Click监听器的事件处理。Post比直接调用的好处在于,Post的同时就可以更新View的外观变化,而直接调用需要等到Click被执行后才能看到。
至此,一个简单的按键分析流程就分析完成了。
在按键分析中我们还有很多知识点没有分析到,需要在后面不断学习,如组合按键的产生、手势、长按的设计流程,还有一些细节的东西,如控件单击后的背景属性的变化流程等。
4.3.1.3 OnClickListener代码分析
应用可以使用SetOnClickListener()给一个View控件注册监听器,其实现在View里面,实际就是给View实例的成员类ListenerInfo的成员mOnClickListener赋值,
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
OnClickListener只是一个接口,定义如下,
public interface OnClickListener {
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
对于注册的OnClickListener监听器,不能想当然的根据代码分析,任务认为他们的会在onTouchEvent()方法里面执行,为了明白他们的执行时机,我们在一个Activity上定义一个按钮,再注册OnClickListener,点击按钮的时候会得到如下的调用栈,可以看出OnClickListener走了一条完全不同的执行过程,所以分析代码一定要结合调试,否则会误入歧途,
DialActivity$15.onClick(View) line: 3484
ImageButton(View).performClick() line: 4240
View$PerformClick.run() line: 17721
Handler.handleCallback(Message) line: 730
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 92
Looper.loop() line: 137
ActivityThread.main(String[]) line: 5265
Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]
Method.invoke(Object, Object...) line: 525
ZygoteInit$MethodAndArgsCaller.run() line: 760
ZygoteInit.main(String[]) line: 576
NativeStart.main(String[]) line: not available [native method]
从调用栈可以看出,OnClick()的执行调用源头是Handler.dispatchMessage(),事件是发送给ViewRootImpl的ViewRootHandler实例的,最终调用Handler的handleCallback,根据我们在另外的文章分析的Handler的三种消息处理方式中了解到,这个消息是在应用层Post出来的,而不是HAL层通过JNI方式上传过来。现在我们有两个方向分析问题,一个是当前消息事件向上的分发处理,一个方向是Post消息的源头。
1)向上:OnClick的执行过程
handleCallback(msg)处理的消息在生成的时候,会有Callback和run方法,所以这种方式就是直接调用run方法,
private static void handleCallback(Message message) {
message.callback.run();
}
本例的callback是一个runnable类PerformClick,它的实现主体是
private final class PerformClick implements Runnable {
public void run() {
performClick();
}
}
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
return true;
}
return false;
}
从代码里就很好理解了,如果View注册了OnClickListener,则执行其onClick方法,到此流程就结束了。
另外,根据代码分析,OnClickListener还有另外一种调用方法,即通过调用callOnClick()来执行onClick,有兴趣的同学可单独分析。
2)向下:谁Post了这个消息
因为我们点击一个控件View的时候,是物理事件,现在onClick处理的是一个间接的应用层消息,我们还要分析物理按键引发的事件和这个消息的关联。
这个在前面分析View的onTouchEvent方法时有讲到,在处理MotionEvent.ACTION_UP事件时,会Post这个消息,进而触发OnClickListener的执行。
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress) {
removeLongPressCallback();
…
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
…