美文网首页iOS开发记录
iOS开发 - 手势调节音量、亮度、播放进度

iOS开发 - 手势调节音量、亮度、播放进度

作者: 啊左 | 来源:发表于2020-04-13 12:17 被阅读0次

    最近负责播放器模块的开发,业务需求也慢慢增加中,包括梳理播放器界面上的交互、加载优化。
    下面大概梳理一下,手势调节音量、亮度、播放进度等交互部分。
    与其他播放器需求上相似,左右滑动用于拖拽播放进度,左右侧两边的上下滑动分别用于亮度、音量调节。这里我把代码大致梳理一下,如果有其他拖拽需求也可以沿用这种方法。
    【本次开发环境:Xcode:11.2.1 iOS 真机:iPhone 8Plus By:啊左。
    本文 Demo下载链接:SystemAdjustProject
    (最近虎牙直播热招,需要内推的可以私聊~)

    为了方便抽离调节音量/亮度,我们创建一个调节的容器(视图)集中处理,命名为 SystemAdjustView。

    1、确定拖拽手势:

    首先,这些交互调节的操作主要是拖拽,我们确定用 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、添加需要的数据变量

    添加相应的调节动画视图,以及方向等属性变量:

    /// 当前滑动方向
    @property (nonatomic, assign) SlidingDirection slidingDirection;
    /// 当前是否为音量滑动
    @property (nonatomic, assign) BOOL isVolume;
    /// 视图容器
    @property (nonatomic, strong) UIView *justContainer;
    /// 调节动画 icon
    @property (nonatomic, strong) UIImageView *justImgView;
    /// 调节动画文案
    @property (nonatomic, strong) UILabel *justLabel;
    /// 系统的音量调节视图
    @property (nonatomic, strong) MPVolumeView *mpVolumeView;
    /// 系统的音量调节视图辅助
    @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 需要在可视区域外
            [_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 框架中的一个组件,包含了对系统音量和AirPlay 设备的音频镜像路由的控制功能。MPVolumeView 有三个 subview,其中 MPVolumeSlider 是用来控制音量大小,继承自 UISlider。 所以我们可以通过创建 MPVolumeView,并拿到它 subViews 中的 UISlider 变量。
    需要注意的是,因为 MPVolumeView 没有定制的功能,所以如果音量变化 UI 由我们定制的话,创建的 MPVolumeView 需要设置在可视区域之外,例如 本文 demo 设置为 CGRectMake(-100, -100, 40, 40),这样音量发生变化的时候,就只会出现我们绘制的 UI 了。
    记得导入:
    #import <MediaPlayer/MediaPlayer.h>
    (by:MPVolumeView 变量的 hidden 属性一定要为 NO,且 frame 应该是不能直接设置为 CGRectZero 的。 )

    ②用户直接用 iPhone 音量键调节,如何显示我们绘制的动画?

    添加 AVSystemController_SystemVolumeDidChangeNotification 音量变化通知,在通知里处理绘制响应的音量变化 UI:

    // 添加系统音量观察者
     [[NSNotificationCenter defaultCenter] addObserver:self
                                              selector:@selector(volumeChanged:)
                                                  name:@"AVSystemController_SystemVolumeDidChangeNotification" 
                                                object:nil];
    

    3、初始化视图控件、手势,添加监听音量变化等:

    先添加需要的宏数据

    // 屏幕宽高
    #define SCREEN_WIDTH                [UIScreen mainScreen].bounds.size.width
    #define SCREEN_HEIGHT               [UIScreen mainScreen].bounds.size.height
    
    // 调节动画宽高
    #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 {
        // 移除 延迟隐藏调节界面操作
        [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];
        
        // 需要先创建活动音频会话,然后才能调用下一行代码的音量变化事件。
        NSError *error;
        [[AVAudioSession sharedInstance] setActive:YES error:&error];
    
        // 添加系统音量观察者
        [[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 调节处理:

    #pragma mark - Private Methods
    
    #pragma mark Horizontal Move
    
    /// 水平方向调节开始
    /// @param value 开始值
    - (void)horizontalStateBeginValue:(CGFloat)value {
    }
    
    /// 水平方向调节变化时
    /// @param value 变化时的值
    - (void)horizontalStateChangedValue:(CGFloat)value {
    }
    
    /// 水平方向调节结束
    /// @param value 结束值
    - (void)horizontalStateEndValue:(CGFloat)value {
    }
    
    #pragma mark Vertical Move
    
    /// 竖直方向调节开始
    /// @param isVolume 是否为音量调节
    - (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];
    }
    
    /// 竖直方向调节变化时
    /// @param value 变化值
    - (void)verticalStateChangedValue:(CGFloat)value {
        if (self.isVolume) {
            // 调节系统音量
            self.volumeViewSlider.value -= value / 10000;
            [self.volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
        } else {
            // 调节系统亮度
            [UIScreen mainScreen].brightness -= value / 10000;
            _justLabel.text = [NSString stringWithFormat:@"%ld%%",(NSInteger)([UIScreen mainScreen].brightness * 100)];
        }
        [self updateVolumeIcon];
    }
    
    /// 竖直方向调节结束
    - (void)verticalStateEnded {
        [self performSelector:@selector(hideContainerAnimation)
                   withObject:nil
                   afterDelay:HIDE_DELAY];
    }
    

    关于水平调节的,是关于播放进度等拖拽处理,读者可自行添加使用,篇幅原因播放进度拖拽这里不做讲解。
    以上的两个关于 icon 更换、动画隐藏/显示视图等私有方法如下所示:

    #pragma mark Common
    
    /// 操作完毕,1s 时间隐藏动画
    - (void)hideContainerAnimation {
        [UIView animateWithDuration:SHOW_DURATION animations:^{
            self.justContainer.alpha = 0.0;
        }];
    }
    
    /// 更新调节图标(音量/亮度)
    - (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 拖拽事件的实现:

    #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:{
                // 滑动时,根据 水平/垂直方向分别进行处理
                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:{
                // 滑动结束时,根据 水平/垂直方向分别进行处理
                switch (self.slidingDirection) {
                    case SlidingDirectionUpOrDown:{
                        [self verticalStateEnded];
                        break;
                    }
                    case SlidingDirectionLeftOrRight:{
                        [self horizontalStateEndValue:locationPoint.x];
                        break;
                    }
                        
                    default:
                        break;
                }
            }
            default:
                break;
        }
    }
    

    另外,还有音量监听的方法如下:

    #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"]) {
                // 音量值,这里我们采用滑块调节的方式,所以这个属性可以不用到
    //            CGFloat value = [userInfo[@"AVSystemController_AudioVolumeNotificationParameter"] doubleValue];
                [self verticalStateBeginIsVolume:YES];
                
                [self performSelector:@selector(hideContainerAnimation)
                           withObject:nil
                           afterDelay:HIDE_DELAY];
            }
        }
    }
    

    开发过程遇到的一些细节问题

    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,不创建 otherView,例如控制用户的点击响应范围等等。
    4、MPVolumeView 添加后,依然出现系统调节图案,检查一下看下是否 frame 没有设置,或者 hidden 设置成了 YES,当然也有另外一种可能,像我遇到使用公司 SDK 的播放器界面无论怎么添加,都出现系统调节图案,我怀疑是可能这个界面上对音量控制做了什么处理,所以我采用以下解决方案:就是把 mpVolumeView 添加在 window 上,而不是添加在这个界面。

        [[UIApplication sharedApplication].keyWindow addSubview:self.mpVolumeView];
    

    (转载请标明原文出处,谢谢支持 ~ - ~)
     by:啊左~

    相关文章

      网友评论

        本文标题:iOS开发 - 手势调节音量、亮度、播放进度

        本文链接:https://www.haomeiwen.com/subject/aaihlhtx.html