1 // Copyright 2014 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 "chrome/browser/ui/cocoa/sprite_view.h"
7 #import <QuartzCore/CAAnimation.h>
8 #import <QuartzCore/CATransaction.h>
10 #include "base/logging.h"
11 #include "ui/base/cocoa/animation_utils.h"
13 static const CGFloat kFrameDuration = 0.03; // 30ms for each animation frame.
15 @implementation SpriteView
17 - (instancetype)initWithFrame:(NSRect)frame {
18 if (self = [super initWithFrame:frame]) {
19 // A layer-hosting view. It will clip its sublayers,
20 // if they exceed the boundary.
21 CALayer* layer = [CALayer layer];
22 layer.masksToBounds = YES;
24 imageLayer_ = [CALayer layer];
25 imageLayer_.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
27 [layer addSublayer:imageLayer_];
28 imageLayer_.frame = layer.bounds;
29 [layer setDelegate:self];
30 [self setLayer:layer];
31 [self setWantsLayer:YES];
37 [[NSNotificationCenter defaultCenter] removeObserver:self];
41 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
43 [[NSNotificationCenter defaultCenter]
45 name:NSWindowWillMiniaturizeNotification
46 object:[self window]];
47 [[NSNotificationCenter defaultCenter]
49 name:NSWindowDidDeminiaturizeNotification
50 object:[self window]];
54 [[NSNotificationCenter defaultCenter]
56 selector:@selector(updateAnimation:)
57 name:NSWindowWillMiniaturizeNotification
59 [[NSNotificationCenter defaultCenter]
61 selector:@selector(updateAnimation:)
62 name:NSWindowDidDeminiaturizeNotification
67 - (void)viewDidMoveToWindow {
68 [self updateAnimation:nil];
71 - (void)updateAnimation:(NSNotification*)notification {
72 if (spriteAnimation_.get()) {
73 // Only animate the sprites if we are attached to a window, and that window
74 // is not currently minimized or in the middle of a minimize animation.
75 // http://crbug.com/350329
76 if ([self window] && ![[self window] isMiniaturized] &&
77 ![[notification name] isEqualToString:
78 NSWindowWillMiniaturizeNotification]) {
79 if ([imageLayer_ animationForKey:[spriteAnimation_ keyPath]] == nil) {
80 [imageLayer_ addAnimation:spriteAnimation_.get()
81 forKey:[spriteAnimation_ keyPath]];
84 [imageLayer_ removeAnimationForKey:[spriteAnimation_ keyPath]];
89 - (void)setImage:(NSImage*)image {
90 ScopedCAActionDisabler disabler;
92 if (spriteAnimation_.get()) {
93 [imageLayer_ removeAnimationForKey:[spriteAnimation_ keyPath]];
94 spriteAnimation_.reset();
97 [imageLayer_ setContents:image];
100 NSSize imageSize = [image size];
101 NSSize spriteSize = NSMakeSize(imageSize.height, imageSize.height);
102 [self setFrameSize:spriteSize];
104 const NSUInteger spriteCount = imageSize.width / spriteSize.width;
105 const CGFloat unitWidth = 1.0 / spriteCount;
107 // Show the first (leftmost) sprite.
108 [imageLayer_ setContentsRect:CGRectMake(0, 0, unitWidth, 1.0)];
110 if (spriteCount > 1) {
111 // Animate the sprite offsets, we use a keyframe animation with discrete
112 // calculation mode to prevent interpolation.
113 NSMutableArray* xOffsets = [NSMutableArray arrayWithCapacity:spriteCount];
114 for (NSUInteger i = 0; i < spriteCount; ++i) {
115 [xOffsets addObject:@(i * unitWidth)];
117 CAKeyframeAnimation* animation =
118 [CAKeyframeAnimation animationWithKeyPath:@"contentsRect.origin.x"];
119 [animation setValues:xOffsets];
120 [animation setCalculationMode:kCAAnimationDiscrete];
121 [animation setRepeatCount:HUGE_VALF];
122 [animation setDuration:kFrameDuration * [xOffsets count]];
123 spriteAnimation_.reset([animation retain]);
125 [self updateAnimation:nil];
130 - (void)setImage:(NSImage*)image withToastAnimation:(BOOL)animate {
131 if (!animate || [imageLayer_ contents] == nil) {
132 [self setImage:image];
134 // Animate away the icon.
135 CABasicAnimation* animation =
136 [CABasicAnimation animationWithKeyPath:@"position.y"];
137 CGFloat height = CGRectGetHeight([imageLayer_ bounds]);
138 [animation setToValue:@(-height)];
139 [animation setDuration:kFrameDuration * height];
141 // Don't remove on completion to prevent the presentation layer from
142 // snapping back to the model layer's value.
143 // It will instead be removed when we add the return animation because they
144 // have the same key.
145 [animation setRemovedOnCompletion:NO];
146 [animation setFillMode:kCAFillModeForwards];
148 [CATransaction begin];
149 [CATransaction setCompletionBlock:^{
150 // At the end of the animation, change to the new image and animate
151 // it back to position.
152 [self setImage:image];
154 CABasicAnimation* reverseAnimation =
155 [CABasicAnimation animationWithKeyPath:[animation keyPath]];
156 [reverseAnimation setFromValue:[animation toValue]];
157 [reverseAnimation setToValue:[animation fromValue]];
158 [reverseAnimation setDuration:[animation duration]];
159 [imageLayer_ addAnimation:reverseAnimation forKey:@"position"];
161 [imageLayer_ addAnimation:animation forKey:@"position"];
162 [CATransaction commit];
166 - (BOOL)layer:(CALayer*)layer
167 shouldInheritContentsScale:(CGFloat)scale
168 fromWindow:(NSWindow*)window {