Index: ios/chrome/browser/ui/tab_switcher/tab_switcher_header_view.mm |
diff --git a/ios/chrome/browser/ui/tab_switcher/tab_switcher_header_view.mm b/ios/chrome/browser/ui/tab_switcher/tab_switcher_header_view.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..bcaec6550fe76a7c3f34b49a1e41bb3215fff010 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/tab_switcher/tab_switcher_header_view.mm |
@@ -0,0 +1,396 @@ |
+// 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_header_view.h" |
+ |
+#import "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/metrics/user_metrics_action.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_cell.h" |
+#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_session_cell_data.h" |
+#import "ios/chrome/browser/ui/uikit_ui_util.h" |
+#include "ios/chrome/grit/ios_strings.h" |
+#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h" |
+#include "ui/base/l10n/l10n_util.h" |
+ |
+namespace { |
+const CGFloat kCollectionViewTopMargin = 39.0; |
+const CGFloat kCollectionViewHeight = 56.0; |
+const CGFloat kDismissButtonWidth = 46.0; |
+const CGFloat kDismissButtonHeight = 39.0; |
+const CGFloat kCollectionViewCellWidth = 238; |
+const CGFloat kActiveSpaceIndicatorHeight = 2; |
+enum PanelSelectionChangeDirection { RIGHT, LEFT }; |
+} |
+ |
+@protocol AccessiblePanelSelectorDelegate |
+// Scrolls to the panel in the direction |direction|, if possible. |
+- (void)moveToPanelInDirection:(PanelSelectionChangeDirection)direction; |
+@end |
+ |
+// An invisible view that offers VoiceOver control of the panel selection |
+// UICollectionView. |
+// Notes: |
+// Directly subclassing UICollectionView resulted in a tons of unwanted |
+// interactions with the cells. |
+// Subclassing UIAccessibilityElement instead of UIView is not possible if |
+// we want the accessibilityFrame to resize itself using autoresizing masks. |
+@interface AccessiblePanelSelectorView : UIView { |
+ // The delegate which receives actions. |
+ base::WeakNSProtocol<id<AccessiblePanelSelectorDelegate>> _delegate; |
+} |
+- (void)setDelegate:(id<AccessiblePanelSelectorDelegate>)delegate; |
+@end |
+ |
+@implementation AccessiblePanelSelectorView |
+ |
+- (void)setDelegate:(id<AccessiblePanelSelectorDelegate>)delegate { |
+ _delegate.reset(delegate); |
+} |
+ |
+- (UIAccessibilityTraits)accessibilityTraits { |
+ return [super accessibilityTraits] | UIAccessibilityTraitAdjustable | |
+ UIAccessibilityTraitCausesPageTurn; |
+} |
+ |
+- (BOOL)isAccessibilityElement { |
+ return YES; |
+} |
+ |
+- (void)accessibilityIncrement { |
+ [_delegate moveToPanelInDirection:RIGHT]; |
+} |
+ |
+- (void)accessibilityDecrement { |
+ [_delegate moveToPanelInDirection:LEFT]; |
+} |
+ |
+@end |
+ |
+@interface TabSwitcherHeaderView ()<UICollectionViewDataSource, |
+ UICollectionViewDelegate, |
+ AccessiblePanelSelectorDelegate> { |
+ base::scoped_nsobject<UICollectionViewFlowLayout> _flowLayout; |
+ base::scoped_nsobject<UICollectionView> _collectionView; |
+ base::scoped_nsobject<AccessiblePanelSelectorView> _accessibilityView; |
+ base::scoped_nsobject<UIButton> _dismissButton; |
+ base::scoped_nsobject<UIView> _activeSpaceIndicatorView; |
+ |
+ BOOL _performingUpdate; |
+} |
+ |
+// Loads and initializes subviews. |
+- (void)loadSubviews; |
+// Performs layout of the collection view. |
+- (void)layoutCollectionView; |
+ |
+@end |
+ |
+@implementation TabSwitcherHeaderView |
+ |
+@synthesize delegate = _delegate; |
+@synthesize dataSource = _dataSource; |
+ |
+- (instancetype)initWithFrame:(CGRect)frame { |
+ self = [super initWithFrame:frame]; |
+ if (self) { |
+ self.backgroundColor = [[MDCPalette greyPalette] tint900]; |
+ [self loadSubviews]; |
+ } |
+ return self; |
+} |
+ |
+- (void)layoutSubviews { |
+ [super layoutSubviews]; |
+ [self layoutCollectionView]; |
+} |
+ |
+- (void)selectItemAtIndex:(NSInteger)index { |
+ NSInteger selectedIndex = [self selectedIndex]; |
+ if (selectedIndex != index) { |
+ [_collectionView |
+ selectItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0] |
+ animated:NO |
+ scrollPosition:UICollectionViewScrollPositionNone]; |
+ [self updateSelectionAtIndex:index animated:YES]; |
+ } |
+} |
+ |
+- (void)reloadData { |
+ [_collectionView reloadData]; |
+ [_collectionView layoutIfNeeded]; |
+} |
+ |
+- (void)performUpdate:(void (^)(TabSwitcherHeaderView* headerView))updateBlock { |
+ [self performUpdate:updateBlock completion:nil]; |
+} |
+ |
+- (void)performUpdate:(void (^)(TabSwitcherHeaderView* headerView))updateBlock |
+ completion:(ProceduralBlock)completion { |
+ DCHECK(updateBlock); |
+ |
+ __block TabSwitcherHeaderView* weakSelf = self; |
+ [_collectionView performBatchUpdates:^{ |
+ if (!weakSelf) |
+ return; |
+ weakSelf->_performingUpdate = YES; |
+ updateBlock(weakSelf); |
+ weakSelf->_performingUpdate = NO; |
+ } |
+ completion:^(BOOL finished) { |
+ // Reestablish selection after the update. |
+ const NSInteger selectedPanelIndex = |
+ [[weakSelf delegate] tabSwitcherHeaderViewSelectedPanelIndex]; |
+ if (selectedPanelIndex != NSNotFound) |
+ [weakSelf selectItemAtIndex:selectedPanelIndex]; |
+ if (completion) |
+ completion(); |
+ }]; |
+} |
+ |
+- (void)insertSessionsAtIndexes:(NSArray*)indexes { |
+ DCHECK(_performingUpdate); |
+ [_collectionView |
+ insertItemsAtIndexPaths:[self indexPathArrayWithIndexes:indexes]]; |
+} |
+ |
+- (void)removeSessionsAtIndexes:(NSArray*)indexes { |
+ DCHECK(_performingUpdate); |
+ [_collectionView |
+ deleteItemsAtIndexPaths:[self indexPathArrayWithIndexes:indexes]]; |
+} |
+ |
+- (UIView*)dismissButton { |
+ return _dismissButton.get(); |
+} |
+ |
+#pragma mark - Private |
+ |
+- (NSInteger)selectedIndex { |
+ NSInteger selectedIndex = NSNotFound; |
+ NSArray* selectedIndexPaths = [_collectionView indexPathsForSelectedItems]; |
+ if (selectedIndexPaths.count) { |
+ NSIndexPath* selectedIndexPath = selectedIndexPaths[0]; |
+ selectedIndex = selectedIndexPath.item; |
+ } |
+ return selectedIndex; |
+} |
+ |
+- (NSInteger)itemCount { |
+ return [_collectionView numberOfItemsInSection:0]; |
+} |
+ |
+// The UICollectionViewFlowLayout enumerate indexes from right to left when the |
+// UI is configured in RTL mode. This method always returns the index from value |
+// for a left to right enumeration order. |
+- (NSInteger)leftToRightIndexForFlowLayoutIndex:(NSInteger)index { |
+ return UseRTLLayout() ? ([self itemCount] - 1) - index : index; |
+} |
+ |
+- (void)updateSelectionAtIndex:(NSInteger)index animated:(BOOL)animated { |
+ const CGRect cellRect = CGRectMake( |
+ [self leftToRightIndexForFlowLayoutIndex:index] * |
+ kCollectionViewCellWidth, |
+ 0, kCollectionViewCellWidth, [_collectionView bounds].size.height); |
+ [_collectionView scrollRectToVisible:cellRect animated:animated]; |
+ [self layoutActiveSpaceIndicatorAnimated:animated]; |
+} |
+ |
+- (NSArray*)indexPathArrayWithIndexes:(NSArray*)indexes { |
+ NSMutableArray* array = |
+ [[[NSMutableArray alloc] initWithCapacity:indexes.count] autorelease]; |
+ for (NSNumber* index in indexes) { |
+ [array |
+ addObject:[NSIndexPath indexPathForItem:[index intValue] inSection:0]]; |
+ } |
+ return array; |
+} |
+ |
+- (void)loadSubviews { |
+ base::scoped_nsobject<UICollectionViewFlowLayout> flowLayout( |
+ [[UICollectionViewFlowLayout alloc] init]); |
+ [flowLayout setMinimumLineSpacing:0]; |
+ [flowLayout setMinimumInteritemSpacing:0]; |
+ const CGSize cellSize = |
+ CGSizeMake(kCollectionViewCellWidth, kCollectionViewHeight); |
+ [flowLayout setItemSize:cellSize]; |
+ [flowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal]; |
+ _flowLayout = flowLayout; |
+ |
+ _collectionView.reset([[UICollectionView alloc] |
+ initWithFrame:[self collectionViewFrame] |
+ collectionViewLayout:flowLayout]); |
+ [_collectionView setDelegate:self]; |
+ [_collectionView setDataSource:self]; |
+ [_collectionView registerClass:[TabSwitcherHeaderCell class] |
+ forCellWithReuseIdentifier:[TabSwitcherHeaderCell identifier]]; |
+ [_collectionView setShowsVerticalScrollIndicator:NO]; |
+ [_collectionView setShowsHorizontalScrollIndicator:NO]; |
+ [_collectionView setBackgroundColor:[[MDCPalette greyPalette] tint900]]; |
+ [_collectionView setAllowsMultipleSelection:NO]; |
+ [_collectionView setAllowsSelection:YES]; |
+ [_collectionView setIsAccessibilityElement:NO]; |
+ [_collectionView setAccessibilityElementsHidden:YES]; |
+ [self addSubview:_collectionView]; |
+ |
+ _accessibilityView.reset([[AccessiblePanelSelectorView alloc] |
+ initWithFrame:[self collectionViewFrame]]); |
+ [_accessibilityView |
+ setAutoresizingMask:UIViewAutoresizingFlexibleBottomMargin | |
+ UIViewAutoresizingFlexibleWidth]; |
+ [_accessibilityView setDelegate:self]; |
+ [_accessibilityView setUserInteractionEnabled:NO]; |
+ [self addSubview:_accessibilityView]; |
+ |
+ _dismissButton.reset([[UIButton alloc] initWithFrame:CGRectZero]); |
+ UIImage* dismissImage = |
+ [UIImage imageNamed:@"tabswitcher_tab_switcher_button"]; |
+ dismissImage = |
+ [dismissImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; |
+ [_dismissButton setContentMode:UIViewContentModeCenter]; |
+ [_dismissButton setBackgroundColor:[UIColor clearColor]]; |
+ [_dismissButton setTintColor:[UIColor whiteColor]]; |
+ [_dismissButton setImage:dismissImage forState:UIControlStateNormal]; |
+ [_dismissButton |
+ setAccessibilityLabel:l10n_util::GetNSString( |
+ IDS_IOS_TAB_STRIP_LEAVE_TAB_SWITCHER)]; |
+ |
+ [_dismissButton addTarget:self |
+ action:@selector(dismissButtonTouchUpInside:) |
+ forControlEvents:UIControlEventTouchUpInside]; |
+ [_dismissButton setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ [self addSubview:_dismissButton]; |
+ |
+ NSArray* constraints = @[ |
+ @"V:|-0-[dismissButton(==buttonHeight)]", |
+ @"H:[dismissButton(==buttonWidth)]-0-|", |
+ ]; |
+ NSDictionary* viewsDictionary = @{ |
+ @"dismissButton" : _dismissButton.get(), |
+ }; |
+ NSDictionary* metrics = @{ |
+ @"buttonHeight" : @(kDismissButtonHeight), |
+ @"buttonWidth" : @(kDismissButtonWidth), |
+ }; |
+ ApplyVisualConstraintsWithMetricsAndOptions( |
+ constraints, viewsDictionary, metrics, LayoutOptionForRTLSupport(), self); |
+ |
+ base::scoped_nsobject<UIView> activeSpaceIndicatorView( |
+ [[UIView alloc] initWithFrame:CGRectZero]); |
+ [activeSpaceIndicatorView |
+ setBackgroundColor:[[MDCPalette cr_bluePalette] tint500]]; |
+ [activeSpaceIndicatorView |
+ setFrame:CGRectMake( |
+ 0, self.bounds.size.height - kActiveSpaceIndicatorHeight, |
+ kCollectionViewCellWidth, kActiveSpaceIndicatorHeight)]; |
+ [self addSubview:activeSpaceIndicatorView]; |
+ _activeSpaceIndicatorView = activeSpaceIndicatorView; |
+} |
+ |
+- (void)layoutCollectionView { |
+ NSInteger selectedIndex = [self selectedIndex]; |
+ [_collectionView setFrame:[self collectionViewFrame]]; |
+ if (selectedIndex != NSNotFound) |
+ [self updateSelectionAtIndex:selectedIndex animated:YES]; |
+} |
+ |
+- (CGRect)collectionViewFrame { |
+ return CGRectMake(0, kCollectionViewTopMargin, self.bounds.size.width, |
+ kCollectionViewHeight); |
+} |
+ |
+- (void)layoutActiveSpaceIndicatorAnimated:(BOOL)animated { |
+ [self setPanelSelectorAccessibility]; |
+ NSInteger selectedIndex = [self selectedIndex]; |
+ if (selectedIndex == NSNotFound) |
+ return; |
+ CGRect indicatorFrame = [_activeSpaceIndicatorView bounds]; |
+ indicatorFrame.origin.y = |
+ self.bounds.size.height - kActiveSpaceIndicatorHeight; |
+ indicatorFrame.origin.x = |
+ kCollectionViewCellWidth * |
+ [self leftToRightIndexForFlowLayoutIndex:selectedIndex] - |
+ [_collectionView contentOffset].x; |
+ if (animated) |
+ [UIView beginAnimations:nil context:NULL]; |
+ [_activeSpaceIndicatorView setFrame:indicatorFrame]; |
+ if (animated) |
+ [UIView commitAnimations]; |
+} |
+ |
+- (void)dismissButtonTouchUpInside:(UIButton*)button { |
+ [self.delegate tabSwitcherHeaderViewDismiss:self]; |
+} |
+ |
+- (void)setPanelSelectorAccessibility { |
+ NSInteger index = [self selectedIndex]; |
+ if (index != NSNotFound) |
+ [_accessibilityView setAccessibilityLabel:[self panelTitleAtIndex:index]]; |
+} |
+ |
+- (NSString*)panelTitleAtIndex:(NSInteger)index { |
+ NSIndexPath* indexPath = [NSIndexPath indexPathForItem:index inSection:0]; |
+ SessionCellData* sessionCellData = |
+ [[self dataSource] sessionCellDataAtIndex:indexPath.row]; |
+ return sessionCellData.title; |
+} |
+ |
+#pragma mark - AccessiblePanelSelectorDelegate |
+ |
+- (void)moveToPanelInDirection:(PanelSelectionChangeDirection)direction { |
+ NSInteger indexDelta = direction == RIGHT ? 1 : -1; |
+ NSInteger newIndex = [self selectedIndex] + indexDelta; |
+ newIndex = std::max<NSInteger>(newIndex, 0); |
+ newIndex = std::min<NSInteger>( |
+ newIndex, |
+ [self collectionView:_collectionView numberOfItemsInSection:0] - 1); |
+ NSIndexPath* newIndexPath = |
+ [NSIndexPath indexPathForItem:newIndex inSection:0]; |
+ [_collectionView |
+ selectItemAtIndexPath:newIndexPath |
+ animated:NO |
+ scrollPosition:UICollectionViewScrollPositionCenteredHorizontally]; |
+ [self updateSelectionAtIndex:newIndexPath.item animated:NO]; |
+ [[self delegate] |
+ tabSwitcherHeaderViewDidSelectSessionAtIndex:newIndexPath.item]; |
+} |
+ |
+#pragma mark - UICollectionViewDataSource |
+ |
+- (NSInteger)collectionView:(UICollectionView*)collectionView |
+ numberOfItemsInSection:(NSInteger)section { |
+ DCHECK([self dataSource]); |
+ DCHECK(section == 0); |
+ return [[self dataSource] tabSwitcherHeaderViewSessionCount]; |
+} |
+ |
+- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
+ cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
+ TabSwitcherHeaderCell* headerCell = [collectionView |
+ dequeueReusableCellWithReuseIdentifier:[TabSwitcherHeaderCell identifier] |
+ forIndexPath:indexPath]; |
+ SessionCellData* sessionCellData = |
+ [[self dataSource] sessionCellDataAtIndex:indexPath.row]; |
+ [headerCell loadSessionCellData:sessionCellData]; |
+ return headerCell; |
+} |
+ |
+#pragma mark - UICollectionViewDelegate |
+ |
+- (void)collectionView:(UICollectionView*)collectionView |
+ didSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
+ [self updateSelectionAtIndex:indexPath.item animated:YES]; |
+ [[self delegate] tabSwitcherHeaderViewDidSelectSessionAtIndex:indexPath.item]; |
+} |
+ |
+#pragma mark - UIScrollViewDelegate |
+ |
+- (void)scrollViewDidScroll:(UIScrollView*)scrollView { |
+ [self layoutActiveSpaceIndicatorAnimated:NO]; |
+} |
+ |
+@end |