最近负责播放器模块的开发,业务需求也慢慢增加中,包括梳理播放器界面上的交互、加载优化。
下面大概梳理一下,手势调节音量、亮度、播放进度等交互部分。
与其他播放器需求上相似,左右滑动用于拖拽播放进度,左右侧两边的上下滑动分别用于亮度、音量调节。这里我把代码大致梳理一下,如果有其他拖拽需求也可以沿用这种方法。
【本次开发环境:Xcode:11.2.1 iOS 真机:iPhone 8Plus By:啊左。 】
(小编在虎牙直播码代码,最近公司各种职位热招,需要内推的可以私聊~)
为了方便抽离调节音量/亮度,我们创建一个调节的容器(视图)集中处理,命名为 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:啊左~