實現(xiàn)過程
實現(xiàn)自定義音量提示視圖嫁乘,需要做到以下幾步:
1.激活A(yù)udioSession配置。
2.隱藏系統(tǒng)的音量提示視圖扭吁。
3.監(jiān)聽音量按鈕的觸發(fā)事件撞蜂。
一、激活A(yù)udioSession配置
[[AVAudioSession sharedInstance] setActive:YES error:nil];
需要注意的點是:當(dāng)APP切換到前臺時侥袜,還需要進行激活A(yù)udioSession配置蝌诡,否則會出現(xiàn)收不到KVO的通知場景。
- (void)private_addWillEnterForegroundNotification {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveWillEnterForegroundNotification:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
}
- (void)didReceiveWillEnterForegroundNotification:(NSNotification *)notification {
[[AVAudioSession sharedInstance] setActive:YES error:nil];
}
二枫吧、隱藏系統(tǒng)的音量提示視圖
創(chuàng)建MPVolumeView并將其添加到當(dāng)前可見的視圖層中浦旱,同時將其 frame 設(shè)置到不可見區(qū)域。
- (void)onHiddenSystemVolumeView {
MPVolumeView *mpVolumeView = [[MPVolumeView alloc] init];
if (@available(iOS 12.0, *)) { //解決卡頓僅處理12以上設(shè)備
id controller = [mpVolumeView valueForKey:@"lightweightRoutingController"];
//解決MPVolumeView導(dǎo)致的卡頓
if (controller && [controller isKindOfClass:NSClassFromString(@"MPAVLightweightRoutingController")] && [controller respondsToSelector:@selector(setDelegate:)])
{
[controller performSelector:@selector(setDelegate:) withObject:nil];
}
}
[mpVolumeView setFrame:CGRectMake(-240, -100, 200, 40)];
}
三九杂、監(jiān)聽音量按鈕的觸發(fā)事件
監(jiān)聽系統(tǒng)音量按鈕的觸發(fā)事件颁湖,有兩種方式可以做到,各有優(yōu)劣例隆,下面來做一個簡單對比甥捺。
音量按鈕每觸發(fā)一次,變化量都是 6.25%镀层,連續(xù)按16次镰禾,即可調(diào)節(jié)至最大或最小。
3.1.KVO方式
通過KVO監(jiān)聽[AVAudioSession sharedInstance]的outputVolume屬性鹿响,然后來顯示自定義的 UI 控件羡微。這種方式有一個不好的地方就是:在音量調(diào)節(jié)至最大/最小時,這個時候再調(diào)大/調(diào)小音量惶我,由于outputVolume的值不變妈倔,所以不會觸發(fā)KVO,也就無法展示自定義音量視圖绸贡。代碼大概長下面這樣:
- (void)addObserver {
[[AVAudioSession sharedInstance] addObserver:self
forKeyPath:NSStringFromSelector(@selector(outputVolume))
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([change isKindOfClass:[NSDictionary class]]) {
NSNumber *volumeNum = change[@"new"];
if (volumeNum) {
[self volumeDidChange:[volumeNum floatValue]];
}
}
}
- (void)volumeDidChange:(CGFloat)volume {
// 顯示自定義音量提示
}
- (void)dealloc {
[[AVAudioSession sharedInstance] removeObserver:self
forKeyPath:NSStringFromSelector(@selector(outputVolume))];
}
3.2.通知
這種方式通過監(jiān)聽系統(tǒng)私有(未公開的)通知盯蝴,名字是AVSystemController_SystemVolumeDidChangeNotification,這個監(jiān)聽不會受到最大/最小音量時听怕,調(diào)大/調(diào)小音量的影響捧挺,只要音量鍵按下,始終都會觸發(fā)尿瞭。但是這個通知由于是私有的闽烙,可能存在被拒風(fēng)險,而且將來系統(tǒng)版本該通知名字發(fā)生改變,由于是硬編碼而不像其它系統(tǒng)通知使用的是常量黑竞,會導(dǎo)致監(jiān)聽不到的問題捕发。代碼大概長這樣:
static NSNotificationName const kSystemVolumeDidChangeNotification = @"AVSystemController_SystemVolumeDidChangeNotification";
- (void)addObserver {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(volumeDidChange:)
name:kSystemVolumeDidChangeNotification
object:nil];
}
- (void)volumeDidChange:(NSNotification *)notification {
NSString *category = notification.userInfo[@"AVSystemController_AudioCategoryNotificationParameter"];
NSString *changeReason = notification.userInfo[@"AVSystemController_AudioVolumeChangeReasonNotificationParameter"];
if (![category isEqualToString:@"Audio/Video"] || ![changeReason isEqualToString:@"ExplicitVolumeChange"]) {
return;
}
CGFloat volume = [[notification userInfo][@"AVSystemController_AudioVolumeNotificationParameter"] floatValue];
// 顯示自定義音量提示
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
以上兩種方式各自優(yōu)劣勢都已經(jīng)列出來了,Instagram使用的是通知的方式很魂,嗶哩嗶哩扎酷、騰訊視頻都是用KVO的方式。如果在最大或最小時遏匆,調(diào)節(jié)音量可以接受不展示音量視圖的話法挨,個人推薦使用KVO的形式。
4幅聘、創(chuàng)建自己的音量提示視圖
為了調(diào)用統(tǒng)一且音量視圖層級永遠在最上方(即不被Alert等擋追材伞),自定義的音量視圖使用UIWindow來實現(xiàn)喊暖,然后自定義視圖和系統(tǒng)的視圖加到這個視圖層級上惫企。由于自己創(chuàng)建的UIWindow的hidden屬性默認是YES,所以需要手動將其設(shè)成NO陵叽。
直接使用UIWindow這種方式不太行,會導(dǎo)致其它地方取keyWindow的時候會取錯丛版,導(dǎo)致其他問題巩掺。最終自定義一個Window繼承自UIWindow,然后復(fù)寫becomeKeyWindow方法页畦,在這個方法里讓自身不成為keyWindow同時將[UIApplication sharedApplication].delegate.window設(shè)置為keyWindow胖替,大致代碼長這樣:
@interface VolumeWindow : UIWindow
@end
@implementation VolumeWindow
- (void)becomeKeyWindow {
[self resignKeyWindow];
[[UIApplication sharedApplication].delegate.window makeKeyWindow];
}
@end
5、自定義后的問題
問題一:
拍攝頁拍攝之前系統(tǒng)音量提示是可以被替換的豫缨,但是拍攝一段之后独令,莫名其妙音量按鈕按下后自定義提示不見了,出現(xiàn)了系統(tǒng)的鈴聲提示好芭。一臉懵逼燃箭,后面發(fā)現(xiàn)是由于設(shè)置了UIAVCaptureSession的usesApplicationAudioSession為NO,會導(dǎo)致在拍攝之后會變成鈴聲舍败,這個和是否替換系統(tǒng)音量提示無關(guān)招狸。這個屬性是由于項目中很久之前需要兼容iOS6,然后一直遺留著這個屬性設(shè)置沒有刪除邻薯。由于iOS7之后裙戏,AVCaptureSession和應(yīng)用使用的是同一個AudioSession,支持同時播放和錄制且不會受到影響和打斷厕诡,所以不需要再去設(shè)置這個屬性累榜。
問題二:
做了以上操作,在 iPhoneX 下灵嫌,當(dāng)拉起控制中心壹罚,并上下滑調(diào)整音量后冀偶,再回到應(yīng)用,會發(fā)現(xiàn)自定義音量視圖會出現(xiàn)在狀態(tài)欄下面渔嚷,猜測雖然在應(yīng)用內(nèi)自定義音量視圖window層級高于狀態(tài)欄window層級进鸠,但是由于狀態(tài)欄是全局的,在重新進入到應(yīng)用時會出現(xiàn)狀態(tài)欄層級高于音量視圖形病。所以就索性僅在應(yīng)用為active的情況下才處理KVO客年。
最后一個需要注意的點是在語音電話(或者其它使用系統(tǒng)音量的場景下)時,去自己應(yīng)用內(nèi)調(diào)節(jié)音量是無效漠吻,因為這個時候音量其實代表的是系統(tǒng)在占用量瓜,系統(tǒng)優(yōu)先級高于應(yīng)用,所以在這些場景下途乃,即使在應(yīng)用內(nèi)調(diào)節(jié)音量绍傲,也無法觸發(fā)出自己的音量視圖。
最后推薦一個VolumeBar開源庫:https://github.com/gizmosachin/VolumeBar