Android的key事件源碼分析

遇到的問題:

用戶的應(yīng)用通過遙控器按鍵切換焦點(diǎn)時(shí),已經(jīng)開啟了系統(tǒng)音量,但是沒有切換焦點(diǎn)時(shí)沒有提示聲迎膜。

所以有了本篇文章书斜,一是Key的事件分發(fā)邏輯伦连,二是AudioManager.playSoundEffect源碼分析。源碼基于android12袭异。

1. Key事件的分發(fā)邏輯

/frameworks/base/core/java/android/view/ViewRootImpl.java


private int processKeyEvent(QueuedInputEvent q) {
            final KeyEvent event = (KeyEvent) q.mEvent;
         if (mUnhandledKeyManager.preViewDispatch(event)) {
                return FINISH_HANDLED;
            }

            // Deliver the key to the view hierarchy.
            if (mView.dispatchKeyEvent(event)) {
                return FINISH_HANDLED;
            }

            if (shouldDropInputEvent(q)) {
                return 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) {
                if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
                    groupNavigationDirection = View.FOCUS_FORWARD;
                } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
                        KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
                    groupNavigationDirection = View.FOCUS_BACKWARD;
                }
            }

            // 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)) {
                    return FINISH_HANDLED;
                }
                if (shouldDropInputEvent(q)) {
                    return FINISH_NOT_HANDLED;
                }
            }

            // Apply the fallback event policy.
            if (mFallbackEventHandler.dispatchKeyEvent(event)) {
                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;
        }
  • 在處理按鍵事件之前钠龙,首先會(huì)調(diào)用mUnhandledKeyManager.preViewDispatch(event)來判斷是否有未處理的按鍵事件。如果有未處理的事件,則直接返回FINISH_HANDLED表示該事件已被處理碴里。
  • 如果沒有未處理的事件沈矿,則調(diào)用mView.dispatchKeyEvent(event)將按鍵事件分發(fā)給View層次結(jié)構(gòu)。如果View層次結(jié)構(gòu)中的任何一個(gè)View處理了該事件咬腋,則直接返回FINISH_HANDLED表示該事件已被處理细睡。
  • 如果按鍵事件不能被處理或應(yīng)該被丟棄,則調(diào)用shouldDropInputEvent(q)方法來決定是否應(yīng)該丟棄該事件帝火。如果應(yīng)該丟棄溜徙,則返回FINISH_NOT_HANDLED表示該事件未被處理;否則繼續(xù)處理該事件犀填。
  • 如果事件仍未被處理蠢壹,則調(diào)用mUnhandledKeyManager.dispatch(mView, event)來判斷是否有未處理的事件。如果有九巡,則直接返回FINISH_HANDLED表示該事件已被處理图贸。
  • 如果按鍵事件是一個(gè)特定的按鍵(如Tab鍵)并且同時(shí)滿足一些特定的條件,則設(shè)置groupNavigationDirection變量并調(diào)用performKeyboardGroupNavigation方法來處理自動(dòng)的聚焦變化冕广。
  • 如果按鍵事件是一個(gè)快捷鍵(即同時(shí)按下一個(gè)或多個(gè)修飾鍵和另一個(gè)鍵)疏日,則調(diào)用mView.dispatchKeyShortcutEvent(event)將事件分發(fā)給View層次結(jié)構(gòu)來嘗試解釋該按鍵事件。如果該事件被處理撒汉,則返回FINISH_HANDLED表示該事件已被處理沟优。
  • 如果事件仍未被處理,則調(diào)用mFallbackEventHandler.dispatchKeyEvent(event)應(yīng)用回退事件策略來嘗試處理該事件睬辐。如果該事件被處理挠阁,則返回FINISH_HANDLED表示該事件已被處理。
  • 如果事件仍未被處理溯饵,則調(diào)用performFocusNavigation方法來處理自動(dòng)的聚焦變化侵俗。如果該事件被處理,則返回FINISH_HANDLED表示該事件已被處理丰刊。
  • 最后隘谣,如果事件仍未被處理,則返回FORWARD表示該事件應(yīng)該被傳遞給下一個(gè)事件處理程序啄巧。
   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:
                    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();
                if (focused != null) {
                    View v = focused.focusSearch(direction);
                    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)) {
                            Log.i(TAG, "v.requestFocus == true");
                            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;
        }

