遥控器Tab键切换时按键音流程


用遥控器进行界面的tab切换时,会有音量出现,梳理下tab切换时按键音的逻辑。基于Android U的代码。

从ViewRootImpl的processKeyEvent方法开始,这个方法用于处理按键事件。首先看下该方法的调用堆栈。

at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:6957)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:6826)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6244)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6301)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6267)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:6432)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6275)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:6489)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6248)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6301)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6267)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6275)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6248)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6301)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6267)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:6465)
at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:6676)
at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:4278)
at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:3715)
at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:3706)
at android.view.inputmethod.InputMethodManager.-$$Nest$mfinishedInputEvent(Unknown Source:0)
at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:4255)
at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:154)
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:335)
at android.os.Looper.loopOnce(Looper.java:162)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8177)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:570)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)

源码分析:

frameworks/base/core/java/android/view/ViewRootImpl.java
private int processKeyEvent(QueuedInputEvent q) {
            final KeyEvent event = (KeyEvent)q.mEvent;

            if (mUnhandledKeyManager.preViewDispatch(event)) {//在event事件被分发给view层级结构之前调用,如果未处理的handler具有焦点并且消费了该事件,则返回true
                return FINISH_HANDLED;//FINISH_HANDLED表示该事件已被处理。
            }

            // Deliver the key to the view hierarchy.
            if (mView.dispatchKeyEvent(event)) {//将key事件传递给mView(Activity的顶层DecordView对象)处理
                return FINISH_HANDLED;//如果mView层级结构中任何一个view处理了该事件,返回FINISH_HANDLED表示该事件已被处理
            }

            if (shouldDropInputEvent(q)) {//是否应该放弃处理该事件
                return FINISH_NOT_HANDLED;//返回FINISH_NOT_HANDLED表示该事件没有被处理
            }

            // This dispatch is for windows that don't have a Window.Callback. Otherwise,
            // the Window.Callback usually will have already called this (see
            // DecorView.superDispatchKeyEvent) leaving this call a no-op.
            if (mUnhandledKeyManager.dispatch(mView, event)) {//是否有未处理的事件
                return FINISH_HANDLED;//表示该事件已经被处理
            }

            int groupNavigationDirection = 0;

            if (event.getAction() == KeyEvent.ACTION_DOWN
                    && event.getKeyCode() == KeyEvent.KEYCODE_TAB) {//tab键处理
                if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_CTRL_ON)) {
                    groupNavigationDirection = View.FOCUS_FORWARD;//将焦点移至下一个可选的item
                } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
                        KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) {
                    groupNavigationDirection = View.FOCUS_BACKWARD;//将焦点移至上一个可选的item
                }
            }

            // If a modifier is held, try to interpret the key as a shortcut.
            if (event.getAction() == KeyEvent.ACTION_DOWN
                    && !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
                    && event.getRepeatCount() == 0
                    && !KeyEvent.isModifierKey(event.getKeyCode())
                    && groupNavigationDirection == 0) {//如果是快捷键
                if (mView.dispatchKeyShortcutEvent(event)) {//如果mView处理了快捷键
                    return FINISH_HANDLED;//表示该事件已被处理
                }
                if (shouldDropInputEvent(q)) {//该快捷键是否应该被放弃处理
                    return FINISH_NOT_HANDLED;
                }
            }

            // Apply the fallback event policy.
            if (mFallbackEventHandler.dispatchKeyEvent(event)) {//下发给PhoneFallbackEventHandler处理该事件
                return FINISH_HANDLED;
            }
            if (shouldDropInputEvent(q)) {//是否应该放弃处理该事件
                return FINISH_NOT_HANDLED;
            }

            // Handle automatic focus changes.
            //处理焦点变化,这里会进行按键音的处理
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                if (groupNavigationDirection != 0) {//
                    if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                        return FINISH_HANDLED;
                    }
                } else {
                    if (performFocusNavigation(event)) {//处理焦点变化
                        return FINISH_HANDLED;
                    }
                }
            }
            return FORWARD;//如果到这里事件仍未被处理,返回FORWARD表示该事件应该被传递给下一个事件处理流程
        }

大致流程:

