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.
4
5#import "chrome/browser/ui/cocoa/sprite_view.h"
6
7#import <QuartzCore/CAAnimation.h>
8#import <QuartzCore/CATransaction.h>
9
10#include "base/logging.h"
11#include "ui/base/cocoa/animation_utils.h"
12
13static const CGFloat kFrameDuration = 0.03;  // 30ms for each animation frame.
14
15@implementation SpriteView
16
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;
23
24    imageLayer_ = [CALayer layer];
25    imageLayer_.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
26
27    [layer addSublayer:imageLayer_];
28    imageLayer_.frame = layer.bounds;
29    [layer setDelegate:self];
30    [self setLayer:layer];
31    [self setWantsLayer:YES];
32  }
33  return self;
34}
35
36- (void)dealloc {
37  [[NSNotificationCenter defaultCenter] removeObserver:self];
38  [super dealloc];
39}
40
41- (void)viewWillMoveToWindow:(NSWindow*)newWindow {
42  if ([self window]) {
43    [[NSNotificationCenter defaultCenter]
44        removeObserver:self
45                  name:NSWindowWillMiniaturizeNotification
46                object:[self window]];
47    [[NSNotificationCenter defaultCenter]
48        removeObserver:self
49                  name:NSWindowDidDeminiaturizeNotification
50                object:[self window]];
51  }
52
53  if (newWindow) {
54    [[NSNotificationCenter defaultCenter]
55        addObserver:self
56           selector:@selector(updateAnimation:)
57               name:NSWindowWillMiniaturizeNotification
58             object:newWindow];
59    [[NSNotificationCenter defaultCenter]
60        addObserver:self
61           selector:@selector(updateAnimation:)
62               name:NSWindowDidDeminiaturizeNotification
63             object:newWindow];
64  }
65}
66
67- (void)viewDidMoveToWindow {
68  [self updateAnimation:nil];
69}
70
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      if ([imageLayer_ animationForKey:[spriteAnimation_ keyPath]] == nil)
78        [imageLayer_ addAnimation:spriteAnimation_.get()
79                     forKey:[spriteAnimation_ keyPath]];
80    } else {
81      [imageLayer_ removeAnimationForKey:[spriteAnimation_ keyPath]];
82    }
83  }
84}
85
86- (void)setImage:(NSImage*)image {
87  ScopedCAActionDisabler disabler;
88
89  if (spriteAnimation_.get()) {
90    [imageLayer_ removeAnimationForKey:[spriteAnimation_ keyPath]];
91    spriteAnimation_.reset();
92  }
93
94  [imageLayer_ setContents:image];
95
96  if (image != nil) {
97    NSSize imageSize = [image size];
98    NSSize spriteSize = NSMakeSize(imageSize.height, imageSize.height);
99    [self setFrameSize:spriteSize];
100
101    const NSUInteger spriteCount = imageSize.width / spriteSize.width;
102    const CGFloat unitWidth = 1.0 / spriteCount;
103
104    // Show the first (leftmost) sprite.
105    [imageLayer_ setContentsRect:CGRectMake(0, 0, unitWidth, 1.0)];
106
107    if (spriteCount > 1) {
108      // Animate the sprite offsets, we use a keyframe animation with discrete
109      // calculation mode to prevent interpolation.
110      NSMutableArray* xOffsets = [NSMutableArray arrayWithCapacity:spriteCount];
111      for (NSUInteger i = 0; i < spriteCount; ++i) {
112        [xOffsets addObject:@(i * unitWidth)];
113      }
114      CAKeyframeAnimation* animation =
115          [CAKeyframeAnimation animationWithKeyPath:@"contentsRect.origin.x"];
116      [animation setValues:xOffsets];
117      [animation setCalculationMode:kCAAnimationDiscrete];
118      [animation setRepeatCount:HUGE_VALF];
119      [animation setDuration:kFrameDuration * [xOffsets count]];
120      spriteAnimation_.reset([animation retain]);
121
122      [self updateAnimation:nil];
123    }
124  }
125}
126
127- (void)setImage:(NSImage*)image withToastAnimation:(BOOL)animate {
128  if (!animate || [imageLayer_ contents] == nil) {
129    [self setImage:image];
130  } else {
131    // Animate away the icon.
132    CABasicAnimation* animation =
133        [CABasicAnimation animationWithKeyPath:@"position.y"];
134    CGFloat height = CGRectGetHeight([imageLayer_ bounds]);
135    [animation setToValue:@(-height)];
136    [animation setDuration:kFrameDuration * height];
137
138    // Don't remove on completion to prevent the presentation layer from
139    // snapping back to the model layer's value.
140    // It will instead be removed when we add the return animation because they
141    // have the same key.
142    [animation setRemovedOnCompletion:NO];
143    [animation setFillMode:kCAFillModeForwards];
144
145    [CATransaction begin];
146    [CATransaction setCompletionBlock:^{
147        // At the end of the animation, change to the new image and animate
148        // it back to position.
149        [self setImage:image];
150
151        CABasicAnimation* reverseAnimation =
152            [CABasicAnimation animationWithKeyPath:[animation keyPath]];
153        [reverseAnimation setFromValue:[animation toValue]];
154        [reverseAnimation setToValue:[animation fromValue]];
155        [reverseAnimation setDuration:[animation duration]];
156        [imageLayer_ addAnimation:reverseAnimation forKey:@"position"];
157    }];
158    [imageLayer_ addAnimation:animation forKey:@"position"];
159    [CATransaction commit];
160  }
161}
162
163- (BOOL)layer:(CALayer*)layer
164    shouldInheritContentsScale:(CGFloat)scale
165                    fromWindow:(NSWindow*)window {
166  return YES;
167}
168
169@end
170