chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_tab_grid_transition_handler.mm

// Copyright 2020 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/ui/tab_switcher/tab_grid/transitions/legacy_tab_grid_transition_handler.h"

#import "ios/chrome/browser/shared/ui/util/named_guide.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_grid_transition_animation.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_grid_transition_animation_layout_providing.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_grid_transition_layout.h"

namespace {
const CGFloat kBrowserToGridDuration = 0.3;
const CGFloat kGridToBrowserDuration = 0.5;
const CGFloat kReducedMotionDuration = 0.25;
const CGFloat kToTabGroupAnimationDuration = 0.25;
}  // namespace

@interface LegacyTabGridTransitionHandler ()
@property(nonatomic, weak) id<LegacyGridTransitionAnimationLayoutProviding>
    layoutProvider;
// Animation object for the transition.
@property(nonatomic, strong) LegacyGridTransitionAnimation* animation;
@end

@implementation LegacyTabGridTransitionHandler

#pragma mark - Public

- (instancetype)initWithLayoutProvider:
    (id<LegacyGridTransitionAnimationLayoutProviding>)layoutProvider {
  self = [super init];
  if (self) {
    _layoutProvider = layoutProvider;
  }
  return self;
}

- (void)transitionFromBrowser:(UIViewController*)browser
                    toTabGrid:(UIViewController*)tabGrid
                   toTabGroup:(BOOL)toTabGroup
                   activePage:(TabGridPage)activePage
               withCompletion:(void (^)(void))completion {
  [browser willMoveToParentViewController:nil];

  if (UIAccessibilityIsReduceMotionEnabled()) {
    __weak __typeof(self) weakSelf = self;
    [self transitionWithFadeForTab:browser.view
                        toTabGroup:toTabGroup
                    beingPresented:NO
                    withCompletion:^{
                      [weakSelf
                          reducedAnimationTransitionCompleteFromBrowser:browser
                                                              toTabGrid:tabGrid
                                                         withCompletion:
                                                             completion];
                    }];
    return;
  }

  GridAnimationDirection direction = GridAnimationDirectionContracting;
  CGFloat duration = self.animationDisabled ? 0 : kBrowserToGridDuration;

  self.animation = [[LegacyGridTransitionAnimation alloc]
          initWithLayout:[self
                             transitionLayoutForTabInViewController:browser
                                                         activePage:activePage]
      gridContainerFrame:[self.layoutProvider gridContainerFrame]
                duration:duration
               direction:direction];

  UIView* animationContainer = [self.layoutProvider animationViewsContainer];
  UIView* bottomViewForAnimations =
      [self.layoutProvider animationViewsContainerBottomView];
  [animationContainer insertSubview:self.animation
                       aboveSubview:bottomViewForAnimations];

  UIView* activeItem = self.animation.activeItem;
  UIView* selectedItem = self.animation.selectionItem;
  BOOL shouldReparentSelectedCell =
      [self.layoutProvider shouldReparentSelectedCell:direction];

  if (shouldReparentSelectedCell) {
    [tabGrid.view addSubview:selectedItem];
    [tabGrid.view addSubview:activeItem];
  }

  [self.animation.animator addAnimations:^{
    [tabGrid setNeedsStatusBarAppearanceUpdate];
  }];

  [self.animation.animator addCompletion:^(UIViewAnimatingPosition position) {
    if (shouldReparentSelectedCell) {
      [activeItem removeFromSuperview];
      [selectedItem removeFromSuperview];
    }
    [self.animation removeFromSuperview];
    if (position == UIViewAnimatingPositionEnd) {
      [browser.view removeFromSuperview];
      [browser removeFromParentViewController];
    }
    if (completion) {
      completion();
    }
  }];

  // TODO(crbug.com/41393272): Have the tab view animate itself out alongside
  // this transition instead of just zeroing the alpha here.
  browser.view.alpha = 0;

  // Run the main animation.
  if (@available(iOS 17, *)) {
    // On iOS 17, there is an issue if the animation is run directly.
    // See crbug.com/1458980.
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                 static_cast<int64_t>(0.01 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
                     [self.animation.animator startAnimation];
                   });
  } else {
    [self.animation.animator startAnimation];
  }
}

