View工作原理之按键消息派发过程

1、按键派发总体过程


ViewRoot中定义了一个InputHandler对象:

    private final InputHandler mInputHandler = new InputHandler() {
        public void handleKey(KeyEvent event, Runnable finishedCallback) {
            startInputEvent(finishedCallback);
            dispatchKey(event, true);
        }

        public void handleMotion(MotionEvent event, Runnable finishedCallback) {
            startInputEvent(finishedCallback);
            dispatchMotion(event, true);
        }
    };

该对象有两个方法,分别是handleKey()和handleMotion()
当底层得到按键消息,会调用InputHandler对象的handleKey(),handleKey()中再调用dispatchKey(),在dispatchKey()中发送一个异步消息,然后调用到了deliverKeyEvent(),代码如下:

    private void deliverKeyEvent(KeyEvent event, boolean sendDone) {
        // If mView is null, we just consume the key event because it doesn't
        // make sense to do anything else with it.
        boolean handled = mView != null
                ? mView.dispatchKeyEventPreIme(event) : true;
        if (handled) {
            if (sendDone) {
                finishInputEvent();
            }
            return;
        }
        // If it is possible for this window to interact with the input
        // method window, then we want to first dispatch our key events
        // to the input method.
        if (mLastWasImTarget) {
            InputMethodManager imm = InputMethodManager.peekInstance();
            if (imm != null && mView != null) {
                int seq = enqueuePendingEvent(event, sendDone);
                if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq="
                        + seq + " event=" + event);
                imm.dispatchKeyEvent(mView.getContext(), seq, event,
                        mInputMethodCallback);
                return;
            }
        }
        deliverKeyEventToViewHierarchy(event, sendDone);
    }

该方法中,在输入法窗口处理前,调用dispatchKeyEventPreIme(),如果需要回执,通过finishInputEvent()告诉WmS已经处理了该按键消息。接着把按键消息转发给输入法,在ViewRoot对象里包含真正的View对象mView,故最后就能通过deliverKeyEventToViewHierarchy()转发给真正的视图了。

   private void deliverKeyEventToViewHierarchy(KeyEvent event, boolean sendDone) {
        try {
            if (mView != null && mAdded) {
                final int action = event.getAction();
                boolean isDown = (action == KeyEvent.ACTION_DOWN);

                if (checkForLeavingTouchModeAndConsume(event)) {
                    return;
                }

                if (Config.LOGV) {
                    captureKeyLog("captureDispatchKeyEvent", event);
                }
                boolean keyHandled = mView.dispatchKeyEvent(event);

                if (!keyHandled && isDown) {
                    int direction = 0;
                    switch (event.getKeyCode()) {
                    case KeyEvent.KEYCODE_DPAD_LEFT:
                        direction = View.FOCUS_LEFT;
                        break;
                    case KeyEvent.KEYCODE_DPAD_RIGHT:
                        direction = View.FOCUS_RIGHT;
                        break;
                    case KeyEvent.KEYCODE_DPAD_UP:
                        direction = View.FOCUS_UP;
                        break;
                    case KeyEvent.KEYCODE_DPAD_DOWN:
                        direction = View.FOCUS_DOWN;
                        break;
                    }

                    if (direction != 0) {

                        View focused = mView != null ? mView.findFocus() : null;
                        if (focused != null) {
                            View v = focused.focusSearch(direction);
                            boolean focusPassed = false;
                            if (v != null && v != focused) {
                                // do the math the get the interesting rect
                                // of previous focused into the coord system of
                                // newly focused view
                                focused.getFocusedRect(mTempRect);
                                if (mView instanceof ViewGroup) {
                                    ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                                            focused, mTempRect);
                                    ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                                            v, mTempRect);
                                }
                                focusPassed = v.requestFocus(direction, mTempRect);
                            }

                            if (!focusPassed) {
                                mView.dispatchUnhandledMove(focused, direction);
                            } else {
                                playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
                            }
                        }
                    }
                }
            }

        } finally {
            if (sendDone) {
                finishInputEvent();
            }
            // Let the exception fall through -- the looper will catch
            // it and take care of the bad app for us.
        }
    }

