chromium/ios/chrome/browser/ui/toolbar/accessory/toolbar_accessory_presenter.mm

// Copyright 2019 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/toolbar/accessory/toolbar_accessory_presenter.h"

#import "base/i18n/rtl.h"
#import "base/logging.h"
#import "ios/chrome/browser/shared/ui/util/image/image_util.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/presenters/contained_presenter_delegate.h"
#import "ios/chrome/browser/ui/toolbar/accessory/toolbar_accessory_constants.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {
const CGFloat kToolbarAccessoryWidthRegularRegular = 375;
const CGFloat kToolbarAccessoryCornerRadiusRegularRegular = 13;
const CGFloat kRegularRegularHorizontalMargin = 5;

// Margin between the beginning of the shadow image and the content being
// shadowed.
const CGFloat kShadowMargin = 196;

// Toolbar Accessory animation drop down duration.
const CGFloat kAnimationDuration = 0.15;
}  // namespace

@interface ToolbarAccessoryPresenter ()

// The `presenting` public property redefined as readwrite.
@property(nonatomic, assign, readwrite, getter=isPresenting) BOOL presenting;

// The view that acts as the background for `presentedView`, redefined as
// readwrite. This is especially important on iPhone, as this view that holds
// everything around the safe area.
@property(nonatomic, strong, readwrite) UIView* backgroundView;

// Layout guide to center the presented view below the safe area layout guide on
// iPhone.
@property(nonatomic, strong) UILayoutGuide* centeringGuide;

// A constraint that constrains any views to their pre-animation positions.
// It should be deactiviated during the presentation animation and replaced with
// a constraint that sets the views to their final position.
@property(nonatomic, strong) NSLayoutConstraint* animationConstraint;

@property(nonatomic, assign) BOOL isIncognito;

@end

@implementation ToolbarAccessoryPresenter

@synthesize baseViewController = _baseViewController;
@synthesize presentedViewController = _presentedViewController;
@synthesize delegate = _delegate;

#pragma mark - Public

- (instancetype)initWithIsIncognito:(BOOL)isIncognito {
  if ((self = [super init])) {
    _isIncognito = isIncognito;
  }
  return self;
}

- (BOOL)isPresentingViewController:(UIViewController*)viewController {
  return self.presenting && self.presentedViewController &&
         self.presentedViewController == viewController;
}

- (void)prepareForPresentation {
  self.presenting = YES;
  self.backgroundView = [self createBackgroundView];
  [self.baseViewController addChildViewController:self.presentedViewController];
  [self.baseViewController.view addSubview:self.backgroundView];

  if (ShouldShowCompactToolbar(self.baseViewController)) {
    [self prepareForPresentationOnIPhone];
  } else {
    [self prepareForPresentationOnIPad];
  }
}

- (void)presentAnimated:(BOOL)animated {
  if (animated) {
    // Force initial layout before the animation.
    [self.baseViewController.view layoutIfNeeded];
  }

  if (ShouldShowCompactToolbar(self.baseViewController)) {
    [self setupFinalConstraintsOnIPhone];
  } else {
    [self setupFinalConstraintsOnIPad];
  }

  __weak __typeof(self) weakSelf = self;
  auto completion = ^void(BOOL) {
    [weakSelf.presentedViewController
        didMoveToParentViewController:weakSelf.baseViewController];
    if ([weakSelf.delegate
            respondsToSelector:@selector(containedPresenterDidPresent:)]) {
      [weakSelf.delegate containedPresenterDidPresent:weakSelf];
    }
  };

  if (animated) {
    [UIView animateWithDuration:kAnimationDuration
                     animations:^() {
                       [self.baseViewController.view layoutIfNeeded];
                     }
                     completion:completion];
  } else {
    completion(YES);
  }
}