- (void)transitionFromTabGrid:(UIViewController*)tabGrid
                    toBrowser:(UIViewController*)browser
                   activePage:(TabGridPage)activePage
               withCompletion:(void (^)(void))completion {
  [tabGrid addChildViewController:browser];

  browser.view.frame = tabGrid.view.bounds;
  [tabGrid.view addSubview:browser.view];

  browser.view.accessibilityViewIsModal = YES;

  if (self.animationDisabled) {
    browser.view.alpha = 1;
    [tabGrid setNeedsStatusBarAppearanceUpdate];
    if (completion) {
      completion();
    }
    return;
  }

  browser.view.alpha = 0;

  __weak __typeof(self) weakSelf = self;
  if (UIAccessibilityIsReduceMotionEnabled() ||
      !self.layoutProvider.selectedCellVisible) {
    [self transitionWithFadeForTab:browser.view
                        toTabGroup:NO
                    beingPresented:YES
                    withCompletion:^{
                      [weakSelf
                          reducedAnimationTransitionCompleteFromTabGrid:tabGrid
                                                              toBrowser:browser
                                                         withCompletion:
                                                             completion];
                    }];
    return;
  }

  GridAnimationDirection direction = GridAnimationDirectionExpanding;
  CGFloat duration = self.animationDisabled ? 0 : kGridToBrowserDuration;

  self.animation = [[LegacyGridTransitionAnimation alloc]
          initWithLayout:[self
                             transitionLayoutForTabInViewController:browser
                                                         activePage:activePage]
      gridContainerFrame:[self.layoutProvider gridContainerFrame]
                duration:duration
               direction:direction];

  UIView* animationContainer = [self.layoutProvider animationViewsContainer];
  UIView* bottomViewForAnimations =
      [self.layoutProvider animationViewsContainerBottomView];
  [animationContainer insertSubview:self.animation
                       aboveSubview:bottomViewForAnimations];

  UIView* activeItem = self.animation.activeItem;
  UIView* selectedItem = self.animation.selectionItem;
  BOOL shouldReparentSelectedCell =
      [self.layoutProvider shouldReparentSelectedCell:direction];

  if (shouldReparentSelectedCell) {
    [tabGrid.view addSubview:selectedItem];
    [tabGrid.view addSubview:activeItem];
  }

  [self.animation.animator addAnimations:^{
    [tabGrid setNeedsStatusBarAppearanceUpdate];
  }];

  [self.animation.animator addCompletion:^(UIViewAnimatingPosition position) {
    if (shouldReparentSelectedCell) {
      [activeItem removeFromSuperview];
      [selectedItem removeFromSuperview];
    }
    [self.animation removeFromSuperview];
    if (position == UIViewAnimatingPositionEnd) {
      browser.view.alpha = 1;
      [browser didMoveToParentViewController:tabGrid];
    }
    if (completion) {
      completion();
    }
  }];

  // Run the main animation.
  [self.animation.animator startAnimation];
}

#pragma mark - Private

// Returns the transition layout for the `activePage`, based on the `browser`.
- (LegacyGridTransitionLayout*)
    transitionLayoutForTabInViewController:
        (UIViewController*)viewControllerForTab
                                activePage:(TabGridPage)activePage {
  LegacyGridTransitionLayout* layout =
      [self.layoutProvider transitionLayout:activePage];

  // Get the fram for the snapshotted content of the active tab.
  // Conceptually the transition is dismissing/presenting a tab (a BVC).
  // However, currently the BVC instances are themselves contanted within a
  // BVCContainer view controller. This means that the
  // `viewControllerForTab.view` is not the BVC's view; rather it's the view of
  // the view controller that contains the BVC. Unfortunatley, the layout guide
  // needed here is attached to the BVC's view, which is the first (and only)
  // subview of the BVCContainerViewController's view.
  // TODO(crbug.com/40583629) Clean up this arrangement.
  UIView* tabContentView = viewControllerForTab.view.subviews[0];

  CGRect contentArea = [NamedGuide guideWithName:kContentAreaGuide
                                            view:tabContentView]
                           .layoutFrame;

  [layout.activeItem populateWithSnapshotsFromView:tabContentView
                                        middleRect:contentArea];
  layout.expandedRect = [[self.layoutProvider animationViewsContainer]
      convertRect:tabContentView.frame
         fromView:viewControllerForTab.view];

  return layout;
}