deliverKeyEventToViewHierarchy()方法可分四步执行:
(1)checkForLeavingTouchModeAndConsume()判断该消息是否会导致离开触摸模式,一般返回false
(2)mView.dispatchKeyEvent(event)把事件派发给根视图
(3)如果是Down事件且是方向键,进行方向键的处理
(4)最后通过finishInputEvent()报告WmS已处理该消息。

2、根视图内部派发过程

接着上一节讲,系统调用到了mView.dispatchKeyEvent(event),这个根视图是DecorView或普通的ViewGroup。现在介绍PhoneWindow.DecorView这个根视图内部的按键消息派发流程,该函数代码如下:

public boolean dispatchKeyEvent(KeyEvent event) {
            final int keyCode = event.getKeyCode();
            final boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN;

            /*
             * If the user hits another key within the play sound delay, then
             * cancel the sound
             */
            if (keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_UP
                    && mVolumeKeyUpTime + VolumePanel.PLAY_SOUND_DELAY
                            > SystemClock.uptimeMillis()) {
                /*
                 * The user has hit another key during the delay (e.g., 300ms)
                 * since the last volume key up, so cancel any sounds.
                 */
                AudioManager audioManager = (AudioManager) getContext().getSystemService(
                        Context.AUDIO_SERVICE);
                if (audioManager != null) {
                    audioManager.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME,
                            mVolumeControlStreamType, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
                }
            }

            if (isDown && (event.getRepeatCount() == 0)) {
                // First handle chording of panel key: if a panel key is held
                // but not released, try to execute a shortcut in it.
                if ((mPanelChordingKey > 0) && (mPanelChordingKey != keyCode)) {
                    // Perform the shortcut (mPreparedPanel can be null since
                    // global shortcuts (such as search) don't rely on a
                    // prepared panel or menu).
                    boolean handled = performPanelShortcut(mPreparedPanel, keyCode, event,
                            Menu.FLAG_PERFORM_NO_CLOSE);

                    if (!handled) {
                        /*
                         * If not handled, then pass it to the view hierarchy
                         * and anyone else that may be interested.
                         */
                        handled = dispatchKeyShortcutEvent(event);

                        if (handled && mPreparedPanel != null) {
                            mPreparedPanel.isHandled = true;
                        }
                    }

                    if (handled) {
                        return true;
                    }
                }

                // If a panel is open, perform a shortcut on it without the
                // chorded panel key
                if ((mPreparedPanel != null) && mPreparedPanel.isOpen) {
                    if (performPanelShortcut(mPreparedPanel, keyCode, event, 0)) {
                        return true;
                    }
                }
            }

            final Callback cb = getCallback();
            final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
                    : super.dispatchKeyEvent(event);
            if (handled) {
                return true;
            }
            return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event)
                    : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event);
        }

现分析处理流程:

(1)处理间量键
(2)调用performPanelShortcut()方法处理系统快捷键
(3)调用Callback对象的dispatchKeyEvent()
(4)最后调用PhoneWindow对象的onKeyDown()和onKeyUp()事件根视图只处理了少数按键消息。
3、Activity内部派发过程
Activity实现了Window.Callback接口,上一节中的(3)调用Callback对象的dispatchKeyEvent(),实际上就是使Activity对象调用了dispatchKeyEvent(),代码如下:

    public boolean dispatchKeyEvent(KeyEvent event) {
        onUserInteraction();
        Window win = getWindow();
        if (win.superDispatchKeyEvent(event)) {
            return true;
        }
        View decor = mDecor;
        if (decor == null) decor = win.getDecorView();
        return event.dispatch(this, decor != null
                ? decor.getKeyDispatcherState() : null, this);
    }

执行过程分析如下:

1、调用onUserInteraction(),当开始和activity交互的时候,用户可以做点什么
2、获取Window对象,实际上就是PhoneWindow对象,因此调用到:

    public boolean superDispatchKeyEvent(KeyEvent event) {
        return mDecor.superDispatchKeyEvent(event);
    }

接着又调用:

        public boolean superDispatchKeyEvent(KeyEvent event) {
            return super.dispatchKeyEvent(event);
        }

