item_drag_controller.mm revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
1// Copyright 2013 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 "ui/app_list/cocoa/item_drag_controller.h"
6
7#include "base/logging.h"
8#import "ui/app_list/cocoa/apps_grid_view_item.h"
9#include "ui/base/cocoa/window_size_constants.h"
10
11namespace {
12
13// Scale to transform the grid cell when a drag starts. Note that 1.5 ensures
14// that integers are used for the layer bounds when the grid cell dimensions
15// are even.
16const CGFloat kDraggingIconScale = 1.5;
17
18const NSTimeInterval kAnimationDuration = 0.2;
19NSString* const kGrowAnimationKey = @"growAnimation";
20
21}  // namespace
22
23@interface ItemDragController ()
24
25- (void)animateTransformFrom:(CATransform3D)fromValue
26                 useDelegate:(BOOL)useDelegate;
27
28- (void)clearAnimations;
29
30@end
31
32@implementation ItemDragController
33
34- (id)initWithGridCellSize:(NSSize)size {
35  if ((self = [super init])) {
36    NSRect frameRect = NSMakeRect(0,
37                                  0,
38                                  size.width * kDraggingIconScale,
39                                  size.height * kDraggingIconScale);
40    scoped_nsobject<NSView> dragView([[NSView alloc] initWithFrame:frameRect]);
41    [dragView setWantsLayer:YES];
42    [dragView setHidden:YES];
43
44    dragLayer_.reset([[CALayer layer] retain]);
45    [dragLayer_ setFrame:NSRectToCGRect(frameRect)];
46    [[dragView layer] addSublayer:dragLayer_];
47
48    [self setView:dragView];
49  }
50  return self;
51}
52
53- (void)initiate:(AppsGridViewItem*)item
54    mouseDownLocation:(NSPoint)mouseDownLocation
55      currentLocation:(NSPoint)currentLocation
56            timestamp:(NSTimeInterval)eventTimestamp {
57  [self clearAnimations];
58  [item setSelected:NO];
59  NSView* itemView = [item view];
60  NSPoint pointInGridCell = [itemView convertPoint:mouseDownLocation
61                                          fromView:nil];
62  mouseOffset_ = NSMakePoint(pointInGridCell.x - NSMidX([itemView bounds]),
63                             NSMidY([itemView bounds]) - pointInGridCell.y);
64
65  // Take a snapshot of the grid cell without the text label and hide the cell.
66  // Also remove the cell highlight on the image, added when it was clicked.
67  NSButton* button = [item button];
68  scoped_nsobject<NSString> oldTitle([[button title] retain]);
69  [button setTitle:[NSString string]];
70  [[button cell] setHighlighted:NO];
71  NSBitmapImageRep* imageRep =
72      [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
73  [itemView cacheDisplayInRect:[itemView visibleRect]
74              toBitmapImageRep:imageRep];
75  [button setHidden:YES];
76  [button setTitle:oldTitle];
77
78  [dragLayer_ setContents:reinterpret_cast<id>([imageRep CGImage])];
79  [dragLayer_ setTransform:CATransform3DIdentity];
80
81  // Add a grow animation to the layer.
82  CATransform3D growFrom = CATransform3DScale(CATransform3DIdentity,
83                                              1.0 / kDraggingIconScale,
84                                              1.0 / kDraggingIconScale,
85                                              1.0);
86  [self animateTransformFrom:growFrom
87                 useDelegate:NO];
88
89  growStart_ = eventTimestamp;
90  [[self view] setHidden:NO];
91}
92
93- (void)update:(NSPoint)currentLocation
94     timestamp:(NSTimeInterval)eventTimestamp {
95  NSPoint pointInSuperview = [[[self view] superview]
96      convertPoint:currentLocation
97          fromView:nil];
98  NSRect rect = [[self view] bounds];
99  NSPoint anchor = NSMakePoint(NSMidX(rect), NSMidY(rect));
100
101  // If the grow animation is still in progress, make the point of the image
102  // that was clicked appear stuck to the mouse cursor.
103  CGFloat progress = (eventTimestamp - growStart_) / kAnimationDuration;
104  CGFloat currentIconScale = progress < 1.0 ?
105      1.0 + (kDraggingIconScale - 1.0) * progress :
106      kDraggingIconScale;
107
108  pointInSuperview.x -= (mouseOffset_.x * currentIconScale + anchor.x);
109  pointInSuperview.y -= (mouseOffset_.y * currentIconScale + anchor.y);
110  [[self view] setFrameOrigin:pointInSuperview];
111}
112
113- (void)complete:(AppsGridViewItem*)item
114    targetOrigin:(NSPoint)targetOrigin {
115  [self clearAnimations];
116
117  NSView* itemView = [item view];
118
119  // Take another snapshot of the grid cell, after restoring the label.
120  NSButton* button = [item button];
121  [button setHidden:NO];
122  NSBitmapImageRep* imageRep =
123      [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
124  [itemView cacheDisplayInRect:[itemView visibleRect]
125              toBitmapImageRep:imageRep];
126  [button setHidden:YES];
127
128  [dragLayer_ setContents:reinterpret_cast<id>([imageRep CGImage])];
129  [dragLayer_ setTransform:CATransform3DScale(CATransform3DIdentity,
130                                              1.0 / kDraggingIconScale,
131                                              1.0 / kDraggingIconScale,
132                                              1.0)];
133
134  // Retain the button so it can be unhidden when the animation completes. Note
135  // that the |item| and corresponding button can differ from the |item| passed
136  // to initiate(), if it moved to a new page during the drag. At this point the
137  // destination page is known, so retain the button.
138  buttonToRestore_.reset([button retain]);
139
140  // Add the shrink animation for the layer.
141  [self animateTransformFrom:CATransform3DIdentity
142                 useDelegate:YES];
143  shrinking_ = YES;
144
145  // Also animate the translation, on the view.
146  // TODO(tapted): This should be merged into the scale transform, instead of
147  // using a separate NSViewAnimation.
148  NSRect startRect = [[self view] frame];
149
150  // The final position needs to be adjusted since it shrinks from each side.
151  NSRect targetRect = NSMakeRect(
152      targetOrigin.x - NSMidX([itemView bounds]) * (kDraggingIconScale - 1),
153      targetOrigin.y - NSMidY([itemView bounds]) * (kDraggingIconScale - 1),
154      startRect.size.width,
155      startRect.size.height);
156
157  NSDictionary* animationDict = @{
158      NSViewAnimationTargetKey:     [self view],
159      NSViewAnimationStartFrameKey: [NSValue valueWithRect:startRect],
160      NSViewAnimationEndFrameKey:   [NSValue valueWithRect:targetRect]
161  };
162
163  scoped_nsobject<NSViewAnimation> translate([[NSViewAnimation alloc]
164      initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
165  [translate setDuration:kAnimationDuration];
166  [translate startAnimation];
167}
168
169- (void)animateTransformFrom:(CATransform3D)fromValue
170                 useDelegate:(BOOL)useDelegate {
171  CABasicAnimation* animation =
172      [CABasicAnimation animationWithKeyPath:@"transform"];
173  [animation setFromValue:[NSValue valueWithCATransform3D:fromValue]];
174  if (useDelegate)
175    [animation setDelegate:self];
176
177  [animation setDuration:kAnimationDuration];
178  [CATransaction begin];
179  [CATransaction setValue:[NSNumber numberWithFloat:kAnimationDuration]
180                   forKey:kCATransactionAnimationDuration];
181  [dragLayer_ addAnimation:animation
182                    forKey:@"transform"];
183  [CATransaction commit];
184}
185
186- (void)clearAnimations {
187  [dragLayer_ removeAllAnimations];
188  if (!shrinking_)
189    return;
190
191  DCHECK(buttonToRestore_);
192  [buttonToRestore_ setHidden:NO];
193  buttonToRestore_.reset();
194  shrinking_ = NO;
195}
196
197- (void)animationDidStop:(CAAnimation*)anim
198                finished:(BOOL)finished {
199  if (!finished)
200    return;
201
202  DCHECK(shrinking_);
203  [self clearAnimations];
204  [dragLayer_ setContents:nil];
205  [[self view] setHidden:YES];
206}
207
208@end
209