該方法用于處理焦點(diǎn)導(dǎo)航相關(guān)的按鍵事件寻歧,例如方向鍵和Tab鍵等。該方法接收一個(gè)KeyEvent對(duì)象作為參數(shù)棵帽,根據(jù)不同的按鍵碼和修飾符熄求,計(jì)算出焦點(diǎn)導(dǎo)航的方向,然后嘗試在View樹中找到新的焦點(diǎn)逗概,并將其設(shè)置為當(dāng)前焦點(diǎn)弟晚。如果找到新的焦點(diǎn)并成功設(shè)置為當(dāng)前焦點(diǎn),則播放焦點(diǎn)變化時(shí)的聲音效果,并返回true表示焦點(diǎn)變化事件已被處理卿城。如果沒有找到新的焦點(diǎn)枚钓,或者新的焦點(diǎn)不接受焦點(diǎn)設(shè)置請求,則返回false表示該事件未被處理瑟押。

具體而言搀捷,該方法會(huì)首先根據(jù)按鍵碼和修飾符計(jì)算出焦點(diǎn)導(dǎo)航的方向,然后調(diào)用View的focusSearch方法來查找新的焦點(diǎn)多望。如果找到了新的焦點(diǎn)嫩舟,則計(jì)算出前一個(gè)焦點(diǎn)和新焦點(diǎn)之間的位置關(guān)系,并將此位置關(guān)系轉(zhuǎn)換為新焦點(diǎn)的坐標(biāo)系中的位置怀偷,然后將焦點(diǎn)設(shè)置為新的焦點(diǎn)家厌,并播放相應(yīng)的聲音效果。如果沒有找到新的焦點(diǎn)椎工,則嘗試將焦點(diǎn)設(shè)置為默認(rèn)的焦點(diǎn)饭于,并返回true表示焦點(diǎn)變化事件已被處理。如果前一個(gè)焦點(diǎn)無法處理該焦點(diǎn)導(dǎo)航事件维蒙,則返回false表示該事件未被處理掰吕。

/frameworks/base/core/java/com/android/internal/policy/DecorView.java

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

        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 ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) {
                boolean handled = dispatchKeyShortcutEvent(event);
                if (handled) {
                    return true;
                }
            }

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

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

這是DecoreView類中的一個(gè)方法,用于處理按鍵事件的分發(fā)和處理颅痊。

該方法接收一個(gè)KeyEvent對(duì)象殖熟,并提取出鍵碼和事件類型。如果該事件為按下事件并且重復(fù)次數(shù)為0八千,則執(zhí)行以下操作:

首先吗讶,處理面板按鍵的彈奏:如果面板按鍵已按下但未釋放燎猛,則嘗試在其中執(zhí)行一個(gè)快捷方式恋捆。如果已處理該快捷方式,則返回true表示事件已處理重绷。

然后沸停,如果面板已經(jīng)打開,則在沒有面板按鍵的情況下執(zhí)行其上的快捷方式昭卓。如果已處理該快捷方式愤钾,則返回true表示事件已處理。

如果事件仍未被處理候醒,則檢查Window對(duì)象是否已被銷毀能颁,如果沒有,則獲取Window.Callback對(duì)象并將事件分派給它倒淫。如果已處理該事件伙菊,則返回true表示事件已處理。

最后,如果事件仍未被處理镜硕,則將其傳遞給Window對(duì)象進(jìn)行處理运翼,并返回該事件是否為按下事件的結(jié)果。

    public boolean superDispatchKeyEvent(KeyEvent event) {
        // Give priority to closing action modes if applicable.
        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            final int action = event.getAction();
            // Back cancels action modes first.
            if (mPrimaryActionMode != null) {
                if (action == KeyEvent.ACTION_UP) {
                    mPrimaryActionMode.finish();
                }
                return true;
            }
        }

        if (super.dispatchKeyEvent(event)) {
            return true;
        }
        boolean handle = (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event);
        return handle;
    }

其作用是對(duì) KeyEvent 事件進(jìn)行分發(fā)處理兴枯。該方法首先判斷事件是否為返回鍵血淌,如果是則優(yōu)先處理當(dāng)前的操作模式,如果存在操作模式則先結(jié)束操作模式财剖,否則將事件交由父類ViewGroup進(jìn)行處理悠夯。如果父類能夠處理該事件,則返回 true躺坟,否則返回 false疗疟,并將事件交由該 DecorView 所對(duì)應(yīng)的 ViewRootImpl 實(shí)例進(jìn)行處理。

