OLD | NEW |
(Empty) | |
| 1 // Copyright 2012 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 "ios/chrome/browser/ui/stack_view/page_animation_util.h" |
| 6 |
| 7 #import <QuartzCore/QuartzCore.h> |
| 8 #import <UIKit/UIKit.h> |
| 9 |
| 10 #import "base/mac/scoped_nsobject.h" |
| 11 #import "ios/chrome/browser/ui/animation_util.h" |
| 12 #include "ios/chrome/browser/ui/rtl_geometry.h" |
| 13 #import "ios/chrome/browser/ui/stack_view/card_view.h" |
| 14 #import "ios/chrome/common/material_timing.h" |
| 15 |
| 16 using ios::material::TimingFunction; |
| 17 |
| 18 namespace { |
| 19 |
| 20 const NSTimeInterval kAnimationDuration = 0.25; |
| 21 const NSTimeInterval kAnimationHesitation = 0.2; |
| 22 |
| 23 // Constants used for rotating/translating in in transition-in animations and |
| 24 // rotating/translating out in transition-out animations. |
| 25 const CGFloat kDefaultRotation = 0.2094; // 12 degrees. |
| 26 // The amount by which the card should be translated along the axis on which |
| 27 // its short side is oriented (horizontal in portrait, vertical in landscape). |
| 28 const CGFloat kDefaultShortSideAxisTranslation = 240; |
| 29 // The amount by which the card should be translated along the axis on which |
| 30 // its long side is oriented (vertical in portrait, horizontal in landscape). |
| 31 const CGFloat kDefaultLongSideAxisTranslation = 10; |
| 32 |
| 33 // Transitioning in on landscape has a special-case animation. |
| 34 const CGFloat kLandscapeAnimateInRotation = 0.9423; // 54 degrees. |
| 35 const CGFloat kLandscapeAnimateInShortSideAxisTranslation = -180; |
| 36 const CGFloat kLandscapeAnimateInLongSideAxisTranslation = 140; |
| 37 |
| 38 NSString* const kViewAnimateInKey = @"ViewAnimateIn"; |
| 39 NSString* const kPaperAnimateInKey = @"PaperAnimateIn"; |
| 40 |
| 41 // When animating out, a card shrinks slightly. |
| 42 const CGFloat kAnimateOutScale = 0.7; |
| 43 const CGFloat kAnimateOutAnchorX = 0.9; |
| 44 const CGFloat kAnimateOutAnchorY = 0; |
| 45 } |
| 46 |
| 47 @interface PaperView : UIView |
| 48 |
| 49 @end |
| 50 |
| 51 @implementation PaperView |
| 52 |
| 53 - (id)initWithFrame:(CGRect)frame { |
| 54 self = [super initWithFrame:frame]; |
| 55 if (self) { |
| 56 const UIEdgeInsets kShadowStretchInsets = {28.0, 28.0, 28.0, 28.0}; |
| 57 const UIEdgeInsets kShadowLayoutOutset = {-10.0, -11.0, -12.0, -11.0}; |
| 58 CGRect shadowFrame = UIEdgeInsetsInsetRect(frame, kShadowLayoutOutset); |
| 59 base::scoped_nsobject<UIImageView> frameShadowImageView( |
| 60 [[UIImageView alloc] initWithFrame:shadowFrame]); |
| 61 [frameShadowImageView |
| 62 setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | |
| 63 UIViewAutoresizingFlexibleHeight)]; |
| 64 [self addSubview:frameShadowImageView]; |
| 65 |
| 66 UIImage* image = [UIImage imageNamed:@"popup_background"]; |
| 67 image = [image resizableImageWithCapInsets:kShadowStretchInsets]; |
| 68 [frameShadowImageView setImage:image]; |
| 69 } |
| 70 return self; |
| 71 } |
| 72 |
| 73 @end |
| 74 |
| 75 namespace ios_internal { |
| 76 |
| 77 namespace page_animation_util { |
| 78 |
| 79 const CGFloat kCardMargin = 14.0; |
| 80 |
| 81 void SetNewTabAnimationStartPositionForView(UIView* view, BOOL isPortrait) { |
| 82 CGAffineTransform transform = CGAffineTransformMakeTranslation( |
| 83 (isPortrait ? kDefaultShortSideAxisTranslation |
| 84 : kLandscapeAnimateInLongSideAxisTranslation), |
| 85 (isPortrait ? kDefaultLongSideAxisTranslation |
| 86 : kLandscapeAnimateInShortSideAxisTranslation)); |
| 87 transform = CGAffineTransformRotate( |
| 88 transform, (isPortrait ? kDefaultRotation : kLandscapeAnimateInRotation)); |
| 89 view.transform = transform; |
| 90 } |
| 91 |
| 92 void AnimateInPaperWithAnimationAndCompletion(UIView* view, |
| 93 CGFloat paperOffset, |
| 94 CGFloat contentOffset, |
| 95 CGPoint origin, |
| 96 BOOL isOffTheRecord, |
| 97 void (^extraAnimation)(void), |
| 98 void (^completion)(void)) { |
| 99 CGRect endFrame = view.frame; |
| 100 UIView* parent = [view superview]; |
| 101 NSInteger index = [[parent subviews] indexOfObject:view]; |
| 102 |
| 103 // Create paper background. |
| 104 CGRect paperFrame = CGRectOffset(endFrame, 0, paperOffset); |
| 105 paperFrame.size.height -= paperOffset; |
| 106 base::scoped_nsobject<PaperView> paper( |
| 107 [[PaperView alloc] initWithFrame:paperFrame]); |
| 108 [parent insertSubview:paper belowSubview:view]; |
| 109 [paper addSubview:view]; |
| 110 [paper setBackgroundColor:isOffTheRecord |
| 111 ? [UIColor colorWithWhite:34 / 255 alpha:1] |
| 112 : [UIColor whiteColor]]; |
| 113 |
| 114 [CATransaction begin]; |
| 115 [CATransaction setCompletionBlock:^{ |
| 116 |
| 117 // Put view back where it belongs, with its original frame. |
| 118 [parent insertSubview:view atIndex:index]; |
| 119 [paper removeFromSuperview]; |
| 120 [[view layer] removeAnimationForKey:kViewAnimateInKey]; |
| 121 view.frame = endFrame; |
| 122 if (completion) |
| 123 completion(); |
| 124 }]; |
| 125 [CATransaction setAnimationDuration:ios::material::kDuration5]; |
| 126 CAMediaTimingFunction* transformCurve2 = ios::material::TransformCurve2(); |
| 127 |
| 128 // // Animate paper to full size. |
| 129 CABasicAnimation* scaleAnimation = |
| 130 [CABasicAnimation animationWithKeyPath:@"transform"]; |
| 131 scaleAnimation.fromValue = |
| 132 [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.03, 0.03, 1)]; |
| 133 scaleAnimation.timingFunction = transformCurve2; |
| 134 scaleAnimation.duration = ios::material::kDuration1; |
| 135 |
| 136 CABasicAnimation* positionAnimation = |
| 137 [CABasicAnimation animationWithKeyPath:@"position"]; |
| 138 positionAnimation.fromValue = [NSValue valueWithCGPoint:origin]; |
| 139 positionAnimation.timingFunction = transformCurve2; |
| 140 positionAnimation.duration = ios::material::kDuration1; |
| 141 |
| 142 CAAnimation* fadeAnimation = OpacityAnimationMake(0, 1); |
| 143 fadeAnimation.timingFunction = transformCurve2; |
| 144 fadeAnimation.duration = ios::material::kDuration1; |
| 145 [[paper layer] |
| 146 addAnimation:AnimationGroupMake( |
| 147 @[ scaleAnimation, positionAnimation, fadeAnimation ]) |
| 148 forKey:kPaperAnimateInKey]; |
| 149 |
| 150 // Animate content from -10px to full size, as a child of the paper parent. |
| 151 // At the half-way point, the child will be offset / 2 vertically higher than |
| 152 // the paper parent, but be sure to account for paperOriginYOffset, which |
| 153 // allows for pages to draw above |parent| (as the new tab page does). |
| 154 CGFloat offset = -10; |
| 155 CGFloat width = endFrame.size.width; |
| 156 CGFloat height = endFrame.size.height - contentOffset; |
| 157 CGRect startFrame = CGRectMake(0, offset, width, height); |
| 158 CGRect middleFrame = |
| 159 CGRectMake(0, offset / 2 - paperOffset + contentOffset, width, height); |
| 160 CGRect childEndFrame = |
| 161 CGRectMake(0, -paperOffset + contentOffset, width, height); |
| 162 |
| 163 CAAnimation* frameAnimation = |
| 164 FrameAnimationMake([view layer], startFrame, middleFrame); |
| 165 frameAnimation.timingFunction = transformCurve2; |
| 166 frameAnimation.duration = ios::material::kDuration1; |
| 167 frameAnimation.fillMode = kCAFillModeBackwards; |
| 168 |
| 169 CAMediaTimingFunction* fadeInCurve = |
| 170 TimingFunction(ios::material::CurveEaseOut); |
| 171 CAAnimation* frameAnimation2 = |
| 172 FrameAnimationMake([view layer], middleFrame, childEndFrame); |
| 173 frameAnimation2.timingFunction = fadeInCurve; |
| 174 frameAnimation2.duration = ios::material::kDuration1; |
| 175 frameAnimation2.beginTime = ios::material::kDuration1; |
| 176 frameAnimation2.fillMode = kCAFillModeForwards; |
| 177 |
| 178 fadeAnimation = OpacityAnimationMake(0, 1); |
| 179 fadeAnimation.timingFunction = fadeInCurve; |
| 180 fadeAnimation.duration = ios::material::kDuration5; |
| 181 [[view layer] |
| 182 addAnimation:AnimationGroupMake( |
| 183 @[ frameAnimation, frameAnimation2, fadeAnimation ]) |
| 184 forKey:kViewAnimateInKey]; |
| 185 |
| 186 [CATransaction commit]; |
| 187 } |
| 188 |
| 189 void AnimateInCardWithAnimationAndCompletion(UIView* view, |
| 190 void (^extraAnimation)(void), |
| 191 void (^completion)(void)) { |
| 192 SetNewTabAnimationStartPositionForView(view, true); |
| 193 [UIView animateWithDuration:kAnimationDuration |
| 194 delay:0 |
| 195 options:UIViewAnimationCurveEaseOut |
| 196 animations:^{ |
| 197 view.transform = CGAffineTransformIdentity; |
| 198 if (extraAnimation) |
| 199 extraAnimation(); |
| 200 } |
| 201 completion:^(BOOL finished) { |
| 202 if (completion) |
| 203 completion(); |
| 204 }]; |
| 205 } |
| 206 |
| 207 void AnimateNewBackgroundPageWithCompletion(CardView* currentPageCard, |
| 208 CGRect displayFrame, |
| 209 BOOL isPortrait, |
| 210 void (^completion)(void)) { |
| 211 // Create paper background. |
| 212 base::scoped_nsobject<PaperView> paper( |
| 213 [[PaperView alloc] initWithFrame:CGRectZero]); |
| 214 UIView* parent = [currentPageCard superview]; |
| 215 [parent insertSubview:paper aboveSubview:currentPageCard]; |
| 216 CGRect pageBounds = currentPageCard.bounds; |
| 217 [paper setCenter:CGPointMake(CGRectGetMidX(pageBounds), |
| 218 CGRectGetMidY(pageBounds))]; |
| 219 [paper setBackgroundColor:[UIColor whiteColor]]; |
| 220 [paper setAlpha:0.0]; |
| 221 |
| 222 CGSize pageSize = currentPageCard.bounds.size; |
| 223 CGRect paperFrame = |
| 224 CGRectMake((displayFrame.size.width - pageSize.width) / 2, |
| 225 CGRectGetMidY(pageBounds), pageSize.width, pageSize.height); |
| 226 |
| 227 // The animation of the current page during the new background card animation |
| 228 // has three parts: |
| 229 // 1. It shrinks the current tab image into an inset card view. |
| 230 // 2. It hesitates for a fraction of a second. |
| 231 // 3. It expands back out, transforming again into the current tab. |
| 232 // |currentPageCard| gives the card at the correct size for step 2, as it |
| 233 // appears in the slight hesitation. Here, the code creates the transform |
| 234 // that will start by displaying the card at full size; the animation then |
| 235 // moves the card into its original state, and back out to full screen size. |
| 236 CGSize displaySize = displayFrame.size; |
| 237 CGFloat fullScreenScale = |
| 238 (displaySize.width + kCardImageInsets.left + kCardImageInsets.right + |
| 239 kCardFrameImageSnapshotOverlap) / |
| 240 currentPageCard.frame.size.width; |
| 241 // Align the bottom of |currentPageCard|'s snapshot with the bottom of the |
| 242 // screen, so that snapshots of any height are correctly aligned with the |
| 243 // tab's content. |
| 244 currentPageCard.center = CGPointMake( |
| 245 displaySize.width / 2.0, displaySize.height - |
| 246 (currentPageCard.image.size.height / 2.0) - |
| 247 kCardImageInsets.top / 2); |
| 248 CGAffineTransform fullScreenTransform = |
| 249 CGAffineTransformMakeScale(fullScreenScale, fullScreenScale); |
| 250 currentPageCard.transform = fullScreenTransform; |
| 251 [currentPageCard setTabOpacity:0.0]; |
| 252 |
| 253 [CATransaction begin]; |
| 254 [CATransaction |
| 255 setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)]; |
| 256 [UIView animateWithDuration:kAnimationDuration |
| 257 delay:0 |
| 258 options:UIViewAnimationCurveLinear |
| 259 animations:^{ |
| 260 [currentPageCard setTabOpacity:1.0]; |
| 261 currentPageCard.transform = CGAffineTransformIdentity; |
| 262 [paper setFrame:paperFrame]; |
| 263 [paper setAlpha:1.0]; |
| 264 } |
| 265 completion:^(BOOL finished) { |
| 266 // Zoom out the top tab, slide away the paper view. |
| 267 [UIView animateWithDuration:kAnimationDuration |
| 268 delay:kAnimationHesitation |
| 269 options:UIViewAnimationCurveLinear |
| 270 animations:^{ |
| 271 [currentPageCard setTabOpacity:0.0]; |
| 272 currentPageCard.transform = fullScreenTransform; |
| 273 [paper setFrame:CGRectOffset(paperFrame, 0, |
| 274 CGRectGetMaxY(paperFrame))]; |
| 275 [paper setAlpha:0.0]; |
| 276 |
| 277 } |
| 278 completion:^(BOOL finished) { |
| 279 [paper removeFromSuperview]; |
| 280 if (completion) |
| 281 completion(); |
| 282 }]; |
| 283 }]; |
| 284 [CATransaction commit]; |
| 285 } |
| 286 |
| 287 void AnimateNewBackgroundTabWithCompletion(CardView* currentPageCard, |
| 288 CardView* newCard, |
| 289 CGRect displayFrame, |
| 290 BOOL isPortrait, |
| 291 void (^completion)(void)) { |
| 292 // The animation of the current page during the new background card animation |
| 293 // has three parts: |
| 294 // 1. It shrinks the current tab image into an inset card view. |
| 295 // 2. It hesitates for a fraction of a second. |
| 296 // 3. It expands back out, transforming again into the current tab. |
| 297 // |currentPageCard| gives the card at the correct size for step 2, as it |
| 298 // appears in the slight hesitation. Here, the code creates the transform |
| 299 // that will start by displaying the card at full size; the animation then |
| 300 // moves the card into its original state, and back out to full screen size. |
| 301 CGSize displaySize = displayFrame.size; |
| 302 CGFloat fullScreenScale = |
| 303 (displaySize.width + kCardImageInsets.left + kCardImageInsets.right) / |
| 304 currentPageCard.frame.size.width; |
| 305 // Align the bottom of |currentPageCard|'s snapshot with the bottom of the |
| 306 // screen, so that snapshots of any height are correctly aligned with the |
| 307 // tab's content. |
| 308 currentPageCard.center = CGPointMake( |
| 309 displaySize.width / 2.0, |
| 310 displaySize.height - (currentPageCard.image.size.height / 2.0)); |
| 311 CGAffineTransform fullScreenTransform = |
| 312 CGAffineTransformMakeScale(fullScreenScale, fullScreenScale); |
| 313 currentPageCard.transform = fullScreenTransform; |
| 314 [currentPageCard setTabOpacity:0.0]; |
| 315 |
| 316 // The new background card animation has three parts: |
| 317 // 1. It moves from offscreen onto the screen (in a "rotating" motion). |
| 318 // 2. It hesitates for a fraction of a second, halfway on the screen. |
| 319 // 3. It moves from the screen offscreen (in a "sliding" motion). |
| 320 // In the setup code below, we position the card on the screen as it will |
| 321 // appear in step 2; in the animation code, we then move it to and from this |
| 322 // original onscreen position. |
| 323 // |
| 324 // In portrait mode, the card in step 2 appears to be halfway off the bottom |
| 325 // edge of the screen; in landscape mode, it appears to be halfway off the |
| 326 // right edge. The x and y offsets below set up this screen position. |
| 327 CGFloat yOffset = -displayFrame.origin.y + kCardMargin + |
| 328 (isPortrait ? displaySize.height / 2.0 : 0); |
| 329 CGFloat xOffset = isPortrait ? kCardMargin : displaySize.width / 2.0; |
| 330 CGRect newCardFrame = newCard.frame; |
| 331 newCardFrame.origin.x += xOffset; |
| 332 newCardFrame.origin.y += yOffset; |
| 333 newCard.frame = newCardFrame; |
| 334 |
| 335 // For step 1, we apply a transform to the card that moves it offscreen and |
| 336 // rotates it away in preparation for the "rotate in" animation that starts |
| 337 // any new tab appearance. |
| 338 SetNewTabAnimationStartPositionForView(newCard, isPortrait); |
| 339 |
| 340 // For step 3, we create a transform which will slide the card offscreen along |
| 341 // its longer axis to end the animation. |
| 342 CGAffineTransform slideAwayTransform = |
| 343 isPortrait |
| 344 ? CGAffineTransformMakeTranslation(0, newCard.frame.size.height) |
| 345 : CGAffineTransformMakeTranslation(newCard.frame.size.width, 0); |
| 346 |
| 347 [UIView animateWithDuration:kAnimationDuration |
| 348 delay:0 |
| 349 options:UIViewAnimationCurveEaseOut |
| 350 animations:^{ |
| 351 [currentPageCard setTabOpacity:1.0]; |
| 352 currentPageCard.transform = CGAffineTransformIdentity; |
| 353 newCard.transform = CGAffineTransformIdentity; |
| 354 } |
| 355 completion:^(BOOL finished) { |
| 356 // Zoom out the top tab, slide away the new card. |
| 357 [UIView animateWithDuration:kAnimationDuration |
| 358 delay:kAnimationHesitation |
| 359 options:UIViewAnimationCurveEaseOut |
| 360 animations:^{ |
| 361 [currentPageCard setTabOpacity:0.0]; |
| 362 currentPageCard.transform = fullScreenTransform; |
| 363 newCard.transform = slideAwayTransform; |
| 364 } |
| 365 completion:^(BOOL finished) { |
| 366 if (completion) |
| 367 completion(); |
| 368 }]; |
| 369 }]; |
| 370 } |
| 371 |
| 372 void UpdateLayorAnchorWithTransform(CALayer* layer, |
| 373 CGPoint newAnchor, |
| 374 CGAffineTransform transform) { |
| 375 CGSize size = layer.bounds.size; |
| 376 CGPoint oldAnchor = layer.anchorPoint; |
| 377 CGPoint newCenter = |
| 378 CGPointMake(size.width * newAnchor.x, size.height * newAnchor.y); |
| 379 CGPoint oldCenter = |
| 380 CGPointMake(size.width * oldAnchor.x, size.height * oldAnchor.y); |
| 381 |
| 382 newCenter = CGPointApplyAffineTransform(newCenter, transform); |
| 383 oldCenter = CGPointApplyAffineTransform(oldCenter, transform); |
| 384 |
| 385 CGPoint position = layer.position; |
| 386 position.x = position.x - oldCenter.x + newCenter.x; |
| 387 position.y = position.y - oldCenter.y + newCenter.y; |
| 388 layer.position = position; |
| 389 |
| 390 layer.anchorPoint = newAnchor; |
| 391 } |
| 392 |
| 393 void AnimateOutWithCompletion(UIView* view, |
| 394 NSTimeInterval delay, |
| 395 BOOL clockwise, |
| 396 BOOL isPortrait, |
| 397 void (^completion)(void)) { |
| 398 // The close animation spec calls for the anchor point to be the upper right. |
| 399 CGPoint newAnchorPoint = CGPointMake(kAnimateOutAnchorX, kAnimateOutAnchorY); |
| 400 CALayer* layer = [view layer]; |
| 401 UpdateLayorAnchorWithTransform(layer, newAnchorPoint, view.transform); |
| 402 |
| 403 [CATransaction begin]; |
| 404 if (completion) |
| 405 [CATransaction setCompletionBlock:completion]; |
| 406 |
| 407 [CATransaction setAnimationDuration:ios::material::kDuration6]; |
| 408 CAMediaTimingFunction* timing = TimingFunction(ios::material::CurveEaseIn); |
| 409 [CATransaction setAnimationTimingFunction:timing]; |
| 410 |
| 411 CABasicAnimation* scaleAnimation = |
| 412 [CABasicAnimation animationWithKeyPath:@"transform"]; |
| 413 CATransform3D transform = CATransform3DScale( |
| 414 layer.transform, kAnimateOutScale, kAnimateOutScale, 1); |
| 415 [scaleAnimation setToValue:[NSValue valueWithCATransform3D:transform]]; |
| 416 |
| 417 CABasicAnimation* fadeAnimation = |
| 418 [CABasicAnimation animationWithKeyPath:@"opacity"]; |
| 419 [fadeAnimation setFromValue:[NSNumber numberWithFloat:[layer opacity]]]; |
| 420 [fadeAnimation setToValue:@0]; |
| 421 |
| 422 [layer addAnimation:AnimationGroupMake(@[ scaleAnimation, fadeAnimation ]) |
| 423 forKey:@"animateOut"]; |
| 424 [CATransaction commit]; |
| 425 } |
| 426 |
| 427 CGAffineTransform AnimateOutTransform(CGFloat fraction, |
| 428 BOOL clockwise, |
| 429 BOOL isPortrait) { |
| 430 CGFloat horizontalTranslation = isPortrait ? kDefaultShortSideAxisTranslation |
| 431 : kDefaultLongSideAxisTranslation; |
| 432 CGFloat verticalTranslation = isPortrait ? kDefaultLongSideAxisTranslation |
| 433 : kDefaultShortSideAxisTranslation; |
| 434 CGFloat rotationAmount = kDefaultRotation; |
| 435 |
| 436 if (!isPortrait && UseRTLLayout()) { |
| 437 rotationAmount *= -1; |
| 438 horizontalTranslation *= -1; |
| 439 } |
| 440 |
| 441 horizontalTranslation *= fraction; |
| 442 verticalTranslation *= fraction; |
| 443 rotationAmount *= fraction; |
| 444 if (!clockwise) |
| 445 rotationAmount *= -1; |
| 446 |
| 447 // In portrait, rotating counterclockwise pushes the animation to the left. |
| 448 if (isPortrait && !clockwise) { |
| 449 horizontalTranslation *= -1; |
| 450 } |
| 451 |
| 452 // In landscape, rotating clockwise pushes the animation up. |
| 453 if (!isPortrait && clockwise) { |
| 454 verticalTranslation *= -1; |
| 455 } |
| 456 |
| 457 // Scale the card between full-scale and the final desired scale based on |
| 458 // |fraction|. |
| 459 CGFloat differenceInScale = 1.0 - kAnimateOutScale; |
| 460 CGFloat scaleAmount = 1.0 - (differenceInScale * fraction); |
| 461 CGAffineTransform transform = CGAffineTransformMakeTranslation( |
| 462 horizontalTranslation, verticalTranslation); |
| 463 transform = CGAffineTransformRotate(transform, rotationAmount); |
| 464 transform = CGAffineTransformScale(transform, scaleAmount, scaleAmount); |
| 465 return transform; |
| 466 } |
| 467 |
| 468 CGFloat AnimateOutTransformBreadth() { |
| 469 return kDefaultShortSideAxisTranslation; |
| 470 } |
| 471 |
| 472 } // namespace page_animation_util |
| 473 |
| 474 } // namespace ios_internal |
OLD | NEW |