| // Copyright 2012 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/toolbar/toolbar_controller.h" |
| |
| #include <QuartzCore/QuartzCore.h> |
| |
| #include "base/format_macros.h" |
| #include "base/i18n/rtl.h" |
| #include "base/ios/ios_util.h" |
| #include "base/mac/bundle_locations.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #import "ios/chrome/browser/ui/animation_util.h" |
| #include "ios/chrome/browser/ui/bubble/bubble_util.h" |
| #import "ios/chrome/browser/ui/commands/application_commands.h" |
| #import "ios/chrome/browser/ui/commands/browser_commands.h" |
| #import "ios/chrome/browser/ui/fullscreen/fullscreen_animator.h" |
| #import "ios/chrome/browser/ui/fullscreen/fullscreen_foreground_animator.h" |
| #import "ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.h" |
| #import "ios/chrome/browser/ui/fullscreen/fullscreen_scroll_to_top_animator.h" |
| #import "ios/chrome/browser/ui/image_util/image_util.h" |
| #import "ios/chrome/browser/ui/reversed_animation.h" |
| #include "ios/chrome/browser/ui/rtl_geometry.h" |
| #import "ios/chrome/browser/ui/toolbar/clean/toolbar_tools_menu_button.h" |
| #import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h" |
| #import "ios/chrome/browser/ui/toolbar/toolbar_controller+protected.h" |
| #include "ios/chrome/browser/ui/toolbar/toolbar_resource_macros.h" |
| #import "ios/chrome/browser/ui/toolbar/tools_menu_button_observer_bridge.h" |
| #import "ios/chrome/browser/ui/tools_menu/tools_menu_configuration.h" |
| #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| #import "ios/chrome/browser/ui/util/constraints_ui_util.h" |
| #import "ios/chrome/common/material_timing.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #include "ios/chrome/grit/ios_theme_resources.h" |
| #import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| using base::UserMetricsAction; |
| using ios::material::TimingFunction; |
| |
| // Helper class to display a UIButton with the image and text centered |
| // vertically and horizontally. |
| @interface ToolbarCenteredButton : UIButton { |
| } |
| @end |
| |
| @implementation ToolbarCenteredButton |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| self.titleLabel.textAlignment = NSTextAlignmentCenter; |
| } |
| return self; |
| } |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| CGSize size = self.bounds.size; |
| CGPoint center = CGPointMake(size.width / 2, size.height / 2); |
| self.imageView.center = center; |
| self.imageView.frame = AlignRectToPixel(self.imageView.frame); |
| self.titleLabel.frame = self.bounds; |
| } |
| |
| @end |
| |
| @interface ToolbarController () { |
| // The shadow view. Only used on iPhone. |
| UIImageView* fullBleedShadowView_; |
| |
| // The backing object for |self.transitionLayers|. |
| NSMutableArray* transitionLayers_; |
| |
| ToolbarToolsMenuButton* toolsMenuButton_; |
| UIButton* stackButton_; |
| UIButton* shareButton_; |
| NSArray* standardButtons_; |
| ToolsMenuButtonObserverBridge* toolsMenuButtonObserverBridge_; |
| ToolbarControllerStyle style_; |
| } |
| |
| // Leading and trailing safe area constraint for faking a safe area. These |
| // constraints are activated by calling activateFakeSafeAreaInsets and |
| // deactivateFakeSafeAreaInsets. |
| @property(nonatomic, strong) NSLayoutConstraint* leadingFakeSafeAreaConstraint; |
| @property(nonatomic, strong) NSLayoutConstraint* trailingFakeSafeAreaConstraint; |
| |
| // These constraints pin the content view to the safe area. They are temporarily |
| // disabled when a fake safe area is simulated by calling |
| // activateFakeSafeAreaInsets. |
| @property(nonatomic, strong) NSLayoutConstraint* leadingSafeAreaConstraint; |
| @property(nonatomic, strong) NSLayoutConstraint* trailingSafeAreaConstraint; |
| // Style of this toolbar. |
| @property(nonatomic, readonly, assign) ToolbarControllerStyle style; |
| // The view containing all the content of the toolbar. It respects the trailing |
| // and leading anchors of the safe area. |
| @property(nonatomic, readonly, strong) UIView* contentView; |
| |
| // Returns the background image that should be used for |style|. |
| - (UIImage*)getBackgroundImageForStyle:(ToolbarControllerStyle)style; |
| |
| // Whether the share button should be visible in the toolbar. |
| - (BOOL)shareButtonShouldBeVisible; |
| |
| @end |
| |
| @implementation ToolbarController |
| |
| @synthesize readingListModel = readingListModel_; |
| @synthesize contentView = contentView_; |
| @synthesize backgroundView = backgroundView_; |
| @synthesize shadowView = shadowView_; |
| @synthesize style = style_; |
| @synthesize heightConstraint = heightConstraint_; |
| @synthesize dispatcher = dispatcher_; |
| @synthesize leadingFakeSafeAreaConstraint = _leadingFakeSafeAreaConstraint; |
| @synthesize trailingFakeSafeAreaConstraint = _trailingFakeSafeAreaConstraint; |
| @synthesize leadingSafeAreaConstraint = _leadingSafeAreaConstraint; |
| @synthesize trailingSafeAreaConstraint = _trailingSafeAreaConstraint; |
| @dynamic view; |
| |
| - (instancetype) |
| initWithStyle:(ToolbarControllerStyle)style |
| dispatcher: |
| (id<ApplicationCommands, BrowserCommands, ToolbarCommands>)dispatcher { |
| self = [super initWithNibName:nil bundle:nil]; |
| if (self) { |
| style_ = style; |
| dispatcher_ = dispatcher; |
| DCHECK_LT(style_, ToolbarControllerStyleMaxStyles); |
| |
| InterfaceIdiom idiom = IsIPadIdiom() ? IPAD_IDIOM : IPHONE_IDIOM; |
| CGRect viewFrame = kToolbarFrame[idiom]; |
| CGRect backgroundFrame = kBackgroundViewFrame[idiom]; |
| CGRect stackButtonFrame = LayoutRectGetRect(kStackButtonFrame); |
| CGRect toolsMenuButtonFrame = |
| LayoutRectGetRect(kToolsMenuButtonFrame[idiom]); |
| |
| if (idiom == IPHONE_IDIOM) { |
| CGFloat statusBarOffset = [self statusBarOffset]; |
| viewFrame.size.height += statusBarOffset; |
| backgroundFrame.size.height += statusBarOffset; |
| stackButtonFrame.origin.y += statusBarOffset; |
| toolsMenuButtonFrame.origin.y += statusBarOffset; |
| } |
| |
| self.view = [[LegacyToolbarView alloc] initWithFrame:viewFrame]; |
| if (IsSafeAreaCompatibleToolbarEnabled()) { |
| [self.view setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| } |
| |
| UIViewAutoresizing autoresizingMask = |
| UIViewAutoresizingFlexibleLeadingMargin() | |
| UIViewAutoresizingFlexibleTopMargin; |
| |
| backgroundView_ = [[UIImageView alloc] initWithFrame:backgroundFrame]; |
| |
| [self.view addSubview:backgroundView_]; |
| [self.view setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; |
| [backgroundView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| UIViewAutoresizingFlexibleHeight]; |
| |
| contentView_ = [[UIView alloc] initWithFrame:viewFrame]; |
| contentView_.translatesAutoresizingMaskIntoConstraints = NO; |
| contentView_.layer.allowsGroupOpacity = YES; |
| [self.view addSubview:contentView_]; |
| NSLayoutConstraint* safeAreaLeading = nil; |
| NSLayoutConstraint* safeAreaTrailing = nil; |
| if (@available(iOS 11.0, *)) { |
| UILayoutGuide* safeArea = self.view.safeAreaLayoutGuide; |
| safeAreaLeading = [contentView_.leadingAnchor |
| constraintEqualToAnchor:safeArea.leadingAnchor]; |
| safeAreaTrailing = [contentView_.trailingAnchor |
| constraintEqualToAnchor:safeArea.trailingAnchor]; |
| } else { |
| safeAreaLeading = [contentView_.leadingAnchor |
| constraintEqualToAnchor:self.view.leadingAnchor]; |
| safeAreaTrailing = [contentView_.trailingAnchor |
| constraintEqualToAnchor:self.view.trailingAnchor]; |
| } |
| _leadingSafeAreaConstraint = safeAreaLeading; |
| _trailingSafeAreaConstraint = safeAreaTrailing; |
| _leadingFakeSafeAreaConstraint = [contentView_.leadingAnchor |
| constraintEqualToAnchor:self.view.leadingAnchor]; |
| _trailingFakeSafeAreaConstraint = [contentView_.trailingAnchor |
| constraintEqualToAnchor:self.view.trailingAnchor]; |
| [NSLayoutConstraint activateConstraints:@[ |
| safeAreaLeading, |
| safeAreaTrailing, |
| [contentView_.topAnchor constraintEqualToAnchor:self.view.topAnchor], |
| [contentView_.bottomAnchor |
| constraintEqualToAnchor:self.view.bottomAnchor], |
| ]]; |
| |
| toolsMenuButton_ = |
| [[ToolbarToolsMenuButton alloc] initWithFrame:toolsMenuButtonFrame |
| style:style_]; |
| [toolsMenuButton_ addTarget:self.dispatcher |
| action:@selector(showToolsMenu) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [toolsMenuButton_ setAutoresizingMask:autoresizingMask]; |
| [contentView_ addSubview:toolsMenuButton_]; |
| |
| if (idiom == IPAD_IDIOM) { |
| CGRect shareButtonFrame = LayoutRectGetRect(kShareMenuButtonFrame); |
| shareButton_ = [[UIButton alloc] initWithFrame:shareButtonFrame]; |
| [shareButton_ setAutoresizingMask:autoresizingMask]; |
| [self setUpButton:shareButton_ |
| withImageEnum:ToolbarButtonNameShare |
| forInitialState:UIControlStateNormal |
| hasDisabledImage:YES |
| synchronously:NO]; |
| [shareButton_ addTarget:self.dispatcher |
| action:@selector(sharePage) |
| forControlEvents:UIControlEventTouchUpInside]; |
| SetA11yLabelAndUiAutomationName(shareButton_, IDS_IOS_TOOLS_MENU_SHARE, |
| kToolbarShareButtonIdentifier); |
| [contentView_ addSubview:shareButton_]; |
| } |
| |
| CGRect shadowFrame = kShadowViewFrame[idiom]; |
| shadowFrame.origin.y = CGRectGetMaxY(backgroundFrame); |
| shadowView_ = [[UIImageView alloc] initWithFrame:shadowFrame]; |
| [shadowView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| UIViewAutoresizingFlexibleTopMargin]; |
| |
| [shadowView_ setUserInteractionEnabled:NO]; |
| [self.view addSubview:shadowView_]; |
| [shadowView_ setImage:NativeImage(IDR_IOS_TOOLBAR_SHADOW)]; |
| |
| if (idiom == IPHONE_IDIOM) { |
| // iPad omnibox does not expand to full bleed. |
| CGRect fullBleedShadowFrame = kFullBleedShadowViewFrame; |
| fullBleedShadowFrame.origin.y = shadowFrame.origin.y; |
| fullBleedShadowView_ = |
| [[UIImageView alloc] initWithFrame:fullBleedShadowFrame]; |
| [fullBleedShadowView_ |
| setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| UIViewAutoresizingFlexibleTopMargin]; |
| |
| [fullBleedShadowView_ setUserInteractionEnabled:NO]; |
| [fullBleedShadowView_ setAlpha:0]; |
| [self.view addSubview:fullBleedShadowView_]; |
| [fullBleedShadowView_ |
| setImage:NativeImage(IDR_IOS_TOOLBAR_SHADOW_FULL_BLEED)]; |
| } |
| |
| transitionLayers_ = |
| [[NSMutableArray alloc] initWithCapacity:kTransitionLayerCapacity]; |
| |
| // UIImageViews do not default to userInteractionEnabled:YES. |
| [self.view setUserInteractionEnabled:YES]; |
| [backgroundView_ setUserInteractionEnabled:YES]; |
| |
| UIImage* tile = [self getBackgroundImageForStyle:style]; |
| [[self backgroundView] |
| setImage:StretchableImageFromUIImage(tile, 0.0, 3.0)]; |
| |
| if (idiom == IPHONE_IDIOM) { |
| stackButton_ = |
| [[ToolbarCenteredButton alloc] initWithFrame:stackButtonFrame]; |
| [[stackButton_ titleLabel] |
| setFont:[self fontForSize:kFontSizeFewerThanTenTabs]]; |
| [stackButton_ |
| setTitleColor:[UIColor colorWithWhite:kStackButtonNormalColors[style_] |
| alpha:1.0] |
| forState:UIControlStateNormal]; |
| UIColor* highlightColor = |
| UIColorFromRGB(kStackButtonHighlightedColors[style_], 1.0); |
| [stackButton_ setTitleColor:highlightColor |
| forState:UIControlStateHighlighted]; |
| |
| [stackButton_ setAutoresizingMask:autoresizingMask]; |
| |
| [self setUpButton:stackButton_ |
| withImageEnum:ToolbarButtonNameStack |
| forInitialState:UIControlStateNormal |
| hasDisabledImage:NO |
| synchronously:NO]; |
| [contentView_ addSubview:stackButton_]; |
| } |
| [self registerEventsForButton:toolsMenuButton_]; |
| |
| self.view.accessibilityIdentifier = |
| style == ToolbarControllerStyleIncognitoMode |
| ? kIncognitoToolbarIdentifier |
| : kToolbarIdentifier; |
| SetA11yLabelAndUiAutomationName(stackButton_, IDS_IOS_TOOLBAR_SHOW_TABS, |
| kToolbarStackButtonIdentifier); |
| SetA11yLabelAndUiAutomationName(toolsMenuButton_, IDS_IOS_TOOLBAR_SETTINGS, |
| kToolbarToolsMenuButtonIdentifier); |
| [self updateStandardButtons]; |
| } |
| return self; |
| } |
| |
| #pragma mark - Public API |
| |
| - (void)setReadingListModel:(ReadingListModel*)readingListModel { |
| readingListModel_ = readingListModel; |
| if (readingListModel_) { |
| toolsMenuButtonObserverBridge_ = |
| [[ToolsMenuButtonObserverBridge alloc] initWithModel:readingListModel_ |
| toolbarButton:toolsMenuButton_]; |
| } |
| } |
| |
| - (void)activateFakeSafeAreaInsets:(UIEdgeInsets)fakeSafeAreaInsets { |
| self.leadingFakeSafeAreaConstraint.constant = |
| UIEdgeInsetsGetLeading(fakeSafeAreaInsets); |
| self.trailingFakeSafeAreaConstraint.constant = |
| -UIEdgeInsetsGetTrailing(fakeSafeAreaInsets); |
| self.leadingSafeAreaConstraint.active = NO; |
| self.trailingSafeAreaConstraint.active = NO; |
| self.leadingFakeSafeAreaConstraint.active = YES; |
| self.trailingFakeSafeAreaConstraint.active = YES; |
| } |
| |
| - (void)deactivateFakeSafeAreaInsets { |
| self.leadingFakeSafeAreaConstraint.active = NO; |
| self.trailingFakeSafeAreaConstraint.active = NO; |
| self.leadingSafeAreaConstraint.active = YES; |
| self.trailingSafeAreaConstraint.active = YES; |
| } |
| |
| - (void)setToolsMenuIsVisibleForToolsMenuButton:(BOOL)isVisible { |
| [toolsMenuButton_ setToolsMenuIsVisible:isVisible]; |
| } |
| |
| #pragma mark Appearance |
| |
| - (void)setBackgroundAlpha:(CGFloat)alpha { |
| [backgroundView_ setAlpha:alpha]; |
| [shadowView_ setAlpha:alpha]; |
| } |
| |
| - (void)setTabCount:(NSInteger)tabCount { |
| if (!stackButton_) |
| return; |
| // Enable or disable the stack view icon based on the number of tabs. This |
| // locks the user in the stack view when there are no tabs. |
| [stackButton_ setEnabled:tabCount > 0 ? YES : NO]; |
| |
| // Update the text shown in the |stackButton_|. Note that the button's title |
| // may be empty or contain an easter egg, but the accessibility value will |
| // always be equal to |tabCount|. Also, the text of |stackButton_| is shifted |
| // up, via |kEasterEggTitleInsets|, to avoid overlapping with the button's |
| // outline. |
| NSString* stackButtonValue = |
| [NSString stringWithFormat:@"%" PRIdNS, tabCount]; |
| NSString* stackButtonTitle; |
| if (tabCount <= 0) { |
| stackButtonTitle = @""; |
| } else if (tabCount > kStackButtonMaxTabCount) { |
| stackButtonTitle = @":)"; |
| [[stackButton_ titleLabel] |
| setFont:[self fontForSize:kFontSizeFewerThanTenTabs]]; |
| } else { |
| stackButtonTitle = stackButtonValue; |
| if (tabCount < 10) { |
| [[stackButton_ titleLabel] |
| setFont:[self fontForSize:kFontSizeFewerThanTenTabs]]; |
| } else { |
| [[stackButton_ titleLabel] |
| setFont:[self fontForSize:kFontSizeTenTabsOrMore]]; |
| } |
| } |
| |
| [stackButton_ setTitle:stackButtonTitle forState:UIControlStateNormal]; |
| [stackButton_ setAccessibilityValue:stackButtonValue]; |
| } |
| |
| - (void)setShareButtonEnabled:(BOOL)enabled { |
| [shareButton_ setEnabled:enabled]; |
| } |
| |
| - (void)setUpButton:(UIButton*)button |
| withImageEnum:(int)imageEnum |
| forInitialState:(UIControlState)initialState |
| hasDisabledImage:(BOOL)hasDisabledImage |
| synchronously:(BOOL)synchronously { |
| [self registerEventsForButton:button]; |
| // Add the non-initial images after a slight delay, to help performance |
| // and responsiveness on startup. |
| dispatch_time_t addImageDelay = |
| dispatch_time(DISPATCH_TIME_NOW, kNonInitialImageAdditionDelayNanosec); |
| |
| void (^normalImageBlock)(void) = ^{ |
| UIImage* image = |
| [self imageForImageEnum:imageEnum forState:ToolbarButtonUIStateNormal]; |
| [button setImage:image forState:UIControlStateNormal]; |
| }; |
| if (synchronously || initialState == UIControlStateNormal) |
| normalImageBlock(); |
| else |
| dispatch_after(addImageDelay, dispatch_get_main_queue(), normalImageBlock); |
| |
| void (^pressedImageBlock)(void) = ^{ |
| UIImage* image = |
| [self imageForImageEnum:imageEnum forState:ToolbarButtonUIStatePressed]; |
| [button setImage:image forState:UIControlStateHighlighted]; |
| }; |
| if (synchronously || initialState == UIControlStateHighlighted) |
| pressedImageBlock(); |
| else |
| dispatch_after(addImageDelay, dispatch_get_main_queue(), pressedImageBlock); |
| |
| if (hasDisabledImage) { |
| void (^disabledImageBlock)(void) = ^{ |
| UIImage* image = [self imageForImageEnum:imageEnum |
| forState:ToolbarButtonUIStateDisabled]; |
| [button setImage:image forState:UIControlStateDisabled]; |
| }; |
| if (synchronously || initialState == UIControlStateDisabled) { |
| disabledImageBlock(); |
| } else { |
| dispatch_after(addImageDelay, dispatch_get_main_queue(), |
| disabledImageBlock); |
| } |
| } |
| } |
| |
| - (BOOL)imageShouldFlipForRightToLeftLayoutDirection:(int)imageEnum { |
| // None of the images this class knows about should flip. |
| return NO; |
| } |
| |
| - (void)hideViewsForNewTabPage:(BOOL)hide { |
| DCHECK(!IsIPadIdiom()); |
| [shadowView_ setHidden:hide]; |
| } |
| |
| - (void)adjustToolbarHeight { |
| self.heightConstraint.constant = |
| ToolbarHeightWithTopOfScreenOffset([self statusBarOffset]); |
| self.heightConstraint.active = YES; |
| } |
| |
| - (NSLayoutConstraint*)heightConstraint { |
| if (!heightConstraint_) { |
| heightConstraint_ = [self.view.heightAnchor constraintEqualToConstant:0]; |
| } |
| return heightConstraint_; |
| } |
| |
| #pragma mark Animations |
| |
| - (void)triggerToolsMenuButtonAnimation { |
| [toolsMenuButton_ triggerAnimation]; |
| } |
| |
| #pragma mark - Protected API |
| |
| - (void)updateStandardButtons { |
| BOOL shareButtonShouldBeVisible = [self shareButtonShouldBeVisible]; |
| [shareButton_ setHidden:!shareButtonShouldBeVisible]; |
| NSMutableArray* standardButtons = [NSMutableArray array]; |
| [standardButtons addObject:toolsMenuButton_]; |
| if (stackButton_) |
| [standardButtons addObject:stackButton_]; |
| if (shareButtonShouldBeVisible) |
| [standardButtons addObject:shareButton_]; |
| standardButtons_ = standardButtons; |
| } |
| |
| - (CGFloat)statusBarOffset { |
| return StatusBarHeight(); |
| } |
| |
| - (IBAction)recordUserMetrics:(id)sender { |
| if (sender == toolsMenuButton_) |
| base::RecordAction(UserMetricsAction("MobileToolbarShowMenu")); |
| else if (sender == stackButton_) |
| base::RecordAction(UserMetricsAction("MobileToolbarShowStackView")); |
| else if (sender == shareButton_) |
| base::RecordAction(UserMetricsAction("MobileToolbarShareMenu")); |
| else |
| NOTREACHED(); |
| } |
| |
| - (UIButton*)stackButton { |
| return stackButton_; |
| } |
| |
| - (CGRect)specificControlsArea { |
| // Return the rect to the leading side of the leading-most trailing control. |
| UIView* trailingControl = toolsMenuButton_; |
| if (!IsIPadIdiom()) |
| trailingControl = stackButton_; |
| if ([self shareButtonShouldBeVisible]) |
| trailingControl = shareButton_; |
| LayoutRect trailing = LayoutRectForRectInBoundingRect( |
| trailingControl.frame, self.contentView.bounds); |
| LayoutRect controlsArea = LayoutRectGetLeadingLayout(trailing); |
| controlsArea.size.height = self.contentView.bounds.size.height; |
| controlsArea.position.originY = self.contentView.bounds.origin.y; |
| CGRect controlsFrame = LayoutRectGetRect(controlsArea); |
| |
| if (!IsIPadIdiom()) { |
| controlsFrame.origin.y += StatusBarHeight(); |
| controlsFrame.size.height -= StatusBarHeight(); |
| } |
| return controlsFrame; |
| } |
| |
| - (void)setStandardControlsVisible:(BOOL)visible { |
| if (visible) { |
| for (UIButton* button in standardButtons_) { |
| [button setAlpha:1.0]; |
| } |
| } else { |
| for (UIButton* button in standardButtons_) { |
| [button setAlpha:0.0]; |
| } |
| } |
| } |
| |
| - (void)setStandardControlsAlpha:(CGFloat)alpha { |
| for (UIButton* button in standardButtons_) { |
| if (![button isHidden]) |
| [button setAlpha:alpha]; |
| } |
| } |
| |
| - (UIImage*)imageForImageEnum:(int)imageEnum |
| forState:(ToolbarButtonUIState)state { |
| int imageID = |
| [self imageIdForImageEnum:imageEnum style:[self style] forState:state]; |
| return NativeReversableImage( |
| imageID, [self imageShouldFlipForRightToLeftLayoutDirection:imageEnum]); |
| } |
| |
| - (int)imageEnumForButton:(UIButton*)button { |
| if (button == stackButton_) |
| return ToolbarButtonNameStack; |
| return NumberOfToolbarButtonNames; |
| } |
| |
| - (int)imageIdForImageEnum:(int)index |
| style:(ToolbarControllerStyle)style |
| forState:(ToolbarButtonUIState)state { |
| DCHECK(index < NumberOfToolbarButtonNames); |
| DCHECK(style < ToolbarControllerStyleMaxStyles); |
| DCHECK(state < NumberOfToolbarButtonUIStates); |
| // Incognito mode gets dark buttons. |
| if (style == ToolbarControllerStyleIncognitoMode) |
| style = ToolbarControllerStyleDarkMode; |
| |
| // Name, style [light, dark], UIControlState [normal, pressed, disabled] |
| static int buttonImageIds[NumberOfToolbarButtonNames][2] |
| [NumberOfToolbarButtonUIStates] = { |
| TOOLBAR_IDR_THREE_STATE(OVERVIEW), |
| TOOLBAR_IDR_THREE_STATE(SHARE), |
| }; |
| |
| DCHECK(buttonImageIds[index][style][state]); |
| return buttonImageIds[index][style][state]; |
| } |
| |
| - (NSMutableArray*)transitionLayers { |
| return transitionLayers_; |
| } |
| |
| #pragma mark Animations |
| |
| - (void)fadeInView:(UIView*)view |
| fromLeadingOffset:(LayoutOffset)leadingOffset |
| withDuration:(NSTimeInterval)duration |
| afterDelay:(NSTimeInterval)delay { |
| [CATransaction begin]; |
| [CATransaction setDisableActions:YES]; |
| [CATransaction setCompletionBlock:^{ |
| [view.layer removeAnimationForKey:@"fadeIn"]; |
| }]; |
| view.alpha = 1.0; |
| |
| // Animate the position of |view| |leadingOffset| pixels after |delay|. |
| CGRect shiftedFrame = CGRectLayoutOffset(view.frame, leadingOffset); |
| CAAnimation* shiftAnimation = |
| FrameAnimationMake(view.layer, shiftedFrame, view.frame); |
| shiftAnimation.duration = duration; |
| shiftAnimation.beginTime = delay; |
| shiftAnimation.timingFunction = TimingFunction(ios::material::CurveEaseInOut); |
| |
| // Animate the opacity of |view| to 1 after |delay|. |
| CAAnimation* fadeAnimation = OpacityAnimationMake(0.0, 1.0); |
| fadeAnimation.duration = duration; |
| fadeAnimation.beginTime = delay; |
| shiftAnimation.timingFunction = TimingFunction(ios::material::CurveEaseInOut); |
| |
| // Add group animation to layer. |
| CAAnimation* group = AnimationGroupMake(@[ shiftAnimation, fadeAnimation ]); |
| [view.layer addAnimation:group forKey:@"fadeIn"]; |
| |
| [CATransaction commit]; |
| } |
| |
| - (void)animateStandardControlsForOmniboxExpansion:(BOOL)growOmnibox { |
| if (growOmnibox) |
| [self fadeOutStandardControls]; |
| else |
| [self fadeInStandardControls]; |
| } |
| |
| |
| #pragma mark - ToolsMenuPresentationProvider |
| |
| - (UIButton*)presentingButtonForToolsMenuCoordinator: |
| (ToolsMenuCoordinator*)coordinator { |
| return toolsMenuButton_; |
| } |
| |
| #pragma mark - Private Methods |
| #pragma mark Animations |
| - (void)fadeOutStandardControls { |
| // The opacity animation has a different duration from the position animation. |
| // Thus they require separate CATransations. |
| |
| // Animate the opacity of the buttons to 0. |
| [CATransaction begin]; |
| [CATransaction setAnimationDuration:ios::material::kDuration2]; |
| [CATransaction |
| setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)]; |
| CABasicAnimation* fadeButtons = |
| [CABasicAnimation animationWithKeyPath:@"opacity"]; |
| fadeButtons.fromValue = @1; |
| fadeButtons.toValue = @0; |
| |
| for (UIButton* button in standardButtons_) { |
| if (![button isHidden]) { |
| [button layer].opacity = 0; |
| [[button layer] addAnimation:fadeButtons forKey:@"fade"]; |
| } |
| } |
| [CATransaction commit]; |
| |
| // Animate the buttons 10 pixels in the leading-to-trailing direction |
| [CATransaction begin]; |
| [CATransaction setAnimationDuration:ios::material::kDuration1]; |
| [CATransaction |
| setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)]; |
| |
| for (UIButton* button in standardButtons_) { |
| CABasicAnimation* shiftButton = |
| [CABasicAnimation animationWithKeyPath:@"position"]; |
| CGPoint startPosition = [button layer].position; |
| CGPoint endPosition = |
| CGPointLayoutOffset(startPosition, kButtonFadeOutXOffset); |
| shiftButton.fromValue = [NSValue valueWithCGPoint:startPosition]; |
| shiftButton.toValue = [NSValue valueWithCGPoint:endPosition]; |
| [[button layer] addAnimation:shiftButton forKey:@"shiftButton"]; |
| } |
| |
| [CATransaction commit]; |
| |
| // Fade to the full bleed shadow. |
| [UIView animateWithDuration:ios::material::kDuration1 |
| animations:^{ |
| [shadowView_ setAlpha:0]; |
| [fullBleedShadowView_ setAlpha:1]; |
| }]; |
| } |
| |
| - (void)fadeInStandardControls { |
| for (UIButton* button in standardButtons_) { |
| [self fadeInView:button |
| fromLeadingOffset:10 |
| withDuration:ios::material::kDuration2 |
| afterDelay:ios::material::kDuration1]; |
| } |
| |
| // Fade to the normal shadow. |
| [UIView animateWithDuration:ios::material::kDuration1 |
| animations:^{ |
| [shadowView_ setAlpha:self.backgroundView.alpha]; |
| [fullBleedShadowView_ setAlpha:0]; |
| }]; |
| } |
| |
| #pragma mark Helpers |
| |
| - (UIFont*)fontForSize:(NSInteger)size { |
| return [[MDCTypography fontLoader] boldFontOfSize:size]; |
| } |
| |
| - (BOOL)shareButtonShouldBeVisible { |
| // The share button only exists on iPad, and when some tabs are visible |
| // (i.e. when not in DarkMode), and when the width is greater than |
| // the tablet mini view. |
| if (!IsIPadIdiom() || style_ == ToolbarControllerStyleDarkMode || |
| IsCompactTablet(self.view)) |
| return NO; |
| return YES; |
| } |
| |
| - (void)standardButtonPressed:(UIButton*)sender { |
| // This check for valid button images assumes that the buttons all have a |
| // different image for the highlighted state as for the normal state. |
| // Currently, that assumption is true. |
| if ([sender imageForState:UIControlStateHighlighted] == |
| [sender imageForState:UIControlStateNormal]) { |
| // Update the button images synchronously - somehow the button was pressed |
| // before the dispatched task completed. |
| [self setUpButton:sender |
| withImageEnum:[self imageEnumForButton:sender] |
| forInitialState:UIControlStateNormal |
| hasDisabledImage:NO |
| synchronously:YES]; |
| } |
| } |
| |
| - (void)registerEventsForButton:(UIButton*)button { |
| if (button != toolsMenuButton_) { |
| // |target| must be |self| (as opposed to |nil|) because |self| isn't in the |
| // responder chain. |
| [button addTarget:self |
| action:@selector(standardButtonPressed:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| } |
| [button addTarget:self |
| action:@selector(recordUserMetrics:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| } |
| |
| - (UIImage*)getBackgroundImageForStyle:(ToolbarControllerStyle)style { |
| int backgroundImageID; |
| if (style == ToolbarControllerStyleLightMode) |
| backgroundImageID = IDR_IOS_TOOLBAR_LIGHT_BACKGROUND; |
| else |
| backgroundImageID = IDR_IOS_TOOLBAR_DARK_BACKGROUND; |
| |
| return NativeImage(backgroundImageID); |
| } |
| |
| #pragma mark - ActivityServicePositioner |
| |
| - (UIView*)shareButtonView { |
| return shareButton_; |
| } |
| |
| #pragma mark - BubbleViewAnchorPointProvider methods. |
| |
| - (CGPoint)anchorPointForTabSwitcherButton:(BubbleArrowDirection)direction { |
| CGPoint anchorPoint = |
| bubble_util::AnchorPoint(stackButton_.imageView.frame, direction); |
| return [stackButton_.imageView.superview |
| convertPoint:anchorPoint |
| toView:stackButton_.imageView.window]; |
| } |
| |
| - (CGPoint)anchorPointForToolsMenuButton:(BubbleArrowDirection)direction { |
| CGPoint anchorPoint = |
| bubble_util::AnchorPoint(toolsMenuButton_.frame, direction); |
| return [toolsMenuButton_.superview convertPoint:anchorPoint |
| toView:toolsMenuButton_.window]; |
| } |
| |
| #pragma mark - FullscreenUIElement |
| |
| - (void)updateForFullscreenProgress:(CGFloat)progress { |
| self.contentView.alpha = progress; |
| } |
| |
| - (void)updateForFullscreenEnabled:(BOOL)enabled { |
| if (!enabled) |
| [self updateForFullscreenProgress:1.0]; |
| } |
| |
| - (void)finishFullscreenScrollWithAnimator: |
| (FullscreenScrollEndAnimator*)animator { |
| [self addFullscreenAnimationsToAnimator:animator]; |
| } |
| |
| - (void)scrollFullscreenToTopWithAnimator: |
| (FullscreenScrollToTopAnimator*)animator { |
| [self addFullscreenAnimationsToAnimator:animator]; |
| } |
| |
| - (void)showToolbarForForgroundWithAnimator: |
| (FullscreenForegroundAnimator*)animator { |
| [self addFullscreenAnimationsToAnimator:animator]; |
| } |
| |
| #pragma mark - FullscreenUIElement helpers |
| |
| - (void)addFullscreenAnimationsToAnimator:(FullscreenAnimator*)animator { |
| CGFloat finalProgress = animator.finalProgress; |
| [animator addAnimations:^() { |
| [self updateForFullscreenProgress:finalProgress]; |
| }]; |
| } |
| |
| #pragma mark - CAAnimationDelegate |
| // WebToolbarController conforms to CAAnimationDelegate. |
| - (void)animationDidStart:(CAAnimation*)anim { |
| // Once the buttons start fading in, set their opacity to 1 so there's no |
| // flicker at the end of the animation. |
| for (UIButton* button in standardButtons_) { |
| if (anim == [[button layer] animationForKey:@"fadeIn"]) { |
| [button layer].opacity = 1; |
| return; |
| } |
| } |
| } |
| |
| @end |