其中瞳氓,getViewRootImpl() 方法返回當(dāng)前 DecorView 所在的 ViewRootImpl 實(shí)例策彤。如果該實(shí)例存在,則調(diào)用 dispatchUnhandledKeyEvent(event) 方法進(jìn)行處理匣摘,否則返回 false店诗。dispatchUnhandledKeyEvent(event) 方法用于將該事件交由輸入法進(jìn)行處理

/frameworks/base/core/java/android/app/Activity.java

    public boolean dispatchKeyEvent(KeyEvent event) {
        onUserInteraction();

        // Let action bars open menus in response to the menu key prioritized over
        // the window handling it
        final int keyCode = event.getKeyCode();
        if (keyCode == KeyEvent.KEYCODE_MENU &&
                mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
            return true;
        }

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

該方法用于分發(fā)鍵事件,當(dāng)用戶按下或釋放某個(gè)按鍵時(shí)音榜,該方法將被調(diào)用庞瘸。首先,方法調(diào)用 onUserInteraction()赠叼,用于通知 Activity 用戶正在與應(yīng)用程序交互擦囊。

接著,方法檢查事件是否為菜單鍵事件嘴办,如果是瞬场,則首先檢查 ActionBar 是否存在,并且將事件傳遞給 ActionBar 的 onMenuKeyEvent() 方法進(jìn)行處理涧郊。如果 ActionBar 成功處理該事件贯被,則直接返回 true,表示事件已被處理妆艘。

如果事件不是菜單鍵事件或者 ActionBar 無法處理該事件彤灶,則將事件傳遞給窗口處理,并返回窗口處理結(jié)果批旺。如果窗口處理了該事件幌陕,則直接返回 true,表示事件已被處理汽煮。

如果事件既不是菜單鍵事件厅须,也無法被窗口處理,則將事件分發(fā)給 DecorView(該 Activity 的根 View)叹誉,并返回 DecorView 的 dispatchKeyEvent() 方法的處理結(jié)果召嘶。

/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

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

/frameworks/base/core/java/android/view/ViewGroup.java

  @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onKeyEvent(event, 1);
        }

        if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
            if (super.dispatchKeyEvent(event)) {
                Log.e("ViewRootImpl","super.dispatchKeyEvent(event)");
                return true;
            }
        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                == PFLAG_HAS_BOUNDS) {
            if (mFocused.dispatchKeyEvent(event)) {
                Log.e("ViewRootImpl","Focused.dispatchKeyEvent(event)");
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
        }
        return false;
    }

用于將按鍵事件分派到該ViewGroup及其子View。該方法首先通過檢查ViewGroup本身的狀態(tài)(是否擁有焦點(diǎn)且有邊界)來決定是否自己處理KeyEvent,如果ViewGroup本身滿足條件,則通過調(diào)用父類的dispatchKeyEvent方法處理事件,并返回true表示已處理摆寄。如果ViewGroup本身不滿足條件,則將KeyEvent分派到當(dāng)前擁有焦點(diǎn)的子View坯门,如果該子View處理了事件微饥,則返回true表示已處理。如果KeyEvent最終未被處理古戴,則返回false表示未處理欠橘。此方法還包含一些調(diào)試代碼,用于確保事件派發(fā)的一致性现恼。

/frameworks/base/core/java/android/view/View.java

 public boolean dispatchKeyEvent(KeyEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onKeyEvent(event, 0);
        }

        // Give any attached key listener a first crack at the event.
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
            Log.e("ViewRootImpl","mOnKeyListener.onKey");
            return true;
        }

        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            Log.e("ViewRootImpl","event.dispatch");
            return true;
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

該方法用于分派鍵盤事件到對(duì)應(yīng)的視圖肃续,并根據(jù)事件的處理結(jié)果返回一個(gè)布爾值。

