| Index: ios/chrome/browser/ui/stack_view/page_animation_util.mm
|
| diff --git a/ios/chrome/browser/ui/stack_view/page_animation_util.mm b/ios/chrome/browser/ui/stack_view/page_animation_util.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..3e66bd4c4b76ecaffb79eb2d6e049eef8334fe7d
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/stack_view/page_animation_util.mm
|
| @@ -0,0 +1,474 @@
|
| +// 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 "base/mac/scoped_nsobject.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"
|
| +
|
| +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);
|
| + base::scoped_nsobject<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;
|
| + base::scoped_nsobject<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.
|
| + base::scoped_nsobject<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
|
|
|