blob: 57611d30e5a6eabb2b8f8bef34d1ffd5a6e4cfa5 [file] [log] [blame]
// Copyright 2015 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/tab_switcher/tab_switcher_view.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/user_metrics.h"
#import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_header_view.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_panel_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_panel_overlay_view.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_panel_view.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/third_party/material_components_ios/src/components/Buttons/src/MaterialButtons.h"
#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h"
#include "ui/base/l10n/l10n_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const CGFloat kHeaderHeight = 95;
const CGFloat kNewTabButtonMarginFromEdges = 48;
const CGFloat kNewTabButtonWidth = 48;
}
@interface TabSwitcherView ()<UIScrollViewDelegate> {
TabSwitcherHeaderView* _headerView;
UIScrollView* _scrollView;
MDCButton* _openNewTabButton;
NSMutableArray* _panels;
ios_internal::NewTabButtonStyle _openNewTabButtonStyle;
NSInteger _previousPanelIndex;
}
// Returns the header view frame.
- (CGRect)headerViewFrame;
// Returns the scrollview frame.
- (CGRect)scrollViewFrame;
// Returns the new tab button frame.
- (CGRect)openNewTabButtonFrame;
// Returns the new tab button frame when hidden.
- (CGRect)openNewTabButtonFrameOffscreen;
// Called when the new tab button is pressed.
- (void)openNewTabButtonPressed;
// Returns the index of the panel presented in |_scrollview|. When two panels
// are visible, it returns the most visible one.
- (NSInteger)currentPageIndex;
// Select the panel at the given index, updating both the header and content.
// The scrollview scroll will be |animated| if requested.
- (void)selectPanelAtIndex:(NSInteger)index animated:(BOOL)animated;
@end
@implementation TabSwitcherView
@synthesize delegate = delegate_;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_openNewTabButtonStyle = ios_internal::NewTabButtonStyle::UNINITIALIZED;
[self loadSubviews];
_panels = [[NSMutableArray alloc] init];
_previousPanelIndex = -1;
}
return self;
}
- (TabSwitcherHeaderView*)headerView {
return _headerView;
}
- (UIScrollView*)scrollView {
return _scrollView;
}
- (void)layoutSubviews {
[super layoutSubviews];
NSInteger pageIndexBeforeLayout = [self currentPageIndex];
[CATransaction begin];
[CATransaction setDisableActions:YES];
[_headerView setFrame:[self headerViewFrame]];
[_scrollView setFrame:[self scrollViewFrame]];
[CATransaction commit];
[self updateScrollViewContent];
[self selectPanelAtIndex:pageIndexBeforeLayout animated:NO];
}
- (void)selectPanelAtIndex:(NSInteger)index {
[self selectPanelAtIndex:index animated:!UIAccessibilityIsVoiceOverRunning()];
}
- (void)addPanelView:(UIView*)view atIndex:(NSUInteger)index {
if (UseRTLLayout())
[view setTransform:CGAffineTransformMakeScale(-1, 1)];
[_scrollView addSubview:view];
[_panels insertObject:view atIndex:index];
[self updateScrollViewContent];
}
- (void)removePanelViewAtIndex:(NSUInteger)index {
[self removePanelViewAtIndex:index updateScrollView:YES];
}
- (void)removePanelViewAtIndex:(NSUInteger)index updateScrollView:(BOOL)update {
DCHECK_EQ([[_panels objectAtIndex:index] superview], _scrollView);
[[_panels objectAtIndex:index] removeFromSuperview];
[_panels removeObjectAtIndex:index];
if (update)
[self updateScrollViewContent];
}
- (NSInteger)currentPanelIndex {
return [self currentPageIndex];
}
- (void)updateOverlayButtonState {
NSInteger panelIndex = [self currentPageIndex];
ios_internal::NewTabButtonStyle newButtonStyle =
[delegate_ buttonStyleForPanelAtIndex:panelIndex];
[self setNewTabButtonStyle:newButtonStyle];
BOOL dismissButtonVisible =
[self.delegate shouldShowDismissButtonForPanelAtIndex:panelIndex];
[UIView beginAnimations:nil context:NULL];
[[_headerView dismissButton] setAlpha:dismissButtonVisible ? 1.0 : 0.0];
[UIView commitAnimations];
}
#pragma mark - Private
- (void)selectPanelAtIndex:(NSInteger)index animated:(BOOL)animated {
CGPoint pageOffset = CGPointZero;
pageOffset.x = self.bounds.size.width * index;
[_scrollView setContentOffset:pageOffset animated:animated];
[_headerView selectItemAtIndex:index];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
nil);
}
- (NSInteger)currentPageIndex {
if ([_scrollView frame].size.width == 0)
return 0;
NSInteger index = floor(
[_scrollView contentOffset].x / [_scrollView frame].size.width + 0.5);
return std::max<NSInteger>(std::min<NSInteger>(index, [_panels count] - 1),
0);
}
- (void)updateHeaderSelection {
[_headerView selectItemAtIndex:[self currentPageIndex]];
}
- (CGRect)frameForPanelAtIndex:(NSInteger)index {
CGSize panelSize = [_scrollView frame].size;
CGRect panelFrame =
CGRectMake(panelSize.width * index, 0, panelSize.width, panelSize.height);
return panelFrame;
}
- (void)updateScrollViewContent {
CGSize panelSize = [_scrollView frame].size;
CGSize currentContentSize = [_scrollView contentSize];
currentContentSize.width = [_panels count] * panelSize.width;
[_scrollView setContentSize:currentContentSize];
for (NSUInteger i = 0; i < [_panels count]; i++) {
id panelView = [_panels objectAtIndex:i];
[panelView setFrame:[self frameForPanelAtIndex:i]];
}
}
- (void)loadSubviews {
// Creates and add the header view showing the list of panels.
TabSwitcherHeaderView* headerView =
[[TabSwitcherHeaderView alloc] initWithFrame:[self headerViewFrame]];
[self addSubview:headerView];
_headerView = headerView;
// Creates and add the scrollview containing the panels.
UIScrollView* scrollView =
[[UIScrollView alloc] initWithFrame:[self scrollViewFrame]];
[scrollView setBackgroundColor:[[MDCPalette greyPalette] tint900]];
[scrollView setAlwaysBounceHorizontal:YES];
[scrollView setDelegate:self];
[scrollView setPagingEnabled:YES];
[scrollView setDelegate:self];
if (UseRTLLayout())
[scrollView setTransform:CGAffineTransformMakeScale(-1, 1)];
[self addSubview:scrollView];
_scrollView = scrollView;
// Creates and add the floating new tab button.
_openNewTabButton = [[MDCFloatingButton alloc] init];
UIImage* openNewTabButtonImage =
[[UIImage imageNamed:@"tabswitcher_new_tab_fab"]
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[_openNewTabButton setImage:openNewTabButtonImage
forState:UIControlStateNormal];
[[_openNewTabButton imageView] setTintColor:[UIColor whiteColor]];
[_openNewTabButton setFrame:[self openNewTabButtonFrame]];
// When the button is positioned with autolayout, the animation of the
// button's position conflicts with other animations.
[_openNewTabButton
setAutoresizingMask:UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleLeadingMargin()];
[_openNewTabButton addTarget:self
action:@selector(openNewTabButtonPressed)
forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_openNewTabButton];
[self setNewTabButtonStyle:ios_internal::NewTabButtonStyle::GRAY];
}
- (CGRect)headerViewFrame {
const CGFloat kStatusBarHeight =
[[UIApplication sharedApplication] statusBarFrame].size.height;
CGRect headerViewFrame = self.bounds;
headerViewFrame.origin.y += kStatusBarHeight;
headerViewFrame.size.height = kHeaderHeight;
return headerViewFrame;
}
- (CGRect)scrollViewFrame {
CGRect scrollViewFrame = self.bounds;
scrollViewFrame.origin.y = CGRectGetMaxY([self headerViewFrame]);
scrollViewFrame.size.height -= scrollViewFrame.origin.y;
return scrollViewFrame;
}
- (CGRect)openNewTabButtonFrame {
CGRect buttonFrame = self.bounds;
buttonFrame.origin.y = self.bounds.size.height -
kNewTabButtonMarginFromEdges - kNewTabButtonWidth;
if (UseRTLLayout()) {
buttonFrame.origin.x = kNewTabButtonMarginFromEdges;
} else {
buttonFrame.origin.x = self.bounds.size.width -
kNewTabButtonMarginFromEdges - kNewTabButtonWidth;
}
buttonFrame.size.width = kNewTabButtonWidth;
buttonFrame.size.height = kNewTabButtonWidth;
return buttonFrame;
}
- (CGRect)openNewTabButtonFrameOffscreen {
CGRect buttonFrame = [self openNewTabButtonFrame];
buttonFrame.origin.y = self.bounds.size.height + kNewTabButtonWidth;
return buttonFrame;
}
- (void)openNewTabButtonPressed {
[self.delegate openNewTabInPanelAtIndex:[self currentPageIndex]];
}
- (void)setNewTabButtonStyle:(ios_internal::NewTabButtonStyle)newStyle {
if (newStyle == _openNewTabButtonStyle)
return;
_openNewTabButtonStyle = newStyle;
[UIView animateWithDuration:0.25
animations:^{
switch (_openNewTabButtonStyle) {
case ios_internal::NewTabButtonStyle::HIDDEN:
[_openNewTabButton setFrame:[self openNewTabButtonFrameOffscreen]];
break;
case ios_internal::NewTabButtonStyle::BLUE: {
[_openNewTabButton setFrame:[self openNewTabButtonFrame]];
MDCPalette* palette = [MDCPalette cr_bluePalette];
[_openNewTabButton
setInkColor:[[palette tint300] colorWithAlphaComponent:0.5f]];
[_openNewTabButton setBackgroundColor:[palette tint500]
forState:UIControlStateNormal];
[_openNewTabButton
setBackgroundColor:[UIColor colorWithWhite:0.8f alpha:1.0f]
forState:UIControlStateDisabled];
[_openNewTabButton
setAccessibilityLabel:l10n_util::GetNSString(
IDS_IOS_TAB_SWITCHER_CREATE_NEW_TAB)];
break;
}
case ios_internal::NewTabButtonStyle::GRAY: {
[_openNewTabButton setFrame:[self openNewTabButtonFrame]];
MDCPalette* palette = [MDCPalette greyPalette];
[_openNewTabButton
setInkColor:[[palette tint300] colorWithAlphaComponent:0.25f]];
[_openNewTabButton setBackgroundColor:[palette tint500]
forState:UIControlStateNormal];
[_openNewTabButton
setBackgroundColor:[UIColor colorWithWhite:0.8f alpha:1.0f]
forState:UIControlStateDisabled];
[_openNewTabButton
setAccessibilityLabel:
l10n_util::GetNSString(
IDS_IOS_TAB_SWITCHER_CREATE_NEW_INCOGNITO_TAB)];
break;
}
case ios_internal::NewTabButtonStyle::UNINITIALIZED:
NOTREACHED();
}
}
completion:^(BOOL finished) {
// Informs VoiceOver that the |_openNewTabButton| visibility may have
// changed.
UIAccessibilityPostNotification(
UIAccessibilityLayoutChangedNotification, nil);
}];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView {
[self updateHeaderSelection];
}
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
if ([scrollView isDragging]) {
[self updateHeaderSelection];
}
[self updateOverlayButtonState];
NSInteger panelIndex = [self currentPanelIndex];
TabSwitcherPanelOverlayView* overlayView =
base::mac::ObjCCast<TabSwitcherPanelOverlayView>(
[_panels objectAtIndex:panelIndex]);
if (panelIndex != _previousPanelIndex && overlayView &&
[overlayView overlayType] ==
TabSwitcherPanelOverlayType::OVERLAY_PANEL_USER_SIGNED_OUT) {
base::RecordAction(
base::UserMetricsAction("Signin_Impression_FromTabSwitcher"));
}
_previousPanelIndex = panelIndex;
}
#pragma mark - UIScrollViewAccessibilityDelegate
- (NSString*)accessibilityScrollStatusForScrollView:(UIScrollView*)scrollView {
NSInteger panelIndex = [self currentPageIndex];
return [_headerView panelTitleAtIndex:panelIndex];
}
#pragma mark - UIAccessibilityAction
- (BOOL)accessibilityPerformEscape {
// If the Dismiss Button is visible, then the escape gesture triggers the
// dismissal of the tab switcher.
NSInteger panelIndex = [self currentPageIndex];
if ([self.delegate shouldShowDismissButtonForPanelAtIndex:panelIndex]) {
[delegate_ tabSwitcherViewDelegateDismissTabSwitcher:self];
return YES;
}
return NO;
}
- (BOOL)accessibilityPerformMagicTap {
// If the New Tab Button is visible, then the magic tap opens a new tab.
NSInteger panelIndex = [self currentPageIndex];
ios_internal::NewTabButtonStyle buttonStyle =
[delegate_ buttonStyleForPanelAtIndex:panelIndex];
switch (buttonStyle) {
case ios_internal::BLUE:
case ios_internal::GRAY:
[self.delegate openNewTabInPanelAtIndex:panelIndex];
return YES;
case ios_internal::UNINITIALIZED:
case ios_internal::HIDDEN:
return NO;
}
}
@end