方法的參數(shù) KeyEvent event 表示一個(gè)鍵盤事件叉袍,該事件將被分派到相應(yīng)的視圖始锚。方法中的第一步是調(diào)用 mInputEventConsistencyVerifier.onKeyEvent(event, 0) 方法來記錄鍵盤事件的一些基本信息,用于后續(xù)的事件一致性檢查喳逛。然后會(huì)首先調(diào)用視圖的 OnKeyListener 對(duì)象(如果有的話)的 onKey 方法瞧捌,如果該方法返回 true,則表示該鍵盤事件被該監(jiān)聽器處理润文,方法返回 true姐呐,否則,該鍵盤事件被傳遞給了該視圖的 dispatch 方法典蝌。在這里調(diào)用了 event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this) 方法曙砂,該方法會(huì)根據(jù)鍵盤事件的類型,將其分發(fā)給視圖的 onKeyDown 或 onKeyUp 方法進(jìn)行處理赠法。如果該事件被處理了麦轰,則返回 true,否則砖织,會(huì)調(diào)用 mInputEventConsistencyVerifier.onUnhandledEvent(event, 0) 方法記錄該事件未被處理的信息,并返回 false 表示該事件未被處理末荐。

/frameworks/base/core/java/android/view/KeyEvent.java

    public final boolean dispatch(Callback receiver, DispatcherState state,
            Object target) {
        switch (mAction) {
            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;
            }
            case ACTION_UP:
                if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state
                        + ": " + this);
                if (state != null) {
                    state.handleUpEvent(this);
                }
                return receiver.onKeyUp(mKeyCode, this);
            case ACTION_MULTIPLE:
                final int count = mRepeatCount;
                final int code = mKeyCode;
                if (receiver.onKeyMultiple(code, count, this)) {
                    return true;
                }
                if (code != KeyEvent.KEYCODE_UNKNOWN) {
                    mAction = ACTION_DOWN;
                    mRepeatCount = 0;
                    boolean handled = receiver.onKeyDown(code, this);
                    if (handled) {
                        mAction = ACTION_UP;
                        receiver.onKeyUp(code, this);
                    }
                    mAction = ACTION_MULTIPLE;
                    mRepeatCount = count;
                    return handled;
                }
                return false;
        }
        return false;
    }

這段代碼是KeyEvent事件分發(fā)的核心代碼侧纯,主要處理按鍵事件的分發(fā)。當(dāng)一個(gè)按鍵事件被分發(fā)到一個(gè)View時(shí)甲脏,該View首先嘗試處理該事件眶熬。如果該View無法處理該事件妹笆,則事件將被分發(fā)給它的parent View或Activity,直到事件被處理或到達(dá)了View層級(jí)的最頂層娜氏。

具體來說拳缠,該方法根據(jù)KeyEvent的不同Action,分別進(jìn)行處理贸弥。如果Action是ACTION_DOWN窟坐,即按下按鍵的事件,首先會(huì)調(diào)用Callback接口的onKeyDown方法來處理該事件绵疲,并根據(jù)事件處理的結(jié)果進(jìn)行相應(yīng)的處理哲鸳。如果返回true,表示事件被成功處理盔憨,并且設(shè)置了FLAG_START_TRACKING標(biāo)志位徙菠,則會(huì)調(diào)用DispatcherState的startTracking方法來開始追蹤該事件。如果事件是長按事件郁岩,且當(dāng)前正在追蹤該事件婿奔,則會(huì)調(diào)用Callback接口的onKeyLongPress方法來處理長按事件。

如果Action是ACTION_UP问慎,即松開按鍵的事件脸秽,會(huì)調(diào)用Callback接口的onKeyUp方法來處理該事件,并根據(jù)DispatcherState是否存在蝴乔,調(diào)用DispatcherState的handleUpEvent方法來結(jié)束追蹤該事件记餐。

如果Action是ACTION_MULTIPLE,即按鍵事件包含多個(gè)重復(fù)事件薇正,會(huì)調(diào)用Callback接口的onKeyMultiple方法來處理該事件片酝,并根據(jù)KeyEvent的repeatCount和keyCode信息,依次調(diào)用Callback接口的onKeyDown和onKeyUp方法來處理每一個(gè)重復(fù)事件挖腰,最后返回處理結(jié)果雕沿。

總的來說,KeyEvent類的dispatch方法實(shí)現(xiàn)了按鍵事件的分發(fā)和處理猴仑,為Android應(yīng)用程序提供了豐富的按鍵事件處理能力审轮。

2. AudioManager.playSoundEffect源碼分析

framework/base/media/java/android/media/AudioManager.java

