mirror of
https://github.com/danog/Telegram.git
synced 2024-12-02 09:27:55 +01:00
582 lines
18 KiB
Objective-C
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
|