| // 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/toolbar/adaptive_toolbar_view_controller.h" |
| |
| #import <MaterialComponents/MaterialProgressView.h> |
| |
| #import "base/metrics/user_metrics.h" |
| #import "base/notreached.h" |
| #import "base/time/time.h" |
| #import "ios/chrome/browser/shared/public/commands/omnibox_commands.h" |
| #import "ios/chrome/browser/shared/public/features/features.h" |
| #import "ios/chrome/browser/shared/ui/symbols/symbols.h" |
| #import "ios/chrome/browser/shared/ui/util/animation_util.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/ui/fullscreen/fullscreen_animator.h" |
| #import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_menus_provider.h" |
| #import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_view.h" |
| #import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller_delegate.h" |
| #import "ios/chrome/browser/ui/toolbar/buttons/toolbar_button.h" |
| #import "ios/chrome/browser/ui/toolbar/buttons/toolbar_button_factory.h" |
| #import "ios/chrome/browser/ui/toolbar/buttons/toolbar_configuration.h" |
| #import "ios/chrome/browser/ui/toolbar/buttons/toolbar_tab_grid_button.h" |
| #import "ios/chrome/browser/ui/toolbar/public/toolbar_constants.h" |
| #import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h" |
| #import "ios/chrome/common/material_timing.h" |
| #import "ios/chrome/common/ui/util/ui_util.h" |
| #import "ui/base/device_form_factor.h" |
| |
| namespace { |
| const CGFloat kRotationInRadians = 5.0 / 180 * M_PI; |
| // Scale factor for the animation, must be < 1. |
| const CGFloat kScaleFactorDiff = 0.50; |
| const CGFloat kTabGridAnimationsTotalDuration = 0.5; |
| // The identifier for the context menu action trigger. |
| NSString* const kContextMenuActionIdentifier = @"kContextMenuActionIdentifier"; |
| // The duration of the slide in animation. |
| const base::TimeDelta kToobarSlideInAnimationDuration = base::Milliseconds(500); |
| // Progress of fullscreen when the toolbars are fully visible. |
| const CGFloat kFullscreenProgressFullyExpanded = 1.0; |
| |
| } // namespace |
| |
| @interface AdaptiveToolbarViewController () |
| |
| // Redefined to be an AdaptiveToolbarView. |
| @property(nonatomic, strong) UIView<AdaptiveToolbarView>* view; |
| // Whether a page is loading. |
| @property(nonatomic, assign, getter=isLoading) BOOL loading; |
| @property(nonatomic, assign) BOOL isNTP; |
| // The last progress of fullscreen registered. The progress range is between 0 |
| // and 1. |
| @property(nonatomic, assign) CGFloat previousFullscreenProgress; |
| // The page's theme color. |
| @property(nonatomic, strong) UIColor* pageThemeColor; |
| // The under page background color. |
| @property(nonatomic, strong) UIColor* underPageBackgroundColor; |
| |
| @end |
| |
| @implementation AdaptiveToolbarViewController |
| |
| @dynamic view; |
| @synthesize buttonFactory = _buttonFactory; |
| @synthesize loading = _loading; |
| @synthesize isNTP = _isNTP; |
| |
| #pragma mark - Public |
| |
| - (ToolbarButton*)toolsMenuButton { |
| return self.view.toolsMenuButton; |
| } |
| |
| - (void)updateForSideSwipeSnapshot:(BOOL)onNonIncognitoNTP { |
| self.view.progressBar.hidden = YES; |
| self.view.progressBar.alpha = 0; |
| } |
| |
| - (void)resetAfterSideSwipeSnapshot { |
| self.view.progressBar.alpha = 1; |
| } |
| |
| - (void)triggerToolbarSlideInAnimationFromBelow:(BOOL)fromBelow { |
| // Toolbar slide-in animations are disabled on iPads. |
| if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) { |
| return; |
| } |
| |
| const UIView* view = self.view; |
| CGFloat toolbarHeight = view.frame.size.height; |
| view.transform = CGAffineTransformMakeTranslation( |
| 0, fromBelow ? toolbarHeight : -toolbarHeight); |
| auto animations = ^{ |
| [UIView addKeyframeWithRelativeStartTime:0 |
| relativeDuration:1 |
| animations:^{ |
| view.transform = CGAffineTransformIdentity; |
| }]; |
| }; |
| |
| [UIView |
| animateKeyframesWithDuration:kToobarSlideInAnimationDuration.InSecondsF() |
| delay:0 |
| options:UIViewAnimationCurveEaseOut |
| animations:animations |
| completion:nil]; |
| } |
| |
| - (void)setTabGridButtonIPHHighlighted:(BOOL)iphHighlighted { |
| self.view.tabGridButton.iphHighlighted = iphHighlighted; |
| } |
| |
| - (void)setNewTabButtonIPHHighlighted:(BOOL)iphHighlighted { |
| self.view.openNewTabButton.iphHighlighted = iphHighlighted; |
| } |
| |
| - (void)showPrerenderingAnimation { |
| __weak __typeof__(self) weakSelf = self; |
| [self.view.progressBar setProgress:0]; |
| if (self.hasOmnibox) { |
| [self.view.progressBar setHidden:NO |
| animated:YES |
| completion:^(BOOL finished) { |
| [weakSelf stopProgressBar]; |
| }]; |
| } |
| } |
| |
| - (BOOL)hasOmnibox { |
| return self.locationBarViewController != nil; |
| } |
| |
| #pragma mark - UIViewController |
| |
| - (void)viewDidAppear:(BOOL)animated { |
| [super viewDidAppear:animated]; |
| [self updateAllButtonsVisibility]; |
| } |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| |
| // The first time, the toolbar is fully displayed. |
| self.previousFullscreenProgress = kFullscreenProgressFullyExpanded; |
| |
| [self addStandardActionsForAllButtons]; |
| |
| // Add the layout guide names to the buttons. |
| self.view.toolsMenuButton.guideName = kToolsMenuGuide; |
| self.view.tabGridButton.guideName = kTabSwitcherGuide; |
| self.view.openNewTabButton.guideName = kNewTabButtonGuide; |
| self.view.forwardButton.guideName = kForwardButtonGuide; |
| self.view.backButton.guideName = kBackButtonGuide; |
| self.view.shareButton.guideName = kShareButtonGuide; |
| |
| [self addLayoutGuideCenterToButtons]; |
| |
| // Add navigation popup menu triggers. |
| [self configureMenuProviderForButton:self.view.backButton |
| buttonType:AdaptiveToolbarButtonTypeBack]; |
| [self configureMenuProviderForButton:self.view.forwardButton |
| buttonType:AdaptiveToolbarButtonTypeForward]; |
| [self configureMenuProviderForButton:self.view.openNewTabButton |
| buttonType:AdaptiveToolbarButtonTypeNewTab]; |
| [self configureMenuProviderForButton:self.view.tabGridButton |
| buttonType:AdaptiveToolbarButtonTypeTabGrid]; |
| |
| // LocationBarContainer initial fullscreen progress. |
| [self updateLocationBarHeightForFullscreenProgress: |
| kFullscreenProgressFullyExpanded]; |
| |
| // CollapsedToolbarButton exit fullscreen. |
| [self.view.collapsedToolbarButton |
| addTarget:self |
| action:@selector(collapsedToolbarButtonTapped) |
| forControlEvents:UIControlEventTouchUpInside]; |
| UIHoverGestureRecognizer* hoverGestureRecognizer = |
| [[UIHoverGestureRecognizer alloc] |
| initWithTarget:self |
| action:@selector(exitFullscreen)]; |
| [self.view.collapsedToolbarButton |
| addGestureRecognizer:hoverGestureRecognizer]; |
| |
| [self traitCollectionDidChange:nil]; |
| } |
| |
| - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| |
| // Progress bar and buttons visibility. |
| [self updateAllButtonsVisibility]; |
| if (IsRegularXRegularSizeClass(self)) { |
| [self.view.progressBar setHidden:YES animated:NO completion:nil]; |
| } else if (self.loading && self.hasOmnibox) { |
| [self.view.progressBar setHidden:NO animated:NO completion:nil]; |
| } |
| |
| // Restore locationBarContainer height with previous fullscreen progress. |
| if (previousTraitCollection.preferredContentSizeCategory != |
| self.traitCollection.preferredContentSizeCategory) { |
| [self updateLocationBarHeightForFullscreenProgress: |
| self.previousFullscreenProgress]; |
| } |
| } |
| |
| - (void)viewDidLayoutSubviews { |
| [super viewDidLayoutSubviews]; |
| // TODO(crbug.com/41413004): Remove this call once iPad trait collection |
| // override issue is fixed. |
| [self updateAllButtonsVisibility]; |
| } |
| |
| - (void)didMoveToParentViewController:(UIViewController*)parent { |
| [super didMoveToParentViewController:parent]; |
| [self updateAllButtonsVisibility]; |
| } |
| |
| #pragma mark - Public Properties |
| |
| - (void)setLayoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter { |
| _layoutGuideCenter = layoutGuideCenter; |
| |
| if (self.isViewLoaded) { |
| [self addLayoutGuideCenterToButtons]; |
| } |
| } |
| |
| - (void)setLocationBarViewController: |
| (UIViewController*)locationBarViewController { |
| _locationBarViewController = locationBarViewController; |
| if (locationBarViewController) { |
| [self addChildViewController:locationBarViewController]; |
| [locationBarViewController didMoveToParentViewController:self]; |
| [self.view setLocationBarView:locationBarViewController.view]; |
| self.view.locationBarContainer.hidden = NO; |
| // Update the constraint of the location bar view to make sure the text is |
| // centered. |
| [locationBarViewController.view updateConstraintsIfNeeded]; |
| } else { |
| [self.view setLocationBarView:nil]; |
| self.view.locationBarContainer.hidden = YES; |
| } |
| [self updateProgressBarVisibility]; |
| } |
| |
| #pragma mark - ToolbarConsumer |
| |
| - (void)setCanGoForward:(BOOL)canGoForward { |
| self.view.forwardButton.enabled = canGoForward; |
| } |
| |
| - (void)setCanGoBack:(BOOL)canGoBack { |
| self.view.backButton.enabled = canGoBack; |
| } |
| |
| - (void)setLoadingState:(BOOL)loading { |
| if (self.loading == loading) { |
| return; |
| } |
| |
| self.loading = loading; |
| self.view.reloadButton.hiddenInCurrentState = loading; |
| self.view.stopButton.hiddenInCurrentState = !loading; |
| [self.view layoutIfNeeded]; |
| |
| if (!loading) { |
| [self stopProgressBar]; |
| } else if (self.view.progressBar.hidden && |
| !IsRegularXRegularSizeClass(self) && !self.isNTP) { |
| [self.view.progressBar setProgress:0]; |
| [self updateProgressBarVisibility]; |
| // Layout if needed the progress bar to avoid having the progress bar |
| // going backward when opening a page from the NTP. |
| [self.view.progressBar layoutIfNeeded]; |
| } |
| } |
| |
| - (void)setLoadingProgressFraction:(double)progress { |
| [self.view.progressBar setProgress:progress animated:YES completion:nil]; |
| } |
| |
| - (void)setTabCount:(int)tabCount addedInBackground:(BOOL)inBackground { |
| if (self.view.tabGridButton.tabCount == tabCount) { |
| return; |
| } |
| |
| CGFloat scaleSign = tabCount > self.view.tabGridButton.tabCount ? 1 : -1; |
| self.view.tabGridButton.tabCount = tabCount; |
| |
| if (IsRegularXRegularSizeClass(self)) { |
| // No animation on Regular x Regular. |
| return; |
| } |
| |
| CGFloat scaleFactor = 1 + scaleSign * kScaleFactorDiff; |
| |
| CGAffineTransform baseTransform = |
| inBackground ? CGAffineTransformMakeRotation(kRotationInRadians) |
| : CGAffineTransformIdentity; |
| |
| auto animations = ^{ |
| [UIView addKeyframeWithRelativeStartTime:0 |
| relativeDuration:0.5 |
| animations:^{ |
| self.view.tabGridButton.transform = |
| CGAffineTransformScale(baseTransform, |
| scaleFactor, |
| scaleFactor); |
| }]; |
| [UIView addKeyframeWithRelativeStartTime:0.5 |
| relativeDuration:0.5 |
| animations:^{ |
| self.view.tabGridButton.transform = |
| CGAffineTransformIdentity; |
| }]; |
| }; |
| |
| [UIView animateKeyframesWithDuration:kTabGridAnimationsTotalDuration |
| delay:0 |
| options:UIViewAnimationCurveEaseInOut |
| animations:animations |
| completion:nil]; |
| } |
| |
| - (void)setVoiceSearchEnabled:(BOOL)enabled { |
| // No-op, should be handled by the location bar. |
| } |
| |
| - (void)setShareMenuEnabled:(BOOL)enabled { |
| self.view.shareButton.enabled = enabled; |
| } |
| |
| - (void)setIsNTP:(BOOL)isNTP { |
| _isNTP = isNTP; |
| } |
| |
| - (void)setPageThemeColor:(UIColor*)pageThemeColor { |
| if ([_pageThemeColor isEqual:pageThemeColor]) { |
| return; |
| } |
| _pageThemeColor = pageThemeColor; |
| [self updateBackgroundColor]; |
| } |
| |
| - (void)setUnderPageBackgroundColor:(UIColor*)underPageBackgroundColor { |
| if ([_underPageBackgroundColor isEqual:underPageBackgroundColor]) { |
| return; |
| } |
| _underPageBackgroundColor = underPageBackgroundColor; |
| [self updateBackgroundColor]; |
| } |
| |
| #pragma mark - NewTabPageControllerDelegate |
| |
| - (void)setScrollProgressForTabletOmnibox:(CGFloat)progress { |
| // No-op, should be handled by the primary toolbar. |
| } |
| |
| #pragma mark - FullscreenUIElement |
| |
| - (void)updateForFullscreenProgress:(CGFloat)progress { |
| self.previousFullscreenProgress = progress; |
| |
| const CGFloat alphaValue = fmax(progress * 2 - 1, 0); |
| |
| [self updateLocationBarHeightForFullscreenProgress:progress]; |
| self.view.locationBarContainer.backgroundColor = |
| [self.buttonFactory.toolbarConfiguration |
| locationBarBackgroundColorWithVisibility:alphaValue]; |
| self.view.collapsedToolbarButton.hidden = progress > 0.05; |
| } |
| |
| - (void)updateForFullscreenEnabled:(BOOL)enabled { |
| if (!enabled) { |
| [self updateForFullscreenProgress:kFullscreenProgressFullyExpanded]; |
| } |
| } |
| |
| - (void)animateFullscreenWithAnimator:(FullscreenAnimator*)animator { |
| CGFloat finalProgress = animator.finalProgress; |
| // Using the animator doesn't work as the animation doesn't trigger a relayout |
| // of the constraints (see crbug.com/978462, crbug.com/950994). |
| [UIView animateWithDuration:animator.duration |
| animations:^{ |
| [self updateForFullscreenProgress:finalProgress]; |
| [self.view layoutIfNeeded]; |
| }]; |
| } |
| |
| #pragma mark - Protected |
| |
| - (void)stopProgressBar { |
| __weak AdaptiveToolbarViewController* weakSelf = self; |
| [self.view.progressBar setProgress:kFullscreenProgressFullyExpanded |
| animated:YES |
| completion:^(BOOL finished) { |
| [weakSelf updateProgressBarVisibility]; |
| }]; |
| } |
| |
| - (void)collapsedToolbarButtonTapped { |
| base::RecordAction(base::UserMetricsAction("MobileFullscreenExitedManually")); |
| [self exitFullscreen]; |
| } |
| |
| - (void)updateBackgroundColor { |
| // Implemented in subclass. |
| } |
| |
| #pragma mark - PopupMenuUIUpdating |
| |
| - (void)updateUIForOverflowMenuIPHDisplayed { |
| self.view.toolsMenuButton.iphHighlighted = YES; |
| } |
| |
| - (void)updateUIForIPHDismissed { |
| self.view.backButton.iphHighlighted = NO; |
| self.view.forwardButton.iphHighlighted = NO; |
| self.view.openNewTabButton.iphHighlighted = NO; |
| self.view.tabGridButton.iphHighlighted = NO; |
| self.view.toolsMenuButton.iphHighlighted = NO; |
| } |
| |
| - (void)setOverflowMenuBlueDot:(BOOL)hasBlueDot { |
| self.view.toolsMenuButton.hasBlueDot = hasBlueDot; |
| } |
| |
| #pragma mark - Private |
| |
| // Updates `locationBarContainer` height and adjusts its corner radius for the |
| // fullscreen `progress` |
| - (void)updateLocationBarHeightForFullscreenProgress:(CGFloat)progress { |
| const CGFloat expandedHeight = |
| LocationBarHeight(self.traitCollection.preferredContentSizeCategory); |
| const CGFloat collapsedHeight = |
| ToolbarCollapsedHeight(self.traitCollection.preferredContentSizeCategory); |
| const CGFloat expandedCollapsedDelta = expandedHeight - collapsedHeight; |
| |
| const CGFloat height = |
| AlignValueToPixel(collapsedHeight + expandedCollapsedDelta * progress); |
| |
| self.view.locationBarContainerHeight.constant = height; |
| self.view.locationBarContainer.layer.cornerRadius = height / 2; |
| } |
| |
| // Makes sure that the visibility of the progress bar is matching the one which |
| // is expected. |
| - (void)updateProgressBarVisibility { |
| __weak __typeof(self) weakSelf = self; |
| |
| BOOL hasOmnibox = self.locationBarViewController != nil; |
| if (!hasOmnibox) { |
| self.view.progressBar.hidden = YES; |
| return; |
| } |
| |
| if (self.loading && self.view.progressBar.hidden) { |
| [self.view.progressBar setHidden:NO |
| animated:YES |
| completion:^(BOOL finished) { |
| [weakSelf updateProgressBarVisibility]; |
| }]; |
| } else if (!self.loading && !self.view.progressBar.hidden) { |
| [self.view.progressBar setHidden:YES |
| animated:YES |
| completion:^(BOOL finished) { |
| [weakSelf updateProgressBarVisibility]; |
| }]; |
| } |
| } |
| |
| // Updates all buttons visibility to match any recent WebState or SizeClass |
| // change. |
| - (void)updateAllButtonsVisibility { |
| for (ToolbarButton* button in self.view.allButtons) { |
| [button updateHiddenInCurrentSizeClass]; |
| } |
| } |
| |
| // Registers the actions which will be triggered when tapping a button. |
| - (void)addStandardActionsForAllButtons { |
| for (ToolbarButton* button in self.view.allButtons) { |
| if (button != self.view.toolsMenuButton && |
| button != self.view.openNewTabButton) { |
| [button addTarget:self.omniboxCommandsHandler |
| action:@selector(cancelOmniboxEdit) |
| forControlEvents:UIControlEventTouchUpInside]; |
| } |
| [button addTarget:self |
| action:@selector(recordUserMetrics:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| } |
| } |
| |
| // Records the use of a button. |
| - (IBAction)recordUserMetrics:(id)sender { |
| if (!sender) |
| return; |
| |
| if (sender == self.view.backButton) { |
| base::RecordAction(base::UserMetricsAction("MobileToolbarBack")); |
| } else if (sender == self.view.forwardButton) { |
| base::RecordAction(base::UserMetricsAction("MobileToolbarForward")); |
| } else if (sender == self.view.reloadButton) { |
| base::RecordAction(base::UserMetricsAction("MobileToolbarReload")); |
| } else if (sender == self.view.stopButton) { |
| base::RecordAction(base::UserMetricsAction("MobileToolbarStop")); |
| } else if (sender == self.view.toolsMenuButton) { |
| base::RecordAction(base::UserMetricsAction("MobileToolbarShowMenu")); |
| } else if (sender == self.view.tabGridButton) { |
| base::RecordAction(base::UserMetricsAction("MobileToolbarShowStackView")); |
| } else if (sender == self.view.shareButton) { |
| base::RecordAction(base::UserMetricsAction("MobileToolbarShareMenu")); |
| } else if (sender == self.view.openNewTabButton) { |
| base::RecordAction(base::UserMetricsAction("MobileToolbarNewTabShortcut")); |
| base::RecordAction(base::UserMetricsAction("MobileTabNewTab")); |
| } else { |
| NOTREACHED_IN_MIGRATION(); |
| } |
| } |
| |
| // Configures `button` with the menu provider, making sure that the items are |
| // updated when the menu is presented. The `buttonType` is passed to the menu |
| // provider. |
| - (void)configureMenuProviderForButton:(UIButton*)button |
| buttonType:(AdaptiveToolbarButtonType)buttonType { |
| // Adds an empty menu so the event triggers the first time. |
| UIMenu* emptyMenu = [UIMenu menuWithChildren:@[]]; |
| button.menu = emptyMenu; |
| |
| [button removeActionForIdentifier:kContextMenuActionIdentifier |
| forControlEvents:UIControlEventMenuActionTriggered]; |
| |
| __weak UIButton* weakButton = button; |
| __weak __typeof(self) weakSelf = self; |
| UIAction* action = [UIAction |
| actionWithTitle:@"" |
| image:nil |
| identifier:kContextMenuActionIdentifier |
| handler:^(UIAction* uiAction) { |
| base::RecordAction( |
| base::UserMetricsAction("MobileMenuToolbarMenuTriggered")); |
| TriggerHapticFeedbackForImpact(UIImpactFeedbackStyleHeavy); |
| weakButton.menu = |
| [weakSelf.menuProvider menuForButtonOfType:buttonType]; |
| }]; |
| [button addAction:action forControlEvents:UIControlEventMenuActionTriggered]; |
| } |
| |
| - (void)addLayoutGuideCenterToButtons { |
| self.view.toolsMenuButton.layoutGuideCenter = self.layoutGuideCenter; |
| self.view.tabGridButton.layoutGuideCenter = self.layoutGuideCenter; |
| self.view.openNewTabButton.layoutGuideCenter = self.layoutGuideCenter; |
| self.view.forwardButton.layoutGuideCenter = self.layoutGuideCenter; |
| self.view.backButton.layoutGuideCenter = self.layoutGuideCenter; |
| self.view.shareButton.layoutGuideCenter = self.layoutGuideCenter; |
| } |
| |
| // Exits fullscreen. |
| - (void)exitFullscreen { |
| [self.adaptiveDelegate exitFullscreen]; |
| } |
| |
| @end |