session-ios/Signal/src/views/OWSExpirationTimerView.m
Michael Kirk 49de772997 Fix slow reloading conversation view. (#1397)
Fix animation memory leak exacerbated every time you reload a
conversation with expiration timers.

Stop animation on cells that aren't currently being displayed. This is
relatively minor compared to the above, but still, no reason to waste
cycles.

// FREEBIE
2016-10-15 16:41:40 -04:00

224 lines
7.3 KiB
Objective-C

// Created by Michael Kirk on 9/29/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "OWSExpirationTimerView.h"
#import "MessagesViewController.h"
#import "UIColor+OWS.h"
#import <QuartzCore/CAShapeLayer.h>
NS_ASSUME_NONNULL_BEGIN
double const OWSExpirationTimerViewBlinkingSeconds = 2;
@interface OWSExpirationTimerView ()
@property (nonatomic) uint32_t initialDurationSeconds;
@property (atomic) uint64_t expiresAtSeconds;
@property (nonatomic, readonly) UIImageView *emptyHourglassImageView;
@property (nonatomic, readonly) UIImageView *fullHourglassImageView;
@property CGFloat ratioRemaining;
@end
@implementation OWSExpirationTimerView
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (!self) {
return self;
}
self.clipsToBounds = YES;
_emptyHourglassImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_hourglass_empty"]];
_emptyHourglassImageView.tintColor = [UIColor ows_blackColor];
[self insertSubview:_emptyHourglassImageView atIndex:0];
_fullHourglassImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_hourglass_full"]];
_fullHourglassImageView.tintColor = [UIColor ows_darkGrayColor];
[self insertSubview:_fullHourglassImageView atIndex:1];
_ratioRemaining = 1.0f;
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (!self) {
return self;
}
self.clipsToBounds = YES;
_emptyHourglassImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_hourglass_empty"]];
_emptyHourglassImageView.tintColor = [UIColor lightGrayColor];
[self insertSubview:_emptyHourglassImageView atIndex:1];
_fullHourglassImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_hourglass_full"]];
_fullHourglassImageView.tintColor = [UIColor lightGrayColor];
[self insertSubview:_fullHourglassImageView atIndex:0];
_ratioRemaining = 1.0f;
return self;
}
- (void)layoutSubviews
{
CGFloat leftMargin = 0.0f;
CGFloat padding = 6.0f;
CGRect hourglassFrame
= CGRectMake(leftMargin, padding / 2, self.frame.size.height - padding, self.frame.size.height - padding);
self.emptyHourglassImageView.frame = hourglassFrame;
self.emptyHourglassImageView.bounds = hourglassFrame;
self.fullHourglassImageView.frame = hourglassFrame;
self.fullHourglassImageView.bounds = hourglassFrame;
}
- (void)handleReappearNotification:(NSNotification *)notification
{
DDLogVerbose(@"%@ handleReappearNotification", self.logTag);
[self startAnimation];
}
- (void)startTimerWithExpiresAtSeconds:(uint64_t)expiresAtSeconds
initialDurationSeconds:(uint32_t)initialDurationSeconds
{
if (expiresAtSeconds == 0) {
DDLogWarn(
@"%@ Asked to animate expiration for message which hasn't started expiring. intitialDurationSeconds:%u",
self.logTag,
initialDurationSeconds);
}
DDLogVerbose(@"%@ Starting timer with expiresAtSeconds: %llu initialDurationSeconds: %d",
self.logTag,
expiresAtSeconds,
initialDurationSeconds);
self.expiresAtSeconds = expiresAtSeconds;
self.initialDurationSeconds = initialDurationSeconds;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleReappearNotification:)
name:OWSMessagesViewControllerDidAppearNotification
object:nil];
[self startAnimation];
}
- (void)startAnimation
{
DDLogVerbose(@"%@ Starting animation with expiresAtSeconds: %llu initialDurationSeconds: %d",
self.logTag,
self.expiresAtSeconds,
self.initialDurationSeconds);
double secondsLeft = (double)self.expiresAtSeconds - [NSDate new].timeIntervalSince1970;
if (secondsLeft < 0) {
secondsLeft = 0;
}
// Get hourglass frames to the proper size.
[self setNeedsLayout];
[self layoutIfNeeded];
CAGradientLayer *maskLayer = [CAGradientLayer new];
self.fullHourglassImageView.layer.mask = maskLayer;
// Without this the hourglass appears empty too soon.
CGFloat borderOffset = 2.0;
maskLayer.frame = CGRectInset(self.fullHourglassImageView.frame, 0, -borderOffset);
// Blur the top of the mask a bit with gradient
maskLayer.colors = @[ (id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor ];
maskLayer.startPoint = CGPointMake(0.5f, 0);
maskLayer.endPoint = CGPointMake(0.5f, 0.2f);
CGFloat ratioRemaining = ((CGFloat)secondsLeft / (CGFloat)self.initialDurationSeconds);
if (ratioRemaining < 0) {
ratioRemaining = 0.0;
}
CGPoint defaultPosition = maskLayer.position;
CGPoint finalPosition
= CGPointMake(defaultPosition.x, defaultPosition.y + maskLayer.bounds.size.height - 2 * borderOffset);
CGPoint startingPosition = CGPointMake(
defaultPosition.x, finalPosition.y - maskLayer.bounds.size.height * ratioRemaining + borderOffset);
maskLayer.position = startingPosition;
CABasicAnimation *revealAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
revealAnimation.duration = secondsLeft;
revealAnimation.fromValue = [NSValue valueWithCGPoint:startingPosition];
revealAnimation.toValue = [NSValue valueWithCGPoint:finalPosition];
[maskLayer addAnimation:revealAnimation forKey:@"revealAnimation"];
maskLayer.position = finalPosition; // don't snap back
__weak typeof(self) wself = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(long long)((secondsLeft - OWSExpirationTimerViewBlinkingSeconds) * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
[wself startBlinking];
});
}
- (void)stopTimer
{
DDLogVerbose(@"%@ Stopping Timer.", self.logTag);
[[NSNotificationCenter defaultCenter] removeObserver:self
name:OWSMessagesViewControllerDidAppearNotification
object:nil];
[self.layer removeAnimationForKey:@"alphaBlink"];
self.layer.opacity = 1;
}
- (BOOL)itIsTimeToBlink
{
double secondsLeft = (double)self.expiresAtSeconds - [NSDate new].timeIntervalSince1970;
return secondsLeft <= OWSExpirationTimerViewBlinkingSeconds;
}
- (void)startBlinking
{
if (![self itIsTimeToBlink]) {
DDLogVerbose(@"Refusing to start blinking too early. Reused cell?");
return;
}
CABasicAnimation *blinkAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
blinkAnimation.duration = 0.5;
blinkAnimation.fromValue = @(1.0);
blinkAnimation.toValue = @(0.0);
blinkAnimation.repeatCount = 4;
blinkAnimation.autoreverses = YES;
blinkAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.layer addAnimation:blinkAnimation forKey:@"alphaBlink"];
}
#pragma mark - Logging
+ (NSString *)logTag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)logTag
{
return self.class.logTag;
}
@end
NS_ASSUME_NONNULL_END