| // Copyright 2019-present the Material Components for iOS authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #import "MDCTabBarView.h" |
| |
| #import "MDCTabBarItemCustomViewing.h" |
| #import "MDCTabBarViewCustomViewable.h" |
| #import "MDCTabBarViewDelegate.h" |
| #import "MDCTabBarViewIndicatorTemplate.h" |
| #import "MDCTabBarViewUnderlineIndicatorTemplate.h" |
| #import "private/MDCTabBarViewIndicatorView.h" |
| #import "private/MDCTabBarViewItemView.h" |
| #import "private/MDCTabBarViewPrivateIndicatorContext.h" |
| |
| #import <CoreGraphics/CoreGraphics.h> |
| #import <MDFInternationalization/MDFInternationalization.h> |
| #import <MaterialComponents/MaterialAnimationTiming.h> |
| #import <QuartzCore/QuartzCore.h> |
| |
| // KVO contexts |
| static char *const kKVOContextMDCTabBarView = "kKVOContextMDCTabBarView"; |
| |
| /** Minimum (typical) height of a Material Tab bar. */ |
| static const CGFloat kMinHeight = 48; |
| |
| /** Maximum width of an item view. */ |
| static const CGFloat kMaxItemWidth = 360; |
| |
| /** The leading edge inset for scrollable tabs. */ |
| static const CGFloat kScrollableTabsLeadingEdgeInset = 52; |
| |
| /** The height of the bottom divider view. */ |
| static const CGFloat kBottomDividerHeight = 1; |
| |
| /// Default duration in seconds for selection change animations. |
| static const NSTimeInterval kSelectionChangeAnimationDuration = 0.3; |
| |
| static NSString *const kSelectedImageKeyPath = @"selectedImage"; |
| static NSString *const kImageKeyPath = @"image"; |
| static NSString *const kTitleKeyPath = @"title"; |
| static NSString *const kAccessibilityLabelKeyPath = @"accessibilityLabel"; |
| static NSString *const kAccessibilityHintKeyPath = @"accessibilityHint"; |
| static NSString *const kAccessibilityIdentifierKeyPath = @"accessibilityIdentifier"; |
| static NSString *const kAccessibilityTraitsKeyPath = @"accessibilityTraits"; |
| |
| @interface MDCTabBarView () |
| |
| /** The views representing each tab bar item. */ |
| @property(nonnull, nonatomic, copy) NSArray<UIView *> *itemViews; |
| |
| /** The bottom divider view shown behind the default indicator template. */ |
| @property(nonnull, nonatomic, strong) UIView *bottomDividerView; |
| |
| /** @c YES if the items are laid-out in a scrollable style. */ |
| @property(nonatomic, readonly) BOOL isScrollableLayoutStyle; |
| |
| /** Used to scroll to the selected item during the first call to @c layoutSubviews. */ |
| @property(nonatomic, assign) BOOL needsScrollToSelectedItem; |
| |
| /** The view that renders @c selectionIndicatorTemplate. */ |
| @property(nonnull, nonatomic, strong) MDCTabBarViewIndicatorView *selectionIndicatorView; |
| |
| /** The title colors for bar items. */ |
| @property(nonnull, nonatomic, strong) NSMutableDictionary<NSNumber *, UIColor *> *stateToTitleColor; |
| |
| /** The image tint colors for bar items. */ |
| @property(nonnull, nonatomic, strong) |
| NSMutableDictionary<NSNumber *, UIColor *> *stateToImageTintColor; |
| |
| /** The title font for bar items. */ |
| @property(nonnull, nonatomic, strong) NSMutableDictionary<NSNumber *, UIFont *> *stateToTitleFont; |
| |
| /** |
| The content padding (as UIEdgeInsets) for each layout style. The layout style is stored as an |
| @c NSNumber of the raw enumeration value. The padding @c UIEdgeInsets is stored as an @c NSValue. |
| */ |
| @property(nonnull, nonatomic, strong) |
| NSMutableDictionary<NSNumber *, NSValue *> *layoutStyleToContentPadding; |
| |
| @end |
| |
| @implementation MDCTabBarView |
| // We're overriding UIScrollViewDelegate's delegate solely to change its type (we don't provide |
| // a getter or setter implementation), thus the @dynamic. |
| @dynamic delegate; |
| |
| #pragma mark - Initialization |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _rippleColor = [[UIColor alloc] initWithWhite:0 alpha:(CGFloat)0.16]; |
| _needsScrollToSelectedItem = YES; |
| _items = @[]; |
| _stateToImageTintColor = [NSMutableDictionary dictionary]; |
| _stateToTitleColor = [NSMutableDictionary dictionary]; |
| _stateToTitleFont = [NSMutableDictionary dictionary]; |
| _preferredLayoutStyle = MDCTabBarViewLayoutStyleFixed; |
| _layoutStyleToContentPadding = [NSMutableDictionary dictionary]; |
| _layoutStyleToContentPadding[@(MDCTabBarViewLayoutStyleScrollable)] = |
| [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, kScrollableTabsLeadingEdgeInset, 0, 0)]; |
| self.backgroundColor = UIColor.whiteColor; |
| self.showsHorizontalScrollIndicator = NO; |
| |
| _selectionIndicatorView = [[MDCTabBarViewIndicatorView alloc] init]; |
| _selectionIndicatorView.translatesAutoresizingMaskIntoConstraints = NO; |
| _selectionIndicatorView.userInteractionEnabled = NO; |
| _selectionIndicatorView.tintColor = UIColor.blackColor; |
| _selectionIndicatorView.indicatorPathAnimationDuration = kSelectionChangeAnimationDuration; |
| _selectionIndicatorView.indicatorPathTimingFunction = |
| [CAMediaTimingFunction mdc_functionWithType:MDCAnimationTimingFunctionEaseInOut]; |
| |
| _selectionIndicatorTemplate = [[MDCTabBarViewUnderlineIndicatorTemplate alloc] init]; |
| |
| // The bottom divider is positioned behind the selection indicator. |
| _bottomDividerView = [[UIView alloc] init]; |
| _bottomDividerView.backgroundColor = UIColor.clearColor; |
| [self addSubview:_bottomDividerView]; |
| |
| // The selection indicator is positioned behind the item views. |
| [self addSubview:_selectionIndicatorView]; |
| |
| // By default, inset the content within the safe area. This is generally the desired behavior, |
| // but clients can override it if they want. |
| if (@available(iOS 11.0, *)) { |
| [super setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentAlways]; |
| } |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [self removeObserversFromTabBarItems]; |
| } |
| |
| #pragma mark - Properties |
| |
| - (void)setBarTintColor:(UIColor *)barTintColor { |
| self.backgroundColor = barTintColor; |
| } |
| |
| - (UIColor *)barTintColor { |
| return self.backgroundColor; |
| } |
| |
| - (void)updateRippleColorForAllViews { |
| for (UIView *subview in self.itemViews) { |
| if (![subview isKindOfClass:[MDCTabBarViewItemView class]]) { |
| continue; |
| } |
| MDCTabBarViewItemView *itemView = (MDCTabBarViewItemView *)subview; |
| itemView.rippleTouchController.rippleView.rippleColor = self.rippleColor; |
| } |
| } |
| |
| - (void)setRippleColor:(UIColor *)rippleColor { |
| _rippleColor = [rippleColor copy]; |
| [self updateRippleColorForAllViews]; |
| } |
| |
| - (void)setSelectionIndicatorStrokeColor:(UIColor *)selectionIndicatorStrokeColor { |
| _selectionIndicatorStrokeColor = selectionIndicatorStrokeColor ?: UIColor.blackColor; |
| self.selectionIndicatorView.tintColor = self.selectionIndicatorStrokeColor; |
| } |
| |
| - (void)setBottomDividerColor:(UIColor *)bottomDividerColor { |
| self.bottomDividerView.backgroundColor = bottomDividerColor; |
| } |
| |
| - (UIColor *)bottomDividerColor { |
| return self.bottomDividerView.backgroundColor; |
| } |
| |
| - (void)setPreferredLayoutStyle:(MDCTabBarViewLayoutStyle)preferredLayoutStyle { |
| _preferredLayoutStyle = preferredLayoutStyle; |
| [self setNeedsLayout]; |
| [self invalidateIntrinsicContentSize]; |
| } |
| |
| - (void)setItems:(NSArray<UITabBarItem *> *)items { |
| NSParameterAssert(items); |
| |
| if (self.items == items || [self.items isEqual:items]) { |
| return; |
| } |
| |
| [self removeObserversFromTabBarItems]; |
| for (UIView *view in self.itemViews) { |
| [view removeFromSuperview]; |
| } |
| |
| _items = [items copy]; |
| NSMutableArray<UIView *> *itemViews = [NSMutableArray array]; |
| |
| for (UITabBarItem *item in self.items) { |
| UIView *itemView; |
| if ([item conformsToProtocol:@protocol(MDCTabBarItemCustomViewing)]) { |
| UITabBarItem<MDCTabBarItemCustomViewing> *customItem = |
| (UITabBarItem<MDCTabBarItemCustomViewing> *)item; |
| UIView *customView = customItem.mdc_customView; |
| if (customView) { |
| itemView = customView; |
| } |
| } |
| if (!itemView) { |
| MDCTabBarViewItemView *mdcItemView = [[MDCTabBarViewItemView alloc] init]; |
| mdcItemView.titleLabel.text = item.title; |
| mdcItemView.accessibilityLabel = item.accessibilityLabel; |
| mdcItemView.accessibilityHint = item.accessibilityHint; |
| mdcItemView.accessibilityIdentifier = item.accessibilityIdentifier; |
| mdcItemView.accessibilityTraits = item.accessibilityTraits == UIAccessibilityTraitNone |
| ? UIAccessibilityTraitButton |
| : item.accessibilityTraits; |
| mdcItemView.titleLabel.textColor = [self titleColorForState:UIControlStateNormal]; |
| mdcItemView.image = item.image; |
| mdcItemView.selectedImage = item.selectedImage; |
| mdcItemView.rippleTouchController.rippleView.rippleColor = self.rippleColor; |
| mdcItemView.rippleTouchController.shouldProcessRippleWithScrollViewGestures = NO; |
| itemView = mdcItemView; |
| } |
| UITapGestureRecognizer *tapGesture = |
| [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapItemView:)]; |
| [itemView addGestureRecognizer:tapGesture]; |
| |
| [self addSubview:itemView]; |
| [itemViews addObject:itemView]; |
| } |
| |
| self.itemViews = itemViews; |
| |
| // Determine new selected item, defaulting to nil. |
| UITabBarItem *newSelectedItem = nil; |
| if (self.selectedItem && [self.items containsObject:self.selectedItem]) { |
| // Previously-selected item still around: Preserve selection. |
| newSelectedItem = self.selectedItem; |
| } |
| |
| self.selectedItem = newSelectedItem; |
| [self addObserversToTabBarItems]; |
| |
| [self invalidateIntrinsicContentSize]; |
| [self setNeedsLayout]; |
| } |
| |
| - (void)setSelectedItem:(UITabBarItem *)selectedItem { |
| [self setSelectedItem:selectedItem animated:YES]; |
| } |
| |
| - (void)setSelectedItem:(UITabBarItem *)selectedItem animated:(BOOL)animated { |
| if (self.selectedItem == selectedItem) { |
| return; |
| } |
| |
| // Sets the old selected item view's traits back. |
| NSUInteger oldSelectedItemIndex = [self.items indexOfObject:self.selectedItem]; |
| if (oldSelectedItemIndex != NSNotFound) { |
| UIView *oldSelectedItemView = self.itemViews[oldSelectedItemIndex]; |
| oldSelectedItemView.accessibilityTraits = |
| (oldSelectedItemView.accessibilityTraits & ~UIAccessibilityTraitSelected); |
| if ([oldSelectedItemView conformsToProtocol:@protocol(MDCTabBarViewCustomViewable)]) { |
| UIView<MDCTabBarViewCustomViewable> *customViewableView = |
| (UIView<MDCTabBarViewCustomViewable> *)oldSelectedItemView; |
| [customViewableView setSelected:NO animated:animated]; |
| } |
| } |
| |
| // Handle setting to `nil` without passing it to the nonnull parameter in `indexOfObject:` |
| if (!selectedItem) { |
| _selectedItem = selectedItem; |
| [self updateTitleColorForAllViews]; |
| [self updateImageTintColorForAllViews]; |
| [self updateTitleFontForAllViews]; |
| [self didSelectItemAtIndex:NSNotFound animateTransition:animated]; |
| return; |
| } |
| |
| NSUInteger itemIndex = [self.items indexOfObject:selectedItem]; |
| // Don't crash, just ignore if `selectedItem` isn't present in `_items`. This is the same behavior |
| // as UITabBar. |
| if (itemIndex == NSNotFound) { |
| return; |
| } |
| _selectedItem = selectedItem; |
| |
| UIView *newSelectedItemView = self.itemViews[itemIndex]; |
| newSelectedItemView.accessibilityTraits = |
| (newSelectedItemView.accessibilityTraits | UIAccessibilityTraitSelected); |
| if ([newSelectedItemView conformsToProtocol:@protocol(MDCTabBarViewCustomViewable)]) { |
| UIView<MDCTabBarViewCustomViewable> *customViewableView = |
| (UIView<MDCTabBarViewCustomViewable> *)newSelectedItemView; |
| [customViewableView setSelected:YES animated:animated]; |
| } |
| [self updateTitleColorForAllViews]; |
| [self updateImageTintColorForAllViews]; |
| [self updateTitleFontForAllViews]; |
| [self scrollRectToVisible:self.itemViews[itemIndex].frame animated:animated]; |
| [self didSelectItemAtIndex:itemIndex animateTransition:animated]; |
| } |
| |
| - (void)updateImageTintColorForAllViews { |
| for (UITabBarItem *item in self.items) { |
| NSUInteger indexOfItem = [self.items indexOfObject:item]; |
| // This is a significant error, but defensive coding is preferred. |
| if (indexOfItem == NSNotFound || indexOfItem >= self.itemViews.count) { |
| NSAssert(NO, @"Unable to find associated item view for (%@)", item); |
| continue; |
| } |
| UIView *itemView = self.itemViews[indexOfItem]; |
| // Skip custom views |
| if (![itemView isKindOfClass:[MDCTabBarViewItemView class]]) { |
| continue; |
| } |
| MDCTabBarViewItemView *tabBarViewItemView = (MDCTabBarViewItemView *)itemView; |
| if (item == self.selectedItem) { |
| tabBarViewItemView.iconImageView.tintColor = |
| [self imageTintColorForState:UIControlStateSelected]; |
| } else { |
| tabBarViewItemView.iconImageView.tintColor = |
| [self imageTintColorForState:UIControlStateNormal]; |
| } |
| } |
| } |
| |
| - (void)setImageTintColor:(UIColor *)imageTintColor forState:(UIControlState)state { |
| self.stateToImageTintColor[@(state)] = imageTintColor; |
| [self updateImageTintColorForAllViews]; |
| } |
| |
| - (UIColor *)imageTintColorForState:(UIControlState)state { |
| UIColor *color = self.stateToImageTintColor[@(state)]; |
| if (color == nil) { |
| color = self.stateToImageTintColor[@(UIControlStateNormal)]; |
| } |
| return color; |
| } |
| |
| - (void)updateTitleColorForAllViews { |
| for (UITabBarItem *item in self.items) { |
| NSUInteger indexOfItem = [self.items indexOfObject:item]; |
| // This is a significant error, but defensive coding is preferred. |
| if (indexOfItem == NSNotFound || indexOfItem >= self.itemViews.count) { |
| NSAssert(NO, @"Unable to find associated item view for (%@)", item); |
| continue; |
| } |
| UIView *itemView = self.itemViews[indexOfItem]; |
| // Skip custom views |
| if (![itemView isKindOfClass:[MDCTabBarViewItemView class]]) { |
| continue; |
| } |
| MDCTabBarViewItemView *tabBarViewItemView = (MDCTabBarViewItemView *)itemView; |
| if (item == self.selectedItem) { |
| tabBarViewItemView.titleLabel.textColor = [self titleColorForState:UIControlStateSelected]; |
| } else { |
| tabBarViewItemView.titleLabel.textColor = [self titleColorForState:UIControlStateNormal]; |
| } |
| } |
| } |
| |
| - (void)setTitleColor:(UIColor *)titleColor forState:(UIControlState)state { |
| self.stateToTitleColor[@(state)] = titleColor; |
| [self updateTitleColorForAllViews]; |
| } |
| |
| - (UIColor *)titleColorForState:(UIControlState)state { |
| UIColor *titleColor = self.stateToTitleColor[@(state)]; |
| if (!titleColor) { |
| titleColor = self.stateToTitleColor[@(UIControlStateNormal)]; |
| } |
| return titleColor; |
| } |
| |
| - (void)updateTitleFontForAllViews { |
| for (UITabBarItem *item in self.items) { |
| NSUInteger indexOfItem = [self.items indexOfObject:item]; |
| // This is a significant error, but defensive coding is preferred. |
| if (indexOfItem == NSNotFound || indexOfItem >= self.itemViews.count) { |
| NSAssert(NO, @"Unable to find associated item view for (%@)", item); |
| continue; |
| } |
| UIView *itemView = self.itemViews[indexOfItem]; |
| // Skip custom views |
| if (![itemView isKindOfClass:[MDCTabBarViewItemView class]]) { |
| continue; |
| } |
| MDCTabBarViewItemView *tabBarViewItemView = (MDCTabBarViewItemView *)itemView; |
| if (item == self.selectedItem) { |
| tabBarViewItemView.titleLabel.font = [self titleFontForState:UIControlStateSelected]; |
| } else { |
| tabBarViewItemView.titleLabel.font = [self titleFontForState:UIControlStateNormal]; |
| } |
| [itemView invalidateIntrinsicContentSize]; |
| [itemView setNeedsLayout]; |
| } |
| } |
| |
| - (void)setTitleFont:(UIFont *)titleFont forState:(UIControlState)state { |
| self.stateToTitleFont[@(state)] = titleFont; |
| [self updateTitleFontForAllViews]; |
| } |
| |
| - (UIFont *)titleFontForState:(UIControlState)state { |
| UIFont *titleFont = self.stateToTitleFont[@(state)]; |
| if (!titleFont) { |
| titleFont = self.stateToTitleFont[@(UIControlStateNormal)]; |
| } |
| return titleFont; |
| } |
| |
| - (void)setSelectionIndicatorTemplate: |
| (id<MDCTabBarViewIndicatorTemplate>)selectionIndicatorTemplate { |
| _selectionIndicatorTemplate = selectionIndicatorTemplate; |
| if (self.selectedItem) { |
| [self.selectionIndicatorView setNeedsLayout]; |
| } |
| } |
| |
| - (void)setContentPadding:(UIEdgeInsets)contentPadding |
| forLayoutStyle:(MDCTabBarViewLayoutStyle)layoutStyle { |
| self.layoutStyleToContentPadding[@(layoutStyle)] = [NSValue valueWithUIEdgeInsets:contentPadding]; |
| if ([self effectiveLayoutStyle] == layoutStyle) { |
| [self setNeedsLayout]; |
| } |
| } |
| |
| - (UIEdgeInsets)contentPaddingForLayoutStyle:(MDCTabBarViewLayoutStyle)layoutStyle { |
| NSValue *paddingValue = self.layoutStyleToContentPadding[@(layoutStyle)]; |
| if (paddingValue) { |
| return paddingValue.UIEdgeInsetsValue; |
| } |
| return UIEdgeInsetsZero; |
| } |
| |
| #pragma mark - UIAccessibility |
| |
| - (BOOL)isAccessibilityElement { |
| return NO; |
| } |
| |
| - (UIAccessibilityTraits)accessibilityTraits { |
| if (@available(iOS 10.0, *)) { |
| return [super accessibilityTraits] | UIAccessibilityTraitTabBar; |
| } |
| return [super accessibilityTraits]; |
| } |
| |
| #pragma mark - Custom APIs |
| |
| - (id)accessibilityElementForItem:(UITabBarItem *)item { |
| NSUInteger itemIndex = [self.items indexOfObject:item]; |
| if (itemIndex == NSNotFound || itemIndex >= self.itemViews.count) { |
| return nil; |
| } |
| return self.itemViews[itemIndex]; |
| } |
| |
| - (CGRect)rectForItem:(UITabBarItem *)item |
| inCoordinateSpace:(id<UICoordinateSpace>)coordinateSpace { |
| if (item == nil) { |
| return CGRectNull; |
| } |
| NSUInteger index = [self.items indexOfObject:item]; |
| if (index == NSNotFound || index >= self.itemViews.count) { |
| return CGRectNull; |
| } |
| CGRect frame = CGRectStandardize(self.itemViews[index].frame); |
| return [coordinateSpace convertRect:frame fromCoordinateSpace:self]; |
| } |
| |
| - (CFTimeInterval)selectionChangeAnimationDuration { |
| return kSelectionChangeAnimationDuration; |
| } |
| |
| - (CAMediaTimingFunction *)selectionChangeAnimationTimingFunction { |
| return [CAMediaTimingFunction mdc_functionWithType:MDCAnimationTimingFunctionEaseInOut]; |
| } |
| |
| #pragma mark - Key-Value Observing (KVO) |
| |
| - (void)addObserversToTabBarItems { |
| for (UITabBarItem *item in self.items) { |
| [item addObserver:self |
| forKeyPath:kImageKeyPath |
| options:NSKeyValueObservingOptionNew |
| context:kKVOContextMDCTabBarView]; |
| [item addObserver:self |
| forKeyPath:kSelectedImageKeyPath |
| options:NSKeyValueObservingOptionNew |
| context:kKVOContextMDCTabBarView]; |
| [item addObserver:self |
| forKeyPath:kTitleKeyPath |
| options:NSKeyValueObservingOptionNew |
| context:kKVOContextMDCTabBarView]; |
| [item addObserver:self |
| forKeyPath:kAccessibilityLabelKeyPath |
| options:NSKeyValueObservingOptionNew |
| context:kKVOContextMDCTabBarView]; |
| [item addObserver:self |
| forKeyPath:kAccessibilityHintKeyPath |
| options:NSKeyValueObservingOptionNew |
| context:kKVOContextMDCTabBarView]; |
| [item addObserver:self |
| forKeyPath:kAccessibilityIdentifierKeyPath |
| options:NSKeyValueObservingOptionNew |
| context:kKVOContextMDCTabBarView]; |
| [item addObserver:self |
| forKeyPath:kAccessibilityTraitsKeyPath |
| options:NSKeyValueObservingOptionNew |
| context:kKVOContextMDCTabBarView]; |
| } |
| } |
| |
| - (void)removeObserversFromTabBarItems { |
| for (UITabBarItem *item in self.items) { |
| [item removeObserver:self forKeyPath:kImageKeyPath context:kKVOContextMDCTabBarView]; |
| [item removeObserver:self forKeyPath:kSelectedImageKeyPath context:kKVOContextMDCTabBarView]; |
| [item removeObserver:self forKeyPath:kTitleKeyPath context:kKVOContextMDCTabBarView]; |
| [item removeObserver:self |
| forKeyPath:kAccessibilityLabelKeyPath |
| context:kKVOContextMDCTabBarView]; |
| [item removeObserver:self |
| forKeyPath:kAccessibilityHintKeyPath |
| context:kKVOContextMDCTabBarView]; |
| [item removeObserver:self |
| forKeyPath:kAccessibilityIdentifierKeyPath |
| context:kKVOContextMDCTabBarView]; |
| [item removeObserver:self |
| forKeyPath:kAccessibilityTraitsKeyPath |
| context:kKVOContextMDCTabBarView]; |
| } |
| } |
| |
| - (void)observeValueForKeyPath:(NSString *)keyPath |
| ofObject:(id)object |
| change:(NSDictionary<NSKeyValueChangeKey, id> *)change |
| context:(void *)context { |
| if (context == kKVOContextMDCTabBarView) { |
| if (!object) { |
| return; |
| } |
| NSUInteger indexOfObject = [self.items indexOfObject:object]; |
| if (indexOfObject == NSNotFound) { |
| return; |
| } |
| // Don't try to update custom views |
| UIView *updatedItemView = self.itemViews[indexOfObject]; |
| if (![updatedItemView isKindOfClass:[MDCTabBarViewItemView class]]) { |
| return; |
| } |
| MDCTabBarViewItemView *tabBarItemView = (MDCTabBarViewItemView *)updatedItemView; |
| id newValue = [object valueForKey:keyPath]; |
| if (newValue == [NSNull null]) { |
| newValue = nil; |
| } |
| if ([keyPath isEqualToString:kImageKeyPath]) { |
| tabBarItemView.image = newValue; |
| [self markIntrinsicContentSizeAndLayoutNeedingUpdateForSelfAndItemView:tabBarItemView]; |
| } else if ([keyPath isEqualToString:kSelectedImageKeyPath]) { |
| tabBarItemView.selectedImage = newValue; |
| [self markIntrinsicContentSizeAndLayoutNeedingUpdateForSelfAndItemView:tabBarItemView]; |
| } else if ([keyPath isEqualToString:kTitleKeyPath]) { |
| tabBarItemView.titleLabel.text = newValue; |
| [self markIntrinsicContentSizeAndLayoutNeedingUpdateForSelfAndItemView:tabBarItemView]; |
| } else if ([keyPath isEqualToString:kAccessibilityLabelKeyPath]) { |
| tabBarItemView.accessibilityLabel = newValue; |
| } else if ([keyPath isEqualToString:kAccessibilityHintKeyPath]) { |
| tabBarItemView.accessibilityHint = newValue; |
| } else if ([keyPath isEqualToString:kAccessibilityIdentifierKeyPath]) { |
| tabBarItemView.accessibilityIdentifier = newValue; |
| } else if ([keyPath isEqualToString:kAccessibilityTraitsKeyPath]) { |
| tabBarItemView.accessibilityTraits = [change[NSKeyValueChangeNewKey] unsignedLongLongValue]; |
| if (tabBarItemView.accessibilityTraits == UIAccessibilityTraitNone) { |
| tabBarItemView.accessibilityTraits = UIAccessibilityTraitButton; |
| } |
| if (object == self.selectedItem) { |
| tabBarItemView.accessibilityTraits = |
| (tabBarItemView.accessibilityTraits | UIAccessibilityTraitSelected); |
| } |
| } |
| } else { |
| [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
| } |
| } |
| |
| - (void)markIntrinsicContentSizeAndLayoutNeedingUpdateForSelfAndItemView:(UIView *)itemView { |
| [itemView invalidateIntrinsicContentSize]; |
| [itemView setNeedsLayout]; |
| [self invalidateIntrinsicContentSize]; |
| [self setNeedsLayout]; |
| } |
| |
| #pragma mark - UIView |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| |
| switch ([self effectiveLayoutStyle]) { |
| case MDCTabBarViewLayoutStyleFixed: { |
| [self layoutSubviewsForJustifiedLayout]; |
| break; |
| } |
| case MDCTabBarViewLayoutStyleFixedClusteredCentered: |
| case MDCTabBarViewLayoutStyleFixedClusteredTrailing: |
| case MDCTabBarViewLayoutStyleFixedClusteredLeading: { |
| [self layoutSubviewsForFixedClusteredLayout:[self effectiveLayoutStyle]]; |
| break; |
| } |
| case MDCTabBarViewLayoutStyleScrollable: { |
| [self layoutSubviewsForScrollableLayout]; |
| break; |
| } |
| } |
| |
| self.contentSize = [self calculatedContentSize]; |
| [self updateSelectionIndicatorToIndex:[self.items indexOfObject:self.selectedItem]]; |
| |
| if (self.needsScrollToSelectedItem) { |
| self.needsScrollToSelectedItem = NO; |
| // In RTL layouts, make sure we "begin" the selected item scroll offset from the leading edge. |
| if (self.mdf_effectiveUserInterfaceLayoutDirection == |
| UIUserInterfaceLayoutDirectionRightToLeft) { |
| CGFloat viewWidth = CGRectGetWidth(self.bounds); |
| if (viewWidth < self.contentSize.width) { |
| self.contentOffset = CGPointMake(self.contentSize.width - viewWidth, self.contentOffset.y); |
| } |
| } |
| [self scrollUntilSelectedItemIsVisibleWithoutAnimation]; |
| } |
| // It's possible that after scrolling the minX of bounds could have changed. Positioning it last |
| // ensures that its frame matches the displayed content bounds. |
| self.bottomDividerView.frame = |
| CGRectMake(CGRectGetMinX(self.bounds), CGRectGetMaxY(self.bounds) - kBottomDividerHeight, |
| CGRectGetWidth(self.bounds), kBottomDividerHeight); |
| } |
| |
| - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| |
| if (self.traitCollectionDidChangeBlock) { |
| self.traitCollectionDidChangeBlock(self, previousTraitCollection); |
| } |
| } |
| |
| - (BOOL)isScrollableLayoutStyle { |
| return [self effectiveLayoutStyle] == MDCTabBarViewLayoutStyleScrollable; |
| } |
| |
| /** |
| The current layout style of the Tab Bar. Although the user sets a preferred layout style, not all |
| combinations of items, bounds, and style can be rendered correctly. |
| */ |
| - (MDCTabBarViewLayoutStyle)effectiveLayoutStyle { |
| if (self.items.count == 0) { |
| return MDCTabBarViewLayoutStyleFixed; |
| } |
| |
| CGSize availableSize = [self availableSizeForSubviewLayout]; |
| switch (self.preferredLayoutStyle) { |
| case MDCTabBarViewLayoutStyleScrollable: { |
| return MDCTabBarViewLayoutStyleScrollable; |
| } |
| case MDCTabBarViewLayoutStyleFixed: { |
| CGFloat requiredWidthForJustifiedLayout = [self intrinsicContentSizeForJustifiedLayout].width; |
| if (availableSize.width < requiredWidthForJustifiedLayout) { |
| return MDCTabBarViewLayoutStyleScrollable; |
| } |
| UIEdgeInsets contentPadding = |
| [self contentPaddingForLayoutStyle:MDCTabBarViewLayoutStyleFixed]; |
| CGFloat itemLayoutWidth = availableSize.width - contentPadding.left - contentPadding.right; |
| if ((itemLayoutWidth / self.items.count) > kMaxItemWidth) { |
| return MDCTabBarViewLayoutStyleFixedClusteredCentered; |
| } |
| return MDCTabBarViewLayoutStyleFixed; |
| } |
| case MDCTabBarViewLayoutStyleFixedClusteredCentered: { |
| CGFloat requiredWidthForClusteredCenteredLayout = |
| [self |
| intrinsicContentSizeForClusteredLayout:MDCTabBarViewLayoutStyleFixedClusteredCentered] |
| .width; |
| if (availableSize.width < requiredWidthForClusteredCenteredLayout) { |
| return MDCTabBarViewLayoutStyleScrollable; |
| } |
| return MDCTabBarViewLayoutStyleFixedClusteredCentered; |
| } |
| case MDCTabBarViewLayoutStyleFixedClusteredLeading: { |
| CGFloat requiredWidthForClusteredLeadingLayout = |
| [self |
| intrinsicContentSizeForClusteredLayout:MDCTabBarViewLayoutStyleFixedClusteredLeading] |
| .width; |
| if (availableSize.width < requiredWidthForClusteredLeadingLayout) { |
| return MDCTabBarViewLayoutStyleScrollable; |
| } |
| return MDCTabBarViewLayoutStyleFixedClusteredLeading; |
| } |
| case MDCTabBarViewLayoutStyleFixedClusteredTrailing: { |
| CGFloat requiredWidthForClusteredTrailingLayout = |
| [self |
| intrinsicContentSizeForClusteredLayout:MDCTabBarViewLayoutStyleFixedClusteredTrailing] |
| .width; |
| if (availableSize.width < requiredWidthForClusteredTrailingLayout) { |
| return MDCTabBarViewLayoutStyleScrollable; |
| } |
| return MDCTabBarViewLayoutStyleFixedClusteredTrailing; |
| } |
| } |
| } |
| |
| - (void)layoutSubviewsForJustifiedLayout { |
| if (self.itemViews.count == 0) { |
| return; |
| } |
| BOOL isRTL = |
| self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; |
| |
| CGSize contentSize = [self availableSizeForSubviewLayout]; |
| UIEdgeInsets contentPadding = [self contentPaddingForLayoutStyle:MDCTabBarViewLayoutStyleFixed]; |
| CGFloat itemLayoutWidth = contentSize.width - contentPadding.left - contentPadding.right; |
| CGFloat itemViewWidth = itemLayoutWidth / self.itemViews.count; |
| CGFloat itemViewOriginX = isRTL ? contentPadding.right : contentPadding.left; |
| CGFloat itemViewOriginY = contentPadding.top; |
| CGFloat itemViewHeight = contentSize.height - contentPadding.top - contentPadding.bottom; |
| NSEnumerator<UIView *> *itemViewEnumerator = |
| isRTL ? [self.itemViews reverseObjectEnumerator] : [self.itemViews objectEnumerator]; |
| |
| for (UIView *itemView in itemViewEnumerator) { |
| itemView.frame = CGRectMake(itemViewOriginX, itemViewOriginY, itemViewWidth, itemViewHeight); |
| itemViewOriginX += itemViewWidth; |
| } |
| } |
| |
| - (void)layoutSubviewsForFixedClusteredLayout:(MDCTabBarViewLayoutStyle)layoutStyle { |
| if (self.itemViews.count == 0) { |
| return; |
| } |
| |
| BOOL isRTL = |
| self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; |
| |
| UIEdgeInsets contentPadding = [self contentPaddingForLayoutStyle:layoutStyle]; |
| CGSize contentSize = [self availableSizeForSubviewLayout]; |
| CGFloat itemViewWidth = [self estimatedItemViewSizeForClusteredFixedLayout].width; |
| CGFloat totalRequiredWidth = itemViewWidth * self.items.count; |
| // Start-out assuming left-aligned because it requires no computation. |
| CGFloat itemViewOriginX = isRTL ? contentPadding.right : contentPadding.left; |
| // Right-aligned |
| if ((isRTL && layoutStyle == MDCTabBarViewLayoutStyleFixedClusteredLeading) || |
| (!isRTL && layoutStyle == MDCTabBarViewLayoutStyleFixedClusteredTrailing)) { |
| itemViewOriginX = (contentSize.width - totalRequiredWidth); |
| itemViewOriginX -= isRTL ? contentPadding.left : contentPadding.right; |
| } |
| // Centered |
| else if (layoutStyle == MDCTabBarViewLayoutStyleFixedClusteredCentered) { |
| itemViewOriginX = |
| (contentSize.width - totalRequiredWidth - contentPadding.left - contentPadding.right) / 2; |
| itemViewOriginX += isRTL ? contentPadding.right : contentPadding.left; |
| } |
| |
| CGFloat itemViewOriginY = contentPadding.top; |
| CGFloat itemViewHeight = contentSize.height - contentPadding.top - contentPadding.bottom; |
| NSEnumerator<UIView *> *itemViewEnumerator = |
| isRTL ? [self.itemViews reverseObjectEnumerator] : [self.itemViews objectEnumerator]; |
| |
| for (UIView *itemView in itemViewEnumerator) { |
| itemView.frame = CGRectMake(itemViewOriginX, itemViewOriginY, itemViewWidth, itemViewHeight); |
| itemViewOriginX += itemViewWidth; |
| } |
| } |
| |
| - (void)layoutSubviewsForScrollableLayout { |
| BOOL isRTL = |
| self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; |
| UIEdgeInsets contentPadding = |
| [self contentPaddingForLayoutStyle:MDCTabBarViewLayoutStyleScrollable]; |
| |
| // Default for LTR |
| CGFloat itemViewOriginX = contentPadding.left; |
| if (isRTL) { |
| itemViewOriginX = 0; |
| CGFloat requiredBarSize = [self intrinsicContentSizeForScrollableLayout].width; |
| CGFloat boundsBarDiff = [self availableSizeForSubviewLayout].width - requiredBarSize; |
| if (boundsBarDiff > 0) { |
| itemViewOriginX = boundsBarDiff; |
| } |
| itemViewOriginX += contentPadding.right; |
| } |
| CGFloat itemViewOriginY = contentPadding.top; |
| CGFloat itemViewHeight = |
| [self availableSizeForSubviewLayout].height - contentPadding.top - contentPadding.bottom; |
| NSEnumerator<UIView *> *itemViewEnumerator = |
| isRTL ? [self.itemViews reverseObjectEnumerator] : [self.itemViews objectEnumerator]; |
| for (UIView *view in itemViewEnumerator) { |
| CGSize intrinsicContentSize = view.intrinsicContentSize; |
| view.frame = |
| CGRectMake(itemViewOriginX, itemViewOriginY, intrinsicContentSize.width, itemViewHeight); |
| itemViewOriginX += intrinsicContentSize.width; |
| } |
| } |
| |
| - (void)willMoveToSuperview:(UIView *)newSuperview { |
| [super willMoveToSuperview:newSuperview]; |
| self.needsScrollToSelectedItem = YES; |
| } |
| |
| - (CGSize)intrinsicContentSize { |
| switch (self.preferredLayoutStyle) { |
| case MDCTabBarViewLayoutStyleFixed: { |
| return [self intrinsicContentSizeForJustifiedLayout]; |
| } |
| case MDCTabBarViewLayoutStyleScrollable: { |
| return [self intrinsicContentSizeForScrollableLayout]; |
| } |
| case MDCTabBarViewLayoutStyleFixedClusteredLeading: |
| case MDCTabBarViewLayoutStyleFixedClusteredCentered: |
| case MDCTabBarViewLayoutStyleFixedClusteredTrailing: { |
| return [self intrinsicContentSizeForClusteredLayout:self.preferredLayoutStyle]; |
| } |
| } |
| } |
| |
| /** |
| The content size of the tabs in their current layout style. |
| |
| For @c FixedJustified: The content size is the maximum of the bounds within the safe area or the |
| intrinsic size of the tabs when all tabs have the widest tab's width. |
| For @c FixedClustered*: The bounds within the safe area. This ensures they are positionined |
| accurately within the content area. |
| For @c Scrollable: The intrinsic size size of the tabs. |
| */ |
| - (CGSize)calculatedContentSize { |
| switch ([self effectiveLayoutStyle]) { |
| case MDCTabBarViewLayoutStyleFixed: { |
| CGSize intrinsicContentSize = [self intrinsicContentSizeForJustifiedLayout]; |
| CGSize boundsSize = CGSizeMake(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)); |
| |
| return CGSizeMake(MAX(boundsSize.width, intrinsicContentSize.width), |
| MAX(boundsSize.height, intrinsicContentSize.height)); |
| } |
| case MDCTabBarViewLayoutStyleFixedClusteredCentered: |
| case MDCTabBarViewLayoutStyleFixedClusteredTrailing: |
| case MDCTabBarViewLayoutStyleFixedClusteredLeading: { |
| return [self availableSizeForSubviewLayout]; |
| } |
| case MDCTabBarViewLayoutStyleScrollable: { |
| return [self intrinsicContentSizeForScrollableLayout]; |
| } |
| } |
| } |
| |
| - (CGSize)intrinsicContentSizeForJustifiedLayout { |
| CGFloat maxWidth = 0; |
| CGFloat maxHeight = kMinHeight; |
| for (UIView *itemView in self.itemViews) { |
| CGSize contentSize = itemView.intrinsicContentSize; |
| maxHeight = MAX(maxHeight, contentSize.height); |
| maxWidth = MAX(maxWidth, contentSize.width); |
| } |
| CGSize contentSize = CGSizeMake(maxWidth * self.items.count, maxHeight); |
| UIEdgeInsets contentPadding = [self contentPaddingForLayoutStyle:MDCTabBarViewLayoutStyleFixed]; |
| contentSize = CGSizeMake(contentSize.width + contentPadding.left + contentPadding.right, |
| contentSize.height + contentPadding.top + contentPadding.bottom); |
| return contentSize; |
| } |
| |
| - (CGSize)intrinsicContentSizeForScrollableLayout { |
| CGFloat totalWidth = 0; |
| CGFloat maxHeight = 0; |
| for (UIView *itemView in self.itemViews) { |
| CGSize contentSize = itemView.intrinsicContentSize; |
| if (contentSize.height > maxHeight) { |
| maxHeight = contentSize.height; |
| } |
| totalWidth += contentSize.width; |
| } |
| CGSize contentSize = CGSizeMake(totalWidth, MAX(kMinHeight, maxHeight)); |
| UIEdgeInsets contentPadding = |
| [self contentPaddingForLayoutStyle:MDCTabBarViewLayoutStyleScrollable]; |
| contentSize = CGSizeMake(contentSize.width + contentPadding.left + contentPadding.right, |
| contentSize.height + contentPadding.top + contentPadding.bottom); |
| return contentSize; |
| } |
| |
| - (CGSize)intrinsicContentSizeForClusteredLayout:(MDCTabBarViewLayoutStyle)layoutStyle { |
| if (self.items.count == 0) { |
| return CGSizeZero; |
| } |
| CGSize estimatedItemSize = [self estimatedItemViewSizeForClusteredFixedLayout]; |
| CGSize contentSize = |
| CGSizeMake(estimatedItemSize.width * self.items.count, estimatedItemSize.height); |
| UIEdgeInsets contentPadding = [self contentPaddingForLayoutStyle:layoutStyle]; |
| contentSize = CGSizeMake(contentSize.width + contentPadding.left + contentPadding.right, |
| contentSize.height + contentPadding.top + contentPadding.bottom); |
| return contentSize; |
| } |
| |
| - (CGSize)sizeThatFits:(CGSize)size { |
| CGSize fitSize = [self intrinsicContentSizeForJustifiedLayout]; |
| return CGSizeMake(size.width, fitSize.height); |
| } |
| |
| #pragma mark - Helpers |
| |
| - (void)scrollUntilSelectedItemIsVisibleWithoutAnimation { |
| NSUInteger index = [self.items indexOfObject:self.selectedItem]; |
| if (index == NSNotFound || index >= self.itemViews.count) { |
| index = 0; |
| } |
| if (self.itemViews.count == 0U) { |
| return; |
| } |
| |
| CGRect estimatedItemFrame = [self estimatedFrameForItemAtIndex:index]; |
| [self scrollRectToVisible:estimatedItemFrame animated:NO]; |
| } |
| |
| - (CGRect)estimatedFrameForItemAtIndex:(NSUInteger)index { |
| if (index == NSNotFound || index >= self.itemViews.count) { |
| return CGRectZero; |
| } |
| |
| BOOL isRTL = |
| self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; |
| CGFloat originAdjustment = self.isScrollableLayoutStyle ? kScrollableTabsLeadingEdgeInset : 0; |
| CGFloat viewOriginX = isRTL ? self.contentSize.width - originAdjustment : originAdjustment; |
| |
| for (NSUInteger i = 0; i < index; ++i) { |
| CGSize viewSize = [self expectedSizeForView:self.itemViews[i]]; |
| if (isRTL) { |
| viewOriginX -= viewSize.width; |
| } else { |
| viewOriginX += viewSize.width; |
| } |
| } |
| CGSize viewSize = [self expectedSizeForView:self.itemViews[index]]; |
| if (isRTL) { |
| viewOriginX -= viewSize.width; |
| } |
| return CGRectMake(viewOriginX, 0, viewSize.width, viewSize.height); |
| } |
| |
| - (CGSize)expectedSizeForView:(UIView *)view { |
| if (self.itemViews.count == 0) { |
| return CGSizeZero; |
| } |
| |
| switch ([self effectiveLayoutStyle]) { |
| case MDCTabBarViewLayoutStyleFixed: { |
| if (CGRectGetWidth(self.bounds) > 0) { |
| CGSize contentSize = [self availableSizeForSubviewLayout]; |
| return CGSizeMake(contentSize.width / self.itemViews.count, contentSize.height); |
| } |
| return [self estimatedIntrinsicSizeForView:view]; |
| } |
| case MDCTabBarViewLayoutStyleFixedClusteredCentered: |
| case MDCTabBarViewLayoutStyleFixedClusteredTrailing: |
| case MDCTabBarViewLayoutStyleFixedClusteredLeading: { |
| return [self estimatedItemViewSizeForClusteredFixedLayout]; |
| } |
| case MDCTabBarViewLayoutStyleScrollable: { |
| return [self estimatedIntrinsicSizeForView:view]; |
| } |
| } |
| } |
| |
| - (CGSize)estimatedIntrinsicSizeForView:(UIView *)view { |
| CGSize expectedItemSize = view.intrinsicContentSize; |
| if (expectedItemSize.width == UIViewNoIntrinsicMetric) { |
| NSAssert(expectedItemSize.width != UIViewNoIntrinsicMetric, |
| @"All tab bar item views must define an intrinsic content size."); |
| expectedItemSize = [view sizeThatFits:self.contentSize]; |
| } |
| return expectedItemSize; |
| } |
| |
| - (CGSize)estimatedItemViewSizeForClusteredFixedLayout { |
| CGFloat largestWidth = 0; |
| CGFloat largestHeight = 0; |
| for (UIView *view in self.itemViews) { |
| CGSize intrinsicContentSize = view.intrinsicContentSize; |
| if (intrinsicContentSize.width > largestWidth) { |
| largestWidth = intrinsicContentSize.width; |
| } |
| if (intrinsicContentSize.height > largestHeight) { |
| largestHeight = intrinsicContentSize.height; |
| } |
| } |
| return CGSizeMake(largestWidth, largestHeight); |
| } |
| |
| - (CGRect)availableBoundsForSubviewLayout { |
| CGRect availableBounds = CGRectStandardize(self.bounds); |
| if (@available(iOS 11.0, *)) { |
| availableBounds = UIEdgeInsetsInsetRect(availableBounds, self.safeAreaInsets); |
| } |
| return availableBounds; |
| } |
| |
| - (CGSize)availableSizeForSubviewLayout { |
| return [self availableBoundsForSubviewLayout].size; |
| } |
| |
| #pragma mark - Actions |
| |
| - (void)didTapItemView:(UITapGestureRecognizer *)tap { |
| NSUInteger index = [self.itemViews indexOfObject:tap.view]; |
| if (index == NSNotFound) { |
| return; |
| } |
| |
| if ([self.tabBarDelegate respondsToSelector:@selector(tabBarView:shouldSelectItem:)] && |
| ![self.tabBarDelegate tabBarView:self shouldSelectItem:self.items[index]]) { |
| return; |
| } |
| |
| self.selectedItem = self.items[index]; |
| if ([self.tabBarDelegate respondsToSelector:@selector(tabBarView:didSelectItem:)]) { |
| [self.tabBarDelegate tabBarView:self didSelectItem:self.items[index]]; |
| } |
| } |
| |
| /// Sets _selectionIndicator's bounds and center to display under the item at the given index with |
| /// no animation. May be called from an animation block to animate the transition. |
| - (void)updateSelectionIndicatorToIndex:(NSUInteger)index { |
| if (index == NSNotFound || index >= self.items.count) { |
| // Hide selection indicator. |
| self.selectionIndicatorView.bounds = CGRectZero; |
| return; |
| } |
| |
| // Place selection indicator under the item's cell. |
| CGRect selectedItemFrame = [self selectedItemView].frame; |
| if (CGRectEqualToRect(selectedItemFrame, CGRectZero)) { |
| selectedItemFrame = |
| [self estimatedFrameForItemAtIndex:[self.items indexOfObject:self.selectedItem]]; |
| } |
| self.selectionIndicatorView.frame = selectedItemFrame; |
| |
| CGRect selectionIndicatorBounds = |
| CGRectMake(0, 0, CGRectGetWidth(self.selectionIndicatorView.bounds), |
| CGRectGetHeight(self.selectionIndicatorView.bounds)); |
| |
| // Extract content frame from item view. |
| CGRect contentFrame = selectionIndicatorBounds; |
| UIView *itemView = self.itemViews[index]; |
| if ([itemView conformsToProtocol:@protocol(MDCTabBarViewCustomViewable)]) { |
| UIView<MDCTabBarViewCustomViewable> *supportingView = |
| (UIView<MDCTabBarViewCustomViewable> *)itemView; |
| contentFrame = supportingView.contentFrame; |
| } |
| |
| // Construct a context object describing the selected tab. |
| UITabBarItem *item = self.items[index]; |
| MDCTabBarViewPrivateIndicatorContext *context = |
| [[MDCTabBarViewPrivateIndicatorContext alloc] initWithItem:item |
| bounds:selectionIndicatorBounds |
| contentFrame:contentFrame]; |
| |
| // Ask the template for attributes. |
| id<MDCTabBarViewIndicatorTemplate> template = self.selectionIndicatorTemplate; |
| MDCTabBarViewIndicatorAttributes *indicatorAttributes = |
| [template indicatorAttributesForContext:context]; |
| |
| // Update the selection indicator. |
| [self.selectionIndicatorView applySelectionIndicatorAttributes:indicatorAttributes]; |
| } |
| |
| /** |
| Updates the selection indicator with or without animation. Passing @c NSNotFound for @c index will |
| cause the indicator to become invisible. |
| |
| @param index The index of the selected item. |
| @param animate @c YES if the change should be animated, @c NO if it should be immediate. |
| */ |
| - (void)didSelectItemAtIndex:(NSUInteger)index animateTransition:(BOOL)animate { |
| void (^animationBlock)(void) = ^{ |
| [self updateSelectionIndicatorToIndex:index]; |
| |
| // Force layout so any changes to the selection indicator are captured by the animation block. |
| [self.selectionIndicatorView layoutIfNeeded]; |
| }; |
| |
| if (animate) { |
| CAMediaTimingFunction *easeInOutFunction = |
| [CAMediaTimingFunction mdc_functionWithType:MDCAnimationTimingFunctionEaseInOut]; |
| // Wrap in explicit CATransaction to allow layer-based animations with the correct duration. |
| [CATransaction begin]; |
| [CATransaction setAnimationDuration:self.selectionChangeAnimationDuration]; |
| [CATransaction setAnimationTimingFunction:easeInOutFunction]; |
| [UIView animateWithDuration:self.selectionChangeAnimationDuration |
| delay:0 |
| options:UIViewAnimationOptionBeginFromCurrentState |
| animations:animationBlock |
| completion:nil]; |
| [CATransaction commit]; |
| |
| } else { |
| animationBlock(); |
| } |
| } |
| |
| - (UIView *)selectedItemView { |
| if (!self.selectedItem) { |
| return nil; |
| } |
| |
| return self.itemViews[[self.items indexOfObject:self.selectedItem]]; |
| } |
| |
| @end |