MobileAndroid技术知识Android开发

Android 输入事件一撸到底之DecorView拦路虎(2)

2021-11-08  本文已影响0人  小鱼人爱编程

前言

1、Android 输入事件一撸到底之源头活水(1)
2、Android 输入事件一撸到底之DecorView拦路虎(2)
3、Android 输入事件一撸到底之View接盘侠(3

image.png
在上篇文章:Android 输入事件一撸到底之源头活水(1)
中分析了输入事件如何传递到App层并经过一系列处理最终分发给了Root View处理,接下来看看Root View 如何处理输入事件。
通过本篇文章你将了解到:

1、DecorView 作为Root View
2、DecorView 分发MotionEvent
3、DecorView 分发KeyEvent
4、DecorView 处理事件的一些异同点

DecorView 作为Root View

ViewPostImeInputStage 分别调用了View dispatchPointerEvent、dispatchKeyEvent 方法:

View.java
    public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            //熟知的dispatchTouchEvent方法,开启View的事件分发
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }

    public boolean dispatchKeyEvent(KeyEvent event) {
        ...
        //先检查View的Listener处理情况
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
            return true;
        }
        //再处理KeyEvent Callback
        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            return true;
        }
        ...
        return false;
    }

以上是View 默认的处理方法。DecorView其继承自FrameLayout,FrameLayout继承自ViewGroup。DecorView作为RootView重写dispatchKeyEvent方法,没有重写dispatchPointerEvent方法。
接下来的分析从这俩方法入手。
有关DecorView 请查看:Android DecorView 一窥全貌(上)

DecorView 分发MotionEvent

DecorView 重写了dispatchTouchEvent(xx)

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //获取Callback
        final Window.Callback cb = mWindow.getCallback();
        //Callback存在且Window没有被销毁,则调用
        //Callback dispatchTouchEvent方法
        //否则,调用父类dispatchTouchEvent方法
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
}

Window.Callback

Callback内容
Window.Callback 是个接口,其内容:

    public interface Callback {
        ...
        public boolean dispatchKeyEvent(KeyEvent event);
        public boolean dispatchTouchEvent(MotionEvent event);
        ...
    }

Window 持有Window.Callback引用

    private Window.Callback mCallback;
    public void setCallback(Window.Callback callback) {
        mCallback = callback;
    }
    public final Window.Callback getCallback() {
        return mCallback;
    }

Callback什么时候赋值
以Activity 为例:

    public class Activity extends ContextThemeWrapper
            implements LayoutInflater.Factory2,
            Window.Callback, KeyEvent.Callback,
            View.OnCreateContextMenuListener, ComponentCallbacks2,
            Window.OnWindowDismissedCallback, WindowControllerCallback,
            AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {
        ...
    }

可以看出Activity实现了Window.Callback接口。既然实现了接口,那么当然有setCallback 过程。

Activity.java
    final void attach(Context context, ActivityThread aThread,
                      Instrumentation instr, IBinder token, int ident,
                      Application application, Intent intent, ActivityInfo info,
                      CharSequence title, Activity parent, String id,
                      NonConfigurationInstances lastNonConfigurationInstances,
                      Configuration config, String referrer, IVoiceInteractor voiceInteractor,
                      Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        ...
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        //给Window.Callback 赋值
        mWindow.setCallback(this);
        ...
    }

在Activity绑定时调用setCallback(this)。
有关Activity启动过程请查看:Android Activity创建到View的显示过程

值得注意的是,如果我们继承自AppCompatActivity,那么Window里的Callback是AppCompatWindowCallback,其内部对Window.Callback又封装了一层。

Activity重写Callback里的方法

Activity.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //Down事件交给用户处理
            onUserInteraction();
        }
        //调用Window方法
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //如果该事件还未处理,则执行下面方法
        return onTouchEvent(ev);
    }

重点看看superDispatchTouchEvent(xx)

PhoneWindow.java
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //mDecor 为Window的Root View
        //也即是DecorView
        return mDecor.superDispatchTouchEvent(event);
    }
DecorView.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //调用父类方法
        //这里调用的是ViewGroup里的dispatchTouchEvent
        return super.dispatchTouchEvent(event);
    }

最后又流转到了ViewGroup的dispatchTouchEvent方法,是不是似曾相识呢?
前面提到过在DecorView的dispatchTouchEvent方法里:

//如果有Callback,则执行Callback的dispatchTouchEvent(1)
//如果没有,则执行super.dispatchTouchEvent(ev)(2)
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
  • 最终发现如果(1)过程没有处理事件,那么将执行到(2),都调用了super.dispatchTouchEvent(ev)
  • 而ViewGroup dispatchTouchEvent(ev)开始就是熟知的ViewTree 事件分发过程

为什么这么做
DecorView和PhoneWindow 互相持有对方引用,而Activity又持有PhoneWindow引用,因此DecorView和Activity通过PhoneWindow联系起来了。
当DecorView拿到事件时,先分给Activity(通过Callback),Activity通过PhoneWindow又发给DecorView,最终开启ViewTree事件分发。
由此看来,将Activity和DecorView解耦了。

