blob: c9184476bb270f7f261c4d3b524936930a13e5c8 [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/popup_menu/public/popup_menu_presenter.h"
#include "base/logging.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_presenter_delegate.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_view_controller.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_view_controller_delegate.h"
#import "ios/chrome/browser/ui/util/named_guide.h"
#import "ios/chrome/common/material_timing.h"
#import "ios/chrome/common/ui_util/constraints_ui_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const CGFloat kMinHeight = 200;
const CGFloat kMinWidth = 200;
const CGFloat kMaxWidth = 300;
const CGFloat kMaxHeight = 435;
const CGFloat kMinWidthDifference = 50;
const CGFloat kMinHorizontalMargin = 5;
const CGFloat kMinVerticalMargin = 15;
const CGFloat kDamping = 0.85;
} // namespace
@interface PopupMenuPresenter () <PopupMenuViewControllerDelegate>
@property(nonatomic, strong) PopupMenuViewController* popupViewController;
// Constraints used for the initial positioning of the popup.
@property(nonatomic, strong) NSArray<NSLayoutConstraint*>* initialConstraints;
// Constraints used for the positioning of the popup when presented.
@property(nonatomic, strong) NSArray<NSLayoutConstraint*>* presentedConstraints;
@end
@implementation PopupMenuPresenter
@synthesize baseViewController = _baseViewController;
@synthesize delegate = _delegate;
@synthesize guideName = _guideName;
@synthesize popupViewController = _popupViewController;
@synthesize initialConstraints = _initialConstraints;
@synthesize presentedConstraints = _presentedConstraints;
@synthesize presentedViewController = _presentedViewController;
#pragma mark - Public
- (void)prepareForPresentation {
DCHECK(self.baseViewController);
if (self.popupViewController)
return;
self.popupViewController = [[PopupMenuViewController alloc] init];
self.popupViewController.delegate = self;
[self.presentedViewController.view
setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh + 1
forAxis:UILayoutConstraintAxisHorizontal];
// Set the frame of the table view to the maximum width to have the label
// resizing correctly.
CGRect frame = self.presentedViewController.view.frame;
frame.size.width = kMaxWidth;
self.presentedViewController.view.frame = frame;
// It is necessary to do a first layout pass so the table view can size
// itself.
[self.presentedViewController.view setNeedsLayout];
[self.presentedViewController.view layoutIfNeeded];
CGSize fittingSize = [self.presentedViewController.view
sizeThatFits:CGSizeMake(kMaxWidth, kMaxHeight)];
// Use preferredSize if it is set.
CGSize preferredSize = self.presentedViewController.preferredContentSize;
CGFloat width = fittingSize.width;
CGFloat height = fittingSize.height;
if (!CGSizeEqualToSize(preferredSize, CGSizeZero)) {
width = preferredSize.width;
height = preferredSize.height;
}
// Set the sizing constraints, in case the UIViewController is using a
// UIScrollView. The priority needs to be non-required to allow downsizing if
// needed, and more than UILayoutPriorityDefaultHigh to take precedence on
// compression resistance.
NSLayoutConstraint* widthConstraint =
[self.presentedViewController.view.widthAnchor
constraintEqualToConstant:width];
widthConstraint.priority = UILayoutPriorityDefaultHigh + 1;
NSLayoutConstraint* heightConstraint =
[self.presentedViewController.view.heightAnchor
constraintEqualToConstant:height];
heightConstraint.priority = UILayoutPriorityDefaultHigh + 1;
UIView* popup = self.popupViewController.contentContainer;
[NSLayoutConstraint activateConstraints:@[
widthConstraint,
heightConstraint,
[popup.heightAnchor constraintLessThanOrEqualToConstant:kMaxHeight],
[popup.widthAnchor constraintLessThanOrEqualToConstant:kMaxWidth],
[popup.widthAnchor constraintGreaterThanOrEqualToConstant:kMinWidth],
]];
[self.popupViewController addContent:self.presentedViewController];
[self.baseViewController addChildViewController:self.popupViewController];
[self.baseViewController.view addSubview:self.popupViewController.view];
self.popupViewController.view.frame = self.baseViewController.view.bounds;
[popup.widthAnchor constraintLessThanOrEqualToAnchor:self.popupViewController
.view.widthAnchor
constant:-kMinWidthDifference]
.active = YES;
UILayoutGuide* namedGuide =
[NamedGuide guideWithName:self.guideName
view:self.baseViewController.view];
self.initialConstraints = @[
[popup.centerXAnchor constraintEqualToAnchor:namedGuide.centerXAnchor],
[popup.centerYAnchor constraintEqualToAnchor:namedGuide.centerYAnchor],
];
[self setUpPresentedConstraints];
// Configure the initial state of the animation.
popup.alpha = 0;
popup.transform = CGAffineTransformMakeScale(0.1, 0.1);
[NSLayoutConstraint activateConstraints:self.initialConstraints];
[self.baseViewController.view layoutIfNeeded];
[self.popupViewController
didMoveToParentViewController:self.baseViewController];
}
- (void)presentAnimated:(BOOL)animated {
[NSLayoutConstraint deactivateConstraints:self.initialConstraints];
[NSLayoutConstraint activateConstraints:self.presentedConstraints];
[self
animate:^{
self.popupViewController.contentContainer.alpha = 1;
[self.baseViewController.view layoutIfNeeded];
self.popupViewController.contentContainer.transform =
CGAffineTransformIdentity;
}
withCompletion:^(BOOL finished) {
[self.delegate containedPresenterDidPresent:self];
}];
}
- (void)dismissAnimated:(BOOL)animated {
[self.popupViewController willMoveToParentViewController:nil];
[NSLayoutConstraint deactivateConstraints:self.presentedConstraints];
[NSLayoutConstraint activateConstraints:self.initialConstraints];
auto completion = ^(BOOL finished) {
[self.popupViewController.view removeFromSuperview];
[self.popupViewController removeFromParentViewController];
self.popupViewController = nil;
[self.delegate containedPresenterDidDismiss:self];
};
if (animated) {
[self
animate:^{
self.popupViewController.contentContainer.alpha = 0;
[self.baseViewController.view layoutIfNeeded];
self.popupViewController.contentContainer.transform =
CGAffineTransformMakeScale(0.1, 0.1);
}
withCompletion:completion];
} else {
completion(YES);
}
}
#pragma mark - Private
// Animate the |animations| then execute |completion|.
- (void)animate:(void (^)(void))animation
withCompletion:(void (^)(BOOL finished))completion {
[UIView animateWithDuration:ios::material::kDuration1
delay:0
usingSpringWithDamping:kDamping
initialSpringVelocity:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:animation
completion:completion];
}
// Sets |presentedConstraints| up, such as they are positioning the popup
// relatively to the |guideName| layout guide. The popup is positioned closest
// to the layout guide, by default it is presented below the layout guide,
// aligned on its leading edge. However, it is respecting the safe area bounds.
- (void)setUpPresentedConstraints {
UIView* parentView = self.baseViewController.view;
UIView* container = self.popupViewController.contentContainer;
UILayoutGuide* namedGuide = [NamedGuide guideWithName:self.guideName
view:parentView];
CGRect guideFrame =
[self.popupViewController.view convertRect:namedGuide.layoutFrame
fromView:namedGuide.owningView];
NSLayoutConstraint* verticalPositioning = nil;
if (CGRectGetMaxY(guideFrame) + kMinHeight >
CGRectGetHeight(parentView.frame)) {
// Display above.
verticalPositioning =
[container.bottomAnchor constraintEqualToAnchor:namedGuide.topAnchor];
} else {
// Display below.
verticalPositioning =
[container.topAnchor constraintEqualToAnchor:namedGuide.bottomAnchor];
}
NSLayoutConstraint* center = [container.centerXAnchor
constraintEqualToAnchor:namedGuide.centerXAnchor];
center.priority = UILayoutPriorityDefaultHigh;
id<LayoutGuideProvider> safeArea = parentView.safeAreaLayoutGuide;
self.presentedConstraints = @[
center,
verticalPositioning,
[container.leadingAnchor
constraintGreaterThanOrEqualToAnchor:safeArea.leadingAnchor
constant:kMinHorizontalMargin],
[container.trailingAnchor
constraintLessThanOrEqualToAnchor:safeArea.trailingAnchor
constant:-kMinHorizontalMargin],
[container.bottomAnchor
constraintLessThanOrEqualToAnchor:safeArea.bottomAnchor
constant:-kMinVerticalMargin],
[container.topAnchor
constraintGreaterThanOrEqualToAnchor:safeArea.topAnchor
constant:kMinVerticalMargin],
];
}
#pragma mark - PopupMenuViewControllerDelegate
- (void)popupMenuViewControllerWillDismiss:
(PopupMenuViewController*)viewController {
[self.delegate popupMenuPresenterWillDismiss:self];
}
@end