- (void)dismissAnimated:(BOOL)animated {
  [self.presentedViewController willMoveToParentViewController:nil];

  __weak __typeof(self) weakSelf = self;
  auto completion = ^void(BOOL) {
    [weakSelf.presentedViewController.view removeFromSuperview];
    [weakSelf.presentedViewController removeFromParentViewController];
    [weakSelf.backgroundView removeFromSuperview];
    weakSelf.backgroundView = nil;
    weakSelf.presenting = NO;
    if ([weakSelf.delegate
            respondsToSelector:@selector(containedPresenterDidDismiss:)]) {
      [weakSelf.delegate containedPresenterDidDismiss:weakSelf];
    }
  };
  if (animated) {
    void (^animation)();
    if (ShouldShowCompactToolbar(self.baseViewController)) {
      CGRect oldFrame = self.backgroundView.frame;
      self.backgroundView.layer.anchorPoint = CGPointMake(0.5, 0);
      self.backgroundView.frame = oldFrame;
      animation = ^{
        self.backgroundView.transform = CGAffineTransformMakeScale(1, 0.05);
        self.backgroundView.alpha = 0;
      };
    } else {
      CGFloat rtlModifier = base::i18n::IsRTL() ? -1 : 1;
      animation = ^{
        self.backgroundView.transform = CGAffineTransformMakeTranslation(
            rtlModifier * self.backgroundView.bounds.size.width, 0);
      };
    }
    [UIView animateWithDuration:kAnimationDuration
                     animations:animation
                     completion:completion];
  } else {
    completion(YES);
  }
}

#pragma mark - Private Helpers

// Positions the view into its initial, pre-animation position on iPhone.
// Specifically, the final position will be on top of the toolbar. The
// pre-animation position is that, but slid upwards so the view is offscreen.
- (void)prepareForPresentationOnIPhone {
  self.animationConstraint = [self.backgroundView.bottomAnchor
      constraintEqualToAnchor:self.baseViewController.view.topAnchor];

  // Use this constraint to force the greater than or equal constraint below to
  // be as small as possible.
  NSLayoutConstraint* centeringGuideTopConstraint =
      [self.centeringGuide.topAnchor
          constraintEqualToAnchor:self.backgroundView.topAnchor];
  centeringGuideTopConstraint.priority = UILayoutPriorityDefaultLow;

  [NSLayoutConstraint activateConstraints:@[
    [self.backgroundView.leadingAnchor
        constraintEqualToAnchor:self.baseViewController.view.leadingAnchor],
    [self.backgroundView.trailingAnchor
        constraintEqualToAnchor:self.baseViewController.view.trailingAnchor],
    [self.centeringGuide.topAnchor
        constraintGreaterThanOrEqualToAnchor:self.backgroundView
                                                 .safeAreaLayoutGuide
                                                 .topAnchor],
    centeringGuideTopConstraint,
    self.animationConstraint,
  ]];
}

// Positions the view into its initial, pre-animation position on iPad.
// Specifically, the final position will be a small accessory just below the
// toolbar on the upper right. The pre-animation position is that, but slid
// offscreen to the right.
- (void)prepareForPresentationOnIPad {
  self.backgroundView.layer.cornerRadius =
      kToolbarAccessoryCornerRadiusRegularRegular;

  UIImageView* shadow =
      [[UIImageView alloc] initWithImage:StretchableImageNamed(@"menu_shadow")];
  shadow.translatesAutoresizingMaskIntoConstraints = NO;
  [self.backgroundView addSubview:shadow];

  // The width should be this value, unless that is too wide for the screen.
  // Using a less than required priority means that the view will attempt to be
  // this width, but use the less than or equal constraint below if the view
  // is too narrow.
  NSLayoutConstraint* widthConstraint = [self.backgroundView.widthAnchor
      constraintEqualToConstant:kToolbarAccessoryWidthRegularRegular];
  widthConstraint.priority = UILayoutPriorityRequired - 1;

  self.animationConstraint = [self.backgroundView.leadingAnchor
      constraintEqualToAnchor:self.baseViewController.view.trailingAnchor];

  [self.baseViewController.view addLayoutGuide:_toolbarLayoutGuide];
  [NSLayoutConstraint activateConstraints:@[
    // Anchors accessory below the the toolbar.
    [self.backgroundView.topAnchor
        constraintEqualToAnchor:_toolbarLayoutGuide.bottomAnchor],
    self.animationConstraint,
    widthConstraint,
    [self.backgroundView.widthAnchor
        constraintLessThanOrEqualToAnchor:self.baseViewController.view
                                              .widthAnchor
                                 constant:-2 * kRegularRegularHorizontalMargin],
    [self.backgroundView.heightAnchor
        constraintEqualToConstant:kPrimaryToolbarWithOmniboxHeight],
  ]];
  // Layouts `shadow` around `self.backgroundView`.
  AddSameConstraintsToSidesWithInsets(
      shadow, self.backgroundView,
      LayoutSides::kTop | LayoutSides::kLeading | LayoutSides::kBottom |
          LayoutSides::kTrailing,
      {-kShadowMargin, -kShadowMargin, -kShadowMargin, -kShadowMargin});
}

