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

来源:互联网 发布:screenflow for mac 编辑:程序博客网 时间:2024/05/21 21:45

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()方法



原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 牙齿长洞很大怎么办 两颗牙中间黑了怎么办 牙齿酸痛怎么办才能好 牙齿被磨小了疼怎么办 牙齿有黑点蛀牙怎么办 小孩有蛀牙牙痛怎么办 牙齿修补后疼痛怎么办 腹部绞痛出冷汗怎么办 结石运动后尿血怎么办 透析病人尿血该怎么办 宝宝吃药就吐怎么办 肚子坠胀尿血怎么办 透析病人回来血尿怎么办 血尿腹痛腰疼怎么办 新诺明 吃多了 怎么办 吃下火药拉肚子怎么办 打哈欠停不下来怎么办 小孩一直咳不停怎么办 孩子咳嗽咳不停怎么办 孩子咳嗽出冷汗怎么办 宝宝一直咳不停怎么办 尿血右上腹疼怎么办 尿道感染尿出血怎么办 儿童血尿腹痛是怎么办 肚子胀痛拉二天血尿怎么办 小牛肚尿道发炎怎么办 吃奶小牛涨肚怎么办 尿路感染尿出血了怎么办 尿血还带血块怎么办 尿道感染尿血了怎么办 老人小便带血怎么办 胎儿双肾盂扩张怎么办 宝宝发烧后血尿怎么办 孕妇肾盂分离16怎么办 急性尿道炎尿血严重怎么办 尿急尿频尿血严重怎么办 打预防针两天后发烧怎么办 孩子发烧心跳快怎么办 拉肚子脱水人犯困怎么办 拉肚子拉脱水了怎么办 孕妇肚子疼拉水怎么办