| // 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" |
| |
| #include "base/logging.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" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| 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. |
| __weak id<AccessiblePanelSelectorDelegate> _delegate; |
| } |
| - (void)setDelegate:(id<AccessiblePanelSelectorDelegate>)delegate; |
| @end |
| |
| @implementation AccessiblePanelSelectorView |
| |
| - (void)setDelegate:(id<AccessiblePanelSelectorDelegate>)delegate { |
| _delegate = 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> { |
| UICollectionViewFlowLayout* _flowLayout; |
| UICollectionView* _collectionView; |
| AccessiblePanelSelectorView* _accessibilityView; |
| UIButton* _dismissButton; |
| 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); |
| |
| __weak TabSwitcherHeaderView* weakSelf = self; |
| [_collectionView performBatchUpdates:^{ |
| TabSwitcherHeaderView* strongSelf = weakSelf; |
| if (!strongSelf) |
| return; |
| strongSelf->_performingUpdate = YES; |
| updateBlock(strongSelf); |
| strongSelf->_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; |
| } |
| |
| #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]; |
| for (NSNumber* index in indexes) { |
| [array |
| addObject:[NSIndexPath indexPathForItem:[index intValue] inSection:0]]; |
| } |
| return array; |
| } |
| |
| - (void)loadSubviews { |
| 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 = |
| [[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 = [[AccessiblePanelSelectorView alloc] |
| initWithFrame:[self collectionViewFrame]]; |
| [_accessibilityView |
| setAutoresizingMask:UIViewAutoresizingFlexibleBottomMargin | |
| UIViewAutoresizingFlexibleWidth]; |
| [_accessibilityView setDelegate:self]; |
| [_accessibilityView setUserInteractionEnabled:NO]; |
| [self addSubview:_accessibilityView]; |
| |
| _dismissButton = [[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, |
| }; |
| NSDictionary* metrics = @{ |
| @"buttonHeight" : @(kDismissButtonHeight), |
| @"buttonWidth" : @(kDismissButtonWidth), |
| }; |
| ApplyVisualConstraintsWithMetricsAndOptions( |
| constraints, viewsDictionary, metrics, LayoutOptionForRTLSupport(), self); |
| |
| 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]; |
| TabSwitcherSessionCellData* 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]; |
| TabSwitcherSessionCellData* 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 |