blob: ceb48ee2bffbdebf2feb457fb88804e5eca8e280 [file] [log] [blame]
// Copyright 2018 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/tab_grid/transitions/grid_transition_animation.h"
#import "base/logging.h"
#import "ios/chrome/browser/ui/tab_grid/transitions/grid_to_tab_transition_view.h"
#import "ios/chrome/browser/ui/tab_grid/transitions/grid_transition_layout.h"
#import "ios/chrome/browser/ui/util/property_animator_group.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Scale factor for inactive items when a tab is expanded.
const CGFloat kInactiveItemScale = 0.95;
}
@interface GridTransitionAnimation ()
// The property animator group backing the public |animator| property.
@property(nonatomic, readonly) PropertyAnimatorGroup* animations;
// The layout of the grid for this animation.
@property(nonatomic, strong) GridTransitionLayout* layout;
// The duration of the animation.
@property(nonatomic, readonly, assign) NSTimeInterval duration;
// The direction this animation is in.
@property(nonatomic, readonly, assign) GridAnimationDirection direction;
// Corner radius that the active cell will have when it is animated into the
// regulat grid.
@property(nonatomic, assign) CGFloat finalActiveCellCornerRadius;
@end
@implementation GridTransitionAnimation
@synthesize activeCell = _activeCell;
@synthesize animations = _animations;
@synthesize layout = _layout;
@synthesize duration = _duration;
@synthesize direction = _direction;
@synthesize finalActiveCellCornerRadius = _finalActiveCellCornerRadius;
- (instancetype)initWithLayout:(GridTransitionLayout*)layout
duration:(NSTimeInterval)duration
direction:(GridAnimationDirection)direction {
if (self = [super initWithFrame:CGRectZero]) {
_animations = [[PropertyAnimatorGroup alloc] init];
_layout = layout;
_duration = duration;
_direction = direction;
_finalActiveCellCornerRadius = _layout.activeItem.cell.cornerRadius;
}
return self;
}
- (id<UIViewImplicitlyAnimating>)animator {
return self.animations;
}
#pragma mark - UIView
- (void)willMoveToSuperview:(UIView*)newSuperview {
self.frame = newSuperview.bounds;
if (newSuperview && self.subviews.count == 0) {
[self prepareForAnimationInSuperview:newSuperview];
}
}
- (void)didMoveToSuperview {
if (!self.superview)
return;
// Positioning the animating items depends on converting points to this
// view's coordinate system, so wait until it's in a view hierarchy.
switch (self.direction) {
case GridAnimationDirectionContracting:
[self positionExpandedActiveItem];
[self prepareInactiveItemsForAppearance];
[self buildContractingAnimations];
break;
case GridAnimationDirectionExpanding:
[self prepareAllItemsForExpansion];
[self buildExpandingAnimations];
break;
}
// Make sure all of the layout after the view setup is complete before any
// animations are run.
[self layoutIfNeeded];
}
#pragma mark - Private methods
- (void)buildContractingAnimations {
// The transition is structured as three or five separate animations. They are
// timed based on various sub-durations and delays which are expressed as
// fractions of the overall animation duration.
CGFloat partialDuration = 0.6;
CGFloat briefDuration = partialDuration * 0.5;
CGFloat shortDelay = 0.2;
// Damping ratio for the resize animation.
CGFloat resizeDamping = 0.8;
// If there's only one cell, the animation has two parts.
// (A) Zooming the active cell into position.
// (B) Crossfading from the tab to cell top view.
// (C) Rounding the corners of the active cell.
//
// {0%}----------------------[A]-------------------{100%}
// {50%}----[B]----{80%}
// {0%}---[C]---{30%}
// If there's more than once cell, the animation adds two more parts:
// (D) Scaling up the inactive cells.
// (E) Fading the inactive cells to 100% opacity.
// The overall timing is as follows:
//
// {0%}----------------------[A]-------------------{100%}
// {50%}----[B]----{80%}
// {0%}---[C]---{30%}
// {20%}--[D]-----------------------------{100%}
// {20%}--[E]-----------------------{80%}
//
// (Changing the timing constants above will change the timing % values)
UIView<GridToTabTransitionView>* activeCell = self.layout.activeItem.cell;
// The final cell snapshot exactly matches the main tab view of the cell, so
// it can have an alpha of 0 for the whole animation.
activeCell.mainTabView.alpha = 0.0;
// The final cell header starts at 0 alpha and is cross-faded in.
activeCell.topCellView.alpha = 0.0;
// A: Zoom the active cell into position.
auto zoomActiveCellAnimation = ^{
[self positionAndScaleActiveItemInGrid];
};
UIViewPropertyAnimator* zoomActiveCell =
[[UIViewPropertyAnimator alloc] initWithDuration:self.duration
dampingRatio:resizeDamping
animations:zoomActiveCellAnimation];
[self.animations addAnimator:zoomActiveCell];
// B: Fade in the active cell top cell view, fade out the active cell's
// top tab view.
auto fadeInAuxillaryKeyframeAnimation =
[self keyframeAnimationFadingView:activeCell.topTabView
throughToView:activeCell.topCellView
relativeStart:0.5
relativeDuration:briefDuration];
UIViewPropertyAnimator* fadeInAuxillary = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveEaseInOut
animations:fadeInAuxillaryKeyframeAnimation];
[self.animations addAnimator:fadeInAuxillary];
// C: Round the corners of the active cell.
UIView<GridToTabTransitionView>* cell = self.layout.activeItem.cell;
cell.cornerRadius = DeviceCornerRadius();
auto roundCornersAnimation = ^{
cell.cornerRadius = self.finalActiveCellCornerRadius;
};
auto roundCornersKeyframeAnimation =
[self keyframeAnimationWithRelativeStart:0
relativeDuration:briefDuration
animations:roundCornersAnimation];
UIViewPropertyAnimator* roundCorners = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveLinear
animations:roundCornersKeyframeAnimation];
[self.animations addAnimator:roundCorners];
// Single cell case.
if (self.layout.inactiveItems.count == 0)
return;
// Additional animations for multiple cells.
// D: Scale up inactive cells.
auto scaleUpCellsAnimation = ^{
for (GridTransitionItem* item in self.layout.inactiveItems) {
item.cell.transform = CGAffineTransformIdentity;
}
};
auto scaleUpCellsKeyframeAnimation =
[self keyframeAnimationWithRelativeStart:shortDelay
relativeDuration:1 - shortDelay
animations:scaleUpCellsAnimation];
UIViewPropertyAnimator* scaleUpCells = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveEaseOut
animations:scaleUpCellsKeyframeAnimation];
[self.animations addAnimator:scaleUpCells];
// E: Fade in inactive cells.
auto fadeInCellsAnimation = ^{
for (GridTransitionItem* item in self.layout.inactiveItems) {
item.cell.alpha = 1.0;
}
};
auto fadeInCellsKeyframeAnimation =
[self keyframeAnimationWithRelativeStart:shortDelay
relativeDuration:partialDuration
animations:fadeInCellsAnimation];
UIViewPropertyAnimator* fadeInCells = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveEaseOut
animations:fadeInCellsKeyframeAnimation];
[self.animations addAnimator:fadeInCells];
}
- (void)buildExpandingAnimations {
// The transition is structured as four to six separate animations. They are
// timed based on two sub-durations which are expressed as fractions of the
// overall animation duration.
CGFloat partialDuration = 0.66;
CGFloat briefDuration = 0.3;
CGFloat delay = 0.1;
// Damping ratio for the resize animation.
CGFloat resizeDamping = 0.7;
// If there's only one cell, the animation has three parts:
// (A) Zooming the active cell out into the expanded position.
// (B) Crossfading the active cell's top views.
// (C) Squaring the corners of the active cell.
// (D) Fading out the main cell view and fading in the main tab view, if
// necessary.
// These parts are timed over |duration| like this:
//
// {0%}--[A]-----------------------------------{100%}
// {0%}--[B]---{30%}
// {0%}--[C]---{30%}
// {10%}--[D]---{40%}
// If there's more than once cell, the animation adds:
// (E) Scaling the inactive cells to 95%
// (F) Fading out the inactive cells.
// The overall timing is as follows:
//
// {0%}--[A]-----------------------------------{100%}
// {0%}--[B]---{30%}
// {0%}--[C]---{30%}
// {10%}--[D]---{40%}
// {0%}--[E]-----------------------------------{100%}
// {0%}--[F]-------------------{66%}
//
// All animations are timed ease-out (so more motion happens sooner), except
// for B, C and D. B is a crossfade and eases in/out. C and D are relatively
// short in duration; they have linear timing so they doesn't seem
// instantaneous, and D is also linear so that identical views animate
// smoothly.
//
// Animation D is necessary because the cell content and the tab content may
// no longer match in aspect ratio; a quick cross-fade in mid-transition
// prevents an abrupt jump when the transition ends and the "real" tab content
// is shown.
UIView<GridToTabTransitionView>* activeCell = self.layout.activeItem.cell;
// The top tab view starts at zero alpha but is crossfaded in.
activeCell.topTabView.alpha = 0.0;
// If the active item is appearing, the main tab view is shown. If not, it's
// hidden, and may be faded in if it's expected to be different in content
// from the existing cell snapshot.
if (!self.layout.activeItem.isAppearing)
activeCell.mainTabView.alpha = 0.0;
// A: Zoom the active cell into position.
UIViewPropertyAnimator* zoomActiveCell =
[[UIViewPropertyAnimator alloc] initWithDuration:self.duration
dampingRatio:resizeDamping
animations:^{
[self positionExpandedActiveItem];
}];
[self.animations addAnimator:zoomActiveCell];
// B: Crossfade the top views.
auto fadeOutAuxilliaryAnimation =
[self keyframeAnimationWithRelativeStart:0
relativeDuration:briefDuration
animations:^{
activeCell.topCellView.alpha = 0;
activeCell.topTabView.alpha = 1.0;
}];
UIViewPropertyAnimator* fadeOutAuxilliary = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveEaseInOut
animations:fadeOutAuxilliaryAnimation];
[self.animations addAnimator:fadeOutAuxilliary];
// C: Square the active cell's corners.
UIView<GridToTabTransitionView>* cell = self.layout.activeItem.cell;
auto squareCornersAnimation = ^{
cell.cornerRadius = DeviceCornerRadius();
};
auto squareCornersKeyframeAnimation =
[self keyframeAnimationWithRelativeStart:0.0
relativeDuration:briefDuration
animations:squareCornersAnimation];
UIViewPropertyAnimator* squareCorners = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveLinear
animations:squareCornersKeyframeAnimation];
[self.animations addAnimator:squareCorners];
// D: crossfade the main cell content, if necessary.
// This crossfade is needed if the aspect ratio of the tab being animated
// to doesn't match the aspect ratio of the tab that originally generated the
// cell content being animated; this happens when the tab grid is exited in a
// diffferent orientation than it was entered.
// Using a linear animation curve means that the sum of the opacities is
// contstant though the animation, which will help it seem less abrupt by
// keeping a relatively constant brightness.
if (self.layout.frameChanged) {
auto crossfadeContentAnimation =
[self keyframeAnimationWithRelativeStart:delay
relativeDuration:briefDuration
animations:^{
activeCell.mainCellView.alpha = 0;
activeCell.mainTabView.alpha = 1.0;
}];
UIViewPropertyAnimator* crossfadeContent = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveLinear
animations:crossfadeContentAnimation];
[self.animations addAnimator:crossfadeContent];
}
// If there's only a single cell, that's all.
if (self.layout.inactiveItems.count == 0)
return;
// Additional animations for multiple cells.
// E: Scale down inactive cells.
auto scaleDownCellsAnimation = ^{
for (GridTransitionItem* item in self.layout.inactiveItems) {
item.cell.transform = CGAffineTransformScale(
item.cell.transform, kInactiveItemScale, kInactiveItemScale);
}
};
UIViewPropertyAnimator* scaleDownCells = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveEaseOut
animations:scaleDownCellsAnimation];
[self.animations addAnimator:scaleDownCells];
// F: Fade out inactive cells.
auto fadeOutCellsAnimation = ^{
for (GridTransitionItem* item in self.layout.inactiveItems) {
item.cell.alpha = 0.0;
}
};
auto fadeOutCellsKeyframeAnimation =
[self keyframeAnimationWithRelativeStart:0
relativeDuration:partialDuration
animations:fadeOutCellsAnimation];
UIViewPropertyAnimator* fadeOutCells = [[UIViewPropertyAnimator alloc]
initWithDuration:self.duration
curve:UIViewAnimationCurveEaseOut
animations:fadeOutCellsKeyframeAnimation];
[self.animations addAnimator:fadeOutCells];
}
// Performs the initial setup for the animation, computing scale based on the
// superview size and adding the transition cells to the view hierarchy.
- (void)prepareForAnimationInSuperview:(UIView*)newSuperview {
// Add the selection item first, so it's under ther other views.
[self addSubview:self.layout.selectionItem.cell];
for (GridTransitionItem* item in self.layout.inactiveItems) {
[self addSubview:item.cell];
}
// Add the active item last so it's always the top subview.
[self addSubview:self.layout.activeItem.cell];
}
// Positions the active item in the expanded grid position with a zero corner
// radius and a 0% opacity auxilliary view.
- (void)positionExpandedActiveItem {
UIView<GridToTabTransitionView>* cell = self.layout.activeItem.cell;
cell.frame = self.layout.expandedRect;
[cell positionTabViews];
}
// Positions all of the inactive items in their grid positions.
// Fades and scales each of those items.
- (void)prepareInactiveItemsForAppearance {
for (GridTransitionItem* item in self.layout.inactiveItems) {
[self positionItemInGrid:item];
item.cell.alpha = 0.2;
item.cell.transform = CGAffineTransformScale(
item.cell.transform, kInactiveItemScale, kInactiveItemScale);
}
[self positionItemInGrid:self.layout.selectionItem];
}
// Positions the active item in the regular grid position with its final
// corner radius.
- (void)positionAndScaleActiveItemInGrid {
UIView<GridToTabTransitionView>* cell = self.layout.activeItem.cell;
cell.transform = CGAffineTransformIdentity;
CGRect frame = cell.frame;
frame.size = self.layout.activeItem.size;
cell.frame = frame;
[self positionItemInGrid:self.layout.activeItem];
[cell positionCellViews];
}
// Prepares all of the items for an expansion anumation.
- (void)prepareAllItemsForExpansion {
for (GridTransitionItem* item in self.layout.inactiveItems) {
[self positionItemInGrid:item];
}
[self positionItemInGrid:self.layout.activeItem];
[self.layout.activeItem.cell positionCellViews];
self.activeCell = self.layout.activeItem.cell;
[self positionItemInGrid:self.layout.selectionItem];
}
// Positions |item| in it grid position.
- (void)positionItemInGrid:(GridTransitionItem*)item {
UIView* cell = item.cell;
CGPoint newCenter = [self.superview convertPoint:item.center fromView:nil];
cell.center = newCenter;
}
// Helper function to construct keyframe animation blocks.
// Given |start| and |duration| (in the [0.0-1.0] interval), returns an
// animation block which runs |animations| starting at |start| (relative to
// |self.duration|) and running for |duration| (likewise).
- (void (^)(void))keyframeAnimationWithRelativeStart:(double)start
relativeDuration:(double)duration
animations:
(void (^)(void))animations {
auto keyframe = ^{
[UIView addKeyframeWithRelativeStartTime:start
relativeDuration:duration
animations:animations];
};
return ^{
[UIView animateKeyframesWithDuration:self.duration
delay:0
options:UIViewAnimationOptionLayoutSubviews
animations:keyframe
completion:nil];
};
}
// Returns a cross-fade keyframe animation between two views.
// |startView| should have an alpha of 1; |endView| should have an alpha of 0.
// |start| and |duration| are in the [0.0]-[1.0] interval and represent timing
// relative to |self.duration|.
// The animation returned by this method will fade |startView| to 0 over the
// first half of |duration|, and then fade |endView| to 1.0 over the second
// half, preventing any blurred frames showing both views. For best results, the
// animation curev should be EaseInEaseOut.
- (void (^)(void))keyframeAnimationFadingView:(UIView*)startView
throughToView:(UIView*)endView
relativeStart:(double)start
relativeDuration:(double)duration {
CGFloat halfDuration = duration / 2;
auto keyframes = ^{
[UIView addKeyframeWithRelativeStartTime:start
relativeDuration:halfDuration
animations:^{
startView.alpha = 0.0;
}];
[UIView addKeyframeWithRelativeStartTime:start + halfDuration
relativeDuration:halfDuration
animations:^{
endView.alpha = 1.0;
}];
};
return ^{
[UIView animateKeyframesWithDuration:self.duration
delay:0
options:UIViewAnimationOptionLayoutSubviews
animations:keyframes
completion:nil];
};
}
@end