1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "spinner_view.h"
7 #import <QuartzCore/QuartzCore.h>
9 #include "base/mac/scoped_cftyperef.h"
10 #include "base/mac/scoped_nsobject.h"
11 #include "skia/ext/skia_utils_mac.h"
14 const CGFloat k90_Degrees = (M_PI / 2);
15 const CGFloat k180_Degrees = (M_PI);
16 const CGFloat k270_Degrees = (3 * M_PI / 2);
17 const CGFloat k360_Degrees = (2 * M_PI);
18 const CGFloat kDesign_Width = 28.0;
19 const CGFloat kArc_Radius = 12.5;
20 const CGFloat kArc_Length = 58.9;
21 const CGFloat kArc_Stroke_Width = 3.0;
22 const CGFloat kArc_Animation_Time = 1.333;
23 const CGFloat kArc_Start_Angle = k180_Degrees;
24 const CGFloat kArc_End_Angle = (kArc_Start_Angle + k270_Degrees);
26 const SkColor kBlue = SkColorSetRGB(66.0, 133.0, 244.0); // #4285f4.
27 const SkColor kRed = SkColorSetRGB(219.0, 68.0, 55.0); // #db4437.
28 const SkColor kYellow = SkColorSetRGB(244.0, 180.0, 0.0); // #f4b400.
29 const SkColor kGreen = SkColorSetRGB(15.0, 157.0, 88.0); // #0f9d58.
32 @interface SpinnerView()
34 base::scoped_nsobject<CAAnimationGroup> spinner_animation_;
35 CAShapeLayer* shape_layer_; // Weak.
40 @implementation SpinnerView
42 - (instancetype)initWithFrame:(NSRect)frame {
43 if (self = [super initWithFrame:frame]) {
44 [self setWantsLayer:YES];
50 [[NSNotificationCenter defaultCenter] removeObserver:self];
54 // Return a custom CALayer for the view (called from setWantsLayer:).
55 - (CALayer *)makeBackingLayer {
56 CGRect bounds = [self bounds];
57 // The spinner was designed to be |kDesign_Width| points wide. Compute the
58 // scale factor needed to scale design parameters like |RADIUS| so that the
59 // spinner scales to fit the view's bounds.
60 CGFloat scale_factor = bounds.size.width / kDesign_Width;
62 shape_layer_ = [CAShapeLayer layer];
63 [shape_layer_ setBounds:bounds];
64 [shape_layer_ setLineWidth:kArc_Stroke_Width * scale_factor];
65 [shape_layer_ setLineCap:kCALineCapSquare];
66 [shape_layer_ setLineDashPattern:[NSArray arrayWithObject:
67 [NSNumber numberWithFloat:kArc_Length * scale_factor]]];
68 [shape_layer_ setFillColor:NULL];
70 // Create the arc that, when stroked, creates the spinner.
71 base::ScopedCFTypeRef<CGMutablePathRef> shape_path(CGPathCreateMutable());
72 CGPathAddArc(shape_path, NULL, bounds.size.width / 2.0,
73 bounds.size.height / 2.0, kArc_Radius * scale_factor,
74 kArc_Start_Angle, kArc_End_Angle, 0);
75 [shape_layer_ setPath:shape_path];
77 // Place |shape_layer_| in a parent layer so that it's easy to rotate
78 // |shape_layer_| around the center of the view.
79 CALayer* parent_layer = [CALayer layer];
80 [parent_layer setBounds:bounds];
81 [parent_layer addSublayer:shape_layer_];
82 [shape_layer_ setPosition:CGPointMake(bounds.size.width / 2.0,
83 bounds.size.height / 2.0)];
88 // The spinner animation consists of four cycles that it continuously repeats.
89 // Each cycle consists of one complete rotation of the spinner's arc drawn in
90 // blue, red, yellow, or green. The arc's length also grows and shrinks over the
91 // course of each cycle, which the spinner achieves by drawing the arc using
92 // a (solid) dashed line pattern and animating the "lineDashPhase" property.
93 - (void)initializeAnimation {
94 CGRect bounds = [self bounds];
95 CGFloat scale_factor = bounds.size.width / kDesign_Width;
97 // Create the first half of the arc animation, where it grows from a short
98 // block to its full length.
99 base::scoped_nsobject<CAMediaTimingFunction> timing_function(
100 [[CAMediaTimingFunction alloc] initWithControlPoints:0.4 :0.0 :0.2 :1]);
101 base::scoped_nsobject<CAKeyframeAnimation> first_half_animation(
102 [[CAKeyframeAnimation alloc] init]);
103 [first_half_animation setTimingFunction:timing_function];
104 [first_half_animation setKeyPath:@"lineDashPhase"];
105 NSMutableArray* animation_values = [NSMutableArray array];
106 // Begin the lineDashPhase animation just short of the full arc length,
107 // otherwise the arc will be zero length at start.
108 [animation_values addObject:
109 [NSNumber numberWithFloat:-(kArc_Length - 0.2) * scale_factor]];
110 [animation_values addObject:[NSNumber numberWithFloat:0.0]];
111 [first_half_animation setValues:animation_values];
112 NSMutableArray* key_times = [NSMutableArray array];
113 [key_times addObject:[NSNumber numberWithFloat:0.0]];
114 [key_times addObject:[NSNumber numberWithFloat:1.0]];
115 [first_half_animation setKeyTimes:key_times];
116 [first_half_animation setDuration:kArc_Animation_Time / 2.0];
117 [first_half_animation setRemovedOnCompletion:NO];
118 [first_half_animation setFillMode:kCAFillModeForwards];
120 // Create the second half of the arc animation, where it shrinks from full
121 // length back to a short block.
122 base::scoped_nsobject<CAKeyframeAnimation> second_half_animation(
123 [[CAKeyframeAnimation alloc] init]);
124 [second_half_animation setTimingFunction:timing_function];
125 [second_half_animation setKeyPath:@"lineDashPhase"];
126 animation_values = [NSMutableArray array];
127 [animation_values addObject:[NSNumber numberWithFloat:0.0]];
128 // Stop the lineDashPhase animation just before it reaches the full arc
129 // length, otherwise the arc will be zero length at the end.
130 [animation_values addObject:
131 [NSNumber numberWithFloat:(kArc_Length - 0.3) * scale_factor]];
132 [second_half_animation setValues:animation_values];
133 [second_half_animation setKeyTimes:key_times];
134 [second_half_animation setDuration:kArc_Animation_Time / 2.0];
135 [second_half_animation setRemovedOnCompletion:NO];
136 [second_half_animation setFillMode:kCAFillModeForwards];
138 // Make four copies of the arc animations, to cover the four complete cycles
139 // of the full animation.
140 NSMutableArray* animations = [NSMutableArray array];
142 CGFloat begin_time = 0;
143 for (i = 0; i < 4; i++, begin_time += kArc_Animation_Time) {
144 [first_half_animation setBeginTime:begin_time];
145 [second_half_animation setBeginTime:begin_time + kArc_Animation_Time / 2.0];
146 [animations addObject:first_half_animation];
147 [animations addObject:second_half_animation];
148 first_half_animation.reset([first_half_animation copy]);
149 second_half_animation.reset([second_half_animation copy]);
152 // Create the rotation animation, which rotates the arc 360 degrees on each
153 // cycle. The animation also includes a separate 90 degree rotation in the
154 // opposite direction at the very end of each cycle. Ignoring the 360 degree
155 // rotation, each arc starts as a short block at degree 0 and ends as a
156 // short block at degree 270. Without a 90 degree rotation at the end of each
157 // cycle, the short block would appear to suddenly jump from 270 degrees to
159 CAKeyframeAnimation *rotation_animation = [CAKeyframeAnimation animation];
160 [rotation_animation setTimingFunction:
161 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
162 [rotation_animation setKeyPath:@"transform.rotation"];
163 animation_values = [NSMutableArray array];
164 // Use a key frame animation to rotate 360 degrees on each cycle, and then
165 // jump back 90 degrees at the end of each cycle.
166 [animation_values addObject:[NSNumber numberWithFloat:0.0]];
167 [animation_values addObject:[NSNumber numberWithFloat:-1 * k360_Degrees]];
168 [animation_values addObject:
169 [NSNumber numberWithFloat:-1 * k360_Degrees + k90_Degrees]];
170 [animation_values addObject:
171 [NSNumber numberWithFloat:-2 * k360_Degrees + k90_Degrees]];
172 [animation_values addObject:
173 [NSNumber numberWithFloat:-2 * k360_Degrees + k180_Degrees]];
174 [animation_values addObject:
175 [NSNumber numberWithFloat:-3 * k360_Degrees + k180_Degrees]];
176 [animation_values addObject:
177 [NSNumber numberWithFloat:-3 * k360_Degrees + k270_Degrees]];
178 [animation_values addObject:
179 [NSNumber numberWithFloat:-4 * k360_Degrees + k270_Degrees]];
180 [rotation_animation setValues:animation_values];
181 key_times = [NSMutableArray array];
182 [key_times addObject:[NSNumber numberWithFloat:0.0]];
183 [key_times addObject:[NSNumber numberWithFloat:0.25]];
184 [key_times addObject:[NSNumber numberWithFloat:0.25]];
185 [key_times addObject:[NSNumber numberWithFloat:0.5]];
186 [key_times addObject:[NSNumber numberWithFloat:0.5]];
187 [key_times addObject:[NSNumber numberWithFloat:0.75]];
188 [key_times addObject:[NSNumber numberWithFloat:0.75]];
189 [key_times addObject:[NSNumber numberWithFloat:1.0]];
190 [rotation_animation setKeyTimes:key_times];
191 [rotation_animation setDuration:kArc_Animation_Time * 4.0];
192 [rotation_animation setRemovedOnCompletion:NO];
193 [rotation_animation setFillMode:kCAFillModeForwards];
194 [rotation_animation setRepeatCount:HUGE_VALF];
195 [animations addObject:rotation_animation];
197 // Create a four-cycle-long key frame animation to transition between
198 // successive colors at the end of each cycle.
199 CAKeyframeAnimation *color_animation = [CAKeyframeAnimation animation];
200 color_animation.timingFunction =
201 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
202 color_animation.keyPath = @"strokeColor";
203 CGColorRef blueColor = gfx::CGColorCreateFromSkColor(kBlue);
204 CGColorRef redColor = gfx::CGColorCreateFromSkColor(kRed);
205 CGColorRef yellowColor = gfx::CGColorCreateFromSkColor(kYellow);
206 CGColorRef greenColor = gfx::CGColorCreateFromSkColor(kGreen);
207 animation_values = [NSMutableArray array];
208 [animation_values addObject:(id)blueColor];
209 [animation_values addObject:(id)blueColor];
210 [animation_values addObject:(id)redColor];
211 [animation_values addObject:(id)redColor];
212 [animation_values addObject:(id)yellowColor];
213 [animation_values addObject:(id)yellowColor];
214 [animation_values addObject:(id)greenColor];
215 [animation_values addObject:(id)greenColor];
216 [animation_values addObject:(id)blueColor];
217 [color_animation setValues:animation_values];
218 CGColorRelease(blueColor);
219 CGColorRelease(redColor);
220 CGColorRelease(yellowColor);
221 CGColorRelease(greenColor);
222 key_times = [NSMutableArray array];
223 // Begin the transition bewtween colors at T - 10% of the cycle.
224 const CGFloat transition_offset = 0.1 * 0.25;
225 [key_times addObject:[NSNumber numberWithFloat:0.0]];
226 [key_times addObject:[NSNumber numberWithFloat:0.25 - transition_offset]];
227 [key_times addObject:[NSNumber numberWithFloat:0.25]];
228 [key_times addObject:[NSNumber numberWithFloat:0.50 - transition_offset]];
229 [key_times addObject:[NSNumber numberWithFloat:0.5]];
230 [key_times addObject:[NSNumber numberWithFloat:0.75 - transition_offset]];
231 [key_times addObject:[NSNumber numberWithFloat:0.75]];
232 [key_times addObject:[NSNumber numberWithFloat:0.999 - transition_offset]];
233 [key_times addObject:[NSNumber numberWithFloat:0.999]];
234 [color_animation setKeyTimes:key_times];
235 [color_animation setDuration:kArc_Animation_Time * 4.0];
236 [color_animation setRemovedOnCompletion:NO];
237 [color_animation setFillMode:kCAFillModeForwards];
238 [color_animation setRepeatCount:HUGE_VALF];
239 [animations addObject:color_animation];
241 // Use an animation group so that the animations are easier to manage, and to
242 // give them the best chance of firing synchronously.
243 CAAnimationGroup* group = [CAAnimationGroup animation];
244 [group setDuration:kArc_Animation_Time * 4];
245 [group setRepeatCount:HUGE_VALF];
246 [group setFillMode:kCAFillModeForwards];
247 [group setRemovedOnCompletion:NO];
248 [group setAnimations:animations];
250 spinner_animation_.reset([group retain]);
253 - (void)updateAnimation:(NSNotification*)notification {
254 // Only animate the spinner if it's within a window, and that window is not
255 // currently minimized or being minimized.
256 if ([self window] && ![[self window] isMiniaturized] && ![self isHidden] &&
257 ![[notification name] isEqualToString:
258 NSWindowWillMiniaturizeNotification]) {
259 if (spinner_animation_.get() == nil) {
260 [self initializeAnimation];
262 // The spinner should never be animating at this point
263 DCHECK(!is_animating_);
264 if (!is_animating_) {
265 [shape_layer_ addAnimation:spinner_animation_.get() forKey:nil];
266 is_animating_ = true;
269 [shape_layer_ removeAllAnimations];
270 is_animating_ = false;
274 // Register/unregister for window miniaturization event notifications so that
275 // the spinner can stop animating if the window is minaturized
276 // (i.e. not visible).
277 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
279 [[NSNotificationCenter defaultCenter]
281 name:NSWindowWillMiniaturizeNotification
282 object:[self window]];
283 [[NSNotificationCenter defaultCenter]
285 name:NSWindowDidDeminiaturizeNotification
286 object:[self window]];
290 [[NSNotificationCenter defaultCenter]
292 selector:@selector(updateAnimation:)
293 name:NSWindowWillMiniaturizeNotification
295 [[NSNotificationCenter defaultCenter]
297 selector:@selector(updateAnimation:)
298 name:NSWindowDidDeminiaturizeNotification
303 // Start or stop the animation whenever the view is added to or removed from a
305 - (void)viewDidMoveToWindow {
306 [self updateAnimation:nil];
309 // Start or stop the animation whenever the view is unhidden or hidden.
310 - (void)setHidden:(BOOL)flag
312 [super setHidden:flag];
313 [self updateAnimation:nil];