#### 1. 觸發音量鍵
音量鍵被按下后,Android輸入系統將該事件一路派發給Activity,如果無人截獲并消費這個事件,承載當前Activity的顯示的PhoneWindow類的onKeyDown()或onKeyUp()函數將會將其處理,從而開始了通過音量鍵調整音量的處理流程。輸入事件的派發機制以及PhoneWindow類的作用將在后續章節中詳細介紹,現在只需要知道,PhoneWindow描述了一片顯示區域,用于顯示與管理我們所看到的Activity、對話框等內容。同時,它還是輸入事件的派發對象,而且只有顯示在最上面的PhoneWindow才會收到事件。
**注意** 按照Android的輸入事件派發策略,Window對象在事件的派發隊列中排在Activity的后面(應該說排在隊尾比較合適),所以應用程序可以重寫自己的onKeyDown()函數,將音量鍵用作其他的功能。比如說,在一個相機應用中,按下音量鍵所執行的動作是拍照而不是調節音量。
PhoneWindow的onKeyDown()函數實現如下:
**PhoneWindow.java-->PhoneWindow.onKeyDown()**
```
......//加省略號, 略過一些內容
switch (keyCode) {
caseKeyEvent.KEYCODE_VOLUME_UP:
caseKeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
// 直接調用到AudioManager的handleKeyUp里面去了。是不是很簡單而且直接呢
getAudioManager().handleKeyDown(event,mVolumeControlStreamType);
return true;
}
……
}
```
* * * * *
**注意** handleKeyDown()函數的第二個參數,它的意義是指定音量鍵將要改變哪一種流類型的音量。在Android中,音量的控制與流類型是密不可分的,每種流類型都獨立地擁有自己的音量設置,絕大部分情況下互不干擾,例如音樂音量、通話音量就是相互獨立的。所以說,離開流類型談音量是沒有意義的。在Android中,音量這個概念一定是描述的某一種流類型的音量。
* * * * *
這里傳入了mVolumeControlStreamType,那么這個變量的值是從哪里來的呢?做過多媒體應用程序的讀者應該知道,Activity類中有一個函數名為setVolumeControlStream(int streamType)。應用可以通過調用這個函數來指定顯示這個Activity時音量鍵所控制的流類型。這個函數的內容很簡單,就一行如下:
**Activity.java-->Activity.setVolumeControlStream()**
```
getWindow().setVolumeControlStream(streamType);
```
getWindow()的返回值的就是用于顯示當前Activity的PhoneWindow。從名字就可以看出,這個調用改變了mVolumeControlStreamType,于是也就改變了按下音量鍵后傳入AudioManager.handleKeyUp()函數的參數,從而達到了setVolumeControlStream的目的。同時,還應該能看出,這個設置是被綁定到Activity的Window上的,不同Activity之間切換時,接受按鍵事件的Window也會隨之切換,所以應用不需要去考慮在其生命周期中音量鍵所控制的流類型的切換問題。
AudioManager的handleKeyDown()的實現很簡單,在一個switch中,它調用了AudioService的adjustSuggestedStreamVolume(),所以直接看一下AudioService的這個函數。
#### 2. adjustSuggestedStreamVolume()分析
我們先來看函數原型,
```
public voidadjustSuggestedStreamVolume(int direction,
int suggestedStreamType,
int flags)
```
adjustSuggestedStreamVolume()有三個參數,而第三個參數flags的意思就不那么容易猜了。其實AudioManager在handleKeyDown()里設置了兩個flags,分別是FLAG\_SHOW\_UI和FLAG\_VIBRATE。從名字上我們就能看出一些端倪。前者用于告訴AudioService我們需要彈出一個音量控制面板。而在handleKeyUp()里設置了FLAG\_PLAY\_SOUND,這是為什么當松開音量鍵后“有時候”會有一個提示音。注意,handleKeyUp()設置了FLAG\_PLAY\_SOUND,但是只是有時候這個flag才會生效,我們在下面的代碼中能看到為什么。還須要注意的是,第二個參數名為suggestedStreamType,從其命名來推斷,這個參數傳入的流類型對于AudioService來說只是一個建議,是否采納這個建議AudioService則有自己的考慮。
**AudioService.java-->AudioService.adjustSuggestedStreamVolume()**
```
public void adjustSuggestedStreamVolume(intdirection, int suggestedStreamType,
int flags) {格式要調整好
int streamType;
// ①從這一小段代碼中,可以看出在 AudioService中還有地方可以強行改變音量鍵控制的流類型
if(mVolumeControlStream != -1) {
streamType = mVolumeControlStream;
} else {
// ②通過getActiveStreamType()函數獲取要控制的流類型
// 這里根據建議的流類型與AudioService的實際情況,返回一個值
streamType = getActiveStreamType(suggestedStreamType);
}
// ③這個啰嗦的if判斷的目的,就是只有在特定的流類型下,并且沒有處于鎖屏狀態時才會播放聲音
if((streamType != STREAM_REMOTE_MUSIC) &&
(flags & AudioManager.FLAG_PLAY_SOUND) != 0 &&
((mStreamVolumeAlias[streamType] != AudioSystem.STREAM_RING)
|| (mKeyguardManager != null &&mKeyguardManager.isKeyguardLocked()))) {
flags&= ~AudioManager.FLAG_PLAY_SOUND;
}
if(streamType == STREAM_REMOTE_MUSIC) {
…… //我們不討論遠程播放的情況
} else {
// ④調用adjustStreamVolume
adjustStreamVolume(streamType, direction, flags);
}
}
```
**注意** 初看著段代碼時,可能有讀者會對下面這句話感到疑惑:
```
VolumeStreamState streamState =mStreamStates[mStreamVolumeAlias[streamType]];
```
其實這是為了滿足所謂的“將鈴聲音量用作通知音量”這種需求。這樣就需要實現在兩個有這個需求的流A與B之間建立起一個A→B映射。當我們對A流進行音量操作時,實際上是在操作B流。其實筆者個人認為這個功能對用戶體驗的提升并不大,但是卻給AudioService的實現增加了不小的復雜度。直觀上來想,我們可能想使用一個HashMap解決這個問題,鍵是源流類型,值目標流類型。而Android使用了一個更簡單那但是卻不是那么好理解的方法來完成這件事。AudioService用一個名為mStreamVolumeAlias的整形數組來描述這個映射關系。
如果想要實現“以鈴聲音量用作音樂音量”,只需要修改相應位置的值為STREAM\_RING即可,就像下面這樣:
```
mStreamVolumeAlias[AudioSystem.STREAM_MUSIC] =AudioSystem.STREAM_RING;
```
之后,因為需求要求對A流進行音量操作時,實際上是在操作B流,所以就不難理解為什么在很多和流相關的函數里都會先做這樣的一個轉換:
```
streamType = mStreamVolumeAlias[streamType];
```
其具體的工作方式就留給讀者進行思考了。在本章的分析過程中,大可忽略這種轉換,這并不影響我們對音量控制原理的理解。
這個函數簡單來說,做三件事:
- 確定要調整音量的流類型。
- 在某些情況下屏蔽FLAG\_PLAY\_SOUND。
- 調用adjustStreamVolume()。
關于這個函數仍然有幾點需要說明一下。它剛開始的時候有一個判斷,條件是一個名為mVolumeControlStream的整型變量是否等于-1,從這塊代碼來看,mVolumeControlStream比參數傳入的suggestedStreamType厲害多了,只要它不是-1,那么要調整音量的流類型就是它。那這么厲害的控制手段,是做什么用的呢?其實,mVolumeControlStream是VolumePanel通過forceVolumeControlStream()函數設置的。什么是VolumePanel呢?就是我們按下音量鍵后的那個音量條提示框了。VolumePanel在顯示時會調用forceVolumeControlStream強制后續的音量鍵操作固定為促使它顯示的那個流類型。并在它關閉時取消這個強制設置,即置mVolumeControlStream為-1。這個我們在后面分析VolumePanel時會看到。
接下來我們繼續看一下adjustStreamVolume()的實現。
#### 3. adjustStreamVolume()分析
**AudioService.java-->AudioService.adjustStreamVolume()**
```
public void adjustStreamVolume(int streamType, intdirection, int flags) {
// 首先還是獲取streamType映射到的流類型。這個映射的機制確實給我們的分析帶來不小的干擾
// 在非必要的情況下忽略它們吧
int streamTypeAlias = mStreamVolumeAlias[streamType];
// 注意VolumeStreamState類
VolumeStreamState streamState = mStreamStates[streamTypeAlias];
final intdevice = getDeviceForStream(streamTypeAlias);
// 獲取當前音量,注意第二個參數的值,它的目的是如果這個流被靜音,則取出它被靜音前的音量
final intaliasIndex = streamState.getIndex(device,
(streamState.muteCount()!= 0)
booleanadjustVolume = true;
// rescaleIndex用于將音量值的變化量從源流類型變換到目標流類型下
// 由于不同的流類型的音量調節范圍不同,所以這個轉換是必需的
int step= rescaleIndex(10, streamType, streamTypeAlias);
//上面準備好了所需的所有信息,接下來要做一些真正有用的動作了
// 比如說checkForRingerModeChange()。調用這個函數可能變更情景模式
// 它的返回值adjustVolume是一個布爾變量,用來表示是否有必要繼續設置音量值
// 這是因為在一些情況下,音量鍵用來改變情景模式,而不是設置音量值
if(((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
(streamTypeAlias == getMasterStreamType())) {
……
adjustVolume = checkForRingerModeChange(aliasIndex, direction, step);
……
}
int index;
// 取出調整前的音量值。這個值稍后被用在sendVolumeUpdate()的調用中
final intoldIndex = mStreamStates[streamType].getIndex(device,
(mStreamStates[streamType].muteCount() != 0) /* lastAudible */);
// 接下來我們可以看到,只有流沒有被靜音時,才會設置音量到底層去,否則只調整其靜音前的音量
// 為了簡單起見,暫不考慮靜音時的情況
if(streamState.muteCount() != 0) {
……
} else {
// 為什么還要判斷streamState.adjustIndex的返回值呢?
// 因為如果音量值在adjust之后并沒有發生變化,比如說達到了最大值,就不需要繼續后面的操作了
if(adjustVolume && streamState.adjustIndex(direction * step, device)) {
// 發送消息給AudioHandler
// 這個消息在setStreamVolumeInt()函數的分析中已經看到過了
// 這個消息將把音量設置到底層去,并將其存儲到SettingsProvider中去
sendMsg(mAudioHandler,
MSG_SET_DEVICE_VOLUME,
SENDMSG_QUEUE,
device,
0,
streamState,
0);
}
index= mStreamStates[streamType].getIndex(device, false /* lastAudible */);
}
// 最后,調用sendVolumeUpdate函數,通知外界音量值發生了變化
sendVolumeUpdate(streamType, oldIndex, index, flags);
}
```
在這個函數的實現中,有一個非常重要的類型:VolumeStreamState。前面我們提到過,Android的音量是依賴于某種流類型的。如果Android定義了N個流類型,AudioService就需要維護N個音量值與之對應。另外每個流類型的音量等級范圍不一樣,所以還需要為每個流類型維護他們的音量調節范圍。VolumeStreamState類的功能就是為了保存了一個流類型所有音量相關的信息。AudioService為每一種流類型都分配了一個VolumeStreamState對象,并以流類型的值為索引,保存在一個名為數組mStreamStates中。在這個函數中調用了VolumeStreamState對象的adjustIndex()函數,于是就改變了這個對象中存儲的音量值。不過,僅僅是改變了它的存儲值,并沒有把這個變化設置到底層。
總結一下這個函數都作了什么:
- 準備工作。計算按下音量鍵的音量步進值。細心的讀者一定注意到了,這個步進值是10而不是1。原來,在VolumeStreamState中保存的音量值是其實際值的10倍。為什么這么做呢?這是為了在不同流類型之間進行音量轉換時能夠保證一定精度的一種奇怪的實現,其轉換過程讀者可以參考rescaleIndex()函數的實現。我們可以將這種做法理解為在轉換過程中保留了小數點后一位的精度。其實,直接使用float類型來保存豈不是更簡單呢?
- 檢查是否需要改變情景模式。checkForRingerModeChange()和情景模式有關。讀者可以自行研究其實現。
- 調用adjustIndex()更改VolumeStreamState對象中保存的音量值。
- 通過sendMsg()發送消息MSG\_SET\_DEVICE\_VOLUME到mAudioHandler。
- 調用sendVolumeUpdate()函數,通知外界音量發生了變化。
我們將重點分析后面三個內容:adjustIndex()、MSG\_SET\_DEVICE\_VOLUME消息的處理和sendVolumeUpdate()。
#### 4. VolumeStreamState的adjustIndex()分析
我們看一下這個函數的定義:
**AudioService.java-->VolumeStreamState.adjustIndex()**
```
public boolean adjustIndex(int deltaIndex, intdevice) {
// 將現有的音量值加上變化量,然后調用setIndex設置下去
// 返回值與setIndex一樣
return setIndex(getIndex(device, false /* lastAudible */) + deltaIndex,
device,
true /* lastAudible */);
}
```
這個函數很簡單,我們再看一下setIndex()的實現:
**AudioService.java-->VolumeStreamState.setIndex()**
```
public synchronized boolean setIndex(int index, intdevice, boolean lastAudible) {
intoldIndex = getIndex(device, false /*lastAudible */);
index =getValidIndex(index);
// 在VolumeStreamState中保存設置的音量值,注意是用了一個HashMap
mIndex.put(device, getValidIndex(index));
if(oldIndex != index) {
// 保存到lastAudible
if(lastAudible) {
mLastAudibleIndex.put(device, index);
}
// 同時設置所有映射到當前流類型的其他流的音量
boolean currentDevice = (device == getDeviceForStream(mStreamType));
intnumStreamTypes = AudioSystem.getNumStreamTypes();
for(int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
……
}
return true;
} else {
return false;
}
}
```
在這個函數中有三個工作要做:
- 首先是保存設置的音量值,這是VolumeStreamState的本職工作,這和4.1之前的版本不一樣,音量值與設備相關聯了。于是對于同一種流類型來說,在不同的音頻設備下將會擁有不同的音量值。
- 然后就是根據參數的要求保存音量值到mLastAudibleIndex里面去。從名字就可以看出,它保存了靜音前的音量。當取消靜音時,AudioService就會恢復到這里保存的音量。
- 再就是對流映射的處理。既然A->B,那么設置B的音量時,同時要改變A的音量。這就是后面那個循環的作用。
可以看出,VolumeStreamState.adjustIndex()除了更新自己所保存的音量值外,沒有做其他的事情,接下來就看一下MSG\_SET\_DEVICE\_VOLUME的消息處理做了什么。
#### 5. MSG\_SET\_DEVICE\_VOLUME消息的處理
adjustStreamVolume()函數使用sendMsg()函數發送了MSG\_SET\_DEVICE\_VOLUME消息給了mAudioHandler,這個Handler運行在AudioService的主線程上。直接看一下在mAudioHandler中負責處理MSG\_SET\_DEVICE\_VOLUME消息的setDeviceVolume()函數:
**AudioService.java-->AudioHandler.setIndex()**
```
private void setDeviceVolume(VolumeStreamStatestreamState, int device) {
// 調用VolumeStreamState的applyDeviceVolume。
// 這個函數的內容很簡單,就是在調用AudioSystem.setStreamVolumeIndex()
// 到這里,音量就被設置到底層的AudioFlinger里面去了
streamState.applyDeviceVolume(device);
// 和上面一樣,需要處理流音量映射的情況。這段代碼和上面setIndex的相關代碼很像,不是么
intnumStreamTypes = AudioSystem.getNumStreamTypes();
for (int streamType = numStreamTypes - 1; streamType >= 0;streamType--) {
……
}
}
// 發送消息給mAudioHandler,其處理函數將會調用persitVolume()函數這將會把音量的
//設置信息存儲到SettingsProvider中
// AudioService在初始化時,將會從SettingsProvider中將音量設置讀取出來并進行設置
sendMsg(mAudioHandler,
MSG_PERSIST_VOLUME,
SENDMSG_QUEUE,
PERSIST_CURRENT|PERSIST_LAST_AUDIBLE,
device,
streamState,
PERSIST_DELAY);
}
```
**注意** sendMsg()是一個異步的操作,這就意味著,完成adjustIndex()更新音量信息后adjustStreamVolume()函數就返回了,但是音量并沒有立刻地被設置到底層。而且由于Handler處理多個消息的過程是串行的,這就隱含著一個風險:當Handler正在處理某一個消息時發生了阻塞,那么當按下音量鍵時,調用adjustStreamVolume()雖然可以立刻返回,而且從界面上看或者用getStreamVolume()獲取音量值發現都是沒有問題的,但是手機發出聲音時的音量大小并沒有改變。
#### 6. sendVolumeUpdate()分析
接下來,分析一下sendVolumeUpdate()函數,它用于通知外界音量發生了變化。
**AudioService.java-->AudioService.sendVolumeUpdate()**
```
private void sendVolumeUpdate(int streamType, intoldIndex, int index, int flags) {
// 讀者可能會對這句話感到有點奇怪,mVoiceCapable是從SettingsProvider中取出來的一個常量
// 從某種意義上來說,它可以用來判斷設備是否擁有通話功能。對于沒有通話能力的設備來說,RING流類
// 型自然也就沒有意義了。這句話應該算是一種從語義操作上進行的保護
if(!mVoiceCapable && (streamType == AudioSystem.STREAM_RING)) {
streamType = AudioSystem.STREAM_NOTIFICATION;
}
//mVolumePanel是一個VolumePanel類的實例,就是它顯示了音量提示框
mVolumePanel.postVolumeChanged(streamType, flags);
// 發送廣播。可以看到它們都有(x+5)/10的一個操作。為什么要除以10可以理解,但是+5的意義呢
// 原來是為了實現四舍五入
oldIndex= (oldIndex + 5) / 10;
index =(index + 5) / 10;
Intentintent = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
intent.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
mContext.sendBroadcast(intent);
}
```
這個函數將音量的變化通過廣播的形式通知給了其他感興趣得模塊。同時,它還特別通知了mVolumePanel。mVolumePanel是VolumePanel類的一個實例。我們所看到的音量調節通知框就是它了。
至此,從按下音量鍵開始的整個處理流程就完結了。在繼續分析音量調節通知框的工作原李之前,先對之前的分析過程作一個總結,請參考下面的序列圖:
:-: 
圖 3-2 音量鍵調整音量的處理流程
結合上面分析的結果,由圖 3-2可知:
- 音量鍵處理流程的發起者是PhoneWindow。
- AudioManager僅僅起到代理的作用。
- AudioService接受AudioManager的調用請求,操作VolumeStreamState的實例進行音量的設置。
- VolumeStreamState負責保存音量設置,并且提供了將音量設置到底層的方法。
- AudioService負責將設置結果以廣播的形式通知外界。
到這里,相信大家對音量量調節的流程已經有了一個比較清晰的認識了。接下來我們將介紹音量調節通知框的工作原理。
#### 4. 音量調節通知框的工作原理
在分析sendVolumeUpdate()函數時曾經注意到它調用了mVolumePanel的postVolumeChanged()函數。mVolumePanel是一個VolumePanel的實例。作為一個Handler的子類,它承接了音量變化的UI/聲音的通知工作。在繼續上面的討論之前,先了解一下其工作的基本原理。
VolumePanel為于android.view包下,但是卻沒有在API中被提供。因為它只能被AudioService使用,所以和AudioService放在一個包下可能更合理一些。從這個類的注釋上可以看到,谷歌的開發人員對它被放在android.view下也有極大的不滿(What A Mass! 他們這么寫道……)。
VolumePanel下定義了兩個重要的子類型,分別是StreamResources和StreamControl。StreamResources實際上是一個枚舉。它的每一個可用元素保存了一個流類型的通知框所需要的各種資源,如圖標、提示文字等等。其定義就像下面這樣:
**VolumePanel.java-->VolumePanel.StreamResources**
```
private enum StreamResources {
BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO,
R.string.volume_icon_description_bluetooth,
R.drawable.ic_audio_bt,
R.drawable.ic_audio_bt,
false),
// 后面的幾個枚舉項我們省略了其構造參數,與BluetoothSCOStream的內容是一致的
RingerStream(……),
VoiceStream(……),
AlarmStream(……),
MediaStream(……),
NotificationStream(……),
MasterStream(……),
RemoteStream(……);
intstreamType; // 流類型
intdescRes; // 描述信息
inticonRes; // 圖標
inticonMuteRes;// 靜音圖標
booleanshow; // 是否顯示
StreamResources(intstreamType, int descRes, int iconRes, int iconMuteRes, boolean show) {
……
}
};
```
這幾個枚舉項組成了一個數組名為STREAM如下:
**VolumePanel.java-->VolumePanel.STREAMS**
```
private static final StreamResources[] STREAMS = {
StreamResources.BluetoothSCOStream,
StreamResources.RingerStream,
StreamResources.VoiceStream,
StreamResources.MediaStream,
StreamResources.NotificationStream,
StreamResources.AlarmStream,
StreamResources.MasterStream,
StreamResources.RemoteStream
};
```
VolumePanel將從這個STREAMS數組中獲取它所支持的流類型的相關資源。這么做是不是覺得有點啰嗦呢?事實上,在這里使用枚舉并沒有什么特殊的意義,使用普通的一個Java類來定義StreamResources就已經足夠了。
StreamControl類則保存了一個流類型的通知框所需要顯示的控件。其定義如下:
**VolumePanel.java-->VolumePanel.StreamControl**
```
private class StreamControl {
intstreamType;
ViewGroupgroup;
ImageViewicon;
SeekBarseekbarView;
inticonRes;
inticonMuteRes;
}
```
很簡單對不對?StreamControl實例中保存了音量條提示框中所需的所用控件。關于這個類在VolumePanel的使用,我們可能很直觀的認為只有一個StreamControl實例,在對話框顯示時,使其保存的控件按需加載指定流類型的StreamResources實例中定義的資源。其實不然,應該是出于對運行效率的考慮,StreamControl實例也是每個流類型人手一份,和StreamResources實例形成了一個一一對應的關系。所有的StreamControl 實例被保存在了一個以流類型的值為鍵的Hashtable中,名為mStreamControls。我們可以在StreamControl的初始化函數createSliders()中一窺其端倪:
**VolumePanel-->VolumePanel.createSliders()**
```
private void createSliders() {
……
// 遍歷STREAM中所有的StreamResources實例
for (inti = 0; i < STREAMS.length; i++) {
StreamResources streamRes = STREAMS[i];
intstreamType = streamRes.streamType;
……
// 為streamType創建一個StreamControl
StreamControl sc = new StreamControl();
// 這里將初始化sc的成員變量
……
// 將初始化好的sc放入mStreamControls中去。
mStreamControls.put(streamType, sc);
}
}
```
值得一提的是,這個初始化的工作并沒有在構造函數中進行,而是在postVolumeChanged()函數里處理的。
既然已經有了通知框所需要的資源和通知框的控件了,那么接下來就要有一個對話框承載它們。沒錯,VolumePanel保存了一個名為mDialog的Dialog實例,這就是通知框的本尊了。每當有新的音量變化到來時,mDialog的內容就會被替換為制定流類型對應的StreamControl中所保存的控件,并根據音量變化情況設置其音量條的位置,最后調用mDialog.show()顯示出來。同時,發送一個延時消息MSG\_TIMEOUT,這條延時消息生效時,將會關閉提示框。
StreamResource、StreamControl與mDialog的關系就像下面這附圖一樣,StreamControl可以說是mDialog的配件,隨需拆卸。
:-: 
圖 3-3 StreamResource、StreamControl與mDialog的關系
接下來具體看一下VolumePanel在收到音量變化通知后都做了什么。我們在上一小節中說到了mVolumePanel.postVolumeChanged()函數。它的內容很簡單,直接發送了一條消息MSG\_VOLUME\_CHANGED,然后在handleMessage中調用onVolumeChanged()函數進行真正的處理。
**注意** VolumePanel在MSG\_VOLUME\_CHANGED的消息處理函數中調用onVolumeChanged()函數而不直接在postVolumeChanged()函數中直接調,。這么做是有實際意義的。由于Android要求只能在創建控件的線程中對控件進行操作。postVolumeChanged()作為一個回調性質的函數,不能要求調用者位于哪個線程中。所以必須通過向Handler發送消息的方式,將后續的操作轉移到指定的線程中去。在大家設計具有UI Controller功能的類時,VolumePanel的實現方式有很好的參考意義。
看一下onVolumeChanged()函數的實現:
**VolumePanel.java-->VolumePanel.onVolumeChanged()**
```
protected void onVolumeChanged(int streamType, intflags) {
// 需要flags中包含AudioManager.FLAG_SHOW_UI才會顯示音量調通知框
if((flags & AudioManager.FLAG_SHOW_UI) != 0) {
synchronized (this) {
if (mActiveStreamType != streamType) {
reorderSliders(streamType); // 在Dialog里裝載需要的StreamControl
}
// 這個函數負責最終的顯示
onShowVolumeChanged(streamType, flags);
}
}
// 是否要播出Tone音,注意有個小延遲
if((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) {
removeMessages(MSG_PLAY_SOUND);
sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags),PLAY_SOUND_DELAY);
}
// 取消聲音與振動的播放
if((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) {
removeMessages(MSG_PLAY_SOUND);
removeMessages(MSG_VIBRATE);
onStopSounds();
}
// 開始安排回收資源
removeMessages(MSG_FREE_RESOURCES);
sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY);
// 重置音量框超時關閉的時間。
resetTimeout();
}
```
注意最后一個resetTimeout()的調用。它其實是重新延時發送了MSG\_TIMEOUT消息。當MSG\_TIMEOUT消息生效時,mDialog將會被關閉。
之后就是onShowVolumeChanged了。這個函數負責為通知框的內容填充音量、圖表等信息,然后再把通知框顯示出來,如果還沒有顯示的話。以鈴聲音量為例,省略掉其他的代碼。
**VolumePanel.java-->VolumePanel.onShowVolumeChanged()**
```
protectedvoid onShowVolumeChanged(int streamType, int flags) {
// 獲取音量值
intindex = getStreamVolume(streamType);
// 獲取音量最大值,這兩個將用來設置進度條
intmax = getStreamMaxVolume(streamType);
switch (streamType) {
// 這個switch語句中,我們要根據每種流類型的特點,進行各種調整。
// 例如Music就有時就需要更新它的圖標,因為使用藍牙耳機時的圖標和和平時的不一樣
// 所以每一次都需要更新一下
case AudioManager.STREAM_MUSIC: {
// Special case for when Bluetooth is active for music
if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) &
(AudioManager.DEVICE_OUT_BLUETOOTH_A2DP |
AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES|
AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) {
setMusicIcon(R.drawable.ic_audio_bt,
R.drawable.ic_audio_bt_mute);// 設置藍牙圖標
} else {
setMusicIcon(R.drawable.ic_audio_vol,
R.drawable.ic_audio_vol_mute);//設置為普通圖標
}
break;
}
……
}
// 取出Music流類型對應的StreamControl。并設置其SeekBar的音量顯示
StreamControl sc = mStreamControls.get(streamType);
if(sc != null) {
if (sc.seekbarView.getMax() != max) {
sc.seekbarView.setMax(max);
}
sc.seekbarView.setProgress(index);
……
}
if(!mDialog.isShowing()) { // 如果對話框還沒有顯示
/* forceVolumeControlStream()的調用在這里,一旦此通知框被顯示,之后的按下音量鍵,
*都只能調節當前流類型的音量。直到通知框關閉時,重新調用forceVolumeControlStream(),
*并設置streamType為-1。
*/
mAudioManager.forceVolumeControlStream(streamType);
// 為Dialog設置顯示控件
// 注意,mView目前已經在reorderSlider()函數中安裝好了Music流所對應的
//StreamControl了
mDialog.setContentView(mView);
……
//顯示對話框
mDialog.show();
}
}
```
至此,音量條提示框就被顯示出來了。總結一下它的工作過程:
- postVolumeChanged() 是VolumePanel顯示的入口。
- 檢查flags中是否有FLAG\_SHOW\_UI。
- VolumePanel會在第一次被要求彈出時初始化其控件資源。
- mDialog 加載指定流類型對應的StreamControl,也就是控件。
- 顯示對話框,并開始超時計時。
- 超時計時到達,關閉對話框。
到此為止,AudioService對音量鍵的處理流程就介紹完了。而 Android還有另外一種改變音量的方式。即音量設置函數setStreamVolume(),下面對其進行介紹
- 前言
- 推薦序
- 第1章 開發環境部署
- 1.1獲取Android源代碼
- 1.2Android的編譯
- 1.3在IDE中導入Android源代碼
- 1.3.1將Android源代碼導入Eclipse
- 1.3.2將Android源代碼導入SourceInsight
- 1.4調試Android源代碼
- 1.4.1使用Eclipse調試Android Java源代碼
- 1.4.2使用gdb調試Android C/C 源代碼
- 1.5本章小結
- 第2章 深入理解Java Binder和MessageQueue
- 2.1概述
- 2.2Java層中的Binder分析
- 2.2.1Binder架構總覽
- 2.2.2初始化Java層Binder框架
- 2.2.3窺一斑,可見全豹乎
- 2.2.4理解AIDL
- 2.2.5Java層Binder架構總結
- 2.3心系兩界的MessageQueue
- 2.3.1MessageQueue的創建
- 2.3.2提取消息
- 2.3.3nativePollOnce函數分析
- 2.3.4MessageQueue總結
- 2.4本章小結
- 第3章 深入理解AudioService
- 3.1概述
- 3.2音量管理
- 3.2.1音量鍵的處理流程
- 3.2.2通用的音量設置函數setStreamVolume()
- 3.2.3靜音控制
- 3.2.4音量控制小結
- 3.3音頻外設的管理
- 3.3.1 WiredAccessoryObserver 設備狀態的監控
- 3.3.2AudioService的外設狀態管理
- 3.3.3音頻外設管理小結
- 3.4AudioFocus機制的實現
- 3.4.1AudioFocus簡單的例子
- 3.4.2AudioFocus實現原理簡介
- 3.4.3申請AudioFocus
- 3.4.4釋放AudioFocus
- 3.4.5AudioFocus小結
- 3.5AudioService的其他功能
- 3.6本章小結
- 第4章 深入理解WindowManager-Service
- 4.1初識WindowManagerService
- 4.1.1一個從命令行啟動的動畫窗口
- 4.1.2WMS的構成
- 4.1.3初識WMS的小結
- 4.2WMS的窗口管理結構
- 4.2.1理解WindowToken
- 4.2.2理解WindowState
- 4.2.3理解DisplayContent
- 4.3理解窗口的顯示次序
- 4.3.1主序、子序和窗口類型
- 4.3.2通過主序與子序確定窗口的次序
- 4.3.3更新顯示次序到Surface
- 4.3.4關于顯示次序的小結
- 4.4窗口的布局
- 4.4.1從relayoutWindow()開始
- 4.4.2布局操作的外圍代碼分析
- 4.4.3初探performLayoutAndPlaceSurfacesLockedInner()
- 4.4.4布局的前期處理
- 4.4.5布局DisplayContent
- 4.4.6布局的階段
- 4.5WMS的動畫系統
- 4.5.1Android動畫原理簡介
- 4.5.2WMS的動畫系統框架
- 4.5.3WindowAnimator分析
- 4.5.4深入理解窗口動畫
- 4.5.5交替運行的布局系統與動畫系統
- 4.5.6動畫系統總結
- 4.6本章小結
- 第5章 深入理解Android輸入系統
- 5.1初識Android輸入系統
- 5.1.1getevent與sendevent工具
- 5.1.2Android輸入系統簡介
- 5.1.3IMS的構成
- 5.2原始事件的讀取與加工
- 5.2.1基礎知識:INotify與Epoll
- 5.2.2 InputReader的總體流程
- 5.2.3 深入理解EventHub
- 5.2.4 深入理解InputReader
- 5.2.5原始事件的讀取與加工總結
- 5.3輸入事件的派發
- 5.3.1通用事件派發流程
- 5.3.2按鍵事件的派發
- 5.3.3DispatcherPolicy與InputFilter
- 5.3.4輸入事件的派發總結
- 5.4輸入事件的發送、接收與反饋
- 5.4.1深入理解InputChannel
- 5.4.2連接InputDispatcher和窗口
- 5.4.3事件的發送
- 5.4.4事件的接收
- 5.4.5事件的反饋與發送循環
- 5.4.6輸入事件的發送、接收與反饋總結
- 5.5關于輸入系統的其他重要話題
- 5.5.1輸入事件ANR的產生
- 5.5.2 焦點窗口的確定
- 5.5.3以軟件方式模擬用戶操作
- 5.6本章小結
- 第6章 深入理解控件系統
- 6.1 初識Android的控件系統
- 6.1.1 另一種創建窗口的方法
- 6.1.2 控件系統的組成
- 6.2 深入理解WindowManager
- 6.2.1 WindowManager的創建與體系結構
- 6.2.2 通過WindowManagerGlobal添加窗口
- 6.2.3 更新窗口的布局
- 6.2.4 刪除窗口
- 6.2.5 WindowManager的總結
- 6.3 深入理解ViewRootImpl
- 6.3.1 ViewRootImpl的創建及其重要的成員
- 6.3.2 控件系統的心跳:performTraversals()
- 6.3.3 ViewRootImpl總結
- 6.4 深入理解控件樹的繪制
- 6.4.1 理解Canvas
- 6.4.2 View.invalidate()與臟區域
- 6.4.3 開始繪制
- 6.4.4 軟件繪制的原理
- 6.4.5 硬件加速繪制的原理
- 6.4.6 使用繪圖緩存
- 6.4.7 控件動畫
- 6.4.8 繪制控件樹的總結
- 6.5 深入理解輸入事件的派發
- 6.5.1 觸摸模式
- 6.5.2 控件焦點
- 6.5.3 輸入事件派發的綜述
- 6.5.4 按鍵事件的派發
- 6.5.5 觸摸事件的派發
- 6.5.6 輸入事件派發的總結
- 6.6 Activity與控件系統
- 6.6.1 理解PhoneWindow
- 6.6.2 Activity窗口的創建與顯示
- 6.7 本章小結
- 第7章 深入理解SystemUI
- 7.1 初識SystemUI
- 7.1.1 SystemUIService的啟動
- 7.1.2 狀態欄與導航欄的創建
- 7.1.3 理解IStatusBarService
- 7.1.4 SystemUI的體系結構
- 7.2 深入理解狀態欄
- 7.2.1 狀態欄窗口的創建與控件樹結構
- 7.2.2 通知信息的管理與顯示
- 7.2.3 系統狀態圖標區的管理與顯示
- 7.2.4 狀態欄總結
- 7.3 深入理解導航欄
- 7.3.1 導航欄的創建
- 7.3.2 虛擬按鍵的工作原理
- 7.3.3 SearchPanel
- 7.3.4 關于導航欄的其他話題
- 7.3.5 導航欄總結
- 7.4 禁用狀態欄與導航欄的功能
- 7.4.1 如何禁用狀態欄與導航欄的功能
- 7.4.2 StatusBarManagerService對禁用標記的維護
- 7.4.3 狀態欄與導航欄對禁用標記的響應
- 7.5 理解SystemUIVisibility
- 7.5.1 SystemUIVisibility在系統中的漫游過程
- 7.5.2 SystemUIVisibility發揮作用
- 7.5.3 SystemUIVisibility總結
- 7.6 本章小結
- 第8章 深入理解Android壁紙
- 8.1 初識Android壁紙
- 8.2深入理解動態壁紙
- 8.2.1啟動動態壁紙的方法
- 8.2.2壁紙服務的啟動原理
- 8.2.3 理解UpdateSurface()方法
- 8.2.4 壁紙的銷毀
- 8.2.5 理解Engine的回調
- 8.3 深入理解靜態壁紙-ImageWallpaper
- 8.3.1 獲取用作靜態壁紙的位圖
- 8.3.2 靜態壁紙位圖的設置
- 8.3.3 連接靜態壁紙的設置與獲取-WallpaperObserver
- 8.4 WMS對壁紙窗口的特殊處理
- 8.4.1 壁紙窗口Z序的確定
- 8.4.2 壁紙窗口的可見性
- 8.4.3 壁紙窗口的動畫
- 8.4.4 壁紙窗口總結
- 8.5 本章小結