// Sets up the constraints on iPhone such that the view is ready to be animated
// to its final position.
- (void)setupFinalConstraintsOnIPhone {
  self.animationConstraint.active = NO;
  [self.backgroundView.topAnchor
      constraintEqualToAnchor:self.baseViewController.view.topAnchor]
      .active = YES;

  // Make sure the background doesn't shrink when the toolbar goes to fullscreen
  // mode.
  [self.baseViewController.view addLayoutGuide:_toolbarLayoutGuide];
  [self.backgroundView.bottomAnchor
      constraintGreaterThanOrEqualToAnchor:_toolbarLayoutGuide.bottomAnchor]
      .active = YES;
}

// Sets up the constraints on iPhone such that the view is ready to be animated
// to its final position.
- (void)setupFinalConstraintsOnIPad {
  self.animationConstraint.active = NO;
  [self.backgroundView.trailingAnchor
      constraintEqualToAnchor:self.baseViewController.view.trailingAnchor
                     constant:-kRegularRegularHorizontalMargin]
      .active = YES;
}

// Creates the background view and adds `self.presentedViewController.view` to
// it
- (UIView*)createBackgroundView {
  UIView* backgroundView = [[UIView alloc] init];
  backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
  backgroundView.accessibilityIdentifier = kToolbarAccessoryContainerViewID;

  backgroundView.overrideUserInterfaceStyle =
      self.isIncognito ? UIUserInterfaceStyleDark
                       : UIUserInterfaceStyleUnspecified;
  backgroundView.backgroundColor = [UIColor colorNamed:kBackgroundColor];

  [backgroundView addSubview:self.presentedViewController.view];

  self.centeringGuide = [[UILayoutGuide alloc] init];
  [backgroundView addLayoutGuide:self.centeringGuide];

  [NSLayoutConstraint activateConstraints:@[
    [self.centeringGuide.trailingAnchor
        constraintEqualToAnchor:backgroundView.trailingAnchor],
    [self.centeringGuide.leadingAnchor
        constraintEqualToAnchor:backgroundView.leadingAnchor],
    [self.centeringGuide.bottomAnchor
        constraintEqualToAnchor:backgroundView.bottomAnchor],
    [self.centeringGuide.heightAnchor
        constraintGreaterThanOrEqualToAnchor:self.presentedViewController.view
                                                 .heightAnchor],
    [self.presentedViewController.view.heightAnchor
        constraintEqualToConstant:kPrimaryToolbarWithOmniboxHeight],
    [self.presentedViewController.view.leadingAnchor
        constraintEqualToAnchor:self.centeringGuide.leadingAnchor],
    [self.presentedViewController.view.trailingAnchor
        constraintEqualToAnchor:self.centeringGuide.trailingAnchor],
    [self.presentedViewController.view.centerYAnchor
        constraintEqualToAnchor:self.centeringGuide.centerYAnchor],
  ]];

  return backgroundView;
}

@end