// Copyright 2015 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 "chrome/browser/ui/cocoa/spinner_view.h" #import #include "base/mac/scoped_cftyperef.h" #include "skia/ext/skia_utils_mac.h" namespace { const CGFloat kDegrees90 = (M_PI / 2); const CGFloat kDegrees180 = (M_PI); const CGFloat kDegrees270 = (3 * M_PI / 2); const CGFloat kDegrees360 = (2 * M_PI); const CGFloat kDesignWidth = 28.0; const CGFloat kArcRadius = 12.5; const CGFloat kArcLength = 58.9; const CGFloat kArcStrokeWidth = 3.0; const CGFloat kArcAnimationTime = 1.333; const CGFloat kArcStartAngle = kDegrees180; const CGFloat kArcEndAngle = (kArcStartAngle + kDegrees270); const SkColor kBlue = SkColorSetRGB(66.0, 133.0, 244.0); // #4285f4. } @interface SpinnerView () { base::scoped_nsobject spinnerAnimation_; CAShapeLayer* shapeLayer_; // Weak. } @end @implementation SpinnerView - (instancetype)initWithFrame:(NSRect)frame { if (self = [super initWithFrame:frame]) { [self setWantsLayer:YES]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } // Overridden to return a custom CALayer for the view (called from // setWantsLayer:). - (CALayer*)makeBackingLayer { CGRect bounds = [self bounds]; // The spinner was designed to be |kDesignWidth| points wide. Compute the // scale factor needed to scale design parameters like |RADIUS| so that the // spinner scales to fit the view's bounds. CGFloat scaleFactor = bounds.size.width / kDesignWidth; shapeLayer_ = [CAShapeLayer layer]; [shapeLayer_ setBounds:bounds]; [shapeLayer_ setLineWidth:kArcStrokeWidth * scaleFactor]; [shapeLayer_ setLineCap:kCALineCapRound]; [shapeLayer_ setLineDashPattern:@[ @(kArcLength * scaleFactor) ]]; [shapeLayer_ setFillColor:NULL]; CGColorRef blueColor = gfx::CGColorCreateFromSkColor(kBlue); [shapeLayer_ setStrokeColor:blueColor]; CGColorRelease(blueColor); // Create the arc that, when stroked, creates the spinner. base::ScopedCFTypeRef shapePath(CGPathCreateMutable()); CGPathAddArc(shapePath, NULL, bounds.size.width / 2.0, bounds.size.height / 2.0, kArcRadius * scaleFactor, kArcStartAngle, kArcEndAngle, 0); [shapeLayer_ setPath:shapePath]; // Place |shapeLayer_| in a parent layer so that it's easy to rotate // |shapeLayer_| around the center of the view. CALayer* parentLayer = [CALayer layer]; [parentLayer setBounds:bounds]; [parentLayer addSublayer:shapeLayer_]; [shapeLayer_ setPosition:CGPointMake(bounds.size.width / 2.0, bounds.size.height / 2.0)]; return parentLayer; } // Overridden to start or stop the animation whenever the view is unhidden or // hidden. - (void)setHidden:(BOOL)flag { [super setHidden:flag]; [self updateAnimation:nil]; } // Register/unregister for window miniaturization event notifications so that // the spinner can stop animating if the window is minaturized // (i.e. not visible). - (void)viewWillMoveToWindow:(NSWindow*)newWindow { if ([self window]) { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillMiniaturizeNotification object:[self window]]; [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowDidDeminiaturizeNotification object:[self window]]; } if (newWindow) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateAnimation:) name:NSWindowWillMiniaturizeNotification object:newWindow]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateAnimation:) name:NSWindowDidDeminiaturizeNotification object:newWindow]; } } // Start or stop the animation whenever the view is added to or removed from a // window. - (void)viewDidMoveToWindow { [self updateAnimation:nil]; } // The spinner animation consists of four cycles that it continuously repeats. // Each cycle consists of one complete rotation of the spinner's arc plus a // rotation adjustment at the end of each cycle (see rotation animation comment // below for the reason for the rotation adjustment and four-cycle length of // the full animation). The arc's length also grows and shrinks over the course // of each cycle, which the spinner achieves by drawing the arc using a (solid) // dashed line pattern and animating the "lineDashPhase" property. - (void)initializeAnimation { CGRect bounds = [self bounds]; CGFloat scaleFactor = bounds.size.width / kDesignWidth; // Create the first half of the arc animation, where it grows from a short // block to its full length. base::scoped_nsobject timingFunction( [[CAMediaTimingFunction alloc] initWithControlPoints:0.4 :0.0 :0.2 :1]); base::scoped_nsobject firstHalfAnimation( [[CAKeyframeAnimation alloc] init]); [firstHalfAnimation setTimingFunction:timingFunction]; [firstHalfAnimation setKeyPath:@"lineDashPhase"]; // Begin the lineDashPhase animation just short of the full arc length, // otherwise the arc will be zero length at start. NSArray* animationValues = @[ @(-(kArcLength - 0.4) * scaleFactor), @(0.0) ]; [firstHalfAnimation setValues:animationValues]; NSArray* keyTimes = @[ @(0.0), @(1.0) ]; [firstHalfAnimation setKeyTimes:keyTimes]; [firstHalfAnimation setDuration:kArcAnimationTime / 2.0]; [firstHalfAnimation setRemovedOnCompletion:NO]; [firstHalfAnimation setFillMode:kCAFillModeForwards]; // Create the second half of the arc animation, where it shrinks from full // length back to a short block. base::scoped_nsobject secondHalfAnimation( [[CAKeyframeAnimation alloc] init]); [secondHalfAnimation setTimingFunction:timingFunction]; [secondHalfAnimation setKeyPath:@"lineDashPhase"]; // Stop the lineDashPhase animation just before it reaches the full arc // length, otherwise the arc will be zero length at the end. animationValues = @[ @(0.0), @((kArcLength - 0.3) * scaleFactor) ]; [secondHalfAnimation setValues:animationValues]; [secondHalfAnimation setKeyTimes:keyTimes]; [secondHalfAnimation setDuration:kArcAnimationTime / 2.0]; [secondHalfAnimation setRemovedOnCompletion:NO]; [secondHalfAnimation setFillMode:kCAFillModeForwards]; // Make four copies of the arc animations, to cover the four complete cycles // of the full animation. NSMutableArray* animations = [NSMutableArray array]; CGFloat beginTime = 0; for (NSUInteger i = 0; i < 4; i++, beginTime += kArcAnimationTime) { [firstHalfAnimation setBeginTime:beginTime]; [secondHalfAnimation setBeginTime:beginTime + kArcAnimationTime / 2.0]; [animations addObject:firstHalfAnimation]; [animations addObject:secondHalfAnimation]; firstHalfAnimation.reset([firstHalfAnimation copy]); secondHalfAnimation.reset([secondHalfAnimation copy]); } // Create the rotation animation, which rotates the arc 360 degrees on each // cycle. The animation also includes a separate 90 degree rotation in the // opposite direction at the very end of each cycle. Ignoring the 360 degree // rotation, each arc starts as a short block at degree 0 and ends as a short // block at degree 270. Without a 90 degree rotation at the end of each cycle, // the short block would appear to suddenly jump from 270 degrees to 360 // degrees. The full animation has to contain four of these -90 degree // adjustments in order for the arc to return to its starting point, at which // point the full animation can smoothly repeat. CAKeyframeAnimation* rotationAnimation = [CAKeyframeAnimation animation]; [rotationAnimation setTimingFunction: [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; [rotationAnimation setKeyPath:@"transform.rotation"]; // Use a key frame animation to rotate 360 degrees on each cycle, and then // jump back 90 degrees at the end of each cycle. animationValues = @[ @(0.0), @(-1 * kDegrees360), @(-1.0 * kDegrees360 + kDegrees90), @(-2.0 * kDegrees360 + kDegrees90), @(-2.0 * kDegrees360 + kDegrees180), @(-3.0 * kDegrees360 + kDegrees180), @(-3.0 * kDegrees360 + kDegrees270), @(-4.0 * kDegrees360 + kDegrees270)]; [rotationAnimation setValues:animationValues]; keyTimes = @[ @(0.0), @(0.25), @(0.25), @(0.5), @(0.5), @(0.75), @(0.75), @(1.0)]; [rotationAnimation setKeyTimes:keyTimes]; [rotationAnimation setDuration:kArcAnimationTime * 4.0]; [rotationAnimation setRemovedOnCompletion:NO]; [rotationAnimation setFillMode:kCAFillModeForwards]; [rotationAnimation setRepeatCount:HUGE_VALF]; [animations addObject:rotationAnimation]; // Use an animation group so that the animations are easier to manage, and to // give them the best chance of firing synchronously. CAAnimationGroup* group = [CAAnimationGroup animation]; [group setDuration:kArcAnimationTime * 4]; [group setRepeatCount:HUGE_VALF]; [group setFillMode:kCAFillModeForwards]; [group setRemovedOnCompletion:NO]; [group setAnimations:animations]; spinnerAnimation_.reset([group retain]); } - (void)updateAnimation:(NSNotification*)notification { // Only animate the spinner if it's within a window, and that window is not // currently minimized or being minimized. if ([self window] && ![[self window] isMiniaturized] && ![self isHidden] && ![[notification name] isEqualToString: NSWindowWillMiniaturizeNotification]) { if (spinnerAnimation_.get() == nil) { [self initializeAnimation]; } // The spinner should never be animating at this point. DCHECK(!isAnimating_); if (!isAnimating_) { [shapeLayer_ addAnimation:spinnerAnimation_.get() forKey:nil]; isAnimating_ = true; } } else { [shapeLayer_ removeAllAnimations]; isAnimating_ = false; } } @end