遇到的问题:
用户的应用通过遥控器按键切换焦点时,已经开启了系统音量,但是没有切换焦点时没有提示声。
所以有了本篇文章,一是Key的事件分发逻辑,二是AudioManager.playSoundEffect源码分析。源码基于android12。
1. Key事件的分发逻辑
/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;
}
- 在处理按键事件之前,首先会调用mUnhandledKeyManager.preViewDispatch(event)来判断是否有未处理的按键事件。如果有未处理的事件,则直接返回FINISH_HANDLED表示该事件已被处理。
- 如果没有未处理的事件,则调用mView.dispatchKeyEvent(event)将按键事件分发给View层次结构。如果View层次结构中的任何一个View处理了该事件,则直接返回FINISH_HANDLED表示该事件已被处理。
- 如果按键事件不能被处理或应该被丢弃,则调用shouldDropInputEvent(q)方法来决定是否应该丢弃该事件。如果应该丢弃,则返回FINISH_NOT_HANDLED表示该事件未被处理;否则继续处理该事件。
- 如果事件仍未被处理,则调用mUnhandledKeyManager.dispatch(mView, event)来判断是否有未处理的事件。如果有,则直接返回FINISH_HANDLED表示该事件已被处理。
- 如果按键事件是一个特定的按键(如Tab键)并且同时满足一些特定的条件,则设置groupNavigationDirection变量并调用performKeyboardGroupNavigation方法来处理自动的聚焦变化。
- 如果按键事件是一个快捷键(即同时按下一个或多个修饰键和另一个键),则调用mView.dispatchKeyShortcutEvent(event)将事件分发给View层次结构来尝试解释该按键事件。如果该事件被处理,则返回FINISH_HANDLED表示该事件已被处理。
- 如果事件仍未被处理,则调用mFallbackEventHandler.dispatchKeyEvent(event)应用回退事件策略来尝试处理该事件。如果该事件被处理,则返回FINISH_HANDLED表示该事件已被处理。
- 如果事件仍未被处理,则调用performFocusNavigation方法来处理自动的聚焦变化。如果该事件被处理,则返回FINISH_HANDLED表示该事件已被处理。
- 最后,如果事件仍未被处理,则返回FORWARD表示该事件应该被传递给下一个事件处理程序。
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;
}
该方法用于处理焦点导航相关的按键事件,例如方向键和Tab键等。该方法接收一个KeyEvent对象作为参数,根据不同的按键码和修饰符,计算出焦点导航的方向,然后尝试在View树中找到新的焦点,并将其设置为当前焦点。如果找到新的焦点并成功设置为当前焦点,则播放焦点变化时的声音效果,并返回true表示焦点变化事件已被处理。如果没有找到新的焦点,或者新的焦点不接受焦点设置请求,则返回false表示该事件未被处理。
具体而言,该方法会首先根据按键码和修饰符计算出焦点导航的方向,然后调用View的focusSearch方法来查找新的焦点。如果找到了新的焦点,则计算出前一个焦点和新焦点之间的位置关系,并将此位置关系转换为新焦点的坐标系中的位置,然后将焦点设置为新的焦点,并播放相应的声音效果。如果没有找到新的焦点,则尝试将焦点设置为默认的焦点,并返回true表示焦点变化事件已被处理。如果前一个焦点无法处理该焦点导航事件,则返回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类中的一个方法,用于处理按键事件的分发和处理。
该方法接收一个KeyEvent对象,并提取出键码和事件类型。如果该事件为按下事件并且重复次数为0,则执行以下操作:
首先,处理面板按键的弹奏:如果面板按键已按下但未释放,则尝试在其中执行一个快捷方式。如果已处理该快捷方式,则返回true表示事件已处理。
然后,如果面板已经打开,则在没有面板按键的情况下执行其上的快捷方式。如果已处理该快捷方式,则返回true表示事件已处理。
如果事件仍未被处理,则检查Window对象是否已被销毁,如果没有,则获取Window.Callback对象并将事件分派给它。如果已处理该事件,则返回true表示事件已处理。
最后,如果事件仍未被处理,则将其传递给Window对象进行处理,并返回该事件是否为按下事件的结果。
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;
}
其作用是对 KeyEvent 事件进行分发处理。该方法首先判断事件是否为返回键,如果是则优先处理当前的操作模式,如果存在操作模式则先结束操作模式,否则将事件交由父类ViewGroup进行处理。如果父类能够处理该事件,则返回 true,否则返回 false,并将事件交由该 DecorView 所对应的 ViewRootImpl 实例进行处理。
其中,getViewRootImpl() 方法返回当前 DecorView 所在的 ViewRootImpl 实例。如果该实例存在,则调用 dispatchUnhandledKeyEvent(event) 方法进行处理,否则返回 false。dispatchUnhandledKeyEvent(event) 方法用于将该事件交由输入法进行处理
/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;
}
该方法用于分发键事件,当用户按下或释放某个按键时,该方法将被调用。首先,方法调用 onUserInteraction(),用于通知 Activity 用户正在与应用程序交互。
接着,方法检查事件是否为菜单键事件,如果是,则首先检查 ActionBar 是否存在,并且将事件传递给 ActionBar 的 onMenuKeyEvent() 方法进行处理。如果 ActionBar 成功处理该事件,则直接返回 true,表示事件已被处理。
如果事件不是菜单键事件或者 ActionBar 无法处理该事件,则将事件传递给窗口处理,并返回窗口处理结果。如果窗口处理了该事件,则直接返回 true,表示事件已被处理。
如果事件既不是菜单键事件,也无法被窗口处理,则将事件分发给 DecorView(该 Activity 的根 View),并返回 DecorView 的 dispatchKeyEvent() 方法的处理结果。
/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本身的状态(是否拥有焦点且有边界)来决定是否自己处理KeyEvent,如果ViewGroup本身满足条件,则通过调用父类的dispatchKeyEvent方法处理事件,并返回true表示已处理。如果ViewGroup本身不满足条件,则将KeyEvent分派到当前拥有焦点的子View,如果该子View处理了事件,则返回true表示已处理。如果KeyEvent最终未被处理,则返回false表示未处理。此方法还包含一些调试代码,用于确保事件派发的一致性。
/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;
}
该方法用于分派键盘事件到对应的视图,并根据事件的处理结果返回一个布尔值。
方法的参数 KeyEvent event 表示一个键盘事件,该事件将被分派到相应的视图。方法中的第一步是调用 mInputEventConsistencyVerifier.onKeyEvent(event, 0) 方法来记录键盘事件的一些基本信息,用于后续的事件一致性检查。然后会首先调用视图的 OnKeyListener 对象(如果有的话)的 onKey 方法,如果该方法返回 true,则表示该键盘事件被该监听器处理,方法返回 true,否则,该键盘事件被传递给了该视图的 dispatch 方法。在这里调用了 event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this) 方法,该方法会根据键盘事件的类型,将其分发给视图的 onKeyDown 或 onKeyUp 方法进行处理。如果该事件被处理了,则返回 true,否则,会调用 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事件分发的核心代码,主要处理按键事件的分发。当一个按键事件被分发到一个View时,该View首先尝试处理该事件。如果该View无法处理该事件,则事件将被分发给它的parent View或Activity,直到事件被处理或到达了View层级的最顶层。
具体来说,该方法根据KeyEvent的不同Action,分别进行处理。如果Action是ACTION_DOWN,即按下按键的事件,首先会调用Callback接口的onKeyDown方法来处理该事件,并根据事件处理的结果进行相应的处理。如果返回true,表示事件被成功处理,并且设置了FLAG_START_TRACKING标志位,则会调用DispatcherState的startTracking方法来开始追踪该事件。如果事件是长按事件,且当前正在追踪该事件,则会调用Callback接口的onKeyLongPress方法来处理长按事件。
如果Action是ACTION_UP,即松开按键的事件,会调用Callback接口的onKeyUp方法来处理该事件,并根据DispatcherState是否存在,调用DispatcherState的handleUpEvent方法来结束追踪该事件。
如果Action是ACTION_MULTIPLE,即按键事件包含多个重复事件,会调用Callback接口的onKeyMultiple方法来处理该事件,并根据KeyEvent的repeatCount和keyCode信息,依次调用Callback接口的onKeyDown和onKeyUp方法来处理每一个重复事件,最后返回处理结果。
总的来说,KeyEvent类的dispatch方法实现了按键事件的分发和处理,为Android应用程序提供了丰富的按键事件处理能力。
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) {
// 如果已经有一个SoundPoolLoader在加载声音,则将新的OnEffectsLoadCompleteHandler添加到该SoundPoolLoader中,
// 并返回
mSoundPoolLoader.addHandler(onComplete);
return;
}
if (mSoundPool != null) {
// 如果SoundPool已经被初始化,则直接运行onComplete回调函数并返回
if (onComplete != null) {
onComplete.run(true /*success*/);
}
return;
}
logEvent("effects loading started");
// 创建一个新的SoundPool实例
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();
// 创建一个新的SoundPoolLoader实例,并将onComplete添加到其handler列表中
mSoundPoolLoader = new SoundPoolLoader();
mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() {
@Override
public void run(boolean success) {
// 当SoundPoolLoader完成加载声音时,将其置为null,并在加载失败时卸载所有声音
mSoundPoolLoader = null;
if (!success) {
Log.w(TAG, "onLoadSoundEffects(), Error while loading samples");
onUnloadSoundEffects();
}
}
});
mSoundPoolLoader.addHandler(onComplete);
int resourcesToLoad = 0;
for (Resource res : mResources) {
// 对于每个Resource对象,获取其文件路径并使用SoundPool加载声音文件
String filePath = getResourceFilePath(res);
int sampleId = mSoundPool.load(filePath, 0);
if (sampleId > 0) {
// 如果成功加载,则将资源标记为“未加载”
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) {
// 如果有资源需要加载,则设置超时定时器
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);
}
}
}
函数 onPlaySoundEffect 接收两个参数:effect 表示要播放的声音效果的标识,volume 表示要播放的音量。如果 volume 参数小于 0,就使用默认音量,否则使用指定的音量。函数内部首先根据 effect 参数获取对应的资源,然后检查当前是否存在可用的 SoundPool 对象并且该资源已经加载成功。如果满足这些条件,就使用 SoundPool 对象播放声音效果;否则,创建一个新的 MediaPlayer 对象,将声音效果的资源设置给它,设置音量、设置播放完成和错误监听器,最后播放声音。
整个函数的核心是根据参数播放声音效果,如果能使用 SoundPool,就使用它进行播放,否则使用 MediaPlayer 进行播放。播放完成和错误监听器的设置可以保证在播放完成或者出现错误时能及时释放资源,避免资源的泄露。