// Animates the transition for the `tab`, whether it is `beingPresented` or not,
// with a fade in/out.
- (void)transitionWithFadeForTab:(UIView*)tab
                      toTabGroup:(BOOL)toTabGroup
                  beingPresented:(BOOL)beingPresented
                  withCompletion:(void (^)(void))completion {
  // The animation here creates a simple quick zoom effect -- the tab view
  // fades in/out as it expands/contracts. The zoom is not large (75% to 100%)
  // and is centered on the view's final center position, so it's not directly
  // connected to any tab grid positions.
  CGFloat tabFinalAlpha;
  CGAffineTransform tabFinalTransform;
  CGFloat tabFinalCornerRadius;

  if (beingPresented) {
    // If presenting, the tab view animates in from 0% opacity, 75% scale
    // transform, and a 26pt corner radius
    tabFinalAlpha = 1;
    tabFinalTransform = tab.transform;
    tab.transform = CGAffineTransformScale(tabFinalTransform, 0.75, 0.75);
    tabFinalCornerRadius = DeviceCornerRadius();
    tab.layer.cornerRadius = 26.0;
  } else {
    // If dismissing, the the tab view animates out to 0% opacity, 75% scale,
    // and 26px corner radius.
    tabFinalAlpha = 0;
    tabFinalTransform = CGAffineTransformScale(tab.transform, 0.75, 0.75);
    tab.layer.cornerRadius = DeviceCornerRadius();
    tabFinalCornerRadius = 26.0;
  }

  // Set clipsToBounds on the animating view so its corner radius will look
  // right.
  BOOL oldClipsToBounds = tab.clipsToBounds;
  tab.clipsToBounds = YES;

  CGFloat duration =
      toTabGroup ? kToTabGroupAnimationDuration : kReducedMotionDuration;
  duration = self.animationDisabled ? 0 : duration;
  [UIView animateWithDuration:duration
      delay:0.0
      options:UIViewAnimationOptionCurveEaseOut
      animations:^{
        tab.alpha = tabFinalAlpha;
        tab.transform = tabFinalTransform;
        tab.layer.cornerRadius = tabFinalCornerRadius;
      }
      completion:^(BOOL finished) {
        // When presenting the FirstRun ViewController, this can be called with
        // `finished` to NO on official builds. For now, the animation not
        // finishing isn't handled anywhere.
        tab.clipsToBounds = oldClipsToBounds;
        if (completion) {
          completion();
        }
      }];
}

// Called when the transition with reduced animations from the `tabGrid` to the
// `browser` is complete.
- (void)reducedAnimationTransitionCompleteFromTabGrid:(UIViewController*)tabGrid
                                            toBrowser:(UIViewController*)browser
                                       withCompletion:
                                           (void (^)(void))completion {
  [browser didMoveToParentViewController:tabGrid];
  [tabGrid setNeedsStatusBarAppearanceUpdate];
  if (completion) {
    completion();
  }
}

// Called when the transition with reduced animations from the `browser` to the
// `tabGrid` is complete.
- (void)reducedAnimationTransitionCompleteFromBrowser:(UIViewController*)browser
                                            toTabGrid:(UIViewController*)tabGrid
                                       withCompletion:
                                           (void (^)(void))completion {
  [browser.view removeFromSuperview];
  [browser removeFromParentViewController];
  [tabGrid setNeedsStatusBarAppearanceUpdate];
  if (completion) {
    completion();
  }
}

@end