受同学之邀,帮忙自定义一控件。需求是:开发慢放滚轮,用手指拨动实现帧级的慢速播放,滚轮可双向拨动,其滚动具有惯性,滚动速度决定视频播放的速度。需求很明朗,可我却是一头雾水。说实话,在此之前我还没有自定义过视频播放器,不懂怎么实现‘帧级慢速播放’。并且滚轮这个东西自定义起来,我也是没谱的。越是自己觉得陌生的东西,越是想办法回避,想着挑战一下自己,就尝试着做了起来。
要实现这个需求,首先滚轮是要自定义的。再说视频播放器,如果要用别人写好的视频播放器框架,需要coder刚好提供了一个方法,专门控制视频播放进度的。为了避免繁琐,我基于系统的AVPlayer自定义了视频播放器,实现了相关功能。
#####设计思路: 1.滚轮有三个状态:开始触碰、持续滚动、结束触碰。基于这三个状态考虑,决定在Control上自定义。
2.滚轮看起来得有立体的感觉,刻度线间距应该有变化,并且长度也应该有变化,达到近大远小的效果。
3.滚轮上的刻度线是绕着圆心转动的,想象着平面上是一个半圆,刻度线的长度与间距从小到大,又从大到小变化,你是不是想起了高中所学三角函数,这里就是利用三角函数与反三角函数实现相关功能。
4.暂无
/*! @method 每条线初始点的集合。 @abstract 初始化每条的初始位置点,并记录下来。 @discussion 利用三角函数,反三角函数,将滚轮滑动的距离转换为弧度,并计算出每条线的初始点。 */- (void)resetLinesArray{ [_linesArray removeAllObjects]; double arcTheLine = asin((_kRadius - _scroledRange)/ (_kRadius)); for (int i = 0; i < (LineCounts +1); i++) { double temArc =arcTheLine - i *_intervalTwoLine ; if (temArc < 0) { temArc += M_PI; } CGPoint pt; // 加π的原因是 滚轮转动的方向跟滑动方向相反,故加上π调整过来。 pt.x = _kRadius - _kRadius*cos(temArc+M_PI); pt.y = (_layerLength * 0.5 + _layerLength* 0.5*sin(temArc+M_PI)); [_linesArray addObject:[NSValue valueWithCGPoint:pt]]; }}复制代码
画出刻度线:
/*! @method 画出每条刻度线。 @abstract 利用CGContext 画出每条刻度线。 @discussion 根据三角函数算出的点,转换成长短不一,距离不一的刻度线。 @param layer layer 图层 @param ctx 上下文 */- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{ if (layer == self.layer) { for (NSValue *num in _linesArray) { CGPoint pt = [num CGPointValue]; double y = pt.y; double x = pt.x; //加10的原因是:最长线条是滚轮宽度减20,所以加上10,以达到刻度线居中的目的。 CGContextMoveToPoint(ctx, x, y+10); CGContextAddLineToPoint(ctx,x,_layerLength+10 - y); CGContextClosePath(ctx); CGContextSetLineWidth(ctx, 1); CGContextSetLineCap(ctx, kCGLineCapRound); CGContextSetStrokeColorWithColor(ctx,[UIColor whiteColor].CGColor); CGContextStrokePath(ctx); } }}复制代码
重写Control的三个系统方法:
/*! @method 重写control的系统方法。 @abstract control开始触碰。 @discussion 开始触碰滚轮的方法,记录下触碰点。 @param touch 触碰点 @param event 事件 @result 返回布尔值 */- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{ _lastTouchPoint = [touch locationInView:self]; if (CGRectContainsPoint(self.layer.bounds, _lastTouchPoint)) { [self.layer setNeedsDisplay]; return YES; } return NO;}/*! @method 重写control的系统方法。 @abstract control持续触碰。 @discussion 持续触碰滚轮的方法,根据触碰点计算滚动距离。 @param touch 触碰点 @param event 事件 @result 返回布尔值 */- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{ CGPoint pt = [touch locationInView:self]; CGFloat temLength = pt.x - _lastTouchPoint.x; float radiuDelta = temLength/_kRadius; self.value = temLength; _scroledRange += temLength; NSLog(@"aaa==%f",temLength); _lastTouchPoint = pt; if (_scroledRange < 0) { _scroledRange =_kRadius + _scroledRange; } if (_scroledRange > _kRadius) { _scroledRange =_scroledRange - _kRadius; } // 有效滚动才重置layer if (radiuDelta != 0) { [self resetLinesArray]; [self.layer setNeedsDisplay]; } // 设置触发事件 [self sendActionsForControlEvents:UIControlEventValueChanged]; return YES;}/*! @method 重写control的系统方法。 @abstract control结束触碰。 @discussion 结束触碰滚轮的方法。 @param touch 触碰点 @param event 事件 */- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{ [self.layer setNeedsDisplay]; // 设置触发事件 [self sendActionsForControlEvents:UIControlEventValueChanged];}复制代码
使用方法:在ViewController中调用如下代码:
DCRotatingWheel *control = [[DCRotatingWheel alloc]initWithFrame:CGRectMake(40, 340, self.view.frame.size.width-80, 50)]; [control addTarget:self action:@selector(onControlTouchDown:) forControlEvents:UIControlEventTouchDown]; [control addTarget:self action:@selector(onControlTouchUpInside:) forControlEvents:UIControlEventTouchUpInside]; [control addTarget:self action:@selector(onControlValueChange:) forControlEvents:UIControlEventValueChanged]; [self.view addSubview:control];复制代码
####二、自定义视频播放器
NSString *filePath = [[NSBundle mainBundle]pathForResource:@"xiujian2" ofType:@"mp4"]; NSURL *url = [NSURL fileURLWithPath:filePath]; self.item = [[AVPlayerItem alloc]initWithURL:url]; self.player = [[AVPlayer alloc]initWithPlayerItem:_item]; AVPlayerLayer *avLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; avLayer.frame = CGRectMake(0, 70, self.view.frame.size.width, 180); avLayer.videoGravity = AVLayerVideoGravityResizeAspect; [self.view.layer addSublayer:avLayer];复制代码
2.注册三个通知:
// 注册观察者,观察status属性 [_item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; // 观察缓冲进度 [_item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; // 播放完成通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];复制代码
3.观察播放进度及缓冲进度:
// 观察播放进度- (void)monitoringPlayBack:(AVPlayerItem *)item { __weak typeof(self)WeekSelf = self; // 播放进度, 每秒执行30次, CMTime 为30分之一秒 _playTimeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 30.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { // 当前播放秒 float currentPlayTime = (double)item.currentTime.value/ item.currentTime.timescale; // 更新播放进度Slider [WeekSelf updateVideoSlider:currentPlayTime]; }];}// 更新滑条- (void)updateVideoSlider:(float)currentTime { self.mp4Slider.value = currentTime; self.startTime.text = [self convertTime:currentTime];}复制代码
// 已缓冲进度- (NSTimeInterval)availableDurationRanges { NSArray *loadedTimeRanges = [_item loadedTimeRanges]; // 获取item的缓冲数组 // CMTimeRange 结构体 start duration 表示起始位置 和 持续时间 CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue]; // 获取缓冲区域 float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); NSTimeInterval result = startSeconds + durationSeconds; // 计算总缓冲时间 = start + duration return result;}复制代码
4.观察item的播放状态:
// 观察status属性- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context{ if ([keyPath isEqualToString:@"status"]) { AVPlayerStatus status = [[change objectForKey:@"new"]integerValue]; // 准备播放 if (status == AVPlayerStatusReadyToPlay) { CMTime duration = _item.duration; NSLog(@"sssss%.2f", CMTimeGetSeconds(duration)); // 设置视频时间 [self setMaxDuration:CMTimeGetSeconds(duration)]; [self.player play]; }// 播放视频失败 else if (status == AVPlayerStatusFailed) { NSLog(@"AVPlayerStatusFailed"); } else { NSLog(@"AVPlayerStatusUnknown"); } // 缓冲进度 }else if ([keyPath isEqualToString:@"loadedTimeRanges"]){ NSTimeInterval timeInterval = [self availableDurationRanges]; CGFloat totalDuration = CMTimeGetSeconds(_item.duration); // 总时间 [self.progressView setProgress:timeInterval / totalDuration animated:YES]; }}复制代码
5.慢放滚轮或Slider控制播放进度的方法:
- (IBAction)ValueChange:(id)sender { [self.player pause]; _play = NO; [self setPlayBtnImage]; CMTime changeTime = CMTimeMakeWithSeconds(self.mp4Slider.value,1.0); NSLog(@"%.2f", self.mp4Slider.value); // seekToTime:控制视频跳转到指定时间播放,慢放滚轮也用相同的方法。 [_item seekToTime:changeTime completionHandler:^(BOOL finished) { // [self.player play]; }]; }复制代码
需要注意的是,注册了通知,最后不要忘记要移除通知。其实这个播放器自定义的也不够完全,什么多倍播放,全屏处理,滑动屏幕快进,调节音量,调节屏幕亮度,调节分辨率什么的,都暂时还未处理。我做到这里就停了是因为我已实现慢放滚轮的功能,至于滚轮要添加到哪里,怎么用,可按照自己的需求添加。以上这些功能也不是做不出来,后期有时间我会慢慢加上。
最后说一下,文中关于慢放滚轮的设计思路,第4点我写的是暂无。关于需求所提到的“惯性”,我暂时还没有思路,不知道要怎么实现。要完全模拟滚轮特性,这惯性是必不可少的!如果各位有好的思路,还望不吝赐教。
#####本文于3月21日下午再次编辑,补充慢放滚轮惯性的设计思路
4.在滚轮将要结束滑动时,判断滑动距离向左或向右超出某一范围值时,设置定时器,让其持续滚动一小段距离。
相关代码:
/*! @method 重写control的系统方法。 @abstract control结束触碰。 @discussion 结束触碰滚轮的方法。 @param touch 触碰点 @param event 事件 */- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{ self.value = _temLength; [self.layer setNeedsDisplay]; // 设置触发事件 [self sendActionsForControlEvents:UIControlEventValueChanged]; if (_temLength > 0 && _temLength < 10 ) { return; } if (_temLength < 0 && _temLength >-10) { return; } _timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(onTimerEvent) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop]addTimer:_timer forMode:NSDefaultRunLoopMode]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [_timer invalidate]; _timer = nil; });}- (void)onTimerEvent{ // 向左惯性滑动 if (_temLength < 0) { _temLength -= 0.5; }// 向右惯性滑动 else if (_temLength > 0) { _temLength += 0.5; } _scroledRange += _temLength; if (_scroledRange < 0) { _scroledRange =self.frame.size.width/2 + _scroledRange; } if (_scroledRange > self.frame.size.width/2) { _scroledRange =_scroledRange - self.frame.size.width/2; } [self resetLinesArray]; [self.layer setNeedsDisplay]; // 设置代理方法,将每一时刻的值传出去 [_delegate onDCRotatingWheelDelegateInertanceEventWithValue:_temLength]; }复制代码
补充的代理方法:
具体使用方法:在主控制器中设置代理,并实现代理方法:
/*! @method 慢放滚轮的惯性代理方法。 @abstract 慢放滚轮的惯性代理方法。 @param value 每次滚动的值 */- (void)onDCRotatingWheelDelegateInertanceEventWithValue:(float)value{ float currentTime = self.mp4Slider.value+(value*0.1 );// 滚轮的拨动距离乘以系数,来控制进度(0.5) if (currentTime > 0) { CMTime changeTime = CMTimeMakeWithSeconds(currentTime,1.0); [self updateVideoSlider:currentTime]; NSLog(@"%.2f", self.mp4Slider.value); [_item seekToTime:changeTime completionHandler:^(BOOL finished) { }]; } }复制代码
转载请注明出处