我们可以在Activity里重写dispatchTouchEvent(xx),进而决定是否继续将事件分发给Window。
我们也可以在Activity里重写onTouchEvent(xx),当ViewTree 没有处理事件时,将会调用该方法。

DecorView 分发MotionEvent 流程如下:


image.png

DecorView 分发KeyEvent

DecorView重写了dispatchKeyEvent(xx)

DecorView.java
    public boolean dispatchKeyEvent(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        final int action = event.getAction();
        final boolean isDown = action == KeyEvent.ACTION_DOWN;
        ...
        if (!mWindow.isDestroyed()) {
            //有Callback 执行对应方法
            //否则调用其父类方法
            final Window.Callback cb = mWindow.getCallback();
            final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
                    : super.dispatchKeyEvent(event);
            if (handled) {
                return true;
            }
        }
    }

依然以Activity为例。
Activity重写Callback里的方法

Activity.java
    public boolean dispatchKeyEvent(KeyEvent event) {
        //用户处理
        onUserInteraction();
        final int keyCode = event.getKeyCode();
        ...
        Window win = getWindow();
        //交给Window处理
        if (win.superDispatchKeyEvent(event)) {
            return true;
        }
        //若Window没有处理,则继续传递
        View decor = mDecor;
        if (decor == null) decor = win.getDecorView();
        return event.dispatch(this, decor != null
                ? decor.getKeyDispatcherState() : null, this);
    }

1、重点看看superDispatchKeyEvent(xx)

PhoneWindow.java
    @Override
    public boolean superDispatchKeyEvent(KeyEvent event) {
        //mDecor 为Window的Root View
        //也即是DecorView
        return mDecor.superDispatchKeyEvent(event);
    }
DecorView.java
    public boolean superDispatchKeyEvent(KeyEvent event) {
        ...
         //调用父类方法
        //这里调用的是ViewGroup里的dispatchKeyEvent
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
        ...
    }

是不是感觉挺熟悉的套路,没错,和DecorView分发MotionEvent一样的套路。
最终调用了View里的方法:

#View.java
    public boolean dispatchKeyEvent(KeyEvent event) {
        ...
        //设置监听按键事件
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
            //如果处理了就不再进行后续处理
            return true;
        }

        //View 默认实现了KeyEvent.Callback 接口
        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            return true;
        }
        ...
        return false;
    }

2、再来看看event.dispatch方法

KeyEvent.java
    public final boolean dispatch(KeyEvent.Callback receiver, KeyEvent.DispatcherState state,
                                  Object target) {
        switch (mAction) {
            case ACTION_DOWN: {
                boolean res = receiver.onKeyDown(mKeyCode, this);
                ...
                return res;
            }
            case ACTION_UP:
                ...
                return receiver.onKeyUp(mKeyCode, this);
            case ACTION_MULTIPLE:
                ...
                if (receiver.onKeyMultiple(code, count, this)) {
                    return true;
                }
                ...
                return false;
        }
        return false;
    }

KeyEvent 交给了KeyEvent.Callback 处理。
KeyEvent.Callback 内容

    public interface Callback {
        //按下事件
        boolean onKeyDown(int keyCode, KeyEvent event);
        //长按事件
        boolean onKeyLongPress(int keyCode, KeyEvent event);
        //抬起事件
        boolean onKeyUp(int keyCode, KeyEvent event);
        //多次操作事件
        boolean onKeyMultiple(int keyCode, int count, KeyEvent event);
    }

KeyEvent.Callback 什么时候赋值
Activity实现了KeyEvent.Callback 接口
重点来看看onKeyUp(xx)方法

    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (getApplicationInfo().targetSdkVersion
                >= Build.VERSION_CODES.ECLAIR) {
            if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
                    && !event.isCanceled()) {
                //最终会调用finish 关闭Activity
                onBackPressed();
                return true;
            }
        }
        return false;
    }

因此若要按返回键不退出Activity,可以在onKeyUp(xx)或者onBackPressed()上做文章。

DecorView 处理事件的一些异同点

以上分析了DecorView 分发MotionEvent和KeyEvent流程,可以看出两者流程很相似,提取两者异同点:
共同点:

1、DecorView 传递输入事件到Activity
2、Activity 优先分发给Window处理,我们可以重写该分发方法。
3、若Window 未处理,则分两种情况

异同点:
对于上面第三点分两种情况:

1、对于MotionEvent,Activity 调用onTouchEvent(xx)处理Window未处理的事件,我们可以重写onTouchEvent(xx)方法
2、对于KeyEvent,Activity 调用KeyEvent.dispatch(xx)处理Window未处理的事件,我们可以重写KeyEvent.dispatch(xx)方法

综上所述,原本DecorView可以直接处理事件的,但实际上它先分发到Activity、再分发到Window,最后分发到ViewTree,这也就是为什么将DecorView称作为"拦路虎"的原因。

本文基于 Android 10.0 源码

上一篇下一篇

猜你喜欢

热点阅读