DecorView的父类是FrameLayout,该函数并没有重载dispatchKeyEvent(),则super最后会调用到ViewGroup的同名函数

ViewGroup的dispatchKeyEvent()的实现如下:

    public boolean dispatchKeyEvent(KeyEvent event) {
        if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
            return super.dispatchKeyEvent(event);
        } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
            return mFocused.dispatchKeyEvent(event);
        }
        return false;
    }
 如果ViewGroup本身拥有焦点,则把消息派发到试图本身,如果是子视图拥有焦点,则把消息派发给子视图。

例如有多重嵌套的视图,由于mFocused变量代表的是拥有焦点的视图或是包含拥有焦点视图的ViewGroup,所以从最上层开始,逐层调用dispatchKeyEvent(),最后到拥有焦点的视图。如果这个视图是View,则最后调用的是:

    public boolean dispatchKeyEvent(KeyEvent event) {
        // If any attached key listener a first crack at the event.
        //noinspection SimplifiableIfStatement

        if (android.util.Config.LOGV) {
            captureViewInfo("captureViewKeyEvent", this);
        }

        if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
            return true;
        }

        return event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this);
    }

这个方法中,首先回调onKey()函数,如果onKey()中没有消费该消息,则最后调用event.dispatch()函数。第一个参数是View本身,因此会调用到View对象的onKeyDown和onKeyUp()方法等。如果在这个地方继续返回false,则会执行到第三步。

3、第2步中派发给用户视图的消息没有被处理,则通过event.dispatch()方法调用Activity内部的onKeyDown()和onKeyUp()等。

分析到这里也解释出了如果自定义View没有获得焦点,那么你复写的onKeyDown()和onKeyUp()方法是不会被调用到,而最终调用了Activity对象的onKeyDown()和onKeyUp()。

下面具体分析KeyEvent的dispatch()中的Down事件过程,代码如下

            case ACTION_DOWN: {
                mFlags &= ~FLAG_START_TRACKING;
                if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
                        + ": " + this);
                boolean res = receiver.onKeyDown(mKeyCode, this);
                if (state != null) {
                    if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
                        if (DEBUG) Log.v(TAG, "  Start tracking!");
                        state.startTracking(this, target);
                    } else if (isLongPress() && state.isTracking(this)) {
                        try {
                            if (receiver.onKeyLongPress(mKeyCode, this)) {
                                if (DEBUG) Log.v(TAG, "  Clear from long press!");
                                state.performedLongPress(this);
                                res = true;
                            }
                        } catch (AbstractMethodError e) {
                        }
                    }
                }
                return res;
            }

1、清除了mFlags标志

2、receiver.onKeyDown()执行,这个receiver可能是View对象或activity对象

3、如果第二步返回true,且是第一次按,并且mFlags包含FLAG_START_TRACKING,才执行后面的动作

这里的这个现象很奇怪,mFlags在前面明明在前被清除过,后面判断mFlags&FLAG_START_TRACKING肯定应该等于0啊

这里给出的解释是这样的,要想让你的自定义View能够执行到onKeyLongPress(),称为”处理生理长按“,须做以下几件事情。

1、你的View必须重载onKeyDown()事件,且返回true

2、onKeyDown()中必须执行event.startTracking(),参见Activity中的onKeyDown()方法也是调用了的。

3、最后要做的就是重载onKeyLongPress(),做你想要做的事情了。

在处理ACTION_UP,先调用state.handleUpEvent(),判断是否发生了“生理长按”,最后调到了receiver.onKeyUp()。


分析View中的onKeyDown()和onKeyUp()

onKeyDown()中实现的功能:

1、只接收DPAD_CENTER和KEYCODE_ENTER这两个按键消息

2、判断是View是否ENABLE,如果不是,返回false

3、如果可点击或可长按,通过setPress(true)设置视图为按下状态

4、postDelay一个Runnable来实现长按,不则于前面讲的“生理长按”,这里超过500ms,则会回调onLongClick()

onKeyUp()中实现的功能

1、判断是否可点

2、如果处于pressed的状态,调用setPress(false)

3、如果释放按键时还没有500ms,则取消前面的Runnable

4、调用performClick()方法,回调onClick()方法



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值