blob: a0fa29dd5abee591f74751047514b90cbbe98577 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/fade_truncated_label.h"
#include <algorithm>
#import "ios/chrome/browser/ui/animation_util.h"
#import "ios/chrome/browser/ui/reversed_animation.h"
#import "ui/gfx/ios/uikit_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Animation key used for frame animations.
NSString* const kFadeTruncatedLabelAnimationKey =
@"FadeTruncatedLabelAnimationKey";
}
@interface FadeTruncatedLabel ()
// Layer used to apply fade truncation to label.
@property(nonatomic, strong) CAGradientLayer* maskLayer;
// Temporary label used during animations.
@property(nonatomic, strong) UILabel* animationLabel;
// Returns the percentage of the label's width at which to begin the fade
// gradient.
+ (CGFloat)gradientPercentageForText:(NSString*)text
withAttributes:(NSDictionary*)attributes
inBounds:(CGRect)bounds;
@end
@implementation FadeTruncatedLabel
@synthesize maskLayer = _maskLayer;
@synthesize animationLabel = _animationLabel;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set background color and line break mode.
self.backgroundColor = [UIColor clearColor];
self.lineBreakMode = NSLineBreakByClipping;
// Instantiate |maskLayer| and add as mask.
self.maskLayer = [[self class] maskLayerForText:self.text
withAttributes:@{
NSFontAttributeName : self.font
}
inBounds:{CGPointZero, frame.size}];
self.layer.mask = self.maskLayer;
}
return self;
}
#pragma mark - UILabel overrides
- (void)drawTextInRect:(CGRect)rect {
// Draw if not animating.
if (!self.animationLabel)
[super drawTextInRect:rect];
// Update the mask gradient.
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.maskLayer.frame = rect;
NSDictionary* fontAttributes = @{NSFontAttributeName : self.font};
CGFloat gradientBeginPercentage =
[[self class] gradientPercentageForText:self.text
withAttributes:fontAttributes
inBounds:self.maskLayer.bounds];
self.maskLayer.locations = @[ @(gradientBeginPercentage), @(1.0) ];
[CATransaction commit];
}
#pragma mark - Animations
- (void)animateFromBeginFrame:(CGRect)beginFrame
toEndFrame:(CGRect)endFrame
duration:(CFTimeInterval)duration
timingFunction:(CAMediaTimingFunction*)timingFunction {
CAAnimation* frameAnimation = nil;
// Animate the label.
frameAnimation = FrameAnimationMake(self.layer, beginFrame, endFrame);
frameAnimation.duration = duration;
frameAnimation.timingFunction = timingFunction;
[self.layer addAnimation:frameAnimation
forKey:kFadeTruncatedLabelAnimationKey];
// When animating, add a temporary label using the larger frame size so that
// truncation can occur via the gradient rather than by the edge of the text
// layer's backing store.
CGSize animationTextSize =
CGSizeMake(std::max(beginFrame.size.width, endFrame.size.width),
std::max(beginFrame.size.height, endFrame.size.height));
self.animationLabel =
[[UILabel alloc] initWithFrame:{CGPointZero, animationTextSize}];
self.animationLabel.text = self.text;
self.animationLabel.textColor = self.textColor;
self.animationLabel.font = self.font;
[self addSubview:self.animationLabel];
// Animate the mask layer.
CGRect beginBounds = {CGPointZero, beginFrame.size};
CGRect endBounds = {CGPointZero, endFrame.size};
frameAnimation = FrameAnimationMake(self.maskLayer, beginBounds, endBounds);
frameAnimation.duration = duration;
frameAnimation.timingFunction = timingFunction;
NSDictionary* attributes = @{NSFontAttributeName : self.font};
CGFloat beginGradientPercentage =
[[self class] gradientPercentageForText:self.text
withAttributes:attributes
inBounds:beginBounds];
CGFloat endGradientPercentage =
[[self class] gradientPercentageForText:self.text
withAttributes:attributes
inBounds:endBounds];
CABasicAnimation* gradientAnimation =
[CABasicAnimation animationWithKeyPath:@"locations"];
gradientAnimation.fromValue = @[ @(beginGradientPercentage), @(1.0) ];
gradientAnimation.toValue = @[ @(endGradientPercentage), @(1.0) ];
gradientAnimation.duration = duration;
gradientAnimation.timingFunction = timingFunction;
CAAnimation* animation =
AnimationGroupMake(@[ frameAnimation, gradientAnimation ]);
[self.maskLayer addAnimation:animation
forKey:kFadeTruncatedLabelAnimationKey];
}
- (void)reverseAnimations {
// Reverse the animations, but leave the animation label in place.
ReverseAnimationsForKeyForLayers(kFadeTruncatedLabelAnimationKey,
@[ self.layer, self.maskLayer ]);
}
- (void)cleanUpAnimations {
// Remove animation label and redraw.
[self.animationLabel removeFromSuperview];
self.animationLabel = nil;
[self setNeedsDisplay];
// Remove animations from layers.
RemoveAnimationForKeyFromLayers(kFadeTruncatedLabelAnimationKey,
@[ self.layer, self.maskLayer ]);
}
#pragma mark - Class methods
+ (CGFloat)gradientPercentageForText:(NSString*)text
withAttributes:(NSDictionary*)attributes
inBounds:(CGRect)bounds {
CGSize textSize =
ui::AlignSizeToUpperPixel([text sizeWithAttributes:attributes]);
CGFloat gradientBeginPercentage = 1.0;
if (textSize.width > bounds.size.width) {
// Fade width is chosen to match GTMFadeTruncatingLabel.
CGFloat fadeWidth =
std::min<CGFloat>(bounds.size.height * 2, floor(bounds.size.width / 4));
gradientBeginPercentage =
(bounds.size.width - fadeWidth) / bounds.size.width;
}
return gradientBeginPercentage;
}
+ (CAGradientLayer*)maskLayerForText:(NSString*)text
withAttributes:(NSDictionary*)attributes
inBounds:(CGRect)bounds {
CAGradientLayer* maskLayer = [CAGradientLayer layer];
maskLayer.bounds = bounds;
maskLayer.colors = @[
reinterpret_cast<id>([UIColor blackColor].CGColor),
reinterpret_cast<id>([UIColor clearColor].CGColor)
];
maskLayer.startPoint = CGPointMake(0.0, 0.5);
maskLayer.endPoint = CGPointMake(1.0, 0.5);
CGFloat gradientBeginPercentage = [self gradientPercentageForText:text
withAttributes:attributes
inBounds:bounds];
maskLayer.locations = @[ @(gradientBeginPercentage), @(1.0) ];
return maskLayer;
}
@end