blob: a6f37c572e03c58d72c1d5b02963624d715ecc71 [file] [log] [blame]
// Copyright 2016 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/toolbar/toolbar_tools_menu_button.h"
#import <QuartzCore/CAAnimation.h>
#import <QuartzCore/CAMediaTimingFunction.h>
#include "ios/chrome/browser/ui/toolbar/toolbar_button_tints.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// The number of dots drawn.
const int kNumberOfDots = 3;
// Position of the topmost dot.
const CGFloat kDotOffsetX = 22;
const CGFloat kDotOffsetY = 18;
// Vertical space between dots.
const CGFloat kVerticalSpaceBetweenDots = 6;
// The duration of the animation, in seconds.
const CFTimeInterval kAnimationDuration = 1;
// The frame offset at which the animation to the intermediary value finishes.
const int kIntermediaryValueBeginFrame = 15;
// The frame offset at which the animation to the final value starts.
const int kIntermediaryValueEndFrame = 29;
// The frame offset at which the animation to the final value finishes.
const int kFinalValueBeginFrame = 37;
// The number of frames between the start of each dot's animation.
const double kFramesBetweenAnimationOfEachDot = 3;
// The maximum width of the segment/dots.
const CGFloat kMaxWidthOfSegment = 7.4;
// Constants for the properties of the stroke during the animations.
// The strokeEnd is slightly more than 0.5, because if the strokeEnd is
// exactly equal to strokeStart, the line is not drawn.
const CGFloat kStrokeStartAtRest = 0.5;
const CGFloat kStrokeEndAtRest = kStrokeStartAtRest + 0.001;
const CGFloat kLineWidthAtRest = 4;
const CGFloat kStrokeStartAtApogee = 0;
const CGFloat kStrokeEndAtApogee = 1;
const CGFloat kLineWidthAtApogee = 3;
} // namespace
@interface ToolbarToolsMenuButton ()<CAAnimationDelegate> {
// The style of the toolbar the button is in.
ToolbarControllerStyle style_;
// Whether the tools menu is visible.
BOOL toolsMenuVisible_;
// Whether the reading list contains unseen items.
BOOL readingListContainsUnseenItems_;
// The CALayers containing the drawn dots.
NSMutableArray<CAShapeLayer*>* pathLayers_;
// Whether the CALayers are being animated.
BOOL animationOnGoing_;
}
// Updates the tint configuration based on the button's situation, e.g. whether
// the tools menu is visible or not.
- (void)updateTintOfButton;
// Initializes the pathLayers.
- (void)initializeShapeLayers;
// Returns a keyframe-based animation of the property identified by |keyPath|.
// The animation immidiately sets the property's value to |initialValue|.
// After |frameStart| frames, the property's value animates to
// |intermediaryValue|, and then to |finalValue|.
- (CAAnimation*)animationWithInitialValue:(id)initialValue
intermediaryValue:(id)intermediaryValue
finalValue:(id)finalValue
frameStart:(int)frameStart
forKeyPath:(NSString*)keyPath;
// Starts animating the button towards the color |targetColor|.
- (void)animateToColor:(UIColor*)targetColor;
@end
@implementation ToolbarToolsMenuButton
- (instancetype)initWithFrame:(CGRect)frame
style:(ToolbarControllerStyle)style {
if (self = [super initWithFrame:frame]) {
style_ = style;
[self setTintColor:toolbar::NormalButtonTint(style_)
forState:UIControlStateNormal];
[self setTintColor:toolbar::HighlighButtonTint(style_)
forState:UIControlStateHighlighted];
pathLayers_ = [[NSMutableArray alloc] initWithCapacity:kNumberOfDots];
}
return self;
}
- (void)setToolsMenuIsVisible:(BOOL)toolsMenuVisible {
toolsMenuVisible_ = toolsMenuVisible;
[self updateTintOfButton];
}
- (void)setReadingListContainsUnseenItems:(BOOL)readingListContainsUnseenItems {
readingListContainsUnseenItems_ = readingListContainsUnseenItems;
[self updateTintOfButton];
}
- (void)triggerAnimation {
[self animateToColor:toolbar::HighlighButtonTint(style_)];
}
#pragma mark - Private
- (void)updateTintOfButton {
if (toolsMenuVisible_ || readingListContainsUnseenItems_) {
[self setTintColor:toolbar::HighlighButtonTint(style_)
forState:UIControlStateNormal];
} else {
[self setTintColor:toolbar::NormalButtonTint(style_)
forState:UIControlStateNormal];
}
}
- (void)initializeShapeLayers {
for (int i = 0; i < kNumberOfDots; i++) {
CAShapeLayer* pathLayer = pathLayers_[i];
if (pathLayer) {
[pathLayer removeFromSuperlayer];
}
const CGFloat x = kDotOffsetX;
const CGFloat y = kDotOffsetY + kVerticalSpaceBetweenDots * i;
UIBezierPath* path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(x - kMaxWidthOfSegment * 0.5, y)];
[path addLineToPoint:CGPointMake(x + kMaxWidthOfSegment * 0.5, y)];
pathLayer = [CAShapeLayer layer];
[pathLayer setFrame:self.bounds];
[pathLayer setPath:path.CGPath];
[pathLayer setStrokeColor:[self.tintColor CGColor]];
[pathLayer setFillColor:nil];
[pathLayer setLineWidth:kLineWidthAtRest];
[pathLayer setLineCap:kCALineCapRound];
[pathLayer setStrokeStart:kStrokeStartAtRest];
[pathLayer setStrokeEnd:kStrokeEndAtRest];
[self.layer addSublayer:pathLayer];
pathLayers_[i] = pathLayer;
}
}
- (CAAnimation*)animationWithInitialValue:(id)initialValue
intermediaryValue:(id)intermediaryValue
finalValue:(id)finalValue
frameStart:(int)frameStart
forKeyPath:(NSString*)keyPath {
// The property is animated the following way:
//
// value
// ^
// | .final...final
// | .
// | .intermediary...intermediary.
// | .
// initial...initial.
// |
// +---------+-----------+--------------+-----------+-------+---> frames
// 0 start | | | 60
// start+ | start+
// kIntermediaryValueBeginFrame | kFinalValueBeginFrame
// |
// start+
// kIntermediaryValueEndFrame
CAKeyframeAnimation* animation =
[CAKeyframeAnimation animationWithKeyPath:keyPath];
animation.duration = kAnimationDuration;
animation.removedOnCompletion = NO;
// Set up the values.
animation.values = @[
initialValue, initialValue, intermediaryValue, intermediaryValue,
finalValue, finalValue
];
// Set up the timing functions.
NSMutableArray* timings = [NSMutableArray array];
for (size_t i = 0; i < [animation.values count] - 1; i++)
[timings addObject:[CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseIn]];
animation.timingFunctions = timings;
// Set up the key times.
const double totalNumberOfFrames = 60 * kAnimationDuration;
animation.keyTimes = @[
@0, @(frameStart / totalNumberOfFrames),
@((frameStart + kIntermediaryValueBeginFrame) / totalNumberOfFrames),
@((frameStart + kIntermediaryValueEndFrame) / totalNumberOfFrames),
@((frameStart + kFinalValueBeginFrame) / totalNumberOfFrames), @1
];
DCHECK_EQ([animation.keyTimes count], [animation.values count]);
DCHECK_EQ([animation.keyTimes count] - 1, [animation.timingFunctions count]);
#if DCHECK_IS_ON()
for (NSNumber* number in animation.keyTimes) {
DCHECK_GE([number floatValue], 0);
DCHECK_LE([number floatValue], 1);
}
#endif
return animation;
}
- (void)animateToColor:(UIColor*)targetColor {
animationOnGoing_ = YES;
// Add four animations for each stroke.
for (int i = 0; i < kNumberOfDots; i++) {
CAShapeLayer* pathLayer = pathLayers_[i];
const int frameStart =
(kNumberOfDots - i) * kFramesBetweenAnimationOfEachDot;
// Start of the stroke animation.
CAAnimation* strokeStartAnimation =
[self animationWithInitialValue:@(kStrokeStartAtRest)
intermediaryValue:@(kStrokeStartAtApogee)
finalValue:@(kStrokeStartAtRest)
frameStart:frameStart
forKeyPath:@"strokeStart"];
// End of the stroke animation.
CAAnimation* strokeEndAnimation =
[self animationWithInitialValue:@(kStrokeEndAtRest)
intermediaryValue:@(kStrokeEndAtApogee)
finalValue:@(kStrokeEndAtRest)
frameStart:frameStart
forKeyPath:@"strokeEnd"];
// Width of the stroke animation.
CAAnimation* lineWidthAnimation =
[self animationWithInitialValue:@(kLineWidthAtRest)
intermediaryValue:@(kLineWidthAtApogee)
finalValue:@(kLineWidthAtRest)
frameStart:frameStart
forKeyPath:@"lineWidth"];
// Color of the stroke animation.
CGColorRef initialColor = self.tintColor.CGColor;
CGColorRef finalColor = targetColor.CGColor;
CAAnimation* colorAnimation =
[self animationWithInitialValue:(__bridge id)initialColor
intermediaryValue:(__bridge id)finalColor
finalValue:(__bridge id)finalColor
frameStart:frameStart
forKeyPath:@"strokeColor"];
colorAnimation.fillMode = kCAFillModeForwards;
// |self| needs to know when the animations are finished. This is achieved
// by having |self| be registered as a CAAnimationDelegate.
// Because all animations have the same duration, any animation can be used.
// Arbitrarly use the |strokeStartAnimation| of the first dot.
if (i == 0) {
strokeStartAnimation.delegate = self;
}
[pathLayer addAnimation:strokeStartAnimation forKey:nil];
[pathLayer addAnimation:strokeEndAnimation forKey:nil];
[pathLayer addAnimation:lineWidthAnimation forKey:nil];
[pathLayer addAnimation:colorAnimation forKey:nil];
}
self.tintColor = targetColor;
}
#pragma mark - UIView
- (void)tintColorDidChange {
// The CAShapeLayer needs to be recreated when the tint color changes, to
// reflect the tint color.
// However, recreating the CAShapeLayer while it is animating cancels the
// animation. To avoid canceling the animation, skip recreating the
// CAShapeLayer when an animation is on going.
// To reflect any potential tint color change, the CAShapeLayer will be
// recreated at the end of the animation.
if (!animationOnGoing_)
[self initializeShapeLayers];
}
#pragma mark - CAAnimationDelegate
- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)flag {
animationOnGoing_ = NO;
// Recreate the CAShapeLayers in case the tint code changed while the
// animation was going on.
[self initializeShapeLayers];
}
@end