blob: cd51038362f69e19bd20b2a478b20c019a024146 [file] [log] [blame]
// 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"
#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
#import "ios/chrome/browser/ui/fullscreen_controller.h"
#import "ios/chrome/browser/ui/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/toolbar_controller+protected.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_controller_private.h"
#include "ios/chrome/browser/ui/toolbar/toolbar_resource_macros.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_tools_menu_button.h"
#import "ios/chrome/browser/ui/toolbar/tools_menu_button_observer_bridge.h"
#import "ios/chrome/browser/ui/tools_menu/tools_popup_controller.h"
#import "ios/chrome/browser/ui/uikit_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/shared/chrome/browser/ui/tools_menu/tools_menu_configuration.h"
#import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoFontLoader.h"
using base::UserMetricsAction;
using ios::material::TimingFunction;
// Animation key used for stack view transition animations
NSString* const kToolbarTransitionAnimationKey = @"ToolbarTransitionAnimation";
// Externed max tab count.
const NSInteger kStackButtonMaxTabCount = 99;
// Font sizes for the button containing the tab count
const NSInteger kFontSizeFewerThanTenTabs = 11;
const NSInteger kFontSizeTenTabsOrMore = 9;
// The initial capacity used to construct |self.transitionLayers|. The value
// is chosen because WebToolbarController animates 11 separate layers during
// transitions; this value should be updated if new subviews are animated in
// the future.
const NSUInteger kTransitionLayerCapacity = 11;
// Externed delay before non-initial button images are loaded.
const int64_t kNonInitialImageAdditionDelayNanosec = 500000LL;
NSString* const kMenuWillShowNotification = @"kMenuWillShowNotification";
NSString* const kMenuWillHideNotification = @"kMenuWillHideNotification";
NSString* const kToolbarIdentifier = @"kToolbarIdentifier";
NSString* const kIncognitoToolbarIdentifier = @"kIncognitoToolbarIdentifier";
NSString* const kToolbarToolsMenuButtonIdentifier =
@"kToolbarToolsMenuButtonIdentifier";
NSString* const kToolbarStackButtonIdentifier =
@"kToolbarStackButtonIdentifier";
NSString* const kToolbarShareButtonIdentifier =
@"kToolbarShareButtonIdentifier";
// Macros for creating CGRects of height H, origin (0,0), with the portrait
// width of phone/pad devices.
// clang-format off
#define IPHONE_FRAME(H) { { 0, 0 }, { kPortraitWidth[IPHONE_IDIOM], H } }
#define IPAD_FRAME(H) { { 0, 0 }, { kPortraitWidth[IPAD_IDIOM], H } }
// Makes a two-element C array of CGRects as described above, one for each
// device idiom.
#define FRAME_PAIR(H) { IPHONE_FRAME(H), IPAD_FRAME(H) }
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// clang-format on
const CGRect kToolbarFrame[INTERFACE_IDIOM_COUNT] = FRAME_PAIR(56);
namespace {
// Color constants for the stack button text, normal and pressed states. These
// arrays are indexed by ToolbarControllerStyle enum values.
const CGFloat kStackButtonNormalColors[] = {
85.0 / 255.0, // ToolbarControllerStyleLightMode
238.0 / 255.0, // ToolbarControllerStyleDarkMode
238.0 / 255.0, // ToolbarControllerStyleIncognitoMode
};
const int kStackButtonHighlightedColors[] = {
0x4285F4, // ToolbarControllerStyleLightMode
0x888a8c, // ToolbarControllerStyleDarkMode
0x888a8c, // ToolbarControllerStyleIncognitoMode
};
// UI frames. iPhone values followed by iPad values.
// Full-width frames that don't change for RTL languages.
const CGRect kBackgroundViewFrame[INTERFACE_IDIOM_COUNT] = FRAME_PAIR(56);
const CGRect kShadowViewFrame[INTERFACE_IDIOM_COUNT] = FRAME_PAIR(2);
// Full bleed shadow frame is iPhone-only
const CGRect kFullBleedShadowViewFrame = IPHONE_FRAME(10);
// Frames that change for RTL.
// clang-format off
const LayoutRect kStackButtonFrame =
{kPortraitWidth[IPHONE_IDIOM], {230, 4}, {48, 48}};
const LayoutRect kShareMenuButtonFrame =
{kPortraitWidth[IPAD_IDIOM], {680, 4}, {46, 48}};
const LayoutRect kToolsMenuButtonFrame[INTERFACE_IDIOM_COUNT] = {
{kPortraitWidth[IPHONE_IDIOM], {276, 4}, {44, 48}},
{kPortraitWidth[IPAD_IDIOM], {723, 4}, {46, 48}}
};
// clang-format on
// Distance to shift buttons when fading out.
const LayoutOffset kButtonFadeOutXOffset = 10;
} // namespace
// 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
@implementation ToolbarView
@synthesize animatingTransition = animatingTransition_;
@synthesize hitTestBoundsContraintRelaxed = hitTestBoundsContraintRelaxed_;
// Some views added to the toolbar have bounds larger than the toolbar bounds
// and still needs to receive touches. The overscroll actions view is one of
// those. That method is overridden in order to still perform hit testing on
// subviews that resides outside the toolbar bounds.
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView || !self.hitTestBoundsContraintRelaxed)
return hitView;
for (UIView* view in [[self subviews] reverseObjectEnumerator]) {
if (!view.userInteractionEnabled || [view isHidden] || [view alpha] < 0.01)
continue;
const CGPoint convertedPoint = [view convertPoint:point fromView:self];
if ([view pointInside:convertedPoint withEvent:event]) {
hitView = [view hitTest:convertedPoint withEvent:event];
if (hitView)
break;
}
}
return hitView;
}
- (void)setDelegate:(id<ToolbarFrameDelegate>)delegate {
delegate_.reset(delegate);
}
- (void)setFrame:(CGRect)frame {
CGRect oldFrame = self.frame;
[super setFrame:frame];
[delegate_ frameDidChangeFrame:frame fromFrame:oldFrame];
}
- (void)didMoveToWindow {
[super didMoveToWindow];
[delegate_ windowDidChange];
}
- (id<CAAction>)actionForLayer:(CALayer*)layer forKey:(NSString*)event {
// Don't allow UIView block-based animations if we're already performing
// explicit transition animations.
if (self.animatingTransition)
return (id<CAAction>)[NSNull null];
return [super actionForLayer:layer forKey:event];
}
@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_;
// The following is nil if not visible.
ToolsPopupController* toolsPopupController_;
}
// 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;
// Update share button visibility and |standardButtons_| array.
- (void)updateStandardButtons;
// Returns an animation for |button| for a toolbar transition animation with
// |style|. |button|'s frame will be interpolated between its layout in the
// screen toolbar to the card's tab frame, and will be faded in for
// ToolbarTransitionStyleToStackView and faded out for
// ToolbarTransitionStyleToBVC.
- (CAAnimation*)transitionAnimationForButton:(UIButton*)button
containerBeginBounds:(CGRect)containerBeginBounds
containerEndBounds:(CGRect)containerEndBounds
withStyle:(ToolbarTransitionStyle)style;
@end
@implementation ToolbarController
@synthesize readingListModel = readingListModel_;
@synthesize view = view_;
@synthesize backgroundView = backgroundView_;
@synthesize shadowView = shadowView_;
@synthesize toolsPopupController = toolsPopupController_;
@synthesize style = style_;
- (void)setReadingListModel:(ReadingListModel*)readingListModel {
readingListModel_ = readingListModel;
if (readingListModel_) {
toolsMenuButtonObserverBridge_ =
[[ToolsMenuButtonObserverBridge alloc] initWithModel:readingListModel_
toolbarButton:toolsMenuButton_];
}
}
- (instancetype)initWithStyle:(ToolbarControllerStyle)style {
self = [super init];
if (self) {
style_ = style;
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;
}
view_ = [[ToolbarView alloc] initWithFrame:viewFrame];
backgroundView_ = [[UIImageView alloc] initWithFrame:backgroundFrame];
toolsMenuButton_ =
[[ToolbarToolsMenuButton alloc] initWithFrame:toolsMenuButtonFrame
style:style_];
[toolsMenuButton_ setTag:IDC_SHOW_TOOLS_MENU];
[toolsMenuButton_
setAutoresizingMask:UIViewAutoresizingFlexibleLeadingMargin() |
UIViewAutoresizingFlexibleBottomMargin];
[view_ addSubview:backgroundView_];
[view_ addSubview:toolsMenuButton_];
[view_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[backgroundView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight];
if (idiom == IPAD_IDIOM) {
CGRect shareButtonFrame = LayoutRectGetRect(kShareMenuButtonFrame);
shareButton_ = [[UIButton alloc] initWithFrame:shareButtonFrame];
[shareButton_ setTag:IDC_SHARE_PAGE];
[shareButton_
setAutoresizingMask:UIViewAutoresizingFlexibleLeadingMargin() |
UIViewAutoresizingFlexibleBottomMargin];
[self setUpButton:shareButton_
withImageEnum:ToolbarButtonNameShare
forInitialState:UIControlStateNormal
hasDisabledImage:YES
synchronously:NO];
SetA11yLabelAndUiAutomationName(shareButton_, IDS_IOS_TOOLS_MENU_SHARE,
kToolbarShareButtonIdentifier);
[view_ addSubview:shareButton_];
}
CGRect shadowFrame = kShadowViewFrame[idiom];
shadowFrame.origin.y = CGRectGetMaxY(backgroundFrame);
shadowView_ = [[UIImageView alloc] initWithFrame:shadowFrame];
[shadowView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[shadowView_ setUserInteractionEnabled:NO];
[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];
[fullBleedShadowView_ setUserInteractionEnabled:NO];
[fullBleedShadowView_ setAlpha:0];
[view_ addSubview:fullBleedShadowView_];
[fullBleedShadowView_
setImage:NativeImage(IDR_IOS_TOOLBAR_SHADOW_FULL_BLEED)];
}
transitionLayers_ =
[[NSMutableArray alloc] initWithCapacity:kTransitionLayerCapacity];
// UIImageViews do not default to userInteractionEnabled:YES.
[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_ setTag:IDC_TOGGLE_TAB_SWITCHER];
[[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:UIViewAutoresizingFlexibleLeadingMargin() |
UIViewAutoresizingFlexibleBottomMargin];
[stackButton_ addTarget:self
action:@selector(stackButtonTouchDown:)
forControlEvents:UIControlEventTouchDown];
[self setUpButton:stackButton_
withImageEnum:ToolbarButtonNameStack
forInitialState:UIControlStateNormal
hasDisabledImage:NO
synchronously:NO];
[view_ 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];
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
return self;
}
- (instancetype)init {
NOTREACHED();
return nil;
}
- (UIFont*)fontForSize:(NSInteger)size {
return [[MDFRobotoFontLoader sharedInstance] boldFontOfSize:size];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[toolsPopupController_ setDelegate:nil];
}
- (CGFloat)statusBarOffset {
return StatusBarHeight();
}
- (NSMutableArray*)transitionLayers {
return transitionLayers_;
}
- (BOOL)imageShouldFlipForRightToLeftLayoutDirection:(int)imageEnum {
// None of the images this class knows about should flip.
return NO;
}
- (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;
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[self updateStandardButtons];
}
- (void)applicationDidEnterBackground:(NSNotification*)notify {
if (toolsPopupController_) {
// Dismiss the tools popup menu without animation.
[toolsMenuButton_ setToolsMenuIsVisible:NO];
toolsPopupController_ = nil;
[[NSNotificationCenter defaultCenter]
postNotificationName:kMenuWillHideNotification
object:nil];
}
}
- (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)setShareButtonEnabled:(BOOL)enabled {
[shareButton_ setEnabled:enabled];
}
- (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];
}
- (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);
}
}
}
- (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];
[button addTarget:button
action:@selector(chromeExecuteCommand:)
forControlEvents:UIControlEventTouchUpInside];
}
- (CGRect)shareButtonAnchorRect {
// Shrink the padding around the shareButton so the popovers are anchored
// correctly.
return CGRectInset([shareButton_ bounds], 10, 0);
}
- (UIView*)shareButtonView {
return shareButton_;
}
- (void)showToolsMenuPopupWithConfiguration:
(ToolsMenuConfiguration*)configuration {
// Because an animation hides and shows the tools popup menu it is possible to
// tap the tools button multiple times before the tools menu is shown. Ignore
// repeated taps between animations.
if (toolsPopupController_)
return;
base::RecordAction(UserMetricsAction("ShowAppMenu"));
// Keep the button pressed.
[toolsMenuButton_ setToolsMenuIsVisible:YES];
[configuration setToolsMenuButton:toolsMenuButton_];
toolsPopupController_ =
[[ToolsPopupController alloc] initWithConfiguration:configuration];
[toolsPopupController_ setDelegate:self];
[[NSNotificationCenter defaultCenter]
postNotificationName:kMenuWillShowNotification
object:nil];
}
- (void)dismissToolsMenuPopup {
if (!toolsPopupController_)
return;
ToolsPopupController* tempTPC = toolsPopupController_;
[tempTPC containerView].userInteractionEnabled = NO;
[tempTPC dismissAnimatedWithCompletion:^{
// Unpress the tools menu button by restoring the normal and
// highlighted images to their usual state.
[toolsMenuButton_ setToolsMenuIsVisible:NO];
// Reference tempTPC so the block retains it.
[tempTPC self];
}];
// reset tabHistoryPopupController_ to prevent -applicationDidEnterBackground
// from posting another kMenuWillHideNotification.
toolsPopupController_ = nil;
[[NSNotificationCenter defaultCenter]
postNotificationName:kMenuWillHideNotification
object:nil];
}
- (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);
}
- (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.view.bounds);
LayoutRect controlsArea = LayoutRectGetLeadingLayout(trailing);
controlsArea.size.height = self.view.bounds.size.height;
controlsArea.position.originY = self.view.bounds.origin.y;
CGRect controlsFrame = LayoutRectGetRect(controlsArea);
if (!IsIPadIdiom()) {
controlsFrame.origin.y += StatusBarHeight();
controlsFrame.size.height -= StatusBarHeight();
}
return controlsFrame;
}
- (void)animateStandardControlsForOmniboxExpansion:(BOOL)growOmnibox {
if (growOmnibox)
[self fadeOutStandardControls];
else
[self fadeInStandardControls];
}
- (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];
}];
}
- (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;
}
}
}
- (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];
}
- (CAAnimation*)transitionAnimationForButton:(UIButton*)button
containerBeginBounds:(CGRect)containerBeginBounds
containerEndBounds:(CGRect)containerEndBounds
withStyle:(ToolbarTransitionStyle)style {
BOOL toStackView = style == TOOLBAR_TRANSITION_STYLE_TO_STACK_VIEW;
CGRect cardBounds = toStackView ? containerEndBounds : containerBeginBounds;
CGRect toolbarBounds =
toStackView ? containerBeginBounds : containerEndBounds;
// |button|'s model layer frame is the button's frame within |toolbarBounds|.
CGRect toolbarButtonFrame = button.layer.frame;
LayoutRect toolbarButtonLayout =
LayoutRectForRectInBoundingRect(toolbarButtonFrame, toolbarBounds);
// |button|'s leading or trailing padding is maintained depending on its
// resizing mask. Its vertical positioning should be centered within the
// container view's card bounds.
LayoutRect cardButtonLayout = toolbarButtonLayout;
cardButtonLayout.boundingWidth = CGRectGetWidth(cardBounds);
BOOL flexibleLeading =
button.autoresizingMask & UIViewAutoresizingFlexibleLeadingMargin();
if (flexibleLeading) {
CGFloat trailingPadding =
LayoutRectGetTrailingLayout(toolbarButtonLayout).size.width;
cardButtonLayout.position.leading = cardButtonLayout.boundingWidth -
trailingPadding -
cardButtonLayout.size.width;
}
cardButtonLayout.position.originY =
CGRectGetMidY(cardBounds) - 0.5 * cardButtonLayout.size.height;
cardButtonLayout.position =
AlignLayoutRectPositionToPixel(cardButtonLayout.position);
CGRect cardButtonFrame = LayoutRectGetRect(cardButtonLayout);
CGRect beginFrame = toStackView ? toolbarButtonFrame : cardButtonFrame;
CGRect endFrame = toStackView ? cardButtonFrame : toolbarButtonFrame;
// Create animations.
CAAnimation* frameAnimation =
FrameAnimationMake(button.layer, beginFrame, endFrame);
frameAnimation.duration = ios::material::kDuration1;
frameAnimation.timingFunction = TimingFunction(ios::material::CurveEaseInOut);
CAAnimation* fadeAnimation =
OpacityAnimationMake(toStackView ? 1.0 : 0.0, toStackView ? 0.0 : 1.0);
fadeAnimation.duration = ios::material::kDuration8;
fadeAnimation.timingFunction = TimingFunction(ios::material::CurveEaseIn);
return AnimationGroupMake(@[ frameAnimation, fadeAnimation ]);
}
- (void)animateTransitionForButtonsInView:(UIView*)containerView
containerBeginBounds:(CGRect)containerBeginBounds
containerEndBounds:(CGRect)containerEndBounds
transitionStyle:(ToolbarTransitionStyle)style {
[containerView.subviews enumerateObjectsUsingBlock:^(
UIButton* button, NSUInteger idx, BOOL* stop) {
if ([button isKindOfClass:[UIButton class]] && button.alpha > 0.0) {
CAAnimation* buttonAnimation =
[self transitionAnimationForButton:button
containerBeginBounds:containerBeginBounds
containerEndBounds:containerEndBounds
withStyle:style];
[self.transitionLayers addObject:button.layer];
[button.layer addAnimation:buttonAnimation
forKey:kToolbarTransitionAnimationKey];
}
}];
}
- (void)reverseTransitionAnimations {
ReverseAnimationsForKeyForLayers(kToolbarTransitionAnimationKey,
[self transitionLayers]);
}
- (UIButton*)stackButton {
return stackButton_;
}
- (void)cleanUpTransitionAnimations {
RemoveAnimationForKeyFromLayers(kToolbarTransitionAnimationKey,
self.transitionLayers);
[self.transitionLayers removeAllObjects];
}
- (void)animateTransitionWithBeginFrame:(CGRect)beginFrame
endFrame:(CGRect)endFrame
transitionStyle:(ToolbarTransitionStyle)style {
// Animation values.
DCHECK(!self.transitionLayers.count);
BOOL transitioningToStackView =
(style == TOOLBAR_TRANSITION_STYLE_TO_STACK_VIEW);
CAAnimation* frameAnimation = nil;
CAMediaTimingFunction* frameTiming =
TimingFunction(ios::material::CurveEaseInOut);
CFTimeInterval frameDuration = ios::material::kDuration1;
CGRect beginBounds = {CGPointZero, beginFrame.size};
CGRect endBounds = {CGPointZero, endFrame.size};
// Update layer geometry.
frameAnimation = FrameAnimationMake(self.view.layer, beginFrame, endFrame);
frameAnimation.duration = frameDuration;
frameAnimation.timingFunction = frameTiming;
[self.transitionLayers addObject:self.view.layer];
[self.view.layer addAnimation:frameAnimation
forKey:kToolbarTransitionAnimationKey];
// Hide background view using CAAnimation so it can be unhidden when the
// animations are removed in |-cleanUpTransitionAnimations|.
CAAnimation* hideAnimation = OpacityAnimationMake(0.0, 0.0);
[self.transitionLayers addObject:self.backgroundView.layer];
[self.backgroundView.layer addAnimation:hideAnimation
forKey:kToolbarTransitionAnimationKey];
// Update shadow. When transitioning to the stack view, hide the shadow.
// When transitioning to the BVC, animate its frame while fading in.
CAAnimation* shadowAnimation = nil;
if (transitioningToStackView) {
shadowAnimation = hideAnimation;
} else {
InterfaceIdiom idiom = IsIPadIdiom() ? IPAD_IDIOM : IPHONE_IDIOM;
CGFloat shadowHeight = kShadowViewFrame[idiom].size.height;
CGFloat shadowVerticalOffset = [[self class] toolbarDropShadowHeight];
beginFrame = CGRectOffset(beginBounds, 0.0,
beginBounds.size.height - shadowVerticalOffset);
beginFrame.size.height = shadowHeight;
endFrame = CGRectOffset(endBounds, 0.0,
endBounds.size.height - shadowVerticalOffset);
endFrame.size.height = shadowHeight;
frameAnimation =
FrameAnimationMake([shadowView_ layer], beginFrame, endFrame);
frameAnimation.duration = frameDuration;
frameAnimation.timingFunction = frameTiming;
CAAnimation* fadeAnimation = OpacityAnimationMake(0.0, 1.0);
fadeAnimation.timingFunction = TimingFunction(ios::material::CurveEaseOut);
fadeAnimation.duration = ios::material::kDuration3;
shadowAnimation = AnimationGroupMake(@[ frameAnimation, fadeAnimation ]);
}
[self.transitionLayers addObject:[shadowView_ layer]];
[[shadowView_ layer] addAnimation:shadowAnimation
forKey:kToolbarTransitionAnimationKey];
// Animate toolbar buttons
[self animateTransitionForButtonsInView:self.view
containerBeginBounds:beginBounds
containerEndBounds:endBounds
transitionStyle:style];
}
- (void)hideViewsForNewTabPage:(BOOL)hide {
DCHECK(!IsIPadIdiom());
[shadowView_ setHidden:hide];
}
- (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];
}
}
- (void)setBackgroundAlpha:(CGFloat)alpha {
[backgroundView_ setAlpha:alpha];
[shadowView_ setAlpha:alpha];
}
- (void)setStandardControlsTransform:(CGAffineTransform)transform {
for (UIButton* button in standardButtons_) {
[button setTransform:transform];
}
}
- (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)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];
}
- (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();
}
- (IBAction)stackButtonTouchDown:(id)sender {
// Exists only for override by subclasses.
}
+ (CGFloat)toolbarDropShadowHeight {
return 0.0;
}
- (uint32_t)snapshotHash {
// Only the 3 lowest bits are used by UIControlState.
uint32_t hash = [toolsMenuButton_ state] & 0x07;
// When the tools popup controller is valid, it means that the images
// representing the tools menu button have been swapped. Factor that in by
// adding in whether or not the tools popup menu is a valid object, rather
// than trying to figure out which image is currently visible.
hash |= toolsPopupController_ ? (1 << 4) : 0;
// The label of the stack button changes with the number of tabs open.
hash ^= [[stackButton_ titleForState:UIControlStateNormal] hash];
return hash;
}
#pragma mark -
#pragma mark PopupMenuDelegate methods.
- (void)dismissPopupMenu:(PopupMenuController*)controller {
if ([controller isKindOfClass:[ToolsPopupController class]] &&
(ToolsPopupController*)controller == toolsPopupController_)
[self dismissToolsMenuPopup];
}
@end