浅谈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的处理都比较简单,大家基于上面的分析自行看源码吧,这里就不做分析了

 

至此,触摸事件的传输机制介绍完毕

0 0