| // 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/ui/omnibox/popup/omnibox_popup_presenter.h" |
| |
| #import "base/time/time.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/ui/omnibox/omnibox_ui_features.h" |
| #import "ios/chrome/browser/ui/omnibox/popup/content_providing.h" |
| #import "ios/chrome/browser/ui/toolbar/buttons/toolbar_configuration.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" |
| #import "ios/chrome/common/ui/util/ui_util.h" |
| #import "ios/chrome/grit/ios_theme_resources.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 bottom anchor of the popup when form factor is tablet. |
| @property(nonatomic, strong) NSLayoutConstraint* bottomConstraintTablet; |
| |
| @property(nonatomic, weak) id<OmniboxPopupPresenterDelegate> delegate; |
| @property(nonatomic, weak) UIViewController<ContentProviding>* viewController; |
| @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; |
| |
| @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; |
| } |
| |
| - (instancetype) |
| initWithPopupPresenterDelegate:(id<OmniboxPopupPresenterDelegate>)delegate |
| popupViewController: |
| (UIViewController<ContentProviding>*)viewController |
| layoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter |
| incognito:(BOOL)incognito { |
| self = [super init]; |
| if (self) { |
| _delegate = delegate; |
| _viewController = viewController; |
| _layoutGuideCenter = layoutGuideCenter; |
| |
| // Popup uses same colors as the toolbar, so the ToolbarConfiguration is |
| // used to get the style. |
| ToolbarConfiguration* configuration = [[ToolbarConfiguration alloc] |
| initWithStyle:incognito ? ToolbarStyle::kIncognito |
| : ToolbarStyle::kNormal]; |
| |
| 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 = configuration.backgroundColor; |
| } |
| |
| _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 updateBottomConstraints]; |
| |
| 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 updateBottomConstraints]; |
| } |
| |
| - (void)updateBottomConstraints { |
| if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) { |
| BOOL showRegularLayout = |
| IsRegularXRegularSizeClass(self.popupContainerView.traitCollection); |
| self.bottomConstraintPhone.active = !showRegularLayout; |
| self.bottomConstraintTablet.active = showRegularLayout; |
| } else { |
| self.bottomConstraintPhone.active = YES; |
| self.bottomSeparator.hidden = NO; |
| } |
| } |
| |
| #pragma mark - ToolbarOmniboxConsumer |
| |
| - (void)steadyStateOmniboxMovedToToolbar:(ToolbarType)toolbarType { |
| _unfocusedOmniboxToolbarType = toolbarType; |
| } |
| |
| #pragma mark - Private |
| |
| /// Layouts the popup when it is just added to the view hierarchy. |
| - (void)initialLayoutAnimated:(BOOL)isAnimated { |
| UIView* popup = self.popupContainerView; |
| // Creates the constraints if the view is newly added to the view hierarchy. |
| |
| if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) { |
| self.bottomConstraintPhone = [popup.superview.safeAreaLayoutGuide |
| .bottomAnchor |
| constraintGreaterThanOrEqualToAnchor:popup.bottomAnchor |
| constant: |
| kPopupBottomPaddingTablet + |
| kSecondaryToolbarWithoutOmniboxHeight]; |
| } else { |
| self.bottomConstraintPhone = [popup.bottomAnchor |
| constraintEqualToAnchor:popup.superview.bottomAnchor]; |
| } |
| |
| // On tablet form factor the popup is padded on the bottom to allow the user |
| // to defocus the omnibox. |
| self.bottomConstraintTablet = [popup.superview.bottomAnchor |
| constraintGreaterThanOrEqualToAnchor:popup.bottomAnchor |
| constant:kPopupBottomPaddingTablet]; |
| |
| // Install in the superview the guide tracking the top 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]; |
| } |
| |
| [self updatePopupLayer]; |
| [self updateConstraints]; |
| |
| [[popup superview] layoutIfNeeded]; |
| |
| if (isAnimated) { |
| [self animatePopupOnOmniboxFocus]; |
| } |
| } |
| |
| // 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; |
| |
| NSLayoutConstraint* topConstraint; |
| if (self.topOmniboxGuide) { |
| // Position the top anchor of the popup relatively to that layout guide. |
| topConstraint = [popup.topAnchor |
| constraintEqualToAnchor:self.topOmniboxGuide.bottomAnchor |
| constant:kVerticalOffset]; |
| } else { |
| topConstraint = [popup.topAnchor |
| constraintEqualToAnchor:[self.delegate popupParentViewForPresenter:self] |
| .topAnchor]; |
| } |
| |
| NSMutableArray<NSLayoutConstraint*>* constraintsToActivate = |
| [NSMutableArray arrayWithObject:topConstraint]; |
| |
| if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET && |
| IsRegularXRegularSizeClass(self.popupContainerView.traitCollection) && |
| 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(); |
| }]; |
| } |
| |
| @end |