public void  playSoundEffect(@SystemSoundEffect int effectType, int userId) {
        if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
            return;
        }

        if (!querySoundEffectsEnabled(userId)) {
            return;
        }

        final IAudioService service = getService();
        try {
            service.playSoundEffect(effectType);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

framework/base/services/core/java/com/android/server/audio/AudioService.java

  public void playSoundEffect(int effectType) {
        playSoundEffectVolume(effectType, -1.0f);
    }

    /** @see AudioManager#playSoundEffect(int, float) */
    public void playSoundEffectVolume(int effectType, float volume) {
        // do not try to play the sound effect if the system stream is muted
        if (isStreamMute(STREAM_SYSTEM)) {
            return;
        }

        if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {
            Log.w(TAG, "AudioService effectType value " + effectType + " out of range");
            return;
        }

        sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_QUEUE,
                effectType, (int) (volume * 1000), null, 0);
    }



                case MSG_PLAY_SOUND_EFFECT:
                    mSfxHelper.playSoundEffect(msg.arg1, msg.arg2);
                    break;

framework/base/services/core/java/com/android/server/audio/SoundEffectsHelper.java

private void onLoadSoundEffects(OnEffectsLoadCompleteHandler onComplete) {
    if (mSoundPoolLoader != null) {
        // 如果已經(jīng)有一個(gè)SoundPoolLoader在加載聲音,則將新的OnEffectsLoadCompleteHandler添加到該SoundPoolLoader中辽俗,
        // 并返回
        mSoundPoolLoader.addHandler(onComplete);
        return;
    }
    if (mSoundPool != null) {
        // 如果SoundPool已經(jīng)被初始化疾渣,則直接運(yùn)行onComplete回調(diào)函數(shù)并返回
        if (onComplete != null) {
            onComplete.run(true /*success*/);
        }
        return;
    }

    logEvent("effects loading started");

    // 創(chuàng)建一個(gè)新的SoundPool實(shí)例
    mSoundPool = new SoundPool.Builder()
            .setMaxStreams(NUM_SOUNDPOOL_CHANNELS)
            .setAudioAttributes(new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build())
            .build();

    // 加載聲音資源
    loadSoundAssets();

    // 創(chuàng)建一個(gè)新的SoundPoolLoader實(shí)例,并將onComplete添加到其handler列表中
    mSoundPoolLoader = new SoundPoolLoader();
    mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() {
        @Override
        public void run(boolean success) {
            // 當(dāng)SoundPoolLoader完成加載聲音時(shí)崖飘,將其置為null榴捡,并在加載失敗時(shí)卸載所有聲音
            mSoundPoolLoader = null;
            if (!success) {
                Log.w(TAG, "onLoadSoundEffects(), Error while loading samples");
                onUnloadSoundEffects();
            }
        }
    });
    mSoundPoolLoader.addHandler(onComplete);

    int resourcesToLoad = 0;
    for (Resource res : mResources) {
        // 對(duì)于每個(gè)Resource對(duì)象,獲取其文件路徑并使用SoundPool加載聲音文件
        String filePath = getResourceFilePath(res);
        int sampleId = mSoundPool.load(filePath, 0);
        if (sampleId > 0) {
            // 如果成功加載朱浴,則將資源標(biāo)記為“未加載”
            res.mSampleId = sampleId;
            res.mLoaded = false;
            resourcesToLoad++;
        } else {
            // 如果加載失敗吊圾,則記錄日志
            logEvent("effect " + filePath + " rejected by SoundPool");
            Log.w(TAG, "SoundPool could not load file: " + filePath);
        }
    }

    if (resourcesToLoad > 0) {
        // 如果有資源需要加載达椰,則設(shè)置超時(shí)定時(shí)器
        sendMsg(MSG_LOAD_EFFECTS_TIMEOUT, 0, 0, null, SOUND_EFFECTS_LOAD_TIMEOUT_MS);
    } else {
        // 如果沒有需要加載的資源,則加載完成
        logEvent("effects loading completed, no effects to load");
        mSoundPoolLoader.onComplete(true /*success*/);
    }
}
    /*package*/ void playSoundEffect(int effect, int volume) {
        sendMsg(MSG_PLAY_EFFECT, effect, volume, null, 0);
    }

                case MSG_PLAY_EFFECT:
                    final int effect = msg.arg1, volume = msg.arg2;
                    // 上面的代碼
                    onLoadSoundEffects(new OnEffectsLoadCompleteHandler() {
                        @Override
                        public void run(boolean success) {
                            if (success) {
                                onPlaySoundEffect(effect, volume);
                            }
                        }
                    });
                    break;


