blob: b301dd40b9810b375a4da95acc6aee808fc9f762 [file] [log] [blame]
// 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/omnibox/ui/popup/omnibox_popup_presenter.h"
#import "base/time/time.h"
#import "ios/chrome/browser/omnibox/public/omnibox_ui_features.h"
#import "ios/chrome/browser/omnibox/ui/popup/content_providing.h"
#import "ios/chrome/browser/shared/public/features/features.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/browser/toolbar/ui_bundled/public/omnibox_position_util.h"
#import "ios/chrome/browser/toolbar/ui_bundled/public/toolbar_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ui/base/device_form_factor.h"
#import "ui/gfx/ios/uikit_util.h"
namespace {
const CGFloat kVerticalOffset = 6;
const CGFloat kPopupBottomPaddingTablet = 80;
/// Duration of the fade in animation.
constexpr NSTimeInterval kFadeInAnimationDuration =
base::Milliseconds(300).InSecondsF();
/// Vertical offset of the suggestions when fading in.
const CGFloat kFadeAnimationVerticalOffset = 12;
} // namespace
@interface OmniboxPopupPresenter ()
/// Constraint for the bottom anchor of the popup when form factor is phone.
@property(nonatomic, strong) NSLayoutConstraint* bottomConstraintPhone;
/// Constraint for the height anchor of the popup when form factor is tablet.
@property(nonatomic, strong) NSLayoutConstraint* heightConstraintTablet;
@property(nonatomic, weak) id<OmniboxPopupPresenterDelegate> delegate;
@property(nonatomic, weak) UIViewController<ContentProviding>* viewController;
/// Readwrite internal redefinition.
@property(nonatomic, strong) UIView* popupContainerView;
/// Separator for the bottom edge of the popup on iPad.
@property(nonatomic, strong) UIView* bottomSeparator;
/// Top constraint between the popup and it's container. This is used to animate
/// suggestions when focusing the omnibox.
@property(nonatomic, strong) NSLayoutConstraint* popupTopConstraint;
// The layout guide center to use to refer to the omnibox.
@property(nonatomic, strong) LayoutGuideCenter* layoutGuideCenter;
@property(nonatomic, strong) UILayoutGuide* topOmniboxGuide;
// Whether to show the omnibox in the bottom when the popup is open.
@property(nonatomic, readonly) BOOL useBottomOmniboxInPopup;
@end
@implementation OmniboxPopupPresenter {
/// Type of the toolbar that contains the omnibox when it's not focused. The
/// animation of focusing/defocusing the omnibox changes depending on this
/// position.
ToolbarType _unfocusedOmniboxToolbarType;
// The context in which the omnibox is presented.
OmniboxPresentationContext _presentationContext;
/// The amount of padding to add to the bottom of the popup.
CGFloat _bottomOmniboxOffset;
}
- (instancetype)
initWithPopupPresenterDelegate:(id<OmniboxPopupPresenterDelegate>)delegate
popupViewController:
(UIViewController<ContentProviding>*)viewController
layoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter
incognito:(BOOL)incognito
presentationContext:
(OmniboxPresentationContext)presentationContext {
self = [super init];
if (self) {
_delegate = delegate;
_viewController = viewController;
_layoutGuideCenter = layoutGuideCenter;
_presentationContext = presentationContext;
UIView* containerView = [[UIView alloc] init];
[containerView addSubview:viewController.view];
_popupContainerView = containerView;
UIUserInterfaceStyle userInterfaceStyle =
incognito ? UIUserInterfaceStyleDark : UIUserInterfaceStyleUnspecified;
// Both the container view and the popup view controller need
// overrideUserInterfaceStyle set because the overall popup background
// comes from the container, but overrideUserInterfaceStyle won't
// propagate from a view to any subviews in a different view controller.
_popupContainerView.overrideUserInterfaceStyle = userInterfaceStyle;
viewController.overrideUserInterfaceStyle = userInterfaceStyle;
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
_popupContainerView.backgroundColor =
[UIColor colorNamed:kPrimaryBackgroundColor];
} else {
_popupContainerView.backgroundColor =
[self.delegate popupBackgroundColorForPresenter:self];
}
_popupContainerView.translatesAutoresizingMaskIntoConstraints = NO;
viewController.view.translatesAutoresizingMaskIntoConstraints = NO;
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
self.viewController.view.layer.masksToBounds = YES;
AddSameConstraints(viewController.view, _popupContainerView);
} else {
AddSameConstraintsToSides(viewController.view, _popupContainerView,
LayoutSides::kLeading | LayoutSides::kTrailing |
LayoutSides::kBottom);
_popupTopConstraint = [viewController.view.topAnchor
constraintEqualToAnchor:_popupContainerView.topAnchor];
_popupTopConstraint.active = YES;
// Add bottom separator. This will only be visible on iPad where
// the omnibox doesn't fill the whole screen.
_bottomSeparator = [[UIView alloc] initWithFrame:CGRectZero];
_bottomSeparator.translatesAutoresizingMaskIntoConstraints = NO;
_bottomSeparator.backgroundColor =
[UIColor colorNamed:kToolbarShadowColor];
[_popupContainerView addSubview:self.bottomSeparator];
CGFloat separatorHeight =
ui::AlignValueToUpperPixel(kToolbarSeparatorHeight);
[NSLayoutConstraint activateConstraints:@[
[self.bottomSeparator.heightAnchor
constraintEqualToConstant:separatorHeight],
[self.bottomSeparator.leadingAnchor
constraintEqualToAnchor:_popupContainerView.leadingAnchor],
[self.bottomSeparator.trailingAnchor
constraintEqualToAnchor:_popupContainerView.trailingAnchor],
[self.bottomSeparator.topAnchor
constraintEqualToAnchor:_popupContainerView.bottomAnchor],
]];
}
}
return self;
}
- (void)updatePopupOnFocus:(BOOL)isFocusingOmnibox {
BOOL popupHasContent = self.viewController.hasContent;
BOOL popupIsOnscreen = self.popupContainerView.superview != nil;
if (!popupHasContent && popupIsOnscreen) {
// If intrinsic size is 0 and popup is onscreen, we want to remove the
// popup view.
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
self.bottomConstraintPhone.active = NO;
self.bottomSeparator.hidden = YES;
}
[self.viewController willMoveToParentViewController:nil];
[self.popupContainerView removeFromSuperview];
[self.viewController removeFromParentViewController];
self.open = NO;
[self.delegate popupDidCloseForPresenter:self];
} else if (popupHasContent && !popupIsOnscreen) {
// If intrinsic size is nonzero and popup is offscreen, we want to add it.
UIViewController* parentVC =
[self.delegate popupParentViewControllerForPresenter:self];
[parentVC addChildViewController:self.viewController];
[[self.delegate popupParentViewForPresenter:self]
addSubview:self.popupContainerView];
[self.viewController didMoveToParentViewController:parentVC];
BOOL enableFocusAnimation =
IsBottomOmniboxAvailable() && isFocusingOmnibox &&
_unfocusedOmniboxToolbarType == ToolbarType::kSecondary;
[self initialLayoutAnimated:enableFocusAnimation];
[self updatePopupConstraints];
self.open = YES;
[self.delegate popupDidOpenForPresenter:self];
}
}
/// With popout omnibox, the popup might be in either of two states:
/// a) regular x regular state, where the popup matches OB width
/// b) compact state, where popup takes whole screen width
/// Therefore, on trait collection change, re-add the popup and recreate the
/// constraints to make sure the correct ones are used.
- (void)updatePopupAfterTraitCollectionChange {
// Re-add the popup container to break any existing constraints.
[self.popupContainerView removeFromSuperview];
[[self.delegate popupParentViewForPresenter:self]
addSubview:self.popupContainerView];
// Re-add necessary constraints.
[self initialLayoutAnimated:NO];
[self updatePopupConstraints];
}
- (void)updatePopupConstraints {
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
BOOL showRegularLayout =
IsRegularXRegularSizeClass(self.popupContainerView.traitCollection);
self.bottomConstraintPhone.active = !showRegularLayout;
self.heightConstraintTablet.active = showRegularLayout;
} else {
self.bottomConstraintPhone.active = YES;
self.bottomSeparator.hidden = NO;
}
}
#pragma mark - ToolbarOmniboxConsumer
- (void)steadyStateOmniboxMovedToToolbar:(ToolbarType)toolbarType {
_unfocusedOmniboxToolbarType = toolbarType;
}
- (void)setBottomOmniboxOffsetForPopup:(CGFloat)bottomOmniboxOffset {
_bottomOmniboxOffset = bottomOmniboxOffset;
self.bottomConstraintPhone.constant = -bottomOmniboxOffset;
}
#pragma mark - Private
/// Layouts the popup when it is just added to the view hierarchy.
- (void)initialLayoutAnimated:(BOOL)isAnimated {
[self updateOmniboxLayoutGuide];
[self updatePopupLayer];
[self updateConstraints];
if (isAnimated) {
[self animatePopupOnOmniboxFocus];
}
}
- (void)updateOmniboxLayoutGuide {
UIView* popup = self.popupContainerView;
// Install in the superview the guide tracking the omnibox.
if (self.topOmniboxGuide) {
[popup.superview removeLayoutGuide:self.topOmniboxGuide];
self.topOmniboxGuide = nil;
}
GuideName* omniboxGuideName =
[self.delegate omniboxGuideNameForPresenter:self];
if (omniboxGuideName) {
self.topOmniboxGuide =
[self.layoutGuideCenter makeLayoutGuideNamed:omniboxGuideName];
[popup.superview addLayoutGuide:self.topOmniboxGuide];
}
}
// Updates the popup's view layer.
- (void)updatePopupLayer {
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
return;
}
_popupContainerView.layer.masksToBounds = NO;
BOOL showRegularLayout =
IsRegularXRegularSizeClass(self.popupContainerView.traitCollection);
_popupContainerView.layer.cornerRadius = showRegularLayout ? 16 : 0;
_popupContainerView.layer.shadowColor = UIColor.blackColor.CGColor;
_popupContainerView.layer.shadowRadius = 60;
_popupContainerView.layer.shadowOffset = CGSizeMake(0, 10);
_popupContainerView.layer.shadowOpacity = 0.2;
self.viewController.view.layer.cornerRadius = showRegularLayout ? 16 : 0;
}
// Updates and activates the constraints based on the popup's current view state
- (void)updateConstraints {
UIView* popup = self.popupContainerView;
// Creates the constraints if the view is newly added to the view hierarchy.
// On tablet form factor the popup is padded on the bottom to allow the user
// to defocus the omnibox.
self.heightConstraintTablet = [popup.heightAnchor
constraintLessThanOrEqualToAnchor:popup.superview.heightAnchor
multiplier:0.7];
BOOL tabletFormFactor =
ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET;
// Bottom constraints.
if (tabletFormFactor) {
BOOL paddingAmmount =
_presentationContext == OmniboxPresentationContext::kLensOverlay
? 0
: kPopupBottomPaddingTablet + kSecondaryToolbarWithoutOmniboxHeight;
NSLayoutAnchor* superviewAnchor =
_presentationContext == OmniboxPresentationContext::kLensOverlay
? popup.superview.bottomAnchor
: popup.superview.safeAreaLayoutGuide.bottomAnchor;
self.bottomConstraintPhone =
[superviewAnchor constraintGreaterThanOrEqualToAnchor:popup.bottomAnchor
constant:paddingAmmount];
} else {
CGFloat offset = self.useBottomOmniboxInPopup ? _bottomOmniboxOffset : 0;
self.bottomConstraintPhone =
[popup.bottomAnchor constraintEqualToAnchor:popup.superview.bottomAnchor
constant:-offset];
}
// Top constraints.
BOOL constraintTopToOmnibox =
self.topOmniboxGuide && !self.useBottomOmniboxInPopup;
NSLayoutConstraint* topConstraint =
constraintTopToOmnibox
? [popup.topAnchor
constraintEqualToAnchor:self.topOmniboxGuide.bottomAnchor
constant:kVerticalOffset]
: [popup.topAnchor
constraintEqualToAnchor:[self.delegate
popupParentViewForPresenter:self]
.safeAreaLayoutGuide.topAnchor];
NSMutableArray<NSLayoutConstraint*>* constraintsToActivate =
[NSMutableArray arrayWithObject:topConstraint];
BOOL regularXRegularSizeClass =
tabletFormFactor &&
IsRegularXRegularSizeClass(self.popupContainerView.traitCollection);
if (regularXRegularSizeClass && self.topOmniboxGuide) {
NSLayoutConstraint* leadingConstraint = [popup.leadingAnchor
constraintEqualToAnchor:self.topOmniboxGuide.leadingAnchor
constant:-16];
leadingConstraint.priority = UILayoutPriorityDefaultHigh;
NSLayoutConstraint* trailingConstraint = [popup.trailingAnchor
constraintEqualToAnchor:self.topOmniboxGuide.trailingAnchor
constant:16];
trailingConstraint.priority = UILayoutPriorityDefaultHigh;
NSLayoutConstraint* centerXConstraint = [popup.centerXAnchor
constraintEqualToAnchor:self.topOmniboxGuide.centerXAnchor];
[constraintsToActivate addObjectsFromArray:@[
leadingConstraint, trailingConstraint, centerXConstraint
]];
} else {
[constraintsToActivate addObjectsFromArray:@[
[popup.leadingAnchor
constraintEqualToAnchor:popup.superview.leadingAnchor],
[popup.trailingAnchor
constraintEqualToAnchor:popup.superview.trailingAnchor],
]];
}
[NSLayoutConstraint activateConstraints:constraintsToActivate];
}
/// Animates the popup for omnibox focus.
- (void)animatePopupOnOmniboxFocus {
__weak __typeof__(self) weakSelf = self;
self.viewController.view.alpha = 0.0;
self.popupTopConstraint.constant = kFadeAnimationVerticalOffset;
[self.popupContainerView.superview layoutIfNeeded];
auto constraintForVisiblePopup = ^{
weakSelf.viewController.view.alpha = 1.0;
weakSelf.popupTopConstraint.constant = 0.0;
[weakSelf.popupContainerView.superview layoutIfNeeded];
};
[UIView animateWithDuration:kFadeInAnimationDuration
animations:constraintForVisiblePopup
completion:^(BOOL _) {
constraintForVisiblePopup();
}];
}
- (BOOL)useBottomOmniboxInPopup {
return omnibox::ShouldFocusedOmniboxFollowSteadyStatePosition() &&
_unfocusedOmniboxToolbarType == ToolbarType::kSecondary;
}
@end