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 13 static 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