這節(jié)課是 Android 開發(fā)(入門)課程 的第二部分《多屏幕應(yīng)用》的第四節(jié)課,導(dǎo)師依然是 Jessica Lin 和 Katherine Kuan,這節(jié)課完成了以下兩點(diǎn)內(nèi)容:
- In Miwok App, allow the user to play an audio file to hear the pronunciation of each word when they touch it.
- Learn about Android’s activity lifecycle and learn how to properly clean up resources in the right callbacks.
這節(jié)課雖然只添加了音頻播放的功能,但實(shí)際上知識點(diǎn)非常多,擴(kuò)展內(nèi)容也非常豐富,完全學(xué)習(xí)下來需要花些時間。因此,這篇筆記僅關(guān)注這節(jié)課的內(nèi)容,主要思路仍按 Miwok App 進(jìn)行,擴(kuò)展學(xué)習(xí)的分享會在以后發(fā)布。
關(guān)鍵詞:合理安排實(shí)現(xiàn)新需求的步驟、MediaPlayer、static method、AdapterView.OnItemClickListener、Anonymous Classes、toString、Async Callback、Activity Lifecycle、Audio Focus、AudioManager、FrameLayout、觸摸反饋、RippleDrawable
合理安排新需求的實(shí)現(xiàn)步驟
首先來看如何給 Miwok App 添加音頻,正如課程 3 介紹的,面對一個新需求時,開發(fā)者要能夠合理安排實(shí)現(xiàn)的步驟:
- Modify list item layout to include a play button
- Handle clicking on a list item to play an audio file
- Add in all audio files
- Modify Word class to store audio resource ID
- Play correct audio file per word
- Visual polish
當(dāng)然上述步驟不是一成不變的,在實(shí)際 coding 的過程中總會發(fā)現(xiàn)有更多的任務(wù)要去處理。
在列出所有實(shí)現(xiàn)功能的步驟后,就要安排各個任務(wù)的優(yōu)先級。由于之前沒有做過添加音頻的功能,所以第一步可以新建一個工程來驗(yàn)證添加音頻功能的可行性,驗(yàn)證成功后再回到 Miwok App 中來。
Google 搜索 "Android play audio" 可以找到 MediaPlayer API 的相關(guān)文檔,還有 MediaPlayer 的入門教程,略讀后發(fā)現(xiàn)它可以實(shí)現(xiàn)播放音頻的功能,所以我們的示例應(yīng)用就可以用 MediaPlayer 來測試。
右鍵 app→res 選擇 New→Android resource directory 打開對話框,選擇 Resource type 為 raw,點(diǎn)擊 OK 新建目錄并將音頻文件放到 raw 目錄下。
延續(xù) Android App 代碼與資源分離的風(fēng)格,并且不同資源之間也分類存放,包括圖片、字符串等不同類型的資源,針對不同分辨率設(shè)備的圖片或不同語言的字符串等替代資源 (Alternative Resources) 都會在不同目錄下存放。更多信息可參考相應(yīng)文檔 (Providing Resources)。實(shí)際上手發(fā)現(xiàn) MediaPlayer 的入門操作非常簡單,調(diào)用
create()
新建一個 MediaPlayer 對象實(shí)例,接著調(diào)用start()
或pause()
就能控制音頻的播放或暫停,其他功能也可以通過簡單調(diào)用 method 實(shí)現(xiàn)。
但是,這只是根據(jù)教程復(fù)制粘貼了幾行代碼而已,距離正確地在 Android 使用媒體播放 (Media Playback) 還很遙遠(yuǎn),不過這已經(jīng)達(dá)到在示例應(yīng)用驗(yàn)證 MediaPlayer 添加音頻功能可行性的目的了。在回到 Miwok App 之前,先簡單介紹一下 MediaPlayer API。
MediaPlayer Class
MediaPlayer 是一個比較復(fù)雜的 class,它同時支持播放音頻和視頻,媒體文件可以與 App 綁定,也可以是網(wǎng)絡(luò)的流媒體,它支持的所有媒體格式可以在 Android 文檔 (Supported Media Formats) 中查看。
MediaPlayer 的操作是由狀態(tài)機(jī) (State Machine) 管理的,狀態(tài)機(jī)控制 MediaPlayer 在不同狀態(tài)之間過渡,開發(fā)者可以根據(jù)不同狀態(tài)對 MediaPlayer 進(jìn)行不同操作,也可以執(zhí)行 method 使 MediaPlayer 在狀態(tài)之間切換。
- Idle: 空閑狀態(tài),等待指令,不會發(fā)出聲音;
- Prepared: 準(zhǔn)備狀態(tài),準(zhǔn)備播放媒體文件,文件已加載,但尚未開始播放;
- Started: 開始狀態(tài),開始播放媒體文件;
- Paused: 暫停狀態(tài),暫停播放媒體文件,此狀態(tài)可返回開始狀態(tài);
- Stopped: 停止?fàn)顟B(tài),停止播放媒體文件,可從 Started 或 Paused 狀態(tài)切換過來,但 Stopped 狀態(tài)只能切換到 Prepared 狀態(tài),重新加載媒體文件。
上面是 MediaPlayer 狀態(tài)機(jī)的簡化版本,只覆蓋了常規(guī)的狀態(tài),讓腦海里有個概念。完整狀態(tài)機(jī)可以在 MediaPlayer 文檔 中看到,包括結(jié)束 (End) 狀態(tài),是釋放 MediaPlayer 資源后的狀態(tài);還有媒體播放完畢后進(jìn)入的 PlaybackCompleted 狀態(tài)。這兩個狀態(tài)的操作會在后面提到。
-
使 MediaPlayer 在不同狀態(tài)之間切換,需要調(diào)用 MediaPlayer 對象的 method,那么就先要將 MediaPlayer 實(shí)例化,即新建一個 MediaPlayer 對象。
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.song);
與常見的新建 Toast 對象的方法類似,這里使用的是工廠方法,而不是構(gòu)造函數(shù)。因此 create()
是一個靜態(tài) (static) method,它屬于 class 而不是單個實(shí)例,所以在調(diào)用 create()
時,.
前面的數(shù)據(jù)類型是 class,在這里是 MediaPlayer
,而不是對象實(shí)例 mediaPlayer
。這樣做的好處是可直接調(diào)用 method 而無需先新建對象實(shí)例。
非靜態(tài) (regular) method 則可以通過對象實(shí)例來調(diào)用。
MediaPlayer API 提供了多個 create()
method,這里選擇的新建方法有兩個輸入?yún)?shù):當(dāng)前 Context 和媒體文件的資源 ID。返回值是一個 MediaPlayer 對象。另外,調(diào)用 create()
method 會自動調(diào)用 prepare()
method,所以在新建 MediaPlayer 對象之后可以直接調(diào)用 start()
method 播放媒體文件,如
// 由于 start() 不是 static method,所以可以用對象實(shí)例 mediaPlayer 調(diào)用。
mediaPlayer.start();
從示例應(yīng)用回到 Miwok App,要播放列表中的每一項 Miwok 發(fā)音,目前僅使用上述兩條 MediaPlayer 指令就可以實(shí)現(xiàn)了,那么接下來就要將 ListView 上的點(diǎn)擊事件操作設(shè)置為使用 MediaPlayer 播放音頻。
AdapterView.OnItemClickListener
與之前為 TextView 和 Button 設(shè)置的監(jiān)聽器 (OnClickListener) 不同,ListView 的列表項 (item) 數(shù)量不是固定的,所以無法給每一項單獨(dú)設(shè)置監(jiān)聽器,比如不可能為一個有 1000 項的 ListView 設(shè)置 1000 個監(jiān)聽器。
- Google 搜索 "handle listview item click android",結(jié)合 ListView 文檔 可以找到
setOnItemClickListener(AdapterView.OnItemClickListener listener)
這個 method 負(fù)責(zé)注冊一個回調(diào)函數(shù) (callback),當(dāng) AdapterView 發(fā)生點(diǎn)擊事件時會調(diào)用這個回調(diào)函數(shù)。
AdapterView 是一個超級類,包括 ListView、GridView、Spinner 都是它的子類 (Subclass)。因?yàn)?OnItemClickListener 是在 AdapterView 中定義的,ListView 將它從父類繼承,所以這里出現(xiàn) AdapterView。同時也說明 GridView 和 Spinner 的點(diǎn)擊事件監(jiān)聽器也是由這個 method 注冊的。
- 與 TextView 和 Button 的 OnClickListener 類似,AdapterView 的 OnItemClickListener 也是一個接口,它有一個抽象方法 onItemClick,需要開發(fā)者定義代碼,也就是點(diǎn)擊事件發(fā)生時執(zhí)行的代碼。
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Create and setup the {@link MediaPlayer} for the audio resource associated with the current word
mMediaPlayer = MediaPlayer.create(NumbersActivity.this, words.get(position).getAudioResourceId());
// Start the audio file
mMediaPlayer.start();
}
});
(1)為 listView 設(shè)置監(jiān)聽器,傳入 AdapterView.OnItemClickListener 接口函數(shù),并在行內(nèi)定義一個抽象 method (Override onItemClick),它被稱作匿名類 (Anonymous Class)。匿名類只能訪問 Activity 的全局變量或聲明為 final 的本地變量,所以這里用到 words 就要把 ArrayList 的聲明前添加 final 關(guān)鍵詞:
// Create a list of words
final ArrayList<Word> words = new ArrayList<>();
如果一個接口需要定義多個抽象 method,那么一般采用在單獨(dú)的文件新建 class,再調(diào)用構(gòu)造函數(shù)的方式。
(2)onItemClick callback 有四個輸入?yún)?shù):
- AdapterView: 點(diǎn)擊事件發(fā)生的 AdapterView,在這里是 listView;
- View: AdapterView 內(nèi)被點(diǎn)擊的項,在這里可以是 listView 內(nèi)的 TextView 或 ImageView;
- position: AdapterView 內(nèi)被點(diǎn)擊項的位置,在這里是 ListView 的列表順序;
- id: AdapterView 內(nèi)被點(diǎn)擊項的數(shù)字 ID,它可以由開發(fā)者任意指定,在大多數(shù)情況下不用。
(3)在 AdapterView.OnItemClickListener 類內(nèi)調(diào)用 MediaPlayer.create()
,第一個輸入?yún)?shù) Context 如果直接用 this
會指向當(dāng)前類,即 OnItemClickListener,所以需要用 NumbersActivity.this
指定 Activity 才能正確傳入 Context。
(4)使 ListView 的每一項都對應(yīng)正確的音頻文件,首先需要修改 Word 自定義類,為 Word 的構(gòu)造函數(shù)添加一個 int 輸入?yún)?shù),傳入音頻資源 ID,并設(shè)置相應(yīng)的 getter method。然后在 words.add()
輸入 raw 資源 ID 后,就將它作為 AdapterView.OnItemClickListener 的第二個輸入?yún)?shù)傳入:
words.get(position).getAudioResourceId()
這里通過 ArrayList words 的 get(position)
獲取發(fā)生點(diǎn)擊事件的列表項,position 在上面提到是 listView 中被點(diǎn)擊項的列表順序;再調(diào)用 Word 類的 getAudioResourceId()
獲取音頻資源 ID。
Tips:
1. 添加代碼后注意添加注釋。
2. 字符過多時注意換行(在 Android Studio→Preferences 搜索 "right margin" 可找到:在 Editor→General→Appearance→Show right margin 勾選開啟字符數(shù)指示豎線;在 Editor→Code Style→Default Options→Right margin (columns) 設(shè)置單行的字符數(shù),默認(rèn)為 100)。
3. 實(shí)現(xiàn) toString()
method,將整個對象當(dāng)作字符串輸出,通常用于調(diào)試,了解 Java 對象的狀態(tài)。(將光標(biāo)放在已有 method 之外,class 之內(nèi),使用快捷鍵 "cmd+N" 自動生成一個 method,支持 getter、setter、toString、構(gòu)造函數(shù)等)
Word 類自動生成的 toString()
method 如下。
/**
* Returns the string representation of the {@link Word} object.
*/
@Override
public String toString() {
return "Word{" +
"mDefaultTranslation='" + mDefaultTranslation + '\'' +
", mMiwokTranslation='" + mMiwokTranslation + '\'' +
", mAudioResourceId=" + mAudioResourceId +
", mImageResourceId=" + mImageResourceId +
'}';
}
使用 Log 語句調(diào)試時,可以如下應(yīng)用:
Log.v("NumbersActivity", "Current word: " + word);
這里字符串連接的是 Word 對象本身,Java 會在后臺調(diào)用其 toString()
method,所以在這里 word
與 word.toString()
的結(jié)果是相同的。
至此,Miwok App 的音頻功能已經(jīng)實(shí)現(xiàn)了,但是正如前面提到的,這離正確使用媒體播放 (Media Playback) 還很遠(yuǎn),下面將朝著這個方向逐步優(yōu)化 Miwok App。
MediaPlayer.OnCompletionListener
通過 MediaPlayer 狀態(tài)機(jī)可知,當(dāng)音頻播放完畢,未設(shè)置循環(huán)播放,且 OnCompletionListener 調(diào)用 onCompletion() 時,MediaPlayer 會切換到 PlaybackCompleted 狀態(tài)。其中,MediaPlayer 的 OnCompletionListener 也是一個接口,它有一個抽象方法 onCompletion,需要開發(fā)者定義代碼,也就是音頻播放完畢時執(zhí)行的代碼。
/**
* This listener gets triggered when the {@link MediaPlayer} has completed playing the audio file.
*/
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
// Now that the sound file has finished playing, release the media player resources.
releaseMediaPlayer();
}
};
這種設(shè)置監(jiān)聽器并定義回調(diào)函數(shù)的模式叫做異步回調(diào) (Asynchronous Callback, abbr. Async Callback),在 Android 中是一種常見的模式。與逐行執(zhí)行的同步 (Synchronous) 代碼不同,異步回調(diào)的代碼僅在指定事件發(fā)生時執(zhí)行,在其他情況下不影響系統(tǒng),應(yīng)用可執(zhí)行其它代碼。
MediaPlayer.OnCompletionListener 同樣用匿名類定義了 onCompletion method,執(zhí)行一條指令
releaseMediaPlayer();
。正如上面提到的,音頻播放完畢后釋放 MediaPlayer 資源,使其切換到 End 狀態(tài)。這是基于管理 Android 內(nèi)存的考慮,尤其是對于移動設(shè)備而言,內(nèi)存是非常珍貴的資源。當(dāng) App 占用大量資源時,設(shè)備不僅會響應(yīng)變慢,還會大量消耗電池電量。與視圖回收類似,節(jié)約內(nèi)存的做法是在使用完資源后,釋放這些資源。
對于 MediaPlayer 而言,在新建對象時需要占用內(nèi)存,播放媒體文件也需要消耗內(nèi)存,所以在媒體文件播放完畢后,調(diào)用release()
釋放這些內(nèi)存,使系統(tǒng)回收這些資源以用于其它地方。這里定義了一個 method,在調(diào)用release()
前判斷 MediaPlayer 對象是否為空 (null),使代碼更健壯 (robust),供需要釋放資源的地方調(diào)用:
/**
* Clean up the media player by releasing its resources.
*/
private void releaseMediaPlayer() {
// If the media player is not null, then it may be currently playing a sound.
if (mediaPlayer != null) {
// Regardless of the current state of the media player, release its resources because we no longer need it.
mMediaPlayer.release();
// Set the media player back to null. For our code, we've decided that
// setting the media player to null is an easy way to tell that the media player
// is not configured to play an audio file at the moment.
mediaPlayer = null;
}
}
這里要求 Activity 中有一個全局 MediaPlayer 對象。
/**
* Handles playback of all the sound files
*/
private MediaPlayer mediaPlayer;
Activity Lifecycle
一般情況下,一個 Activity 對應(yīng)一個屏幕,當(dāng)用戶離開當(dāng)前屏幕或離開 App 時,Android 最終會銷毀 Activity 以節(jié)省資源,這個策略會對 App 造成很大的影響。例如用戶在郵件 App 編寫郵件的過程中,屏幕切換到桌面或其它應(yīng)用時,App 應(yīng)該在 Activity 被銷毀前保存草稿,否則當(dāng)用戶回到 App 時發(fā)現(xiàn)之前編寫的內(nèi)容消失了,這會造成很不好的體驗(yàn)。因此,App 應(yīng)對用戶隨時離開 Activity 做好準(zhǔn)備,這里就需要引入 Activity 生命周期 (Lifecycle) 的概念了。
與 MediaPlayer 的狀態(tài)機(jī)類似,Activity 生命周期也有不同階段,而且可以在不同階段之間過渡。不同的是,Activity 的階段切換由 Android 控制,開發(fā)者只能在階段過渡時通過回調(diào)函數(shù)進(jìn)行操作,而無法控制階段切換。
一個 Activity 的完整生命周期如上圖所示。
- Activity 啟動時,它會通過
onCreate()
進(jìn)入 Created 狀態(tài),然后通過onStart()
進(jìn)入 Started 狀態(tài),這個階段 Activity 開始在屏幕上顯示,對用戶可見; - 接下來通過
onResume()
進(jìn)入 Resumed 狀態(tài),此時 Activity 不僅對用戶可見,還支持用戶交互,如播放媒體文件,使用攝像頭或 GPS 傳感器等,只要用戶一直待在這一界面,Activity 就會一直保持 Resumed 狀態(tài); - 當(dāng)用戶切換到其它 Activity 時,它會通過
onPaused()
進(jìn)入 Paused 狀態(tài),接著通過onStop()
進(jìn)入 Stopped 狀態(tài),此時 Activity 不再用戶可見; - 在 Stopped 狀態(tài)時,如果 Android 判斷 Activity 不再需要,它就會通過
onDestroyed()
銷毀 Activity 以釋放資源,使 Activity 進(jìn)入 Destroyed 狀態(tài);否則,Android 會保留 Activity 在 Stopped 狀態(tài),直到用戶回到 Activity,它會通過onRestart()
回到 Started 狀態(tài)。
可以看到,Activity 的每個階段都有相應(yīng)的 callback,Android Activity 文檔 列出了 Activity 生命周期的所有回調(diào)函數(shù)。開發(fā)者就是通過 override 這些 callback 來操作 Activity,例如 Miwok App 中大部分代碼都寫在 onCreated
中,表示 Activity 在創(chuàng)建時進(jìn)行的操作。
在 Miwok App 中,要求切換 Activity 時,釋放 MediaPlayer 資源,所以需要 override onStop() 調(diào)用 releaseMediaPlayer();
。
@Override
protected void onStop() {
super.onStop();
// Release MediaPlayer resource
releaseMediaPlayer();
}
- 在 class 內(nèi)輸入快捷鍵 "ctrl+O" 打開對話框,快速新建 method;
-
super.onStop();
這條指令是 Android Studio 自動添加的,每個 callback 都要調(diào)用對應(yīng)的超級類 callback,因?yàn)樗鼤?zhí)行 AppCompatActivity 的代碼,這才是實(shí)現(xiàn) Activity 的源碼,如顯示窗口等。
能夠釋放不需要的資源對 Miwok App 來說是一大優(yōu)化,不過它還能做得更好。目前 Miwok App 還沒有引入任何 MediaPlayer 音頻播放的管理,這會導(dǎo)致一些問題,下面詳細(xì)描述這一點(diǎn)。
Audio Focus
在 Android 設(shè)備上,如果 App 不對音頻播放進(jìn)行管理,那么可能就會發(fā)生兩個音樂應(yīng)用同時播放,或者用戶收到來電時音樂仍在播放的問題。為了避免這種情況,做一個 Android 良民,App 需要使用 Audio Focus 來管理音頻播放。
引入 Audio Focus 后,只有獲取 Audio Focus 的 App 才能播放音頻,失去 Audio Focus 時需要暫停或停止播放,具體的操作可以閱讀這篇博客,里面提到:
(1)調(diào)用 AudioManager 的 requestAudioFocus() 來請求 Audio Focus;
(2)調(diào)用 AudioManger 的 abandonAudioFocus() 來釋放 Audio Focus;
(3)當(dāng) Audio Focus 狀態(tài)改變時,新建 AudioManger.OnAudioFocusChangeListener 接口對象,定義 onAudioFocusChange 抽象 method 來作出響應(yīng)。
可以看出,Audio Focus 事實(shí)上是由 AudioManager 提供支持的。
- 獲取 AudioManager
// Create and setup the {@link AudioManager} to request audio focus
AudioManager mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
從上面的代碼可以看到,AudioManager 是一項系統(tǒng)服務(wù) (System Service)。系統(tǒng)服務(wù)可以為 App 提供常用功能,如通知、鬧鐘管理器服務(wù);有些服務(wù)可訪問設(shè)備硬件,如位置信息管理器。AudioManger 就是為 App 提供音頻管理服務(wù)的。但說到底,系統(tǒng)服務(wù)也只是一個 Java class,App 通過對象實(shí)例化然后調(diào)用其 method 來獲取各種功能。類似的,API (Application Programming Interface) 就是 Android 框架提供給開發(fā)者的 class 和 method。
- 調(diào)用
requestAudioFocus()
請求 Audio Focus
// Request audio focus so in order to play the audio file. The app needs to play a
// short audio file, so we will request audio focus with a short amount of time
// with AUDIOFOCUS_GAIN_TRANSIENT.
int result = mAudioManager.requestAudioFocus(mOnAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
requestAudioFocus()
有三個輸入?yún)?shù):
(1)AudioManager.OnAudioFocusChangeListener: 在 Audio Focus 狀態(tài)改變時響應(yīng)的監(jiān)聽器,在對象實(shí)例化后傳入。
(2)streamType: 音頻文件的類型,從 API 提供的類型中選擇,它們是全部大寫的常量,如
- STREAM_ALARM: 鬧鐘類型的音頻
- STREAM_MUSIC: 音樂類型的音頻
- STREAM_RING: 鈴聲類型的音頻
(3)durationHint: 音頻文件的長度聲明,從 API 提供的類型中選擇,它們是全部大寫的常量,有
- AUDIOFOCUS_GAIN_TRANSIENT: 聲明暫時獲取 Audio Focus,持續(xù)時間短,例如消息提示音。
- AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: 聲明暫時獲取 Audio Focus,持續(xù)時間短,同時可接受其它應(yīng)用降低音量后 (ducking) 繼續(xù)播放,例如導(dǎo)航提示語音。
- AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: 聲明暫時獲取 Audio Focus,持續(xù)時間短,期間不允許其它應(yīng)用或系統(tǒng)聲音,例如語音助手。
- AUDIOFOCUS_GAIN: 獲取未知持續(xù)時間的 Audio Focus,即一直占用 Audio Focus 直到失去它。
requestAudioFocus()
的返回值為 AUDIOFOCUS_REQUEST_FAILED (0) 或 AUDIOFOCUS_REQUEST_GRANTED (1),指示是否成功獲取 Audio Focus。
Note:
上述請求 Audio Focus 的 method 在 API 26 中已被棄用,應(yīng)使用以下 method:
int requestAudioFocus (AudioFocusRequest focusRequest)
(1)輸入?yún)?shù):AudioFocusRequest 的對象實(shí)例,不能為 null
(2)返回值:與上述 method 類似,但多了 AUDIOFOCUS_REQUEST_DELAYED,表示延遲獲取 Audio Focus(需要設(shè)置 setAcceptsDelayedFocusGain 為 true)
- 當(dāng) Audio Focus 狀態(tài)改變時作出響應(yīng)
/**
* This listener gets triggered whenever the audio focus changes
* (i.e., we gain or lose audio focus because of another app or device).
*/
private AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// The AUDIOFOCUS_LOSS_TRANSIENT case means that we've lost audio focus for a
// short amount of time. The AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK case means that
// our app is allowed to continue playing sound but at a lower volume. We'll treat
// both cases the same way because our app is playing short sound files.
// Pause playback and reset player to the start of the file. That way, we can
// play the word from the beginning when we resume playback.
mMediaPlayer.pause();
mMediaPlayer.seekTo(0);
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// The AUDIOFOCUS_GAIN case means we have regained focus and can resume playback.
mMediaPlayer.start();
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
// The AUDIOFOCUS_LOSS case means we've lost audio focus and
// Stop playback and clean up resources
releaseMediaPlayer();
}
}
};
與 AdapterView 的 OnItemClickListener 接口和 MediaPlayer 的 OnCompletionListener 接口類似,AudioManger 的 OnAudioFocusChangeListener 接口也有一個抽象 method: onAudioFocusChange
,只有一個輸入?yún)?shù):focusChange,表示 Audio Focus 變化的四種狀態(tài),它們是全部大寫的常量:
- AUDIOFOCUS_GAIN: 重新獲取 Audio Focus,此時應(yīng)該恢復(fù)播放。
- AUDIOFOCUS_LOSS: 永久失去 Audio Focus,此時應(yīng)該停止播放,但考慮到用戶誤觸其它應(yīng)用的播放按鈕的情況,可先暫停一段時間,如果用戶一直沒有回到 App 再停止播放。
- AUDIOFOCUS_LOSS_TRANSIENT: 暫時失去 Audio Focus,此時應(yīng)該暫停播放,等待重新獲取 Audio Focus。
- AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 暫時失去 Audio Focus,但是期間允許降低音量后 (ducking) 繼續(xù)播放;根據(jù)不同應(yīng)用的需求,也可以暫停播放,如 Miwok App 就選擇暫停并重置音頻,因?yàn)閱卧~的發(fā)音需要清晰地傳遞給用戶。
開發(fā)者就是根據(jù)這個參數(shù)(引用時在常量名前添加類名 AudioManager)做出不同的響應(yīng)的。與前兩者不同,這里沒有匿名類,而是另外新建 method,保持代碼模塊化,不會局部臃腫。
- 調(diào)用
abandonAudioFocus()
釋放 Audio Focus
// Regardless of whether or not we were granted audio focus, abandon it. This also
// unregisters the AudioFocusChangeListener so we don't get anymore callbacks.
mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener);
輸入?yún)?shù)傳入 AudioManager.OnAudioFocusChangeListener 的對象實(shí)例即可。
Note:
上述釋放 Audio Focus 的 method 在 API 26 中已被棄用,應(yīng)使用以下 method:
int abandonAudioFocusRequest (AudioFocusRequest focusRequest)
輸入?yún)?shù)為 AudioFocusRequest 的對象實(shí)例,與獲取 Audio Focus 時的相同,不能為 null。
至此,Miwok App 對 MediaPlayer 做了資源管理,也對音頻播放做了管理,這基本上就已經(jīng)優(yōu)化完全了,最后完成幾點(diǎn)視覺優(yōu)化。
觸摸反饋
觸摸反饋是指在用戶與 UI 元素交互的接觸點(diǎn)為用戶提供一種即時外觀確認(rèn)。 App 一定要有觸摸反饋,這樣可以讓 App 看起來響應(yīng)速度很快。在 Android Lollipop 5.0 采用 Material Design 后,觸摸反饋是圓形漣漪的動畫效果,在先前的 Android 版本上則是靜態(tài)的彩色按下反饋。為視圖添加觸摸反饋的一個簡單做法是,利用 Android 特性,設(shè)置背景為
android:background="?android:attr/selectableItemBackground"
這樣就使視圖具有觸摸反饋了,但同時視圖背景也變成了透明的。簡單的解決辦法是將每個視圖放進(jìn) FrameLayout 中,在 FrameLayout 中設(shè)置背景顏色。
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/category_numbers">
<TextView
android:id="@+id/numbers"
style="@style/CategoryStyle"
android:background="?android:attr/selectableItemBackground"
android:text="@string/category_numbers" />
</FrameLayout>
FrameLayout 被設(shè)計用于分隔出一部分屏幕來顯示單個視圖,以保證該視圖在不同尺寸的屏幕上能夠完整顯示,而不會與其它視圖重疊。所以通常 FrameLayout 只有一個子視圖,當(dāng)然也可以有多個子視圖,這種情況下可以通過設(shè)置 gravity 來控制它們的位置)。
上面是為視圖添加觸摸反饋的一個簡單方法,可以看到這種方法向視圖層級結(jié)構(gòu)中引入了更多視圖,因此效率不高。事實(shí)上,圓形漣漪的動畫觸摸反饋是由 RippleDrawable class (API level 21) 實(shí)現(xiàn)的,開發(fā)者可以自定義反饋效果,如設(shè)置漣漪效果的顯示范圍或持續(xù)時間。上面用到的 selectableItemBackground
特性就是由 RippleDrawable 實(shí)現(xiàn)的,具體介紹會在 進(jìn)階 課程中出現(xiàn),這里暫時不做解釋。
針對 ListView,設(shè)置 android:drawSelectorOnTop 為 true 也可以使每個列表項都將顯示按下狀態(tài)。
<?xml version="1.0" encoding="utf-8"?>
<ListView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="true"/>
Tips:
Material Design 提供了超過 900 個圖標(biāo)供開發(fā)者免費(fèi)使用,并且提供黑白兩色,18dp、24dp、36dp、48dp 四組大小選擇,圖標(biāo)格式包括 SVG、PNGS、ICON FONT。圖標(biāo)按照不同分辨率分類放在不同文件夾內(nèi)。