浅谈Android之Activity触摸事件传输机制介绍
来源:互联网 发布:淘宝750图片怎么设置 编辑:程序博客网 时间:2024/05/14 21:01
8 Activity触摸事件传输机制介绍
当我们触摸屏幕的时候,程序会收到对应的触摸事件,这个事件是在app端去读取的吗?肯定不是,如果app能读取,那会乱套的,所以app不会有这个权限,系统按键的读取以及分发都是通过WindowManagerService来完成
在WMS中,它的管理单位是WindowState,当你点击屏幕时,它会根据Z-Order顺序找到top & focus WindowState来handle这个事件,然后再跨进程传给App端对应的Window, App端Window对应的代码主体就是ViewRootImpl
接下去我们来看看ViewRootImpl是如何接收WMS发来的事件以及发送到对应的Décor view的
8.1 触摸事件数据如何跨进程传输
触摸事件从WMS跨进程传给App端,跨进程通讯方式采用的是基于socketpair双工通讯,可能大家会问,Android进程间通讯不都是基于Binder来传输的吗?为什么不用binder?
让我们回顾下Binder的优势:
1) 支持RPC,也就是说我们可以很方便实现复杂的数据交互指令
2) 内存拷贝次数会比socket等方式少
但是考虑到触摸事件数据是非常小的,而且就是简单的数据传输,不需要RPC操作,这个时候如果采用binder,基本上就不存在什么优势,可能效率还赶不上socketpair双工通讯,因为数据量太小,内存拷贝次数减少的优势基本可以忽略,而且支持RPC还需要额外的开销
还有更重要的原因,就是socket Pair返回的是file descriptor,这样就意味着,可以共用主线程Looper对其file descriptor的状态进行监听,具体细节下面会介绍
socketPair是基于c++的,Android实现InputChannel对这部分代码进行封装
创建过程如下:
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
openInputChannelPair最终会调用native函数,通过jni调用c++代码创建socketpair,然后基于新创建的socket file descriptor对来创建InputChannel对并返回到Java层,由于
InputChannel是parcelable的,接下去只需要把其中一个InputChannel通过Binder传到另一进程,另外进程拿到后,二者就可以基于InputChannel进行数据共享了
但是InputChannel只是利用JNI基于C++对file descriptor和数据的读取操作进行封装,所以还需要封装一个类用于对InputChannel 对应的filedescriptor 进行监听,并在
file descriptor ready的时候,及时触发InputChannel的读取操作并将数据通过JNI回调到Java层(发送在WMS端,如果扩展开,篇幅太大,这边就不做介绍了)
这个类是WindowInputEventReceiver,它派生自InputEventReceiver,详细的下面介绍
接着看ViewRootImpl.setView中相关代码:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
……
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mInputChannel);
} catch (RemoteException e) {
……
} finally {
if (restore) {
attrs.restore();
}
}
……
if (mInputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
……
// Set up the input pipeline.
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
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);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
}
}
}
首先调用new InputChannel创建InputChannel对象并保存到mInputChannel,不过目前这个对象是不包含实质的File Descriptor的
接着调用addToDisplay传入mInputChannel,WMS在创建WindowSate的同时会对应的创建InputChannelPair,然后将其中一个InputChannel保存到mInputChannel返回,至此,
mInputChannel才真正包含了跟WMS中对应WindowState中保存的InputChannel相关联的File Descriptor
接下去调用:
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());
WindowInputEventReceiver构造时传入两个参数,一个是InputChannel,另外一个是当前线程,也就是主线程的Looper
InputChannel包含要监听的File descriptor,那Looper当然就是用于File descriptor的监听了,
Looper原理是在构造的时候,创建epoll和pipe描述符,并且通过对pipe描述符的监听以及设置监听超时时间来触发从messagequeue中获取queue item数据并回调到handler,所以,通过Looper来监听并触发InputChannel读取没任何问题(详细可以看Looper的源码,这里不做过多介绍了)
那还有个疑问,为什么要用主线程的Looper,而不是新创建一个呢?大家记得ANR吗?
当你连续触摸屏幕的时候,如果按键事件超过一定时间没被处理,系统会弹出对话框,显示App无响应
翻译成代码逻辑就是,WMS往App当前显示的窗口对应的WindowState中包含的InputChannel写入了按键数据,然后由于App端的Looper在处理当次回调时存在耗时操作,从而导致Looper的下一次pollOnce被延后执行,进而导致App过晚监听到InputChannel的file descriptor的状态改变,影响了对InputChannel中按键数据的及时读取并下发
这就是采用主线程Looper的原因
数据读取到后,最终会通过Jni回调到Java类InputEventReceiver的如下函数:
@SuppressWarnings("unused")
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event);
}
接着调用WindowInputEventReceiver的onInputEvent
最后用一句话总结下,WMS端通过WindowState关联的InputChannel发送按键数据后,App端的ViewRootImpl内的WindowInputEventReceiver实例对应的onInputEvent函数会被回调,参数即为按键数据
8.2 App收到触摸事件如何传到DecorView
在App端收到onInputEvent后,接下去数据的传递就是按函数顺序调用,接着直接基于代码来分析:
//WindowInputEventReceiver
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
接着调用enqueueInputEvent:
//ViewRootImpl
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
……
QueuedInputEvent last = mPendingInputEventTail;
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
……
if (processImmediately) {
doProcessInputEvents();
} else {
scheduleProcessInputEvents();
}
}
将新event添加到InputEvent queue,由于processImmediately为true,接着调用
doProcessInputEvents:
//ViewRootImpl
void doProcessInputEvents() {
while (mPendingInputEventHead != null) {
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
if (mPendingInputEventHead == null) {
mPendingInputEventTail = null;
}
q.mNext = null;
mPendingInputEventCount -= 1;
......
deliverInputEvent(q);
}
……
}
循环从Input event queue中取出event然后调用deliverInputEvent:
//ViewRootImpl
private void deliverInputEvent(QueuedInputEvent q) {
……
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
if (stage != null) {
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
先介绍下InputStage,其实它是一个Input process chain,每一个InputStage构造时,需要传入其next process InputStage,接着在每一个InputStage的deliver被调用时,都有权决定是否要将event继续传递给next input stage
mFirstPostImeInputStage和mFirstInputStage这两个InputStagechain的是在setView函数执行到最后创建的,详细的可以回过头看上面的setView代码,这两个chain的last InputStage都是ViewPostImeInputStage,这里我们先忽略IME相关的一大堆InputStage,假定Input Event最终都传递到了ViewPostImeInputStage,接着看其onProcess函数:
// ViewPostImeInputStage
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:
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
……
boolean handled = mView.dispatchPointerEvent(event);
……
return handled ? FINISH_HANDLED : FORWARD;
}
直接调用Décor View的dispatchPointerEvent,由于Décor View以及ViewGroup未实现该函数,所以默认跑到View.dispatchPointerEvent:
//View
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
接着调用DecorView的dispatchTouchEvent:
//PhoneWindow.DecorView
public boolean dispatchTouchEvent(MotionEvent ev) {
final Callback cb = getCallback();
return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)
: super.dispatchTouchEvent(ev);
}
先拿到PhoneWindow设置的callback,之前在介绍Activity初始化的时候说过,
在Activity.attach时创建PhoneWindow的时候会将Activity设置为PhoneWindow的callback,所以这里的cb肯定不为null,接着调用Activity的dispatchTouchEvent:
//Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
到这里可以看出:
1) Activity. dispatchTouchEvent是Activity事件分发的总入口,我们可以在自定义
Activity中重新实现该函数,即可达到对指定事件的预处理或者截获
2) 调用getWindow().superDispatchTouchEvent(ev)来实现事件在DecorView的分发
3) 如果DecorView没有处理这个事件,则调用Activity.onTouchEvent作默认处理
getWindow().superDispatchTouchEvent(ev)直接调用mDecor.superDispatchTouchEvent:
//PhoneWindow.DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
直接调用super. dispatchTouchEvent,也就是FrameLayout. dispatchTouchEvent来开始事件在
View中的分发,接下去介绍这一块的分发规则
8.3 触摸事件在DecorView中的分发规则
Android的Touch Event主要分四种类型:
1) ACTION_DOWN
当用户手指按压屏幕会产生,这里就称它为前置目标锁定事件,当这个事件传递到
Decor View时,其必须要根据事件对应的坐标来锁定一个target view来处理后续事件
2) ACTION_UP
用户手指抬起时会产生该事件,也就是上面所说的手续事件之一
3) ACTION_MOVE
用户在屏幕移动手指时产生的事件,也是上面所说的后续事件之一
4) ACTION_CANCEL
当一个View在ACTION_DOWN时被判定为target view后,后续ACTION_UP和
ACTION_MOVE事件都会被发送到当前target view来处理,也就是说taretview在这个时候是独享当前事件的输入的,不过targetview的parent view,也就是说它爸爸,或者它爸爸的爸爸,反正比它大的直系ViewGroup都可以在其onInterceptTouchEvent被调用时返回true完成处理权的剥夺,剥夺完成后,当前targetview会收到
ACTION_CANCEL被告知你的事件处理权被取消了,然后刚刚完成剥夺的它爹或爷爷会被设置成新的target view,用以接收后续的ACTION_UP和ACTION_MOVE事件,还有就是这种剥夺是不可逆的,一旦完成对处理权的剥夺,就无法还回去
接下去用一个简单的列子来介绍Décor View在收到ACTION_DOWN事件时是如何锁定targetview的
先看图:
假定DecorView有一个child view叫ViewGroup1,然后ViewGroup1有两个child view分别叫child view1和child view2
我们假定用户手指按在child view2和child view1的重叠区域内,这个时候DecorView会收到类型为ACTION_DOWN的触摸事件,接下去将这个ACTION_DOWN分发下去用以锁定target view,流程是这样的:
1) Décor View会根据用户点击区域来判定点击在哪个子View上,这里当然是ViewGroup1
2) ViewGroup1同样的,也是通过点击区域来判定目标子View,不同的是,这里child View1和child view2都符合要求,那谁先来处理ACTION_DOWN事件,当然是谁在上面谁先处理,即后添加的child view会先享有处理权,也就说,ChildView2会先处理;如果Child View2返回true,那它就会被设置为target view,反之就继续传给Child View1处理,如果Child View1返回true,锁定结束,否则就只能继续传给其parentview(ViewGroup1)处理了,因为同级已经不存在符合要求的childview了,如果ViewGroup1也是返回false,那就继续往上传,直到找到处理该事件的View为止,如果到达Décor View了都没有被处理,那最终只能调用Activity.onTouchEvent做默认处理了
当一个child view通过ACTION_DOWN被设置为target view后,后续它这个target view被取消,只有两种情况:
1) 收到ACTION_UP事件,用户当次触摸结束
2) 上面说过,其parent ViewGroup在处理后续事件时,在onInterceptTouchEvent被调用时返回true完成target view的切换
所以,如果同级child view存在重叠区域,当用户点击这个重叠区域时,最上面的child view返回true告知其处理了这个ACTION_DOWN事件,那么重叠区域下面所有的childview都是无法收到任何后续事件的
我们都知道Android视图是一个树形结构,所以对于在这个树形结构中的每一个ViewGroup节点来说,它只要保存它的目标childview就可以了,这样数据就能从根节点(DecorView)一级一级的传到最终的target view
因此,从代码的角度,我们其实只需要分析ViewGroup是如何确定其direct target child view来处理数据的,就可以以此来推出整个View Tree的数据传递过程了
由于分发逻辑主要在ViewGroup.dispatchTouchEvent中,接下去就基于这个函数来分析,先分析ACTION_DOWN事件是如何确定directtarget child view的 :
//ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
……
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
……
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
……
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
……
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
……
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
……
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
……
}
predecessor = target;
target = next;
}
}
……
return handled;
}
ViewGroup用一个链表保存target view,链表头保存到mFirstTouchTarget,其实只对于
ACTION_DOWN事件来说,链表只会存在一个数据,也就是target child view
在函数一开始,判定如果是ACTION_DOWN事件,则将重置状态,包括将清除target view链表,并将mFirstTouchTarget置空等
接着调用ViewGroup的onInterceptTouchEvent看其是否要截断ACTION_DOWN的传输,这里假定不截断,也就是返回false,intercepted为false
然后从后向前遍历所有child view,通过isTransformedTouchPointInView判断点击是否在这个child view内,如果在,则调用dispatchTransformedTouchEvent将事件传给该child view处理,如果返回true,说明被这个child view处理了,然后addTouchTarget将这个child view保存为
mFirstTouchTarget并跳出循环;如果返回false,则继续调用下一个child view按相同方式进行处理
接下去判断mFirstTouchTarget是否为null,如果为null,说明ViewGroup要么没有child view,要么所有的child view都没有处理ACTION_DOWN事件,接着调用ViewGroup自身的
dispatchTransformedTouchEvent进行处理
如果mFirstTouchTarget不为null,说明有child view处理了ACTION_DOWN事件,接着遍历
mFirstTouchTarget链表依次进行事件分发,由于ACTION_DOWN事件上面已经分发过,
这里alreadyDispatchedToNewTouchTarget和target == newTarget都为true,不做任何处理,直接将handled置为true
mFirstTouchTarget即为ACTION_DOWN最终锁定的target view,至于其他ACTION_UP和
ACTION_MOVE还有ACTION_CANCEL的处理都比较简单,大家基于上面的分析自行看源码吧,这里就不做分析了
至此,触摸事件的传输机制介绍完毕
- 浅谈Android之Activity触摸事件传输机制介绍
- Android触摸事件派发机制源码分析之Activity
- Android 触摸事件机制(二) Activity中触摸事件详解
- 浅谈Android 触摸事件分发机制
- Android触摸事件处理机制之requestDisallowInterceptTouchEvent
- Android触摸事件机制
- Android触摸事件机制
- Android触摸事件机制
- Android事件触摸机制
- 浅谈Android之Activity相关介绍
- Activity触摸事件的分发机制
- 触摸事件分法机制-Activity
- Android 触摸事件传递机制
- android屏幕触摸事件机制
- android屏幕触摸事件机制
- android屏幕触摸事件机制
- android 屏幕触摸事件机制
- Android触摸事件分发机制
- Socket通信自定义mina 框架过滤器解析(处理粘包、断包问题)
- 论版本号的正确打开方式
- Glide-内存缓存与磁盘缓存
- Mybatis中Mapper内置方法细解
- opencv2 摄像机标定代码简化版 (ubuntu 16)
- 浅谈Android之Activity触摸事件传输机制介绍
- SylixOS 共用中断号机制
- 传智播客.黑马程序员,学C++不再难!
- System.Data.SQLite数据库简介
- git commit时出现的问题The file will have its original line endings in your working directory
- Mybatis if 判断字符串
- Bayer RGB和RGB Raw
- 嵌套滑动 NestedScrolling
- Django And Django-Rest-Framework 异常记录