1.在event事件被分发到mView之前,调用mUnhandledKeyManager.preViewDispatch(event)判断是否有未处理按键事件,如果有返回FINISH_HANDLED表示该事件已被处理。


        boolean preViewDispatch(KeyEvent event) {
            mDispatched = false;
            if (mCurrentReceiver == null) {//mCurrentReceiver当前接收器。此值是瞬态的,在预分派和预览阶段之间使用,以确保其他输入阶段不会干扰跟踪。
                mCurrentReceiver = mCapturedKeys.get(event.getKeyCode());
            }
            if (mCurrentReceiver != null) {//如果存在mCurrentReceiver
                View target = mCurrentReceiver.get();
                if (event.getAction() == KeyEvent.ACTION_UP) {
                    mCurrentReceiver = null;
                }
                if (target != null && target.isAttachedToWindow()) {
                    target.onUnhandledKeyEvent(event);
                }
                // consume anyways so that we don't feed uncaptured key events to other views
                return true;//到这里的话,event事件无论如何都会消耗,返回true表示事件已被处理,这样就不会将未捕获的键事件提供给其他视图
            }
            return false;
        }

2.调用mView.dispatchKeyEvent()方法将event事件传递给mView(Activity的顶层DecordView对象)处理,如果view层级结构中任何一个view处理了该事件,返回FINISH_HANDLED表示该事件已被处理。最主要的就是这部分,大部分按键都会被mView.dispatchKeyEvent()处理。

3.判断是否应该放弃处理该事件,如果放弃处理,那么返回FINISH_NOT_HANDLED表示该事件没有被处理。看下哪些场景应该放弃处理该事件

    protected boolean shouldDropInputEvent(QueuedInputEvent q) {
            if (mView == null || !mAdded) {//mView不存在
                Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);
                return true;
            }

            // Find a reason for dropping or canceling the event.
            final String reason;//放弃或取消处理该事件的reason
            if (!mAttachInfo.mHasWindowFocus
                    && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)
                    && !isAutofillUiShowing()) {
                // This is a non-pointer event and the window doesn't currently have input focus
                // This could be an event that came back from the previous stage
                // but the window has lost focus or stopped in the meantime.
                reason = "no window focus";//window没有焦点
            } else if (mStopped) {
                reason = "window is stopped";//window处于stop状态
            } else if (mIsAmbientMode
                    && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) {
                reason = "non-button event in ambient mode";//ambient mode下没有button事件
            } else if (mPausedForTransition && !isBack(q.mEvent)) {
                reason = "paused for transition";//activity处于转换阶段,暂停处理event事件
            } else {//大多会走这里,一般不会放弃处理event事件
                // Most common path: no reason to drop or cancel the event
                return false;
            }

            if (isTerminalInputEvent(q.mEvent)) {//不要删除终端输入事件,但将其标记为已取消
                // Don't drop terminal input events, however mark them as canceled.
                q.mEvent.cancel();
                Slog.w(mTag, "Cancelling event (" + reason + "):" + q.mEvent);
                return false;
            }

            // Drop non-terminal input events.//放弃处理非终端输入事件
            Slog.w(mTag, "Dropping event (" + reason + "):" + q.mEvent);
            return true;
        }

4.如果事件仍未被处理,调用mUnhandledKeyManager.dispatch来判断是否有未处理的事件。如果有,则直接返回FINISH_HANDLED表示该事件已被处理。

5.Tab键和快捷键的处理。

6.如果事件仍未被处理,调用mFallbackEventHandler.dispatchKeyEvent(event)方法继续下发给PhoneFallbackEventHandler处理。

7.调用performFocusNavigation方法处理焦点变化逻辑

