iOS開發(fā) - 手勢調(diào)節(jié)音量、亮度、播放進度

最近負責播放器模塊的開發(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:啊左~

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容