1
0
mirror of https://github.com/danog/Telegram.git synced 2024-12-02 09:27:55 +01:00
Telegram/Telegraph/TGAudioSliderView.m
2016-02-25 01:03:51 +01:00

582 lines
18 KiB
Objective-C

/*
* This is the source code of Telegram for iOS v. 1.1
* It is licensed under GNU GPL v. 2 or later.
* You should have received a copy of the license in this archive (see LICENSE).
*
* Copyright Peter Iakovlev, 2013.
*/
#import "TGAudioSliderView.h"
#import <MTProtoKit/MTTime.h>
#import "TGFont.h"
#import "TGAudioSliderButton.h"
#import "TGAudioSliderArea.h"
#import "TGImageUtils.h"
#import "TGAudioWaveformView.h"
#import "TGAudioWaveform.h"
#import "TGSharedMediaUtils.h"
#import "TGMusicPlayer.h"
#import <pop/POP.h>
@interface TGAudioSliderView () <TGAudioSliderAreaDelegate>
{
TGAudioWaveformView *_waveformView;
CGPoint _sliderButtonStartLocation;
float _sliderButtonStartValue;
TGAudioSliderArea *_sliderArea;
UILabel *_durationLabel;
int32_t _durationLabelSeconds;
UIImageView *_statusIconView;
bool _isScrubbing;
CGFloat _scrubbingPosition;
CGFloat _scrubbingAbsoluteDistance;
SMetaDisposable *_waveformDisposable;
TGMusicPlayerStatus *_status;
CGFloat _immediateProgress;
}
@property (nonatomic, strong) NSString *viewIdentifier;
@property (nonatomic, strong) NSString *viewStateIdentifier;
@end
@implementation TGAudioSliderView
- (UIImage *)trackImageWithColor:(UIColor *)color
{
CGFloat radius = 2.0f;
UIGraphicsBeginImageContextWithOptions(CGSizeMake(4.0f, 2.0f), false, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillEllipseInRect(context, CGRectMake(0.0f, 0.0f, radius, radius));
CGContextFillRect(context, CGRectMake(1.0f, 0.0f, 2.0f, 2.0f));
CGContextFillEllipseInRect(context, CGRectMake(2.0f, 0.0f, radius, radius));
UIImage *image = [UIGraphicsGetImageFromCurrentImageContext() stretchableImageWithLeftCapWidth:2 topCapHeight:0];
UIGraphicsEndImageContext();
return image;
}
- (UIImage *)trackImage:(TGAudioSliderViewStyle)style
{
static UIImage *incomingImage = nil;
static UIImage *outgoingImage = nil;
static UIImage *notificationImage = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
incomingImage = [self trackImageWithColor:UIColorRGB(0xd0d0d0)];
outgoingImage = [self trackImageWithColor:UIColorRGB(0x9ce192)];
notificationImage = [self trackImageWithColor:UIColorRGB(0x9c9c9c)];
});
switch (style)
{
case TGAudioSliderViewStyleIncoming:
return incomingImage;
case TGAudioSliderViewStyleOutgoing:
return outgoingImage;
case TGAudioSliderViewStyleNotification:
return notificationImage;
default:
return nil;
}
}
- (UIImage *)trackForegroundImage:(TGAudioSliderViewStyle)style
{
static UIImage *incomingImage = nil;
static UIImage *outgoingImage = nil;
static UIImage *notificationImage = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
incomingImage = [self trackImageWithColor:TGAccentColor()];
outgoingImage = [self trackImageWithColor:UIColorRGB(0x3fc33b)];
notificationImage = [self trackImageWithColor:[UIColor whiteColor]];
});
switch (style)
{
case TGAudioSliderViewStyleIncoming:
return incomingImage;
case TGAudioSliderViewStyleOutgoing:
return outgoingImage;
case TGAudioSliderViewStyleNotification:
return notificationImage;
default:
return nil;
}
}
- (UIImage *)thumbImageWithColor:(UIColor *)color
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(5.0f, 14.0f), false, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillRect(context, CGRectMake(2.0f, 0.0f, 1.5f, 14.0f));
UIImage *image = [UIGraphicsGetImageFromCurrentImageContext() stretchableImageWithLeftCapWidth:2 topCapHeight:0];
UIGraphicsEndImageContext();
return image;
}
- (UIImage *)thumbImage:(TGAudioSliderViewStyle)style
{
static UIImage *incomingImage = nil;
static UIImage *outgoingImage = nil;
static UIImage *notificationImage = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
incomingImage = [self thumbImageWithColor:TGAccentColor()];
outgoingImage = [self thumbImageWithColor:UIColorRGB(0x3fc33b)];
notificationImage = [self thumbImageWithColor:UIColorRGB(0xf6f6f6)];
});
switch (style)
{
case TGAudioSliderViewStyleIncoming:
return incomingImage;
case TGAudioSliderViewStyleOutgoing:
return outgoingImage;
case TGAudioSliderViewStyleNotification:
return notificationImage;
default:
return nil;
}
}
- (UIImage *)emptyThumbImage
{
static UIImage *image = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
image = [self thumbImageWithColor:[UIColor clearColor]];
});
return image;
}
- (UIColor *)durationColor:(TGAudioSliderViewStyle)style
{
static UIColor *incomingColor = nil;
static UIColor *outgoingColor = nil;
static UIColor *notificationColor = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
incomingColor = UIColorRGBA(0x525252, 0.6f);
outgoingColor = UIColorRGBA(0x008c09, 0.8f);
notificationColor = [UIColor whiteColor];
});
switch (style)
{
case TGAudioSliderViewStyleIncoming:
return incomingColor;
case TGAudioSliderViewStyleOutgoing:
return outgoingColor;
case TGAudioSliderViewStyleNotification:
return notificationColor;
default:
return nil;
}
}
- (UIImage *)circleImageWithColor:(UIColor *)color radius:(CGFloat)radius
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(radius, radius), false, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillEllipseInRect(context, CGRectMake(0.0f, 0.0f, radius, radius));
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
- (UIImage *)listenedStatusImage:(TGAudioSliderViewStyle)style
{
static UIImage *incomingImage = nil;
static UIImage *outgoingImage = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
incomingImage = [self circleImageWithColor:UIColorRGB(0x1581e2) radius:3.0f];
outgoingImage = [self circleImageWithColor:UIColorRGB(0x19c700) radius:3.0f];
});
switch (style)
{
case TGAudioSliderViewStyleIncoming:
return incomingImage;
case TGAudioSliderViewStyleOutgoing:
return outgoingImage;
default:
return nil;
}
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
_waveformView = [[TGAudioWaveformView alloc] init];
[self addSubview:_waveformView];
switch (_style)
{
case TGAudioSliderViewStyleIncoming:
[_waveformView setForegroundColor:TGAccentColor() backgroundColor:UIColorRGB(0xcacaca)];
break;
case TGAudioSliderViewStyleOutgoing:
[_waveformView setForegroundColor:UIColorRGB(0x3fc33b) backgroundColor:UIColorRGB(0x93d987)];
break;
case TGAudioSliderViewStyleNotification:
[_waveformView setForegroundColor:UIColorRGB(0xf6f6f6) backgroundColor:UIColorRGB(0x838282)];
break;
default:
break;
}
//[self addSubview:_sliderButton];
_sliderArea = [[TGAudioSliderArea alloc] init];
_sliderArea.userInteractionEnabled = false;
_sliderArea.delegate = self;
[self addSubview:_sliderArea];
_durationLabel = [[UILabel alloc] init];
_durationLabel.textAlignment = NSTextAlignmentLeft;
_durationLabel.backgroundColor = [UIColor clearColor];
_durationLabel.textColor = [self durationColor:_style];
_durationLabel.font = TGSystemFontOfSize(11.0f);
[self addSubview:_durationLabel];
_durationLabelSeconds = -1;
_statusIconView = [[UIImageView alloc] initWithImage:[self listenedStatusImage:_style]];
[self addSubview:_statusIconView];
_waveformDisposable = [[SMetaDisposable alloc] init];
}
return self;
}
- (void)dealloc
{
[_waveformDisposable dispose];
}
- (void)willBecomeRecycled
{
[_waveformDisposable setDisposable:nil];
[_waveformView setWaveform:nil];
[self pop_removeAnimationForKey:@"scrubbingIndicator"];
_immediateProgress = 0.0f;
}
- (void)setStyle:(TGAudioSliderViewStyle)style
{
if (_style != style)
{
_style = style;
switch (_style)
{
case TGAudioSliderViewStyleIncoming:
[_waveformView setForegroundColor:TGAccentColor() backgroundColor:UIColorRGB(0xced9e0)];
break;
case TGAudioSliderViewStyleOutgoing:
[_waveformView setForegroundColor:UIColorRGB(0x3fc33b) backgroundColor:UIColorRGB(0x93d987)];
break;
case TGAudioSliderViewStyleNotification:
[_waveformView setForegroundColor:UIColorRGB(0xf6f6f6) backgroundColor:UIColorRGB(0xf6f6f6)];
break;
default:
break;
}
_durationLabel.textColor = [self durationColor:_style];
if (_style == TGAudioSliderViewStyleNotification)
{
_durationLabel.font = TGSystemFontOfSize(13.0f);
_durationLabel.textAlignment = NSTextAlignmentCenter;
}
_statusIconView.image = [self listenedStatusImage:_style];
}
[self setNeedsLayout];
}
- (void)setDurationLabelFromSeconds:(int32_t)seconds {
if (_durationLabelSeconds != seconds) {
_durationLabelSeconds = seconds;
_durationLabel.text = [[NSString alloc] initWithFormat:@"%d:%02d", seconds / 60, seconds % 60];
[_durationLabel sizeToFit];
}
}
- (void)setDuration:(int32_t)duration
{
_duration = duration;
int32_t seconds = (int32_t)duration;
if (_status == nil) {
[self setDurationLabelFromSeconds:seconds];
}
}
- (void)setStatus:(TGMusicPlayerStatus *)status {
_status = status;
static POPAnimatableProperty *property = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
property = [POPAnimatableProperty propertyWithName:@"playbackOffset" initializer:^(POPMutableAnimatableProperty *prop)
{
prop.readBlock = ^(TGAudioSliderView *strongSelf, CGFloat *values)
{
values[0] = strongSelf->_immediateProgress;
};
prop.writeBlock = ^(TGAudioSliderView *strongSelf, CGFloat const *values)
{
[strongSelf setImmediateProgress:values[0]];
[strongSelf layoutProgress];
};
}];
});
if (status == nil || status.paused || status.duration < FLT_EPSILON || status.offset < 0.01)
{
[self pop_removeAnimationForKey:@"scrubbingIndicator"];
if (status == nil) {
if (_style == TGAudioSliderViewStyleIncoming && !_listenedStatus) {
[self setImmediateProgress:1.0f];
} else {
[self setImmediateProgress:0.0f];
}
} else {
[self setImmediateProgress:status.offset];
}
[self layoutProgress];
}
else
{
[self pop_removeAnimationForKey:@"scrubbingIndicator"];
POPBasicAnimation *animation = [self pop_animationForKey:@"scrubbingIndicator"];
if (animation == nil)
{
animation = [POPBasicAnimation linearAnimation];
[animation setProperty:property];
animation.removedOnCompletion = true;
if (ABS(status.offset - _immediateProgress) < 0.3) {
animation.fromValue = @(_immediateProgress);
} else {
animation.fromValue = @(status.offset);
}
animation.toValue = @(1.0f);
animation.beginTime = status.timestamp;
animation.duration = (1.0f - status.offset) * status.duration;
[self pop_addAnimation:animation forKey:@"scrubbingIndicator"];
}
}
}
- (void)setWaveformSignal:(SSignal *)waveformSignal {
__weak TGAudioSliderView *weakSelf = self;
[_waveformDisposable setDisposable:[[waveformSignal deliverOn:[SQueue mainQueue]] startWithNext:^(TGAudioWaveform *waveform) {
__strong TGAudioSliderView *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf->_waveformView setWaveform:waveform];
}
}]];
}
- (void)setImmediateProgress:(CGFloat)immediateProgress {
_immediateProgress = immediateProgress;
_waveformView.foregroundClippingView.frame = [self waveformClippingFrameForProgress:immediateProgress];
if (_status == nil) {
[self setDurationLabelFromSeconds:_duration];
} else {
[self setDurationLabelFromSeconds:(int32_t)(_status.offset * _status.duration)];
}
}
- (void)setManualPositionAdjustmentEnabled:(bool)manualPositionAdjustmentEnabled
{
if (_manualPositionAdjustmentEnabled != manualPositionAdjustmentEnabled)
{
_manualPositionAdjustmentEnabled = manualPositionAdjustmentEnabled;
_sliderArea.userInteractionEnabled = _manualPositionAdjustmentEnabled;
}
}
- (CGRect)waveformClippingFrameForProgress:(CGFloat)progress
{
return CGRectMake(0.0f, 0.0f, CGFloor(_waveformView.backgroundView.frame.size.width * progress), 22.0f);
}
- (void)layoutProgress
{
CGRect bounds = self.bounds;
CGFloat progressValue = 0.0f;
if (_isScrubbing) {
progressValue = _scrubbingPosition;
} else {
progressValue = _immediateProgress;
}
CGRect sliderFrame = CGRectMake(2.0f, CGFloor((bounds.size.height - 22.0f) / 2.0f), bounds.size.width - 2.0f, 2.0f);
_waveformView.frame = CGRectMake(2.0f, CGFloor((bounds.size.height - 22.0f) / 2.0f), bounds.size.width - 2.0f, 22.0f);
[_waveformView backgroundView].frame = CGRectMake(0.0f, 0.0f, bounds.size.width - 2.0f, 22.0f);
[_waveformView foregroundView].frame = CGRectMake(0.0f, 0.0f, bounds.size.width - 2.0f, 22.0f);
_waveformView.foregroundClippingView.frame = [self waveformClippingFrameForProgress:progressValue];
_sliderArea.frame = CGRectMake(sliderFrame.origin.x, 0.0f, sliderFrame.size.width, bounds.size.height);
}
- (void)setListenedStatus:(bool)listenedStatus
{
_listenedStatus = listenedStatus;
_statusIconView.hidden = listenedStatus;
[self setNeedsLayout];
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect bounds = self.bounds;
CGSize durationSize = _durationLabel.frame.size;
if (_style == TGAudioSliderViewStyleNotification)
_durationLabel.frame = CGRectMake(bounds.size.width, -1, 39, durationSize.height);
else
_durationLabel.frame = CGRectMake(0.0f, CGFloor((bounds.size.height - durationSize.height) / 2.0f) + 27.0f, durationSize.width, durationSize.height);
_statusIconView.frame = CGRectMake(CGRectGetMaxX(_durationLabel.frame) + 2.0f + TGRetinaPixel, _durationLabel.frame.origin.y + 5.0f + TGRetinaPixel, _statusIconView.frame.size.width, _statusIconView.frame.size.height);
[self layoutProgress];
}
- (void)audioSliderDidBeginDragging:(TGAudioSliderArea *)__unused sliderArea withTouch:(UITouch *)touch
{
_isScrubbing = true;
_scrubbingAbsoluteDistance = 0.0f;
_sliderButtonStartLocation = [touch locationInView:self];
_sliderButtonStartValue = (float)_status.offset;
_scrubbingPosition = _sliderButtonStartValue;
id<TGAudioSliderViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(audioSliderViewDidBeginPositionAdjustment:)])
[delegate audioSliderViewDidBeginPositionAdjustment:self];
}
- (void)audioSliderDidFinishDragging:(TGAudioSliderArea *)__unused sliderArea
{
_isScrubbing = false;
bool smallChange = ABS(_scrubbingAbsoluteDistance) < 2.0f;
_scrubbingAbsoluteDistance = 0.0f;
[self setDurationLabelFromSeconds:(int32_t)(_status.offset * _status.duration)];
[self layoutProgress];
id<TGAudioSliderViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(audioSliderViewDidEndPositionAdjustment:atPosition:smallChange:)])
[delegate audioSliderViewDidEndPositionAdjustment:self atPosition:_scrubbingPosition smallChange:smallChange];
}
- (void)audioSliderDidCancelDragging:(TGAudioSliderArea *)__unused sliderArea
{
_isScrubbing = false;
_scrubbingAbsoluteDistance = 0.0f;
[self layoutProgress];
[self setDurationLabelFromSeconds:(int32_t)(_status.offset * _status.duration)];
id<TGAudioSliderViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(audioSliderViewDidCancelPositionAdjustment:)])
[delegate audioSliderViewDidCancelPositionAdjustment:self];
}
- (void)audioSliderWillMove:(TGAudioSliderButton *)__unused button withTouch:(UITouch *)touch
{
if (_isScrubbing && _waveformView.frame.size.width > 1.0f)
{
CGFloat positionDistance = [touch locationInView:self].x - _sliderButtonStartLocation.x;
if (ABS(_scrubbingAbsoluteDistance) < ABS(positionDistance)) {
_scrubbingAbsoluteDistance = ABS(positionDistance);
}
CGRect bounds = self.bounds;
CGRect sliderFrame = CGRectMake(2.0f, CGFloor((bounds.size.height - 22.0f) / 2.0f), bounds.size.width - 2.0f, 2.0f);
CGFloat newValue = MAX(0.0f, MIN(1.0f, _sliderButtonStartValue + positionDistance / sliderFrame.size.width));
_scrubbingPosition = newValue;
int32_t currentPosition = (int32_t)(_status.duration * _scrubbingPosition);
[self setDurationLabelFromSeconds:currentPosition];
[self layoutProgress];
}
}
@end