最近負責播放器模塊的開發(fā),業(yè)務需求也慢慢增加中,包括梳理播放器界面上的交互、加載優(yōu)化。
下面大概梳理一下,手勢調(diào)節(jié)音量、亮度、播放進度等交互部分。
與其他播放器需求上相似,左右滑動用于拖拽播放進度,左右側兩邊的上下滑動分別用于亮度、音量調(diào)節(jié)。這里我把代碼大致梳理一下,如果有其他拖拽需求也可以沿用這種方法。
【本次開發(fā)環(huán)境:Xcode:11.2.1 iOS 真機:iPhone 8Plus By:啊左。 】
(小編在深圳小廠碼代碼,最近公司各種職位熱招,需要內(nèi)推的可以私聊~)
為了方便抽離調(diào)節(jié)音量/亮度,我們創(chuàng)建一個調(diào)節(jié)的容器(視圖)集中處理,命名為 SystemAdjustView。
1、確定拖拽手勢:
首先,這些交互調(diào)節(jié)的操作主要是拖拽,我們確定用 UIPanGestureRecognizer:
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self
action:@selector(panDirection:)];
panGesture.delegate = self;
[self addGestureRecognizer:panGesture];
不管上下還是左右滑動,我們做判斷處理,所以先定義一個滑動方向的枚舉:
// 滑動方向枚舉
typedef NS_ENUM(NSUInteger, SlidingDirection) {
SlidingDirectionLeftOrRight,
SlidingDirectionUpOrDown,
SlidingDirectionNone
};
2、添加需要的數(shù)據(jù)變量
添加相應的調(diào)節(jié)動畫視圖,以及方向等屬性變量:
/// 當前滑動方向
@property (nonatomic, assign) SlidingDirection slidingDirection;
/// 當前是否為音量滑動
@property (nonatomic, assign) BOOL isVolume;
/// 視圖容器
@property (nonatomic, strong) UIView *justContainer;
/// 調(diào)節(jié)動畫 icon
@property (nonatomic, strong) UIImageView *justImgView;
/// 調(diào)節(jié)動畫文案
@property (nonatomic, strong) UILabel *justLabel;
/// 系統(tǒng)的音量調(diào)節(jié)視圖
@property (nonatomic, strong) MPVolumeView *mpVolumeView;
/// 系統(tǒng)的音量調(diào)節(jié)視圖輔助
@property (nonatomic, strong) UISlider *volumeViewSlider;
以下是控件的懶加載,平時都用慣 Masonry,為方便大家測試 demo,這里用 frame 計算布局:
#pragma mark - Setter && Getter
- (UIView *)justContainer {
if (!_justContainer) {
CGFloat x = SCREEN_WIDTH/2 - COTAINER_WIDTH/2;
CGFloat y = SCREEN_HEIGHT/2 - COTAINER_HEIGHT/2;
_justContainer = [[UIView alloc] initWithFrame:CGRectMake(x, y, COTAINER_WIDTH, COTAINER_HEIGHT)];
_justContainer.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2];
_justContainer.layer.cornerRadius = 4;
_justContainer.alpha = 0.0;
}
return _justContainer;
}
- (UILabel *)justLabel {
if (!_justLabel) {
_justLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 45, COTAINER_WIDTH, 16)];
_justLabel.textAlignment = NSTextAlignmentCenter;
_justLabel.textColor = [UIColor whiteColor];
_justLabel.font = [UIFont fontWithName:@"PingFangSC-Regular"size:12];
_justLabel.textAlignment = NSTextAlignmentCenter;
}
return _justLabel;
}
- (UIImageView *)justImgView {
if (!_justImgView) {
CGFloat defaultSize = 30;
CGFloat x = COTAINER_WIDTH/2 - defaultSize/2;
_justImgView = [[UIImageView alloc] initWithFrame:CGRectMake(x, 10, defaultSize, defaultSize)];
}
return _justImgView;
}
- (MPVolumeView *)mpVolumeView {
if (!_mpVolumeView) {
_mpVolumeView = [[MPVolumeView alloc] init];
[_mpVolumeView setShowsRouteButton:YES];
// hidden 一定要設置為 NO,當然這里不設置也行,因為默認為 NO.
_mpVolumeView.hidden = NO;
// frame 需要在可視區(qū)域外
[_mpVolumeView setFrame:CGRectMake(-100, -100, 40, 40)];
[_mpVolumeView setShowsVolumeSlider:YES];
for (UIView *view in [_mpVolumeView subviews]){
if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
self.volumeViewSlider =(UISlider*)view;
[self.volumeViewSlider addTarget:self action:@selector(volumeViewSliderClick:) forControlEvents:UIControlEventTouchUpInside];
break;
}
}
}
return _mpVolumeView;
}
分析:
① MPVolumeView 的作用?
MPVolumeView 是 MediaPlayer 框架中的一個組件,包含了對系統(tǒng)音量和AirPlay 設備的音頻鏡像路由的控制功能。MPVolumeView 有三個 subview,其中 MPVolumeSlider 是用來控制音量大小,繼承自 UISlider。 所以我們可以通過創(chuàng)建 MPVolumeView,并拿到它 subViews 中的 UISlider 變量。
需要注意的是,因為 MPVolumeView 沒有定制的功能,所以如果音量變化 UI 由我們定制的話,創(chuàng)建的 MPVolumeView 需要設置在可視區(qū)域之外,例如 本文 demo 設置為 CGRectMake(-100, -100, 40, 40),這樣音量發(fā)生變化的時候,就只會出現(xiàn)我們繪制的 UI 了。
記得導入:
#import <MediaPlayer/MediaPlayer.h>
(by:MPVolumeView 變量的 hidden 屬性一定要為 NO,且 frame 應該是不能直接設置為 CGRectZero 的。 )
②用戶直接用 iPhone 音量鍵調(diào)節(jié),如何顯示我們繪制的動畫?
添加 AVSystemController_SystemVolumeDidChangeNotification 音量變化通知,在通知里處理繪制響應的音量變化 UI:
// 添加系統(tǒng)音量觀察者
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(volumeChanged:)
name:@"AVSystemController_SystemVolumeDidChangeNotification"
object:nil];
3、初始化視圖控件、手勢,添加監(jiān)聽音量變化等:
先添加需要的宏數(shù)據(jù)
// 屏幕寬高
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
// 調(diào)節(jié)動畫寬高
#define COTAINER_WIDTH 64
#define COTAINER_HEIGHT 72
// 滑動時間
#define SHOW_DURATION 1.0
// 隱藏延遲時間
#define HIDE_DELAY 0.8
初始化控件
#pragma mark - Life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initViews];
[self appendPanGesture];
}
return self;
}
- (void)dealloc {
// 移除 延遲隱藏調(diào)節(jié)界面操作
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(hideContainerAnimation)
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:@"AVSystemController_SystemVolumeDidChangeNotification"
object:nil];
}
- (void)initViews {
[self addSubview:self.justContainer];
[self.justContainer addSubview:self.justImgView];
[self.justContainer addSubview:self.justLabel];
[self addSubview:self.mpVolumeView];
// 需要先創(chuàng)建活動音頻會話,然后才能調(diào)用下一行代碼的音量變化事件。
NSError *error;
[[AVAudioSession sharedInstance] setActive:YES error:&error];
// 添加系統(tǒng)音量觀察者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(volumeChanged:) name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];
}
- (void)appendPanGesture {
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panDirection:)];
panGesture.delegate = self;
[self addGestureRecognizer:panGesture];
}
4、分別對用戶上下、左右滑動手勢進行 UI 調(diào)節(jié)處理:
#pragma mark - Private Methods
#pragma mark Horizontal Move
/// 水平方向調(diào)節(jié)開始
/// @param value 開始值
- (void)horizontalStateBeginValue:(CGFloat)value {
}
/// 水平方向調(diào)節(jié)變化時
/// @param value 變化時的值
- (void)horizontalStateChangedValue:(CGFloat)value {
}
/// 水平方向調(diào)節(jié)結束
/// @param value 結束值
- (void)horizontalStateEndValue:(CGFloat)value {
}
#pragma mark Vertical Move
/// 豎直方向調(diào)節(jié)開始
/// @param isVolume 是否為音量調(diào)節(jié)
- (void)verticalStateBeginIsVolume:(BOOL)isVolume {
// cancel hardware volume adjustment
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(hideContainerAnimation)
object:nil];
self.isVolume = isVolume;
[self updateVolumeIcon];
[self.volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
}
/// 豎直方向調(diào)節(jié)變化時
/// @param value 變化值
- (void)verticalStateChangedValue:(CGFloat)value {
if (self.isVolume) {
// 調(diào)節(jié)系統(tǒng)音量
self.volumeViewSlider.value -= value / 10000;
[self.volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
} else {
// 調(diào)節(jié)系統(tǒng)亮度
[UIScreen mainScreen].brightness -= value / 10000;
_justLabel.text = [NSString stringWithFormat:@"%ld%%",(NSInteger)([UIScreen mainScreen].brightness * 100)];
}
[self updateVolumeIcon];
}
/// 豎直方向調(diào)節(jié)結束
- (void)verticalStateEnded {
[self performSelector:@selector(hideContainerAnimation)
withObject:nil
afterDelay:HIDE_DELAY];
}
關于水平調(diào)節(jié)的,是關于播放進度等拖拽處理,讀者可自行添加使用,篇幅原因播放進度拖拽這里不做講解。
以上的兩個關于 icon 更換、動畫隱藏/顯示視圖等私有方法如下所示:
#pragma mark Common
/// 操作完畢,1s 時間隱藏動畫
- (void)hideContainerAnimation {
[UIView animateWithDuration:SHOW_DURATION animations:^{
self.justContainer.alpha = 0.0;
}];
}
/// 更新調(diào)節(jié)圖標(音量/亮度)
- (void)updateVolumeIcon {
NSString *imgName;
if (self.isVolume) {
imgName = (self.volumeViewSlider.value <= 0) ?
@"video_system_volume_mute" : @"video_system_volume";
} else {
imgName = @"video_system_brightness";
}
[_justImgView setImage:[UIImage imageNamed:imgName]];
_justContainer.alpha = 1.0;
}
5、事件處理
以下分別是 UISlider 的滑動事件和 UIPanGestureRecognizer 拖拽事件的實現(xiàn):
#pragma mark - Event Click
- (void)volumeViewSliderClick:(UISlider *)volumeViewSlider {
// 更新音量顯示值
_justLabel.text = [NSString stringWithFormat:@"%ld%%",(NSInteger)(self.volumeViewSlider.value * 100)];
}
- (void)panDirection:(UIPanGestureRecognizer *)pan {
// 手指在視圖上移動的速度,可用于判斷 水平/豎直 方向滑動
CGPoint velocityPoint = [pan velocityInView:self];
// 手指在視圖上的位置
CGPoint locationPoint = [pan locationInView:self];
switch (pan.state) {
case UIGestureRecognizerStateBegan:{
CGFloat x = fabs(velocityPoint.x);
CGFloat y = fabs(velocityPoint.y);
if (x > y) {
// 水平方向滑動
self.slidingDirection = SlidingDirectionLeftOrRight;
[self horizontalStateBeginValue:locationPoint.x];
} else if (x < y){
// 豎直方向滑動
self.slidingDirection = SlidingDirectionUpOrDown;
if (locationPoint.x <= self.frame.size.width / 2.0) {
[self verticalStateBeginIsVolume:NO];
} else {
[self verticalStateBeginIsVolume:YES];
}
}
break;
}
case UIGestureRecognizerStateChanged:{
// 滑動時,根據(jù) 水平/垂直方向分別進行處理
switch (self.slidingDirection){
case SlidingDirectionUpOrDown:{
[self verticalStateChangedValue:velocityPoint.y];
break;
}
case SlidingDirectionLeftOrRight:{
CGPoint movePoint = [pan translationInView:self];
[self horizontalStateChangedValue:movePoint.x];
break;
}
default:
break;
}
break;
}
case UIGestureRecognizerStateEnded:{
// 滑動結束時,根據(jù) 水平/垂直方向分別進行處理
switch (self.slidingDirection) {
case SlidingDirectionUpOrDown:{
[self verticalStateEnded];
break;
}
case SlidingDirectionLeftOrRight:{
[self horizontalStateEndValue:locationPoint.x];
break;
}
default:
break;
}
}
default:
break;
}
}
另外,還有音量監(jiān)聽的方法如下:
#pragma mark - Notification
- (void)volumeChanged:(NSNotification *)notification {
if ([notification.name isEqualToString:@"AVSystemController_SystemVolumeDidChangeNotification"]) {
NSDictionary *userInfo = notification.userInfo;
NSString *reasonString = userInfo[@"AVSystemController_AudioVolumeChangeReasonNotificationParameter"];
if ([reasonString isEqualToString:@"ExplicitVolumeChange"]) {
// 音量值,這里我們采用滑塊調(diào)節(jié)的方式,所以這個屬性可以不用到
// CGFloat value = [userInfo[@"AVSystemController_AudioVolumeNotificationParameter"] doubleValue];
[self verticalStateBeginIsVolume:YES];
[self performSelector:@selector(hideContainerAnimation)
withObject:nil
afterDelay:HIDE_DELAY];
}
}
}
開發(fā)過程遇到的一些細節(jié)問題
1、如果與 UITableview 沖突,例如類似抖音首頁,上下互動可以切換視頻操作的界面。添加的 UIPanGestureRecognizer 使 UITableview 上下滑動沖突失效,那么需要在以下代理方法中做沖突處理:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if (!_panHorizontalEnabled &&
[otherGestureRecognizer.view isKindOfClass:[UITableView class]]) {
return YES;
}
return NO;
}
2、如果是在類似播放器這種帶有滑動條的情況下,為了避免對其影響,需要代理方法中進行判斷(記得添加<UIGestureRecognizerDelegate>):
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldReceiveTouch:(UITouch*)touch {
if ([touch.view isKindOfClass:[UISlider class]]) {
return NO;
} else {
return YES;
}
}
3、手勢滑動的視圖(容器) justContainer 是覆蓋到整個 self.view 的 fame,如果有需求,添加另外一個 view(例如叫 otherView) 需要覆蓋到 self.view 整個 fame。
那么會有一下問題:
如果 justContainer 添加在 otherView 上,那么 justContainer 因為在整個 view 下面,所以無法響應用戶手勢;
同理,justContainer 添加在 otherView 下層,那么這個 view 上的添加是所有控件也無法響應用戶手勢。
解決辦法:
// justContainer 先添加,otherView 則在上層。
[self addSubview:self.justContainer];
[self addSubview:self.otherView];
這一步很重要,把控制器的 self.view 傳給 justContainer,命名為 parentView,記得用 weak 修飾,然后用 parentView 添加手勢就可以解決啦。(詳情可參見 demo~)
[self.parentView addGestureRecognizer:panGesture];
當然還有其他辦法,例如把該有控件添加到 justContainer,不創(chuàng)建 otherView,例如控制用戶的點擊響應范圍等等。
4、MPVolumeView 添加后,依然出現(xiàn)系統(tǒng)調(diào)節(jié)圖案,檢查一下看下是否 frame 沒有設置,或者 hidden 設置成了 YES,當然也有另外一種可能,像我遇到使用公司 SDK 的播放器界面無論怎么添加,都出現(xiàn)系統(tǒng)調(diào)節(jié)圖案,我懷疑是可能這個界面上對音量控制做了什么處理,所以我采用以下解決方案:就是把 mpVolumeView 添加在 window 上,而不是添加在這個界面。
[[UIApplication sharedApplication].keyWindow addSubview:self.mpVolumeView];
(轉(zhuǎn)載請標明原文出處,謝謝支持 ~ - ~)
? by:啊左~