【回顾基础】— 细聊 view 触摸事件分发

709 阅读3分钟

简介

对于一些触摸操作引起的界面变换,不管是UI切换,动画等等。这些是离不开触摸事件分发的。如果不了解触摸事件分发机制,在做一些触摸事件处理的时候就会刚觉很迷茫和无从下手。这篇文章带大家一起了解触摸事件分发机制。

分发

当我们手指触摸手机屏幕的时候,手机硬件会采集到触摸信号,然后通过转换传递给系统,再由系统传递给应用,应用接收到后对事件进行分发。

分析

分析触摸事件分发,总得有个起点,不可能从触摸事件最开始处分析,我们得找一个比较合适的分析点。其实我们可以从Activity(PhoneWindow)开始,因为触摸事件会先传递给Activity,然后传递给window,再由window分发给View组件。

Activity-> window -> View组件

Activity

可能很多人都知道,触摸事件首先会调用dispatchTouchEvent(MotionEvent ev)方法,那么是谁将事件传递给它的呢?会不会有其它方法比它更早调用呢?下面一一解答。

查看系统源码 /frameworks/base/core/jni/android_view_InputEventReceiver.cpp 定位到consumeEvents方法,组装触摸事件并通知给Java层。

case AINPUT_EVENT_TYPE_MOTION: {
    ...
    MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
    if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
        *outConsumedBatch = true;
    }
    inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
    break;
}

通知给Java层

if (inputEventObj) {
    ...
    env->CallVoidMethod(receiverObj.get(),
            gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj,
            displayId);
    ... 
} else {
    ...
}

看一下有关的注册信息

int register_android_view_InputEventReceiver(JNIEnv* env) {
    ...
    jclass clazz = FindClassOrDie(env, "android/view/InputEventReceiver");
    gInputEventReceiverClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);

    gInputEventReceiverClassInfo.dispatchInputEvent = GetMethodIDOrDie(env,
            gInputEventReceiverClassInfo.clazz,
            "dispatchInputEvent", "(ILandroid/view/InputEvent;I)V");
    ...
}

并通过InputEventReceiver(WindowInputEventReceiver)的dispatchInputEvent()进行处理,这里就返回到我们常见的Java世界了。WindowInputEventReceiver是ViewRootImpl中的一个内部类。而在dispatchInputEvent()方法中会调用onInputEvent(InputEvent event, int displayId)方法。

public void onInputEvent(InputEvent event, int displayId) {
    enqueueInputEvent(event, this, 0, true);
}

在 enqueueInputEvent(event, this, 0, true)方法中会执行doProcessInputEvents()。

void doProcessInputEvents() {
    while (mPendingInputEventHead != null) {
        ...
        long eventTime = q.mEvent.getEventTimeNano();
        long oldestEventTime = eventTime;
        if (q.mEvent instanceof MotionEvent) {
            MotionEvent me = (MotionEvent)q.mEvent;
            if (me.getHistorySize() > 0) {
                oldestEventTime = me.getHistoricalEventTimeNano(0);
            }
        }
        mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
        deliverInputEvent(q);
    }
   ...
}

处理时间参数,然后调用deliverInputEvent(q)方法。

private void deliverInputEvent(QueuedInputEvent q) {
    ...
    InputStage stage;
    if (q.shouldSendToSynthesizer()) {
        stage = mSyntheticInputStage;
    } else {
        stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
    }
    ...
    if (stage != null) {
        handleWindowFocusChanged();
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}

递归每一个InputStage进行处理,看一下mFirstPostImeInputStage和mFirstInputStage,mSyntheticInputStage初始化。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ...
        // Set up the input pipeline.
    CharSequence counterSuffix = attrs.getTitle();
    mSyntheticInputStage = new SyntheticInputStage();
    InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStag
    InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStag
            "aq:native-post-ime:" + counterSuffix);
    InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStag
    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;
}

看一下ViewPostImeInputStage类的onProcess(QueuedInputEvent q)方法。

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);
        }
    }
}

调用processPointerEvent(q)方法

private int processPointerEvent(QueuedInputEvent q) {
    ...
    boolean handled = mView.dispatchPointerEvent(event);
    ...
}

读过前面文章的朋友应该知道mView是DecorView。看一下DecorView中的dispatchPointerEvent(event)方法。DecorView没有实现dispatchPointerEvent方法,但是继承View类实现了。

public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

调用DecorView的dispatchTouchEvent(event)方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
    final Window.Callback cb = mWindow.getCallback();
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

mWindow是那里来的呢,可以看一下【view】- setContentView方法和UI绘制流程(源码分析)这篇文章。mWindow是Activity中的传家的Window实例。而在执行Activity的attach方法时,会设置回调。

mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setCallback(this);

this就是Activity实例对象。于是就传递到了Activity的dispatchTouchEvent(MotionEvent ev)方法中。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

getWindow()获取mWindow,调用superDispatchTouchEvent(ev)。mWindow是PhoneWindow类型。

public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

mDecor是顶层布局DecorView实例。调用DecorView中的superDispatchTouchEvent(MotionEvent event)方法。

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

调用父类dispatchTouchEvent方法,这时就会调用到ViewGroup中的dispatchTouchEvent(MotionEvent ev)方法。接下来就是View之间的传递的分发了。

总结

经过上面的分析:

触摸事件由底层调用ViewRootImpl $WindowInputEventReceiver中的dispatchInputEvent方法。

然后调用DecorView中的dispatchPointerEvent方法,这时候并没有在View之间进行传递,而是先把触摸事件传递给Activity,通过Window.Callback调用Activity的dispatchTouchEvent(MotionEvent ev)方法。

在Activity又调用PhoneWindow中的superDispatchTouchEvent(MotionEvent event)方法。

在PhoneWindow中调用DecorView中的superDispatchTouchEvent(MotionEvent event),然后进行View间的触摸事件分发。

所以有了下面:

Activity -> window -> view组件

最后

由于水平有限,有错误的地方在所难免,未免误导他人,欢迎大佬指正!码字不易,感谢大家的点赞关注!🙏有一起学习的小伙伴可以关注下我的公众号——【❤️程序猿养成中心❤️】每周会定期做关于Android的技术分享。