void onPlaySoundEffect(int effect, int volume) {
        float volFloat;
        // use default if volume is not specified by caller
        if (volume < 0) {
            volFloat = (float) Math.pow(10, (float) mSfxAttenuationDb / 20);
        } else {
            volFloat = volume / 1000.0f;
        }

        Resource res = mResources.get(mEffects[effect]);
        if (mSoundPool != null && res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) {
            mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f);
        } else {
            MediaPlayer mediaPlayer = new MediaPlayer();
            try {
                String filePath = getResourceFilePath(res);
                mediaPlayer.setDataSource(filePath);
                mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
                mediaPlayer.prepare();
                mediaPlayer.setVolume(volFloat);
                mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                    public void onCompletion(MediaPlayer mp) {
                        cleanupPlayer(mp);
                    }
                });
                mediaPlayer.setOnErrorListener(new OnErrorListener() {
                    public boolean onError(MediaPlayer mp, int what, int extra) {
                        cleanupPlayer(mp);
                        return true;
                    }
                });
                mediaPlayer.start();
            } catch (IOException ex) {
                Log.w(TAG, "MediaPlayer IOException: " + ex);
            } catch (IllegalArgumentException ex) {
                Log.w(TAG, "MediaPlayer IllegalArgumentException: " + ex);
            } catch (IllegalStateException ex) {
                Log.w(TAG, "MediaPlayer IllegalStateException: " + ex);
            }
        }
    }

函數(shù) onPlaySoundEffect 接收兩個(gè)參數(shù):effect 表示要播放的聲音效果的標(biāo)識(shí)项乒,volume 表示要播放的音量啰劲。如果 volume 參數(shù)小于 0,就使用默認(rèn)音量檀何,否則使用指定的音量蝇裤。函數(shù)內(nèi)部首先根據(jù) effect 參數(shù)獲取對(duì)應(yīng)的資源,然后檢查當(dāng)前是否存在可用的 SoundPool 對(duì)象并且該資源已經(jīng)加載成功埃碱。如果滿足這些條件猖辫,就使用 SoundPool 對(duì)象播放聲音效果;否則砚殿,創(chuàng)建一個(gè)新的 MediaPlayer 對(duì)象啃憎,將聲音效果的資源設(shè)置給它,設(shè)置音量似炎、設(shè)置播放完成和錯(cuò)誤監(jiān)聽器辛萍,最后播放聲音。

整個(gè)函數(shù)的核心是根據(jù)參數(shù)播放聲音效果羡藐,如果能使用 SoundPool贩毕,就使用它進(jìn)行播放,否則使用 MediaPlayer 進(jìn)行播放仆嗦。播放完成和錯(cuò)誤監(jiān)聽器的設(shè)置可以保證在播放完成或者出現(xiàn)錯(cuò)誤時(shí)能及時(shí)釋放資源辉阶,避免資源的泄露。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瘩扼,一起剝皮案震驚了整個(gè)濱河市谆甜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌集绰,老刑警劉巖规辱,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異栽燕,居然都是意外死亡罕袋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門碍岔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浴讯,“玉大人,你說我怎么就攤上這事付秕±颊洌” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵询吴,是天一觀的道長掠河。 經(jīng)常有香客問我,道長猛计,這世上最難降的妖魔是什么唠摹? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮奉瘤,結(jié)果婚禮上勾拉,老公的妹妹穿的比我還像新娘。我一直安慰自己盗温,他們只是感情好藕赞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著卖局,像睡著了一般斧蜕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上砚偶,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天批销,我揣著相機(jī)與錄音,去河邊找鬼染坯。 笑死均芽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的单鹿。 我是一名探鬼主播掀宋,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼仲锄!你這毒婦竟也來了劲妙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤昼窗,失蹤者是張志新(化名)和其女友劉穎是趴,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體澄惊,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡唆途,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掸驱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肛搬。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖毕贼,靈堂內(nèi)的尸體忽然破棺而出温赔,到底是詐尸還是另有隱情,我是刑警寧澤鬼癣,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布陶贼,位于F島的核電站啤贩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拜秧。R本人自食惡果不足惜痹屹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枉氮。 院中可真熱鬧志衍,春花似錦、人聲如沸聊替。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惹悄。三九已至春叫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間俘侠,已是汗流浹背象缀。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爷速,地道東北人央星。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像惫东,于是被迫代替她去往敵國和親莉给。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容