frameworks/base/core/java/android/view/ViewRootImpl.java
   private boolean performFocusNavigation(KeyEvent event) {
            int direction = 0;//焦点转换的方向
            switch (event.getKeyCode()) {
                case KeyEvent.KEYCODE_DPAD_LEFT://左移
                    if (event.hasNoModifiers()) {
                        direction = View.FOCUS_LEFT;
                    }
                    break;
                case KeyEvent.KEYCODE_DPAD_RIGHT://右移
                    if (event.hasNoModifiers()) {
                        direction = View.FOCUS_RIGHT;
                    }
                    break;
                case KeyEvent.KEYCODE_DPAD_UP://上移
                    if (event.hasNoModifiers()) {
                        direction = View.FOCUS_UP;
                    }
                    break;
                case KeyEvent.KEYCODE_DPAD_DOWN://下移
                    if (event.hasNoModifiers()) {
                        direction = View.FOCUS_DOWN;
                    }
                    break;
                case KeyEvent.KEYCODE_TAB://tab键
                    if (event.hasNoModifiers()) {
                        direction = View.FOCUS_FORWARD;
                    } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                        direction = View.FOCUS_BACKWARD;
                    }
                    break;
            }
            if (direction != 0) {
                View focused = mView.findFocus();//找到控件树中当前获取焦点的view
                if (focused != null) {
                    View v = focused.focusSearch(direction);//根据当前获取焦点的view,在direction方向上找到下一个获取焦点的view
                    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);
                        }
                        if (v.requestFocus(direction, mTempRect)) {//旧焦点的移除,新焦点的建立
                            boolean isFastScrolling = event.getRepeatCount() > 0;
                            playSoundEffect(
                                    SoundEffectConstants.getConstantForFocusDirection(direction,
                                            isFastScrolling));//播放按键音的地方
                            return true;
                        }
                    }

                    // Give the focused view a last chance to handle the dpad key.
                    if (mView.dispatchUnhandledMove(focused, direction)) {
                        return true;
                    }
                } else {
                    if (mView.restoreDefaultFocus()) {
                        return true;
                    }
                }
            }
            return false;
        }

主要流程:首先计算焦点转换的方向,然后在控件树中找到当前获取焦点的view,根据当前获取焦点的view,在direction方向上找到下一个获取焦点的view,最后移除旧焦点,建立新焦点,并播放按键音。

playSoundEffect()方法就是播放按键音的地方

public void playSoundEffect(@SoundEffectConstants.SoundEffect int effectId) {
        if ((mDisplay.getFlags() & Display.FLAG_TOUCH_FEEDBACK_DISABLED) != 0) {
            return;
        }

        checkThread();

        try {
            final AudioManager audioManager = getAudioManager();

            if (mFastScrollSoundEffectsEnabled
                    && SoundEffectConstants.isNavigationRepeat(effectId)) {
                audioManager.playSoundEffect(
                        SoundEffectConstants.nextNavigationRepeatSoundEffectId());
                return;
            }

            switch (effectId) {
                case SoundEffectConstants.CLICK:
                    audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
                    return;
                case SoundEffectConstants.NAVIGATION_DOWN:
                case SoundEffectConstants.NAVIGATION_REPEAT_DOWN:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
                    return;
                case SoundEffectConstants.NAVIGATION_LEFT:
                case SoundEffectConstants.NAVIGATION_REPEAT_LEFT:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
                    return;
                case SoundEffectConstants.NAVIGATION_RIGHT:
                case SoundEffectConstants.NAVIGATION_REPEAT_RIGHT:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
                    return;
                case SoundEffectConstants.NAVIGATION_UP:
                case SoundEffectConstants.NAVIGATION_REPEAT_UP:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
                    return;
                default:
                    throw new IllegalArgumentException("unknown effect id " + effectId +
                            " not defined in " + SoundEffectConstants.class.getCanonicalName());
            }
        } catch (IllegalStateException e) {
            // Exception thrown by getAudioManager() when mView is null
            Log.e(mTag, "FATAL EXCEPTION when attempting to play sound effect: " + e);
            e.printStackTrace();
        }
    }

根据不同的direction方向播放不同的音效,最终调用AudioService.playSoundEffect()方法播放音效。传递的effectType参数值表示音效类型,在audio_assets.xml里配置:

    <asset id="FX_KEY_CLICK" file="Effect_Tick.ogg"/>
    <asset id="FX_FOCUS_NAVIGATION_UP" file="Effect_Tick.ogg"/>
    <asset id="FX_FOCUS_NAVIGATION_DOWN" file="Effect_Tick.ogg"/>
    <asset id="FX_FOCUS_NAVIGATION_LEFT" file="Effect_Tick.ogg"/>
    <asset id="FX_FOCUS_NAVIGATION_RIGHT" file="Effect_Tick.ogg"/>
    <asset id="FX_KEYPRESS_STANDARD" file="KeypressStandard.ogg"/>
    <asset id="FX_KEYPRESS_SPACEBAR" file="KeypressSpacebar.ogg"/>
    <asset id="FX_KEYPRESS_DELETE" file="KeypressDelete.ogg"/>
    <asset id="FX_KEYPRESS_RETURN" file="KeypressReturn.ogg"/>
    <asset id="FX_KEYPRESS_INVALID" file="KeypressInvalid.ogg"/>
    <asset id="FX_BACK" file="Effect_Tick.ogg"/>

可以通过修改这个配置或者对应的ogg文件来定制按键音效。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值