blob: 91dad9ec57c8da7747697d0debf1b6a69ec63eb1 [file] [log] [blame]
//
// Created by Jesse Squires
// http://www.jessesquires.com
//
//
// Documentation
// http://cocoadocs.org/docsets/JSQMessagesViewController
//
//
// GitHub
// https://github.com/jessesquires/JSQMessagesViewController
//
//
// License
// Copyright (c) 2014 Jesse Squires
// Released under an MIT license: http://opensource.org/licenses/MIT
//
#import "JSQAudioMediaItem.h"
#import "JSQMessagesMediaPlaceholderView.h"
#import "JSQMessagesMediaViewBubbleImageMasker.h"
#import "UIImage+JSQMessages.h"
#import "UIColor+JSQMessages.h"
@interface JSQAudioMediaItem ()
@property (strong, nonatomic) UIView *cachedMediaView;
@property (strong, nonatomic) UIButton *playButton;
@property (strong, nonatomic) UIProgressView *progressView;
@property (strong, nonatomic) UILabel *progressLabel;
@property (strong, nonatomic) NSTimer *progressTimer;
@property (strong, nonatomic) AVAudioPlayer *audioPlayer;
@end
@implementation JSQAudioMediaItem
#pragma mark - Initialization
- (instancetype)initWithData:(NSData *)audioData audioViewAttributes:(JSQAudioMediaViewAttributes *)audioViewAttributes
{
NSParameterAssert(audioViewAttributes != nil);
self = [super init];
if (self) {
_cachedMediaView = nil;
_audioData = [audioData copy];
_audioViewAttributes = audioViewAttributes;
}
return self;
}
- (instancetype)initWithData:(NSData *)audioData
{
return [self initWithData:audioData audioViewAttributes:[[JSQAudioMediaViewAttributes alloc] init]];
}
- (instancetype)initWithAudioViewAttributes:(JSQAudioMediaViewAttributes *)audioViewAttributes
{
return [self initWithData:nil audioViewAttributes:audioViewAttributes];
}
- (instancetype)init
{
return [self initWithData:nil audioViewAttributes:[[JSQAudioMediaViewAttributes alloc] init]];
}
- (void)dealloc
{
_audioData = nil;
[self clearCachedMediaViews];
}
- (void)clearCachedMediaViews
{
[_audioPlayer stop];
_audioPlayer = nil;
_playButton = nil;
_progressView = nil;
_progressLabel = nil;
[self stopProgressTimer];
_cachedMediaView = nil;
[super clearCachedMediaViews];
}
#pragma mark - Setters
- (void)setAudioData:(NSData *)audioData
{
_audioData = [audioData copy];
[self clearCachedMediaViews];
}
- (void)setAudioDataWithUrl:(NSURL *)audioURL
{
_audioData = [NSData dataWithContentsOfURL:audioURL];
[self clearCachedMediaViews];
}
- (void)setAppliesMediaViewMaskAsOutgoing:(BOOL)appliesMediaViewMaskAsOutgoing
{
[super setAppliesMediaViewMaskAsOutgoing:appliesMediaViewMaskAsOutgoing];
_cachedMediaView = nil;
}
#pragma mark - Private
- (void)startProgressTimer
{
self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(updateProgressTimer:)
userInfo:nil
repeats:YES];
}
- (void)stopProgressTimer
{
[_progressTimer invalidate];
_progressTimer = nil;
}
- (void)updateProgressTimer:(NSTimer *)sender
{
if (self.audioPlayer.playing) {
self.progressView.progress = self.audioPlayer.currentTime / self.audioPlayer.duration;
self.progressLabel.text = [self timestampString:self.audioPlayer.currentTime
forDuration:self.audioPlayer.duration];
}
}
- (NSString *)timestampString:(NSTimeInterval)currentTime forDuration:(NSTimeInterval)duration
{
// print the time as 0:ss or ss.x up to 59 seconds
// print the time as m:ss up to 59:59 seconds
// print the time as h:mm:ss for anything longer
if (duration < 60) {
if (self.audioViewAttributes.showFractionalSeconds) {
return [NSString stringWithFormat:@"%.01f", currentTime];
}
else if (currentTime < duration) {
return [NSString stringWithFormat:@"0:%02d", (int)round(currentTime)];
}
return [NSString stringWithFormat:@"0:%02d", (int)ceil(currentTime)];
}
else if (duration < 3600) {
return [NSString stringWithFormat:@"%d:%02d", (int)currentTime / 60, (int)currentTime % 60];
}
return [NSString stringWithFormat:@"%d:%02d:%02d", (int)currentTime / 3600, (int)currentTime / 60, (int)currentTime % 60];
}
- (void)onPlayButton:(UIButton *)sender
{
NSString *category = [AVAudioSession sharedInstance].category;
AVAudioSessionCategoryOptions options = [AVAudioSession sharedInstance].categoryOptions;
if (category != self.audioViewAttributes.audioCategory || options != self.audioViewAttributes.audioCategoryOptions) {
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:self.audioViewAttributes.audioCategory
withOptions:self.audioViewAttributes.audioCategoryOptions
error:&error];
if (self.delegate) {
[self.delegate audioMediaItem:self didChangeAudioCategory:category options:options error:error];
}
}
if (self.audioPlayer.playing) {
self.playButton.selected = NO;
[self stopProgressTimer];
[self.audioPlayer stop];
}
else {
// fade the button from play to pause
[UIView transitionWithView:self.playButton
duration:.2
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
self.playButton.selected = YES;
}
completion:nil];
[self startProgressTimer];
[self.audioPlayer play];
}
}
#pragma mark - AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player
successfully:(BOOL)flag {
// set progress to full, then fade back to the default state
[self stopProgressTimer];
self.progressView.progress = 1;
[UIView transitionWithView:self.cachedMediaView
duration:.2
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
self.progressView.progress = 0;
self.playButton.selected = NO;
self.progressLabel.text = [self timestampString:self.audioPlayer.duration
forDuration:self.audioPlayer.duration];
}
completion:nil];
}
#pragma mark - JSQMessageMediaData protocol
- (CGSize)mediaViewDisplaySize
{
return CGSizeMake(160.0f,
self.audioViewAttributes.controlInsets.top +
self.audioViewAttributes.controlInsets.bottom +
self.audioViewAttributes.playButtonImage.size.height);
}
- (UIView *)mediaView
{
if (self.audioData && self.cachedMediaView == nil) {
if (self.audioData) {
self.audioPlayer = [[AVAudioPlayer alloc] initWithData:self.audioData error:nil];
self.audioPlayer.delegate = self;
}
// reverse the insets based on the message direction
CGFloat leftInset, rightInset;
if (self.appliesMediaViewMaskAsOutgoing) {
leftInset = self.audioViewAttributes.controlInsets.left;
rightInset = self.audioViewAttributes.controlInsets.right;
} else {
leftInset = self.audioViewAttributes.controlInsets.right;
rightInset = self.audioViewAttributes.controlInsets.left;
}
// create container view for the various controls
CGSize size = [self mediaViewDisplaySize];
UIView * playView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, size.width, size.height)];
playView.backgroundColor = self.audioViewAttributes.backgroundColor;
playView.contentMode = UIViewContentModeCenter;
playView.clipsToBounds = YES;
// create the play button
CGRect buttonFrame = CGRectMake(leftInset,
self.audioViewAttributes.controlInsets.top,
self.audioViewAttributes.playButtonImage.size.width,
self.audioViewAttributes.playButtonImage.size.height);
self.playButton = [[UIButton alloc] initWithFrame:buttonFrame];
[self.playButton setImage:self.audioViewAttributes.playButtonImage forState:UIControlStateNormal];
[self.playButton setImage:self.audioViewAttributes.pauseButtonImage forState:UIControlStateSelected];
[self.playButton addTarget:self action:@selector(onPlayButton:) forControlEvents:UIControlEventTouchUpInside];
[playView addSubview:self.playButton];
// create a label to show the duration / elapsed time
NSString *durationString = [self timestampString:self.audioPlayer.duration
forDuration:self.audioPlayer.duration];
NSString *maxWidthString = [@"" stringByPaddingToLength:[durationString length] withString:@"0" startingAtIndex:0];
// this is cheesy, but it centers the progress bar without extra space and
// without causing it to wiggle from side to side as the label text changes
CGSize labelSize = CGSizeMake(36, 18);
if ([durationString length] < 4) {
labelSize = CGSizeMake(18,18);
}
else if ([durationString length] < 5) {
labelSize = CGSizeMake(24,18);
}
else if ([durationString length] < 6) {
labelSize = CGSizeMake(30, 18);
}
CGRect labelFrame = CGRectMake(size.width - labelSize.width - rightInset,
self.audioViewAttributes.controlInsets.top, labelSize.width, labelSize.height);
self.progressLabel = [[UILabel alloc] initWithFrame:labelFrame];
self.progressLabel.textAlignment = NSTextAlignmentLeft;
self.progressLabel.adjustsFontSizeToFitWidth = YES;
self.progressLabel.textColor = self.audioViewAttributes.tintColor;
self.progressLabel.font = self.audioViewAttributes.labelFont;
self.progressLabel.text = maxWidthString;
// sizeToFit adjusts the frame's height to the font
[self.progressLabel sizeToFit];
labelFrame.origin.x = size.width - self.progressLabel.frame.size.width - rightInset;
labelFrame.origin.y = ((size.height - self.progressLabel.frame.size.height) / 2);
labelFrame.size.width = self.progressLabel.frame.size.width;
labelFrame.size.height = self.progressLabel.frame.size.height;
self.progressLabel.frame = labelFrame;
self.progressLabel.text = durationString;
[playView addSubview:self.progressLabel];
// create a progress bar
self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
CGFloat xOffset = self.playButton.frame.origin.x + self.playButton.frame.size.width + self.audioViewAttributes.controlPadding;
CGFloat width = labelFrame.origin.x - xOffset - self.audioViewAttributes.controlPadding;
self.progressView.frame = CGRectMake(xOffset, (size.height - self.progressView.frame.size.height) / 2,
width, self.progressView.frame.size.height);
self.progressView.tintColor = self.audioViewAttributes.tintColor;
[playView addSubview:self.progressView];
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:playView isOutgoing:self.appliesMediaViewMaskAsOutgoing];
self.cachedMediaView = playView;
}
return self.cachedMediaView;
}
- (NSUInteger)mediaHash
{
return self.hash;
}
#pragma mark - NSObject
- (BOOL)isEqual:(id)object
{
if (![super isEqual:object]) {
return NO;
}
JSQAudioMediaItem *audioItem = (JSQAudioMediaItem *)object;
if (self.audioData && ![self.audioData isEqualToData:audioItem.audioData]) {
return NO;
}
return YES;
}
- (NSUInteger)hash
{
return super.hash ^ self.audioData.hash;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: audioData=%ld bytes, appliesMediaViewMaskAsOutgoing=%@>",
[self class], (unsigned long)[self.audioData length],
@(self.appliesMediaViewMaskAsOutgoing)];
}
#pragma mark - NSCoding
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
NSData *data = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(audioData))];
return [self initWithData:data];
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeObject:self.audioData forKey:NSStringFromSelector(@selector(audioData))];
}
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone
{
JSQAudioMediaItem *copy = [[[self class] allocWithZone:zone] initWithData:self.audioData
audioViewAttributes:self.audioViewAttributes];
copy.appliesMediaViewMaskAsOutgoing = self.appliesMediaViewMaskAsOutgoing;
return copy;
}
@end