受同學之邀,幫忙自定義一控制元件。需求是:開發慢放滾輪,用手指撥動實現幀級的慢速播放,滾輪可雙向撥動,其滾動具有慣性,滾動速度決定視訊播放的速度。需求很明朗,可我卻是一頭霧水。說實話,在此之前我還沒有自定義過視訊播放器,不懂怎麼實現‘幀級慢速播放’。並且滾輪這個東西自定義起來,我也是沒譜的。越是自己覺得陌生的東西,越是想辦法迴避,想著挑戰一下自己,就嘗試著做了起來。
要實現這個需求,首先滾輪是要自定義的。再說視訊播放器,如果要用別人寫好的視訊播放器框架,需要coder剛好提供了一個方法,專門控制視訊播放進度的。為了避免繁瑣,我基於系統的AVPlayer自定義了視訊播放器,實現了相關功能。DCVideoPlyer
####一、自定義慢放滾輪#####設計思路: 1.滾輪有三個狀態:開始觸碰、持續滾動、結束觸碰。基於這三個狀態考慮,決定在Control上自定義。
2.滾輪看起來得有立體的感覺,刻度線間距應該有變化,並且長度也應該有變化,達到近大遠小的效果。
3.滾輪上的刻度線是繞著圓心轉動的,想象著平面上是一個半圓,刻度線的長度與間距從小到大,又從大到小變化,你是不是想起了高中所學三角函式,這裡就是利用三角函式與反三角函式實現相關功能。
4.暫無
#####實現功能: 實現滾輪刻度線的整體效果,其關鍵核心就是利用三角函式,計算出每條線條的起始點。要驗證函式的正確性,可將0、π/2、π,三個弧度代入檢驗。/*!
@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];
複製程式碼
####二、自定義視訊播放器
1.建立AVPlayer: 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<NSKeyValueChangeKey,id> *)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) {
}];
}
}
複製程式碼
轉載請註明出處