chromium/ios/chrome/browser/tabs/ui_bundled/background_tab_animation_view.mm

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/tabs/ui_bundled/background_tab_animation_view.h"

#import "base/check.h"
#import "ios/chrome/browser/shared/ui/util/animation_util.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/common/material_timing.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {
const CGFloat kImageSize = 28;
const CGFloat kMaxScale = 1.3;
const CGFloat kMinScale = 0.7;
CGFloat kRotationAngleInRadians = 20.0 / 180 * M_PI;
}  // namespace

@interface BackgroundTabAnimationView ()

// Whether the animation is taking place in incognito.
@property(nonatomic, assign) BOOL incognito;

@end

@implementation BackgroundTabAnimationView

- (instancetype)initWithFrame:(CGRect)frame incognito:(BOOL)incognito {
  self = [super initWithFrame:frame];
  if (self) {
    _incognito = incognito;

    self.overrideUserInterfaceStyle =
        incognito ? UIUserInterfaceStyleDark : UIUserInterfaceStyleUnspecified;
  }
  return self;
}

#pragma mark - Public

- (void)animateFrom:(CGPoint)originPoint
    toTabGridButtonWithCompletion:(void (^)())completion {
  DCHECK(self.superview);
  CGPoint origin = [self.superview convertPoint:originPoint fromView:nil];
  CGPoint destination = [self destinationPoint];

  // It can be negative.
  CGFloat xDiff = destination.x - origin.x;
  CGFloat yDiff = origin.y - destination.y;

  UIBezierPath* positionPath =
      [self positionPathWithParentHeight:self.superview.frame.size.height
                                   xDiff:xDiff
                                   yDiff:yDiff
                                  origin:origin
                             destination:destination];

  [CATransaction begin];

  [CATransaction setCompletionBlock:^{
    completion();
  }];
  CAMediaTimingFunction* easeIn = MaterialTimingFunction(MaterialCurveEaseIn);
  CGFloat timing =
      [self animationDurationWithParentSize:self.superview.frame.size
                                      xDiff:xDiff
                                      yDiff:yDiff];

  CAKeyframeAnimation* scaleAnimation =
      [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
  scaleAnimation.values = @[ @(1), @(kMaxScale), @(kMinScale) ];
  scaleAnimation.keyTimes = @[ @0, @0.25, @1 ];
  scaleAnimation.timingFunction = easeIn;
  scaleAnimation.duration = timing;

  CAKeyframeAnimation* rotateAnimation =
      [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
  rotateAnimation.values = @[ @(0), @(0), @(kRotationAngleInRadians) ];
  rotateAnimation.keyTimes = @[ @0, @0.5, @1 ];
  rotateAnimation.timingFunction = easeIn;
  rotateAnimation.duration = timing;

  CAKeyframeAnimation* fadeAnimation =
      [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
  fadeAnimation.values = @[ @(1), @(1), @(0) ];
  fadeAnimation.keyTimes = @[ @0, @0.9, @1 ];
  fadeAnimation.timingFunction = easeIn;
  fadeAnimation.duration = timing;

  CAKeyframeAnimation* positionAnimation =
      [CAKeyframeAnimation animationWithKeyPath:@"position"];
  positionAnimation.path = positionPath.CGPath;
  positionAnimation.duration = timing;
  positionAnimation.timingFunction = easeIn;

  [self.layer
      addAnimation:AnimationGroupMake(@[
        positionAnimation, rotateAnimation, fadeAnimation, scaleAnimation
      ])
            forKey:@"OpenInNewTabAnimation"];
  [CATransaction commit];
}

#pragma mark - UIView

- (void)didMoveToSuperview {
  [super didMoveToSuperview];

  if (self.subviews.count == 0) {
    self.backgroundColor = [UIColor colorNamed:kBackgroundColor];
    self.layer.shadowRadius = 20;
    self.layer.shadowOpacity = 0.4;
    self.layer.shadowOffset = CGSizeMake(0, 3);
    self.layer.cornerRadius = 13;
    UIImageView* linkImage = [[UIImageView alloc]
        initWithImage:
            [[UIImage imageNamed:@"open_new_tab_background"]
                imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
    linkImage.translatesAutoresizingMaskIntoConstraints = NO;
    linkImage.tintColor = [UIColor colorNamed:kToolbarButtonColor];

    [self addSubview:linkImage];

    [linkImage.widthAnchor constraintEqualToConstant:kImageSize].active = YES;
    [linkImage.heightAnchor constraintEqualToConstant:kImageSize].active = YES;
    AddSameCenterConstraints(self, linkImage);
  }
}

#pragma mark - Private

// Returns the destination point for the animation, in the superview
// coordinates.
- (CGPoint)destinationPoint {
  DCHECK(self.layoutGuideCenter);
  UILayoutGuide* tabGridButtonLayoutGuide =
      [self.layoutGuideCenter makeLayoutGuideNamed:kTabSwitcherGuide];
  // Note: adding to the superview, as adding to `self` breaks its layout.
  [self.superview addLayoutGuide:tabGridButtonLayoutGuide];
  CGRect frame = [tabGridButtonLayoutGuide layoutFrame];
  return CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
}

// Returns the animation duration, based on the `parentSize` and the `yDiff` and
// `xDiff` between the origin and destination point. The animation is faster the
// closer the origin and destination are.
- (CGFloat)animationDurationWithParentSize:(CGSize)parentSize
                                     xDiff:(CGFloat)xDiff
                                     yDiff:(CGFloat)yDiff {
  CGFloat parentWidth = parentSize.width;
  CGFloat parentHeight = parentSize.height;

  CGFloat parentViewDiagonal =
      parentWidth * parentWidth + parentHeight * parentHeight;
  CGFloat distance = xDiff * xDiff + yDiff * yDiff;

  return 0.8 * sqrt(distance / parentViewDiagonal) + 0.2;
}

// Returns the BezierPath that should be followed by the animated view, based on
// the `parentSize` and the `yDiff` and `xDiff` between the `origin` and
// `destination` point.
- (UIBezierPath*)positionPathWithParentHeight:(CGFloat)parentHeight
                                        xDiff:(CGFloat)xDiff
                                        yDiff:(CGFloat)yDiff
                                       origin:(CGPoint)origin
                                  destination:(CGPoint)destination {
  CGFloat absYDiff = fabs(yDiff);
  CGFloat firstControlPointYDifference =
      absYDiff > parentHeight / 2 ? parentHeight / 2 * (yDiff / absYDiff)
                                  : yDiff;
  CGPoint firstControlPoint = CGPointMake(
      origin.x + xDiff * 0.5, origin.y + firstControlPointYDifference);
  CGPoint secondControlPoint =
      CGPointMake(destination.x, destination.y + yDiff / 2);

  UIBezierPath* path = UIBezierPath.bezierPath;
  [path moveToPoint:origin];
  [path addCurveToPoint:destination
          controlPoint1:firstControlPoint
          controlPoint2:secondControlPoint];
  return path;
}

@end