blob: a43792e0921d03856b0bc31a432e6dcb40226ed2 [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/stack_view/page_animation_util.h"
#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIKit.h>
#import "ios/chrome/browser/ui/animation_util.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#import "ios/chrome/browser/ui/stack_view/card_view.h"
#import "ios/chrome/common/material_timing.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using ios::material::TimingFunction;
namespace {
const NSTimeInterval kAnimationDuration = 0.25;
const NSTimeInterval kAnimationHesitation = 0.2;
// Constants used for rotating/translating in in transition-in animations and
// rotating/translating out in transition-out animations.
const CGFloat kDefaultRotation = 0.2094; // 12 degrees.
// The amount by which the card should be translated along the axis on which
// its short side is oriented (horizontal in portrait, vertical in landscape).
const CGFloat kDefaultShortSideAxisTranslation = 240;
// The amount by which the card should be translated along the axis on which
// its long side is oriented (vertical in portrait, horizontal in landscape).
const CGFloat kDefaultLongSideAxisTranslation = 10;
// Transitioning in on landscape has a special-case animation.
const CGFloat kLandscapeAnimateInRotation = 0.9423; // 54 degrees.
const CGFloat kLandscapeAnimateInShortSideAxisTranslation = -180;
const CGFloat kLandscapeAnimateInLongSideAxisTranslation = 140;
NSString* const kViewAnimateInKey = @"ViewAnimateIn";
NSString* const kPaperAnimateInKey = @"PaperAnimateIn";
// When animating out, a card shrinks slightly.
const CGFloat kAnimateOutScale = 0.7;
const CGFloat kAnimateOutAnchorX = 0.9;
const CGFloat kAnimateOutAnchorY = 0;
}
@interface PaperView : UIView
@end
@implementation PaperView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
const UIEdgeInsets kShadowStretchInsets = {28.0, 28.0, 28.0, 28.0};
const UIEdgeInsets kShadowLayoutOutset = {-10.0, -11.0, -12.0, -11.0};
CGRect shadowFrame = UIEdgeInsetsInsetRect(frame, kShadowLayoutOutset);
UIImageView* frameShadowImageView =
[[UIImageView alloc] initWithFrame:shadowFrame];
[frameShadowImageView
setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight)];
[self addSubview:frameShadowImageView];
UIImage* image = [UIImage imageNamed:@"popup_background"];
image = [image resizableImageWithCapInsets:kShadowStretchInsets];
[frameShadowImageView setImage:image];
}
return self;
}
@end
namespace ios_internal {
namespace page_animation_util {
const CGFloat kCardMargin = 14.0;
void SetNewTabAnimationStartPositionForView(UIView* view, BOOL isPortrait) {
CGAffineTransform transform = CGAffineTransformMakeTranslation(
(isPortrait ? kDefaultShortSideAxisTranslation
: kLandscapeAnimateInLongSideAxisTranslation),
(isPortrait ? kDefaultLongSideAxisTranslation
: kLandscapeAnimateInShortSideAxisTranslation));
transform = CGAffineTransformRotate(
transform, (isPortrait ? kDefaultRotation : kLandscapeAnimateInRotation));
view.transform = transform;
}
void AnimateInPaperWithAnimationAndCompletion(UIView* view,
CGFloat paperOffset,
CGFloat contentOffset,
CGPoint origin,
BOOL isOffTheRecord,
void (^extraAnimation)(void),
void (^completion)(void)) {
CGRect endFrame = view.frame;
UIView* parent = [view superview];
NSInteger index = [[parent subviews] indexOfObject:view];
// Create paper background.
CGRect paperFrame = CGRectOffset(endFrame, 0, paperOffset);
paperFrame.size.height -= paperOffset;
PaperView* paper = [[PaperView alloc] initWithFrame:paperFrame];
[parent insertSubview:paper belowSubview:view];
[paper addSubview:view];
[paper setBackgroundColor:isOffTheRecord
? [UIColor colorWithWhite:34 / 255 alpha:1]
: [UIColor whiteColor]];
[CATransaction begin];
[CATransaction setCompletionBlock:^{
// Put view back where it belongs, with its original frame.
[parent insertSubview:view atIndex:index];
[paper removeFromSuperview];
[[view layer] removeAnimationForKey:kViewAnimateInKey];
view.frame = endFrame;
if (completion)
completion();
}];
[CATransaction setAnimationDuration:ios::material::kDuration5];
CAMediaTimingFunction* transformCurve2 = ios::material::TransformCurve2();
// // Animate paper to full size.
CABasicAnimation* scaleAnimation =
[CABasicAnimation animationWithKeyPath:@"transform"];
scaleAnimation.fromValue =
[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.03, 0.03, 1)];
scaleAnimation.timingFunction = transformCurve2;
scaleAnimation.duration = ios::material::kDuration1;
CABasicAnimation* positionAnimation =
[CABasicAnimation animationWithKeyPath:@"position"];
positionAnimation.fromValue = [NSValue valueWithCGPoint:origin];
positionAnimation.timingFunction = transformCurve2;
positionAnimation.duration = ios::material::kDuration1;
CAAnimation* fadeAnimation = OpacityAnimationMake(0, 1);
fadeAnimation.timingFunction = transformCurve2;
fadeAnimation.duration = ios::material::kDuration1;
[[paper layer]
addAnimation:AnimationGroupMake(
@[ scaleAnimation, positionAnimation, fadeAnimation ])
forKey:kPaperAnimateInKey];
// Animate content from -10px to full size, as a child of the paper parent.
// At the half-way point, the child will be offset / 2 vertically higher than
// the paper parent, but be sure to account for paperOriginYOffset, which
// allows for pages to draw above |parent| (as the new tab page does).
CGFloat offset = -10;
CGFloat width = endFrame.size.width;
CGFloat height = endFrame.size.height - contentOffset;
CGRect startFrame = CGRectMake(0, offset, width, height);
CGRect middleFrame =
CGRectMake(0, offset / 2 - paperOffset + contentOffset, width, height);
CGRect childEndFrame =
CGRectMake(0, -paperOffset + contentOffset, width, height);
CAAnimation* frameAnimation =
FrameAnimationMake([view layer], startFrame, middleFrame);
frameAnimation.timingFunction = transformCurve2;
frameAnimation.duration = ios::material::kDuration1;
frameAnimation.fillMode = kCAFillModeBackwards;
CAMediaTimingFunction* fadeInCurve =
TimingFunction(ios::material::CurveEaseOut);
CAAnimation* frameAnimation2 =
FrameAnimationMake([view layer], middleFrame, childEndFrame);
frameAnimation2.timingFunction = fadeInCurve;
frameAnimation2.duration = ios::material::kDuration1;
frameAnimation2.beginTime = ios::material::kDuration1;
frameAnimation2.fillMode = kCAFillModeForwards;
fadeAnimation = OpacityAnimationMake(0, 1);
fadeAnimation.timingFunction = fadeInCurve;
fadeAnimation.duration = ios::material::kDuration5;
[[view layer]
addAnimation:AnimationGroupMake(
@[ frameAnimation, frameAnimation2, fadeAnimation ])
forKey:kViewAnimateInKey];
[CATransaction commit];
}
void AnimateInCardWithAnimationAndCompletion(UIView* view,
void (^extraAnimation)(void),
void (^completion)(void)) {
SetNewTabAnimationStartPositionForView(view, true);
[UIView animateWithDuration:kAnimationDuration
delay:0
options:UIViewAnimationCurveEaseOut
animations:^{
view.transform = CGAffineTransformIdentity;
if (extraAnimation)
extraAnimation();
}
completion:^(BOOL finished) {
if (completion)
completion();
}];
}
void AnimateNewBackgroundPageWithCompletion(CardView* currentPageCard,
CGRect displayFrame,
BOOL isPortrait,
void (^completion)(void)) {
// Create paper background.
PaperView* paper = [[PaperView alloc] initWithFrame:CGRectZero];
UIView* parent = [currentPageCard superview];
[parent insertSubview:paper aboveSubview:currentPageCard];
CGRect pageBounds = currentPageCard.bounds;
[paper setCenter:CGPointMake(CGRectGetMidX(pageBounds),
CGRectGetMidY(pageBounds))];
[paper setBackgroundColor:[UIColor whiteColor]];
[paper setAlpha:0.0];
CGSize pageSize = currentPageCard.bounds.size;
CGRect paperFrame =
CGRectMake((displayFrame.size.width - pageSize.width) / 2,
CGRectGetMidY(pageBounds), pageSize.width, pageSize.height);
// The animation of the current page during the new background card animation
// has three parts:
// 1. It shrinks the current tab image into an inset card view.
// 2. It hesitates for a fraction of a second.
// 3. It expands back out, transforming again into the current tab.
// |currentPageCard| gives the card at the correct size for step 2, as it
// appears in the slight hesitation. Here, the code creates the transform
// that will start by displaying the card at full size; the animation then
// moves the card into its original state, and back out to full screen size.
CGSize displaySize = displayFrame.size;
CGFloat fullScreenScale =
(displaySize.width + kCardImageInsets.left + kCardImageInsets.right +
kCardFrameImageSnapshotOverlap) /
currentPageCard.frame.size.width;
// Align the bottom of |currentPageCard|'s snapshot with the bottom of the
// screen, so that snapshots of any height are correctly aligned with the
// tab's content.
currentPageCard.center = CGPointMake(
displaySize.width / 2.0, displaySize.height -
(currentPageCard.image.size.height / 2.0) -
kCardImageInsets.top / 2);
CGAffineTransform fullScreenTransform =
CGAffineTransformMakeScale(fullScreenScale, fullScreenScale);
currentPageCard.transform = fullScreenTransform;
[currentPageCard setTabOpacity:0.0];
[CATransaction begin];
[CATransaction
setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)];
[UIView animateWithDuration:kAnimationDuration
delay:0
options:UIViewAnimationCurveLinear
animations:^{
[currentPageCard setTabOpacity:1.0];
currentPageCard.transform = CGAffineTransformIdentity;
[paper setFrame:paperFrame];
[paper setAlpha:1.0];
}
completion:^(BOOL finished) {
// Zoom out the top tab, slide away the paper view.
[UIView animateWithDuration:kAnimationDuration
delay:kAnimationHesitation
options:UIViewAnimationCurveLinear
animations:^{
[currentPageCard setTabOpacity:0.0];
currentPageCard.transform = fullScreenTransform;
[paper setFrame:CGRectOffset(paperFrame, 0,
CGRectGetMaxY(paperFrame))];
[paper setAlpha:0.0];
}
completion:^(BOOL finished) {
[paper removeFromSuperview];
if (completion)
completion();
}];
}];
[CATransaction commit];
}
void AnimateNewBackgroundTabWithCompletion(CardView* currentPageCard,
CardView* newCard,
CGRect displayFrame,
BOOL isPortrait,
void (^completion)(void)) {
// The animation of the current page during the new background card animation
// has three parts:
// 1. It shrinks the current tab image into an inset card view.
// 2. It hesitates for a fraction of a second.
// 3. It expands back out, transforming again into the current tab.
// |currentPageCard| gives the card at the correct size for step 2, as it
// appears in the slight hesitation. Here, the code creates the transform
// that will start by displaying the card at full size; the animation then
// moves the card into its original state, and back out to full screen size.
CGSize displaySize = displayFrame.size;
CGFloat fullScreenScale =
(displaySize.width + kCardImageInsets.left + kCardImageInsets.right) /
currentPageCard.frame.size.width;
// Align the bottom of |currentPageCard|'s snapshot with the bottom of the
// screen, so that snapshots of any height are correctly aligned with the
// tab's content.
currentPageCard.center = CGPointMake(
displaySize.width / 2.0,
displaySize.height - (currentPageCard.image.size.height / 2.0));
CGAffineTransform fullScreenTransform =
CGAffineTransformMakeScale(fullScreenScale, fullScreenScale);
currentPageCard.transform = fullScreenTransform;
[currentPageCard setTabOpacity:0.0];
// The new background card animation has three parts:
// 1. It moves from offscreen onto the screen (in a "rotating" motion).
// 2. It hesitates for a fraction of a second, halfway on the screen.
// 3. It moves from the screen offscreen (in a "sliding" motion).
// In the setup code below, we position the card on the screen as it will
// appear in step 2; in the animation code, we then move it to and from this
// original onscreen position.
//
// In portrait mode, the card in step 2 appears to be halfway off the bottom
// edge of the screen; in landscape mode, it appears to be halfway off the
// right edge. The x and y offsets below set up this screen position.
CGFloat yOffset = -displayFrame.origin.y + kCardMargin +
(isPortrait ? displaySize.height / 2.0 : 0);
CGFloat xOffset = isPortrait ? kCardMargin : displaySize.width / 2.0;
CGRect newCardFrame = newCard.frame;
newCardFrame.origin.x += xOffset;
newCardFrame.origin.y += yOffset;
newCard.frame = newCardFrame;
// For step 1, we apply a transform to the card that moves it offscreen and
// rotates it away in preparation for the "rotate in" animation that starts
// any new tab appearance.
SetNewTabAnimationStartPositionForView(newCard, isPortrait);
// For step 3, we create a transform which will slide the card offscreen along
// its longer axis to end the animation.
CGAffineTransform slideAwayTransform =
isPortrait
? CGAffineTransformMakeTranslation(0, newCard.frame.size.height)
: CGAffineTransformMakeTranslation(newCard.frame.size.width, 0);
[UIView animateWithDuration:kAnimationDuration
delay:0
options:UIViewAnimationCurveEaseOut
animations:^{
[currentPageCard setTabOpacity:1.0];
currentPageCard.transform = CGAffineTransformIdentity;
newCard.transform = CGAffineTransformIdentity;
}
completion:^(BOOL finished) {
// Zoom out the top tab, slide away the new card.
[UIView animateWithDuration:kAnimationDuration
delay:kAnimationHesitation
options:UIViewAnimationCurveEaseOut
animations:^{
[currentPageCard setTabOpacity:0.0];
currentPageCard.transform = fullScreenTransform;
newCard.transform = slideAwayTransform;
}
completion:^(BOOL finished) {
if (completion)
completion();
}];
}];
}
void UpdateLayorAnchorWithTransform(CALayer* layer,
CGPoint newAnchor,
CGAffineTransform transform) {
CGSize size = layer.bounds.size;
CGPoint oldAnchor = layer.anchorPoint;
CGPoint newCenter =
CGPointMake(size.width * newAnchor.x, size.height * newAnchor.y);
CGPoint oldCenter =
CGPointMake(size.width * oldAnchor.x, size.height * oldAnchor.y);
newCenter = CGPointApplyAffineTransform(newCenter, transform);
oldCenter = CGPointApplyAffineTransform(oldCenter, transform);
CGPoint position = layer.position;
position.x = position.x - oldCenter.x + newCenter.x;
position.y = position.y - oldCenter.y + newCenter.y;
layer.position = position;
layer.anchorPoint = newAnchor;
}
void AnimateOutWithCompletion(UIView* view,
NSTimeInterval delay,
BOOL clockwise,
BOOL isPortrait,
void (^completion)(void)) {
// The close animation spec calls for the anchor point to be the upper right.
CGPoint newAnchorPoint = CGPointMake(kAnimateOutAnchorX, kAnimateOutAnchorY);
CALayer* layer = [view layer];
UpdateLayorAnchorWithTransform(layer, newAnchorPoint, view.transform);
[CATransaction begin];
if (completion)
[CATransaction setCompletionBlock:completion];
[CATransaction setAnimationDuration:ios::material::kDuration6];
CAMediaTimingFunction* timing = TimingFunction(ios::material::CurveEaseIn);
[CATransaction setAnimationTimingFunction:timing];
CABasicAnimation* scaleAnimation =
[CABasicAnimation animationWithKeyPath:@"transform"];
CATransform3D transform = CATransform3DScale(
layer.transform, kAnimateOutScale, kAnimateOutScale, 1);
[scaleAnimation setToValue:[NSValue valueWithCATransform3D:transform]];
CABasicAnimation* fadeAnimation =
[CABasicAnimation animationWithKeyPath:@"opacity"];
[fadeAnimation setFromValue:[NSNumber numberWithFloat:[layer opacity]]];
[fadeAnimation setToValue:@0];
[layer addAnimation:AnimationGroupMake(@[ scaleAnimation, fadeAnimation ])
forKey:@"animateOut"];
[CATransaction commit];
}
CGAffineTransform AnimateOutTransform(CGFloat fraction,
BOOL clockwise,
BOOL isPortrait) {
CGFloat horizontalTranslation = isPortrait ? kDefaultShortSideAxisTranslation
: kDefaultLongSideAxisTranslation;
CGFloat verticalTranslation = isPortrait ? kDefaultLongSideAxisTranslation
: kDefaultShortSideAxisTranslation;
CGFloat rotationAmount = kDefaultRotation;
if (!isPortrait && UseRTLLayout()) {
rotationAmount *= -1;
horizontalTranslation *= -1;
}
horizontalTranslation *= fraction;
verticalTranslation *= fraction;
rotationAmount *= fraction;
if (!clockwise)
rotationAmount *= -1;
// In portrait, rotating counterclockwise pushes the animation to the left.
if (isPortrait && !clockwise) {
horizontalTranslation *= -1;
}
// In landscape, rotating clockwise pushes the animation up.
if (!isPortrait && clockwise) {
verticalTranslation *= -1;
}
// Scale the card between full-scale and the final desired scale based on
// |fraction|.
CGFloat differenceInScale = 1.0 - kAnimateOutScale;
CGFloat scaleAmount = 1.0 - (differenceInScale * fraction);
CGAffineTransform transform = CGAffineTransformMakeTranslation(
horizontalTranslation, verticalTranslation);
transform = CGAffineTransformRotate(transform, rotationAmount);
transform = CGAffineTransformScale(transform, scaleAmount, scaleAmount);
return transform;
}
CGFloat AnimateOutTransformBreadth() {
return kDefaultShortSideAxisTranslation;
}
} // namespace page_animation_util
} // namespace ios_internal