Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(288)

Side by Side Diff: chrome/browser/ui/cocoa/circular_activity_indicator_view.mm

Issue 1048733004: Add a Material Design Circular Activity Indicator (Spinner) view for Mac. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix nits and address other comments. Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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.
4
5 #import "circular_activity_indicator_view.h"
6
7 #import <QuartzCore/QuartzCore.h>
8
9 #include "base/mac/scoped_cftyperef.h"
10 #include "base/mac/scoped_nsobject.h"
11 #include "skia/ext/skia_utils_mac.h"
12
13 namespace {
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);
25
26 const SkColor kBlue = SkColorSetRGB(66.0, 133.0, 244.0); // blue = #4285f4
groby-ooo-7-16 2015/03/31 23:07:01 Tiny nit: Please end all comments with a period.
shrike 2015/04/01 15:30:46 Done.
27 const SkColor kRed = SkColorSetRGB(219.0, 68.0, 55.0); // red = #db4437
28 const SkColor kYellow = SkColorSetRGB(244.0, 180.0, 0.0); // yellow = #f4b400
29 const SkColor kGreen = SkColorSetRGB(15.0, 157.0, 88.0); // green = #0f9d58
30 }
31
32 @interface CircularActivityIndicatorView()
33 {
34 base::scoped_nsobject<CAAnimationGroup> spinner_animation_;
35 CAShapeLayer* shape_layer_; // Weak
36 }
37 @end
38
39
40 @implementation CircularActivityIndicatorView
41
42 - (instancetype)initWithFrame:(NSRect)frame {
43 if (self = [super initWithFrame:frame]) {
44 [self setWantsLayer:YES];
45 }
46 return self;
47 }
48
49 - (void)dealloc {
50 [[NSNotificationCenter defaultCenter] removeObserver:self];
51 [super dealloc];
52 }
53
54 // Return a custom CALayer for the view (called from setWantsLayer:).
55 - (CALayer *)makeBackingLayer {
56 CGRect bounds = [self bounds];
57 // The circular activity indicator was designed to be |kDesign_Width| points
58 // wide. Compute the scale factor needed to scale design parameters like
59 // |RADIUS| so that the activity indicator scales to fit the view's bounds.
60 CGFloat scale_factor = bounds.size.width / kDesign_Width;
61
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];
69
70 // Create the arc that, when stroked, creates the activity indicator.
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];
76
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)];
84
85 return parent_layer;
86 }
87
88 // The circular activity indicator animation consists of four cycles that it
89 // continually repeats. Each cycle consists of one complete rotation of the
90 // circular activity indicator's arc drawn in one of four colors (blue, red,
91 // yellow, or green). The arc's length also grows and shrinks over the course
92 // of each cycle, which the activity indicator achieves by drawing the arc using
93 // a (solid) dashed line pattern and animating the "lineDashPhase" property.
94 - (void)initializeAnimation {
95 CGRect bounds = [self bounds];
96 CGFloat scale_factor = bounds.size.width / kDesign_Width;
97
98 // Create the first half of the arc animation, where it grows from a short
99 // block to its full length.
100 base::scoped_nsobject<CAMediaTimingFunction> timing_function(
101 [[CAMediaTimingFunction alloc] initWithControlPoints:0.4 :0.0 :0.2 :1]);
102 base::scoped_nsobject<CAKeyframeAnimation> first_half_animation(
103 [[CAKeyframeAnimation alloc] init]);
104 [first_half_animation setTimingFunction:timing_function];
105 [first_half_animation setKeyPath:@"lineDashPhase"];
106 NSMutableArray* animation_values = [NSMutableArray array];
107 // Begin the lineDashPhase animation just short of the full arc length,
108 // otherwise the arc will be zero length at start.
109 [animation_values addObject:
110 [NSNumber numberWithFloat:-(kArc_Length - 0.4) * scale_factor]];
111 [animation_values addObject:[NSNumber numberWithFloat:0.0]];
112 [first_half_animation setValues:animation_values];
113 NSMutableArray* key_times = [NSMutableArray array];
114 [key_times addObject:[NSNumber numberWithFloat:0.0]];
115 [key_times addObject:[NSNumber numberWithFloat:1.0]];
116 [first_half_animation setKeyTimes:key_times];
117 [first_half_animation setDuration:kArc_Animation_Time / 2.0];
118 [first_half_animation setRemovedOnCompletion:NO];
119 [first_half_animation setFillMode:kCAFillModeForwards];
120
121 // Create the second half of the arc animation, where it shrinks from full
122 // length back to a short block.
123 base::scoped_nsobject<CAKeyframeAnimation> second_half_animation(
124 [[CAKeyframeAnimation alloc] init]);
125 [second_half_animation setTimingFunction:timing_function];
126 [second_half_animation setKeyPath:@"lineDashPhase"];
127 animation_values = [NSMutableArray array];
128 [animation_values addObject:[NSNumber numberWithFloat:0.0]];
129 // Stop the lineDashPhase animation just before it reaches the full arc
130 // length, otherwise the arc will be zero length at the end.
131 [animation_values addObject:
132 [NSNumber numberWithFloat:(kArc_Length - 0.4) * scale_factor]];
133 [second_half_animation setValues:animation_values];
134 [second_half_animation setKeyTimes:key_times];
135 [second_half_animation setDuration:kArc_Animation_Time / 2.0];
136 [second_half_animation setRemovedOnCompletion:NO];
137 [second_half_animation setFillMode:kCAFillModeForwards];
138
139 // Make four copies of the arc animations, to cover the four complete cycles
140 // of the full animation.
141 NSMutableArray* animations = [NSMutableArray array];
142 NSUInteger i;
143 CGFloat begin_time = 0;
144 for (i = 0; i < 4; i++, begin_time += kArc_Animation_Time) {
145 [first_half_animation setBeginTime:begin_time];
146 [second_half_animation setBeginTime:begin_time + kArc_Animation_Time / 2.0];
147 [animations addObject:first_half_animation];
148 [animations addObject:second_half_animation];
149 first_half_animation.reset([first_half_animation copy]);
150 second_half_animation.reset([second_half_animation copy]);
151 }
152
153 // Create the rotation animation, which rotates the arc 360 degrees on each
154 // cycle. The animation also includes a separate 90 degree rotation in the
155 // opposite direction at the very end of each cycle. Ignoring the 360 degree
156 // rotation, each arc starts as a short block at degree 0 and ends as a
157 // short block at degree 270. Without a 90 degree rotation at the end of each
158 // cycle, the short block would appear to suddenly jump from 270 degrees to
159 // 360 degrees.
groby-ooo-7-16 2015/03/31 23:07:01 Thank you for this explanation, btw - makes it ver
shrike 2015/04/01 15:30:45 You bet!
160 CAKeyframeAnimation *rotation_animation = [CAKeyframeAnimation animation];
161 [rotation_animation setTimingFunction:
162 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
163 [rotation_animation setKeyPath:@"transform.rotation"];
164 animation_values = [NSMutableArray array];
165 // Use a key frame animation to rotate 360 degrees on each cycle, and then
166 // jump back 90 degrees at the end of each cycle.
167 [animation_values addObject:[NSNumber numberWithFloat:0.0]];
168 [animation_values addObject:[NSNumber numberWithFloat:-1 * k360_Degrees]];
169 [animation_values addObject:
170 [NSNumber numberWithFloat:-1 * k360_Degrees + k90_Degrees]];
171 [animation_values addObject:
172 [NSNumber numberWithFloat:-2 * k360_Degrees + k90_Degrees]];
173 [animation_values addObject:
174 [NSNumber numberWithFloat:-2 * k360_Degrees + k180_Degrees]];
175 [animation_values addObject:
176 [NSNumber numberWithFloat:-3 * k360_Degrees + k180_Degrees]];
177 [animation_values addObject:
178 [NSNumber numberWithFloat:-3 * k360_Degrees + k270_Degrees]];
179 [animation_values addObject:
180 [NSNumber numberWithFloat:-4 * k360_Degrees + k270_Degrees]];
181 [rotation_animation setValues:animation_values];
182 key_times = [NSMutableArray array];
183 [key_times addObject:[NSNumber numberWithFloat:0.0]];
184 [key_times addObject:[NSNumber numberWithFloat:0.25]];
185 [key_times addObject:[NSNumber numberWithFloat:0.25]];
186 [key_times addObject:[NSNumber numberWithFloat:0.5]];
187 [key_times addObject:[NSNumber numberWithFloat:0.5]];
188 [key_times addObject:[NSNumber numberWithFloat:0.75]];
189 [key_times addObject:[NSNumber numberWithFloat:0.75]];
190 [key_times addObject:[NSNumber numberWithFloat:1.0]];
191 [rotation_animation setKeyTimes:key_times];
192 [rotation_animation setDuration:kArc_Animation_Time * 4.0];
193 [rotation_animation setRemovedOnCompletion:NO];
194 [rotation_animation setFillMode:kCAFillModeForwards];
195 [rotation_animation setRepeatCount:HUGE_VALF];
196 [animations addObject:rotation_animation];
197
198 // Create a four-cycle long key frame animation to transition between
199 // successive colors at the end of each cycle.
200 CAKeyframeAnimation *color_animation = [CAKeyframeAnimation animation];
201 color_animation.timingFunction =
202 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
203 color_animation.keyPath = @"strokeColor";
204 CGColorRef blueColor = gfx::CGColorCreateFromSkColor(kBlue);
205 CGColorRef redColor = gfx::CGColorCreateFromSkColor(kRed);
206 CGColorRef yellowColor = gfx::CGColorCreateFromSkColor(kYellow);
207 CGColorRef greenColor = gfx::CGColorCreateFromSkColor(kGreen);
208 animation_values = [NSMutableArray array];
209 [animation_values addObject:(id)blueColor];
210 [animation_values addObject:(id)blueColor];
211 [animation_values addObject:(id)redColor];
212 [animation_values addObject:(id)redColor];
213 [animation_values addObject:(id)yellowColor];
214 [animation_values addObject:(id)yellowColor];
215 [animation_values addObject:(id)greenColor];
216 [animation_values addObject:(id)greenColor];
217 [animation_values addObject:(id)blueColor];
218 [color_animation setValues:animation_values];
219 CGColorRelease(blueColor);
220 CGColorRelease(redColor);
221 CGColorRelease(yellowColor);
222 CGColorRelease(greenColor);
223 key_times = [NSMutableArray array];
224 // Begin the transition bewtween colors at T - 10% of the cycle.
225 const CGFloat transition_offset = 0.1 * 0.25;
226 [key_times addObject:[NSNumber numberWithFloat:0.0]];
227 [key_times addObject:[NSNumber numberWithFloat:0.25 - transition_offset]];
228 [key_times addObject:[NSNumber numberWithFloat:0.25]];
229 [key_times addObject:[NSNumber numberWithFloat:0.50 - transition_offset]];
230 [key_times addObject:[NSNumber numberWithFloat:0.5]];
231 [key_times addObject:[NSNumber numberWithFloat:0.75 - transition_offset]];
232 [key_times addObject:[NSNumber numberWithFloat:0.75]];
233 [key_times addObject:[NSNumber numberWithFloat:0.999 - transition_offset]];
234 [key_times addObject:[NSNumber numberWithFloat:0.999]];
235 [color_animation setKeyTimes:key_times];
236 [color_animation setDuration:kArc_Animation_Time * 4.0];
237 [color_animation setRemovedOnCompletion:NO];
238 [color_animation setFillMode:kCAFillModeForwards];
239 [color_animation setRepeatCount:HUGE_VALF];
240 [animations addObject:color_animation];
241
242 // Use an animation group so that the animations are easier to manage, and to
243 // given them the best chance of firing to the same timing.
244 CAAnimationGroup* group = [CAAnimationGroup animation];
245 [group setDuration:kArc_Animation_Time * 4];
246 [group setRepeatCount:HUGE_VALF];
247 [group setFillMode:kCAFillModeForwards];
248 [group setRemovedOnCompletion:NO];
249 [group setAnimations:animations];
250
251 spinner_animation_.reset([group retain]);
252 }
253
254 - (void)updateAnimation:(NSNotification*)notification {
255 // Only animate the activity indicator if we are within a window, and that
256 // window is not currently minimized or being minimized.
257 if ([self window] && ![[self window] isMiniaturized] && ![self isHidden] &&
258 ![[notification name] isEqualToString:
259 NSWindowWillMiniaturizeNotification]) {
260 if (spinner_animation_.get() == nil) {
261 [self initializeAnimation];
262 }
263 if (!is_animating_) {
264 [shape_layer_ addAnimation:spinner_animation_.get() forKey:nil];
265 is_animating_ = true;
266 } else {
groby-ooo-7-16 2015/03/31 23:07:01 Question: Can we ever enter here with a spinner th
shrike 2015/04/01 15:30:46 Done.
267 }
groby-ooo-7-16 2015/03/31 23:07:01 nit: remove empty else
shrike 2015/04/01 15:30:46 Done.
268 } else {
269 [shape_layer_ removeAllAnimations];
270 is_animating_ = false;
271 }
272 }
273
274 // Register/unregister for window miniaturization event notifications so that
275 // we can stop the animation if the window is minaturized (i.e. not visible).
276 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
277 if ([self window]) {
278 [[NSNotificationCenter defaultCenter]
279 removeObserver:self
280 name:NSWindowWillMiniaturizeNotification
281 object:[self window]];
282 [[NSNotificationCenter defaultCenter]
283 removeObserver:self
284 name:NSWindowDidDeminiaturizeNotification
285 object:[self window]];
286 }
287
288 if (newWindow) {
289 [[NSNotificationCenter defaultCenter]
290 addObserver:self
291 selector:@selector(updateAnimation:)
292 name:NSWindowWillMiniaturizeNotification
293 object:newWindow];
294 [[NSNotificationCenter defaultCenter]
295 addObserver:self
296 selector:@selector(updateAnimation:)
297 name:NSWindowDidDeminiaturizeNotification
298 object:newWindow];
299 }
300 }
301
302 // Start or stop the animation whenever the view is added to or removed from a
303 // window.
304 - (void)viewDidMoveToWindow {
305 [self updateAnimation:nil];
306 }
307
308 // Start or stop the animation whenever the view is unhidden or hidden.
309 - (void)setHidden:(BOOL)flag
310 {
311 [super setHidden:flag];
312 [self updateAnimation:nil];
313 }
314
315
316 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698