blob: 7b1984bda7e2eeeabb0793498e743e5378e2482d [file] [log] [blame]
// Copyright 2017-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 <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "MDCAvailability.h"
#import "MDCBottomNavigationBar.h"
#import "UIView+MaterialElevationResponding.h"
#import "private/MDCBottomNavigationBar+Private.h"
#import "private/MDCBottomNavigationItemView.h"
#import "MDCBadgeAppearance.h"
#import "MDCBottomNavigationBarDelegate.h"
#import "MDCBottomNavigationBarItem.h"
#import "MDCBottomNavigationBar+ItemView.h"
#import "MDCPalettes.h"
#import "MDCRippleTouchController.h"
#import "MDCRippleTouchControllerDelegate.h"
#import "MDCRippleView.h"
#import "MDCShadow.h"
#import "MDCShadowsCollection.h"
#import "MDCShadowElevations.h"
#import "MDCShadowLayer.h"
#import "MDCFontTextStyle.h"
#import "UIFont+MaterialTypography.h"
#import "MDCMath.h"
NS_ASSUME_NONNULL_BEGIN
// KVO context
static char *const kKVOContextMDCBottomNavigationBar = "kKVOContextMDCBottomNavigationBar";
static char *const kKVOContextMDCBottomNavigationBarItem = "kKVOContextMDCBottomNavigationBarItem";
static const CGFloat kMinItemWidth = 80;
static const CGFloat kPreferredItemWidth = 120;
static const CGFloat kMaxItemWidth = 350;
// The default amount of internal padding on the leading/trailing edges of each bar item.
static const CGFloat kDefaultItemHorizontalPadding = 0;
static const CGFloat kBarHeightStackedTitle = 56;
static const CGFloat kBarHeightAdjacentTitle = 40;
static const CGFloat kItemsHorizontalMargin = 12;
static const CGFloat kBadgeFontSize = 8;
// Active indicator
static const CGFloat kDefaultActiveIndicatorHeight = 30;
static const CGFloat kDefaultActiveIndicatorWidth = 60;
// Vertical layout
static const CGFloat kDefaultVerticalLayoutWidth = 80;
static const CGFloat kDefaultVerticalPadding = 45;
static const CGFloat kDefaultItemSpacingInVerticalLayoutOniPad = 15;
static const CGFloat kDefaultItemSpacingInVerticalLayout = 8;
@interface MDCBottomNavigationBar () <MDCRippleTouchControllerDelegate>
@property(nonatomic, strong) NSMutableArray<MDCBottomNavigationItemView *> *itemViews;
@property(nonatomic, readonly) UIEdgeInsets mdc_safeAreaInsets;
@property(nonatomic, strong) UIView *barView;
@property(nonatomic, strong) UIVisualEffectView *blurEffectView;
@property(nonatomic, strong) UIStackView *itemsLayoutView;
@property(nonatomic, strong) UILayoutGuide *barItemsLayoutGuide NS_AVAILABLE_IOS(9_0);
@property(nonatomic, strong) NSLayoutConstraint *itemsLayoutViewAlignmentConstraint;
@property(nonatomic, strong) NSLayoutConstraint *itemsLayoutViewHeightConstraint;
@property(nonatomic, strong) NSLayoutConstraint *itemsLayoutViewBottomAnchorConstraint;
@property(nonatomic, strong) NSMutableArray<NSLayoutConstraint *> *itemViewHeightConstraints;
@property(nonatomic, strong) NSMutableArray<NSLayoutConstraint *> *itemViewWidthConstraints;
@property(nonatomic, strong)
NSMutableArray<NSLayoutConstraint *> *itemsLayoutViewHorizontalConstraints;
@property(nonatomic, strong)
NSMutableArray<NSLayoutConstraint *> *itemsLayoutViewVerticalConstraints;
#if MDC_AVAILABLE_SDK_IOS(13_0)
/**
The last large content viewer item displayed by the content viewer while the interaction is
running. When the interaction ends this property is nil.
*/
@property(nonatomic, nullable) id<UILargeContentViewerItem> lastLargeContentViewerItem
NS_AVAILABLE_IOS(13_0);
@property(nonatomic, assign) BOOL isLargeContentLongPressInProgress;
#endif // MDC_AVAILABLE_SDK_IOS(13_0)
@end
@implementation MDCBottomNavigationBar
static BOOL gEnablePerformantShadow = NO;
@synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation;
@synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock;
@synthesize shadowsCollection = _shadowsCollection;
@synthesize elevation = _elevation;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.autoresizingMask = (UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth);
self.isAccessibilityElement = NO;
[self commonMDCBottomNavigationBarInit];
}
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonMDCBottomNavigationBarInit];
}
return self;
}
- (void)commonMDCBottomNavigationBarInit {
_itemsContentHorizontalMargin = kItemsHorizontalMargin;
_selectedItemTintColor = UIColor.blackColor;
_unselectedItemTintColor = UIColor.grayColor;
_selectedItemTitleColor = _selectedItemTintColor;
_titleVisibility = MDCBottomNavigationBarTitleVisibilitySelected;
_alignment = MDCBottomNavigationBarAlignmentJustified;
_barTintColor = UIColor.whiteColor;
_truncatesLongTitles = YES;
_titlesNumberOfLines = 1;
_mdc_overrideBaseElevation = -1;
_rippleEnabled = YES;
_itemsAlignmentInVerticalMode = MDCNavigationBarItemsVerticalAlignmentCenter;
_itemBadgeAppearance = [[MDCBadgeAppearance alloc] init];
_itemBadgeAppearance.textColor = UIColor.whiteColor;
_itemBadgeAppearance.font = [UIFont systemFontOfSize:kBadgeFontSize];
_itemBadgeAppearance.backgroundColor = MDCPalette.redPalette.tint700;
_itemsHorizontalPadding = kDefaultItemHorizontalPadding;
_showsSelectionIndicator = NO;
_selectionIndicatorColor = [UIColor colorWithRed:195.f / 255.f
green:217.f / 255.f
blue:242.f / 255.f
alpha:1];
_selectionIndicatorSize = CGSizeMake(kDefaultActiveIndicatorWidth, kDefaultActiveIndicatorHeight);
// Remove any unarchived subviews and reconfigure the view hierarchy
if (self.subviews.count) {
NSArray *subviews = self.subviews;
for (UIView *view in subviews) {
[view removeFromSuperview];
}
}
_barView = [[UIView alloc] init];
_barView.clipsToBounds = YES;
_barView.backgroundColor = _barTintColor;
[self addSubview:_barView];
_itemsLayoutView = [[UIStackView alloc] initWithFrame:CGRectZero];
_itemsLayoutView.layoutMarginsRelativeArrangement = YES;
_itemsLayoutView.spacing = kDefaultItemHorizontalPadding;
_itemsLayoutView.alignment = UIStackViewAlignmentCenter;
_itemsLayoutView.distribution = UIStackViewDistributionFillEqually;
_itemsLayoutView.translatesAutoresizingMaskIntoConstraints = NO;
_itemsLayoutView.clipsToBounds = NO;
[_barView addSubview:_itemsLayoutView];
_itemsLayoutView.accessibilityTraits = UIAccessibilityTraitTabBar;
self.elevation = MDCShadowElevationBottomNavigationBar;
self.shadowColor = gEnablePerformantShadow ? MDCShadowColor() : UIColor.blackColor;
_itemViews = [NSMutableArray array];
_itemViewHeightConstraints = [NSMutableArray array];
_itemViewWidthConstraints = [NSMutableArray array];
_itemsLayoutViewHorizontalConstraints = [NSMutableArray array];
_itemsLayoutViewVerticalConstraints = [NSMutableArray array];
_itemTitleFont = [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleCaption];
// Horizontal layout constraints.
[_itemsLayoutViewHorizontalConstraints
addObject:[_itemsLayoutView.leadingAnchor constraintEqualToAnchor:_barView.leadingAnchor]];
[_itemsLayoutViewHorizontalConstraints
addObject:[_itemsLayoutView.trailingAnchor constraintEqualToAnchor:_barView.trailingAnchor]];
[_itemsLayoutViewHorizontalConstraints
addObject:[_itemsLayoutView.topAnchor constraintEqualToAnchor:_barView.topAnchor]];
_itemsLayoutViewHeightConstraint =
[_itemsLayoutView.heightAnchor constraintEqualToConstant:[self calculateBarHeight]];
[_itemsLayoutViewHorizontalConstraints addObject:_itemsLayoutViewHeightConstraint];
// Vertical layout constraints.
[_itemsLayoutViewVerticalConstraints
addObject:[_itemsLayoutView.widthAnchor
constraintEqualToConstant:kDefaultVerticalLayoutWidth]];
[_itemsLayoutViewVerticalConstraints
addObject:[_itemsLayoutView.leadingAnchor
constraintEqualToAnchor:_barView.safeAreaLayoutGuide.leadingAnchor]];
_itemsLayoutViewAlignmentConstraint =
[_itemsLayoutView.centerYAnchor constraintEqualToAnchor:_barView.centerYAnchor];
// Layout guide to control items stack view's bottom anchor.
_barItemsLayoutGuide = [[UILayoutGuide alloc] init];
_barItemsLayoutGuide.identifier = @"MDCBottomNavigationBarItemsLayoutGuide";
[_itemsLayoutView addLayoutGuide:_barItemsLayoutGuide];
_itemsLayoutViewBottomAnchorConstraint =
[_barItemsLayoutGuide.bottomAnchor constraintEqualToAnchor:_itemsLayoutView.bottomAnchor];
_enableVerticalLayout = NO;
_displayItemTitlesInVerticalLayout = NO;
[self loadConstraints];
}
- (CGFloat)barWidthForVerticalLayout {
return kDefaultVerticalLayoutWidth;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGRect standardBounds = CGRectStandardize(self.bounds);
if (self.blurEffectView) {
self.blurEffectView.frame = standardBounds;
}
self.barView.frame = standardBounds;
self.layer.shadowColor = self.shadowColor.CGColor;
for (NSUInteger i = 0; i < self.itemViews.count; i++) {
MDCBottomNavigationItemView *itemView = self.itemViews[i];
[self configureTitleStateForItemView:itemView];
}
UIUserInterfaceLayoutDirection layoutDirection = self.effectiveUserInterfaceLayoutDirection;
if (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight) {
_itemsLayoutView.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
} else {
_itemsLayoutView.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
}
if (gEnablePerformantShadow) {
[self updateShadow];
}
}
- (void)safeAreaInsetsDidChange {
[super safeAreaInsetsDidChange];
[self setNeedsLayout];
}
- (NSUInteger)itemCount {
return self.barItems.count > 0 ? self.barItems.count : self.items.count;
}
- (CGSize)intrinsicContentSize {
if (self.enableVerticalLayout) {
return CGSizeMake([self barWidthForVerticalLayout], UIViewNoIntrinsicMetric);
} else {
CGFloat height = [self calculateBarHeight];
CGFloat itemWidth = [self widthForItemsWhenCenteredWithAvailableWidth:CGFLOAT_MAX
height:height];
return CGSizeMake(itemWidth * [self itemCount], height);
}
}
- (CGFloat)widthForItemsWhenCenteredWithAvailableWidth:(CGFloat)availableWidth
height:(CGFloat)barHeight {
CGFloat maxItemWidth = kPreferredItemWidth;
for (UIView *itemView in self.itemViews) {
maxItemWidth =
MAX(maxItemWidth, [itemView sizeThatFits:CGSizeMake(availableWidth, barHeight)].width +
self.itemsHorizontalPadding * 2);
}
maxItemWidth = MIN(kMaxItemWidth, maxItemWidth);
NSUInteger itemCount = [self itemCount];
CGFloat totalWidth = maxItemWidth * itemCount;
if (totalWidth > availableWidth) {
maxItemWidth = availableWidth / itemCount;
}
if (maxItemWidth < kMinItemWidth) {
maxItemWidth = kMinItemWidth;
}
return maxItemWidth;
}
- (NSLayoutYAxisAnchor *)barItemsBottomAnchor {
return self.barItemsLayoutGuide.bottomAnchor;
}
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height;
CGFloat width = size.width;
if ([self shouldUseAnchoredLayout]) {
height = [self calculateBarHeight];
} else {
height = self.barHeight;
if (height <= 0) {
height = kBarHeightStackedTitle;
if (self.alignment == MDCBottomNavigationBarAlignmentJustifiedAdjacentTitles &&
self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
height = kBarHeightAdjacentTitle;
}
}
}
if (self.enableVerticalLayout) {
width = kDefaultVerticalLayoutWidth;
}
return CGSizeMake(width, height);
}
+ (Class)layerClass {
if (gEnablePerformantShadow) {
return [super layerClass];
} else {
return [MDCShadowLayer class];
}
}
- (void)setElevation:(MDCShadowElevation)elevation {
if (MDCCGFloatEqual(_elevation, elevation)) {
return;
}
_elevation = elevation;
if (gEnablePerformantShadow) {
[self updateShadow];
} else {
MDCShadowLayer *shadowLayer = (MDCShadowLayer *)self.layer;
shadowLayer.elevation = elevation;
}
[self mdc_elevationDidChange];
}
- (void)updateShadow {
MDCShadow *shadow = [self.shadowsCollection shadowForElevation:self.mdc_currentElevation];
shadow = [[MDCShadowBuilder builderWithColor:self.shadowColor
opacity:shadow.opacity
radius:shadow.radius
offset:shadow.offset
spread:shadow.spread] build];
MDCConfigureShadowForView(self, shadow);
}
- (void)setShadowColor:(UIColor *)shadowColor {
UIColor *shadowColorCopy = [shadowColor copy];
_shadowColor = shadowColorCopy;
self.layer.shadowColor = shadowColorCopy.CGColor;
}
- (BOOL)isTitleBelowIcon {
switch (self.alignment) {
case MDCBottomNavigationBarAlignmentCentered:
case MDCBottomNavigationBarAlignmentJustified:
return YES;
break;
case MDCBottomNavigationBarAlignmentJustifiedAdjacentTitles:
return self.traitCollection.horizontalSizeClass != UIUserInterfaceSizeClassRegular;
break;
}
}
- (void)setBarHeight:(CGFloat)barHeight {
_barHeight = barHeight;
_itemsLayoutViewHeightConstraint.constant = [self calculateBarHeight];
}
- (void)setBarHeightWithoutTitles:(CGFloat)barHeightWithoutTitles {
_barHeightWithoutTitles = barHeightWithoutTitles;
_itemsLayoutViewHeightConstraint.constant = [self calculateBarHeight];
}
- (CGFloat)calculateBarHeight {
if ([self shouldUseAnchoredLayout]) {
if ([self itemViewsShouldAlwaysHideTitles] ||
[self barHeightShouldShrinkBasedOnTraitCollection:self.traitCollection]) {
// Return _barHeightWithoutTitles if it has been set to a positive value.
// If _barHeightWithoutTitles is 0 (default value) or negative, return _barHeight instead.
return _barHeightWithoutTitles > 0 ? _barHeightWithoutTitles : _barHeight;
} else {
return _barHeight;
}
}
CGFloat height = self.isTitleBelowIcon ? kBarHeightStackedTitle : kBarHeightAdjacentTitle;
if (self.barHeight > 0) {
height = self.barHeight;
}
return height;
}
- (void)recalculateBarHeightAndUpdateLayout {
_itemsLayoutViewHeightConstraint.constant = [self calculateBarHeight];
[self invalidateIntrinsicContentSize];
}
- (void)setEnableVerticalLayout:(BOOL)enableVerticalLayout {
if (_enableVerticalLayout == enableVerticalLayout) {
return;
}
_enableVerticalLayout = enableVerticalLayout;
for (MDCBottomNavigationItemView *item in self.itemViews) {
item.enableVerticalLayout = enableVerticalLayout;
}
[self loadConstraints];
[self invalidateIntrinsicContentSize];
}
- (void)setDisplayItemTitlesInVerticalLayout:(BOOL)displayItemTitlesInVerticalLayout {
if (_displayItemTitlesInVerticalLayout == displayItemTitlesInVerticalLayout) {
return;
}
_displayItemTitlesInVerticalLayout = displayItemTitlesInVerticalLayout;
for (MDCBottomNavigationItemView *item in self.itemViews) {
item.displayTitleInVerticalLayout = _displayItemTitlesInVerticalLayout;
}
}
- (void)setItemsAlignmentInVerticalMode:
(MDCNavigationBarItemsVerticalAlignment)itemsAlignmentInVerticalMode {
_itemsAlignmentInVerticalMode = itemsAlignmentInVerticalMode;
_itemsLayoutViewAlignmentConstraint.active = NO;
switch (_itemsAlignmentInVerticalMode) {
case MDCNavigationBarItemsVerticalAlignmentCenter:
_itemsLayoutViewAlignmentConstraint =
[_itemsLayoutView.centerYAnchor constraintEqualToAnchor:_barView.centerYAnchor];
break;
case MDCNavigationBarItemsVerticalAlignmentTop:
_itemsLayoutViewAlignmentConstraint =
[_itemsLayoutView.topAnchor constraintEqualToAnchor:_barView.topAnchor
constant:kDefaultVerticalPadding];
break;
case MDCNavigationBarItemsVerticalAlignmentBottom:
_itemsLayoutViewAlignmentConstraint =
[_itemsLayoutView.bottomAnchor constraintEqualToAnchor:_barView.bottomAnchor
constant:-kDefaultVerticalPadding];
break;
}
[self loadConstraints];
}
- (void)loadConstraints {
if (self.enableVerticalLayout) {
[self activateVerticalLayoutConstraints];
} else {
[self activateHorizontalLayoutConstraints];
}
}
- (void)activateVerticalLayoutConstraints {
_itemsLayoutViewBottomAnchorConstraint.active = NO;
[NSLayoutConstraint deactivateConstraints:self.itemsLayoutViewHorizontalConstraints];
for (NSLayoutConstraint *constraint in self.itemViewHeightConstraints) {
constraint.constant = kBarHeightStackedTitle;
}
_itemsLayoutViewAlignmentConstraint.active = YES;
self.itemsLayoutView.axis = UILayoutConstraintAxisVertical;
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
self.itemsLayoutView.spacing = kDefaultItemSpacingInVerticalLayoutOniPad;
} else if (self.displayItemTitlesInVerticalLayout) {
self.itemsLayoutView.spacing = kDefaultItemSpacingInVerticalLayout;
}
[NSLayoutConstraint activateConstraints:self.itemViewWidthConstraints];
[NSLayoutConstraint activateConstraints:self.itemsLayoutViewVerticalConstraints];
}
- (void)activateHorizontalLayoutConstraints {
_itemsLayoutViewAlignmentConstraint.active = NO;
[NSLayoutConstraint deactivateConstraints:self.itemViewWidthConstraints];
[NSLayoutConstraint deactivateConstraints:self.itemsLayoutViewVerticalConstraints];
CGFloat barHeight = [self calculateBarHeight];
_itemsLayoutViewHeightConstraint.constant = barHeight;
self.itemsLayoutView.axis = UILayoutConstraintAxisHorizontal;
self.itemsLayoutView.spacing = kDefaultItemHorizontalPadding;
_itemsLayoutViewBottomAnchorConstraint.active = YES;
for (NSLayoutConstraint *constraint in self.itemViewHeightConstraints) {
constraint.constant = barHeight;
}
[NSLayoutConstraint activateConstraints:self.itemsLayoutViewHorizontalConstraints];
}
- (void)dealloc {
[self removeObserversFromTabBarItems];
[self removeObserversFromBarItems];
}
- (NSArray<NSString *> *)barItemKVOKeyPaths {
static NSArray<NSString *> *keyPaths;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
keyPaths = @[
NSStringFromSelector(@selector(item)),
];
});
return keyPaths;
}
- (NSArray<NSString *> *)kvoKeyPaths {
static NSArray<NSString *> *keyPaths;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
keyPaths = @[
NSStringFromSelector(@selector(badgeColor)),
NSStringFromSelector(@selector(badgeValue)),
NSStringFromSelector(@selector(title)),
NSStringFromSelector(@selector(image)),
NSStringFromSelector(@selector(selectedImage)),
NSStringFromSelector(@selector(accessibilityValue)),
NSStringFromSelector(@selector(accessibilityLabel)),
NSStringFromSelector(@selector(accessibilityHint)),
NSStringFromSelector(@selector(accessibilityIdentifier)),
NSStringFromSelector(@selector(isAccessibilityElement)),
NSStringFromSelector(@selector(titlePositionAdjustment)),
NSStringFromSelector(@selector(largeContentSizeImage)),
NSStringFromSelector(@selector(largeContentSizeImageInsets)),
NSStringFromSelector(@selector(tag)),
];
});
return keyPaths;
}
- (void)addObserversToTabBarItemsForBarItems {
NSArray<NSString *> *keyPaths = [self kvoKeyPaths];
NSArray<NSString *> *barItemKeyPaths = [self barItemKVOKeyPaths];
for (MDCBottomNavigationBarItem *barItem in self.barItems) {
for (NSString *keyPath in keyPaths) {
[barItem.item addObserver:self
forKeyPath:keyPath
options:NSKeyValueObservingOptionNew
context:kKVOContextMDCBottomNavigationBar];
}
for (NSString *keyPath in barItemKeyPaths) {
[barItem addObserver:self
forKeyPath:keyPath
options:NSKeyValueObservingOptionNew
context:kKVOContextMDCBottomNavigationBarItem];
}
}
}
- (void)removeObserversFromBarItems {
NSArray<NSString *> *keyPaths = [self kvoKeyPaths];
NSArray<NSString *> *barItemKeyPaths = [self barItemKVOKeyPaths];
for (MDCBottomNavigationBarItem *barItem in self.barItems) {
for (NSString *keyPath in keyPaths) {
@try {
[barItem.item removeObserver:self
forKeyPath:keyPath
context:kKVOContextMDCBottomNavigationBar];
} @catch (NSException *exception) {
if (exception) {
// No need to do anything if there are no observers.
}
}
}
for (NSString *keyPath in barItemKeyPaths) {
@try {
[barItem removeObserver:self
forKeyPath:keyPath
context:kKVOContextMDCBottomNavigationBarItem];
} @catch (NSException *exception) {
if (exception) {
// No need to do anything if there are no observers.
}
}
}
}
}
// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to
// setBarItems.
- (void)addObserversToTabBarItems {
NSArray<NSString *> *keyPaths = [self kvoKeyPaths];
for (UITabBarItem *item in self.items) {
for (NSString *keyPath in keyPaths) {
[item addObserver:self
forKeyPath:keyPath
options:NSKeyValueObservingOptionNew
context:kKVOContextMDCBottomNavigationBar];
}
}
}
// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to
// setBarItems.
- (void)removeObserversFromTabBarItems {
NSArray<NSString *> *keyPaths = [self kvoKeyPaths];
for (UITabBarItem *item in self.items) {
for (NSString *keyPath in keyPaths) {
@try {
[item removeObserver:self forKeyPath:keyPath context:kKVOContextMDCBottomNavigationBar];
} @catch (NSException *exception) {
if (exception) {
// No need to do anything if there are no observers.
}
}
}
}
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context {
if (context == kKVOContextMDCBottomNavigationBar) {
if (!object) {
return;
}
NSUInteger itemIndex = [self.items indexOfObject:object];
// Since the object returned is of type UITabBarItem, we need to create an array from the
// barItems array and get the index of the UITabBarItem.
if (self.barItems.count > 0) {
itemIndex = NSNotFound;
for (NSUInteger i = 0; i < self.barItems.count; i++) {
if ([self.barItems[i].item isEqual:object]) {
itemIndex = i;
break;
}
}
}
if (itemIndex == NSNotFound || itemIndex >= _itemViews.count) {
return;
}
id newValue = [object valueForKey:keyPath];
if (newValue == [NSNull null]) {
newValue = nil;
}
MDCBottomNavigationItemView *itemView = _itemViews[itemIndex];
if ([keyPath isEqualToString:NSStringFromSelector(@selector(badgeColor))]) {
itemView.badgeColor = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(accessibilityValue))]) {
itemView.accessibilityValue = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(badgeValue))]) {
itemView.badgeText = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(image))]) {
itemView.image = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(selectedImage))]) {
itemView.selectedImage = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(title))]) {
itemView.title = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(accessibilityIdentifier))]) {
itemView.accessibilityElementIdentifier = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(accessibilityLabel))]) {
itemView.accessibilityLabel = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(accessibilityHint))]) {
itemView.accessibilityHint = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(isAccessibilityElement))]) {
itemView.isAccessibilityElement = [newValue boolValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(titlePositionAdjustment))]) {
itemView.titlePositionAdjustment = [newValue UIOffsetValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(largeContentSizeImage))]) {
itemView.largeContentImage = newValue;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tag))]) {
itemView.tag = [newValue integerValue];
} else if ([keyPath
isEqualToString:NSStringFromSelector(@selector(largeContentSizeImageInsets))]) {
itemView.largeContentImageInsets = [newValue UIEdgeInsetsValue];
}
} else if (context == kKVOContextMDCBottomNavigationBarItem) {
if (!object) {
return;
}
NSUInteger itemIndex = [self.barItems indexOfObject:object];
if (itemIndex == NSNotFound || itemIndex >= _itemViews.count) {
return;
}
id newValue = [object valueForKey:keyPath];
if (newValue == [NSNull null]) {
newValue = nil;
}
if ([keyPath isEqualToString:NSStringFromSelector(@selector(item))]) {
// Remove all existing item views and repopulate them with the new bar items.
[self removeItemViews];
[self updateBarItems];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
[self layoutIfNeeded];
}
- (UIEdgeInsets)mdc_safeAreaInsets {
return self.safeAreaInsets;
}
- (nullable UIView *)viewForItem:(UITabBarItem *)item {
NSUInteger itemIndex = [_items indexOfObject:item];
if (itemIndex == NSNotFound) {
return nil;
}
if (itemIndex >= _itemViews.count) {
NSAssert(NO, @"Item index should not be out of item view bounds");
return nil;
}
return _itemViews[itemIndex];
}
- (nullable UITabBarItem *)tabBarItemForPoint:(CGPoint)point {
for (NSUInteger i = 0; (i < self.itemViews.count) && (i < [self itemCount]); i++) {
UIView *itemView = self.itemViews[i];
BOOL isPointInView = CGRectContainsPoint(itemView.frame, point);
if (isPointInView) {
return self.items[i];
}
}
return nil;
}
/** Returns the item view at the given point. Nil if there is no view at the given point. */
- (MDCBottomNavigationItemView *_Nullable)itemViewForPoint:(CGPoint)point {
for (NSUInteger i = 0; i < self.itemViews.count; i++) {
MDCBottomNavigationItemView *itemView = self.itemViews[i];
CGRect rect = [itemView convertRect:itemView.bounds toView:self];
if (CGRectContainsPoint(rect, point)) {
return itemView;
}
}
return nil;
}
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
if (self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass ||
self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass) {
[self recalculateBarHeightAndUpdateLayout];
}
}
#pragma mark - Touch handlers
- (void)didTouchUpInsidebarItemButton:(UIButton *)button {
for (NSUInteger i = 0; i < self.barItems.count; i++) {
MDCBottomNavigationBarItem *barItem = self.barItems[i];
MDCBottomNavigationItemView *itemView = self.itemViews[i];
if (itemView.button == button) {
BOOL shouldSelect = YES;
if ([self.delegate respondsToSelector:@selector(bottomNavigationBar:shouldSelectItem:)]) {
shouldSelect = [self.delegate bottomNavigationBar:self shouldSelectItem:barItem.item];
}
if (shouldSelect) {
[self setSelectedBarItem:barItem animated:YES];
if ([self.delegate respondsToSelector:@selector(bottomNavigationBar:didSelectItem:)]) {
[self.delegate bottomNavigationBar:self didSelectItem:barItem.item];
}
}
}
}
}
// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to
// setBarItems.
- (void)didTouchUpInsideButton:(UIButton *)button {
for (NSUInteger i = 0; i < self.items.count; i++) {
UITabBarItem *item = self.items[i];
MDCBottomNavigationItemView *itemView = self.itemViews[i];
if (itemView.button == button) {
BOOL shouldSelect = YES;
if ([self.delegate respondsToSelector:@selector(bottomNavigationBar:shouldSelectItem:)]) {
shouldSelect = [self.delegate bottomNavigationBar:self shouldSelectItem:item];
}
if (shouldSelect) {
[self setSelectedItem:item animated:YES];
if ([self.delegate respondsToSelector:@selector(bottomNavigationBar:didSelectItem:)]) {
[self.delegate bottomNavigationBar:self didSelectItem:item];
}
}
}
}
}
#pragma mark - Setters
- (void)setBarItems:(NSArray<MDCBottomNavigationBarItem *> *)barItems {
if ([_barItems isEqual:barItems] || _barItems == barItems) {
return;
}
// If clients report conflicting gesture recognizers please see proposed solution in the
// internal document: go/mdc-ios-bottomnavigation-largecontentvieweritem
[self addInteraction:[[UILargeContentViewerInteraction alloc] initWithDelegate:self]];
[self removeItemViews];
_barItems = [barItems copy];
[self updateBarItems];
}
- (void)removeItemViews {
// Remove existing item views from the bottom navigation so it can be repopulated with new items.
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
[itemView removeFromSuperview];
}
if (self.itemViews.count > 0) {
[self.itemViews removeAllObjects];
[self.itemViewHeightConstraints removeAllObjects];
[self.itemViewWidthConstraints removeAllObjects];
[self removeObserversFromBarItems];
}
}
- (void)updateBarItems {
CGFloat barHeight = [self calculateBarHeight];
for (NSUInteger i = 0; i < self.barItems.count; i++) {
MDCBottomNavigationItemView *itemView =
[[MDCBottomNavigationItemView alloc] initWithFrame:CGRectZero];
itemView.rippleTouchController.delegate = self;
itemView.selected = NO;
itemView.displayTitleInVerticalLayout = self.displayItemTitlesInVerticalLayout;
itemView.enableVerticalLayout = self.enableVerticalLayout;
itemView.selectionIndicatorColor = self.selectionIndicatorColor;
itemView.selectionIndicatorSize = self.selectionIndicatorSize;
[self configureTitleStateForItemView:itemView];
// If a given badge appearance has a `nil` for its textColor and font, then the values from the
// itemBadgeAppearance are used.
if (!self.barItems[i].badgeAppearance.textColor) {
self.barItems[i].badgeAppearance.textColor = _itemBadgeAppearance.textColor;
}
if (!self.barItems[i].badgeAppearance.font) {
self.barItems[i].badgeAppearance.font = _itemBadgeAppearance.font;
}
[self configureItemView:itemView
withItem:self.barItems[i].item
appearance:self.barItems[i].badgeAppearance];
[itemView.button addTarget:self
action:@selector(didTouchUpInsidebarItemButton:)
forControlEvents:UIControlEventTouchUpInside];
[self.itemViews addObject:itemView];
[self.itemsLayoutView addArrangedSubview:itemView];
itemView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *itemViewHeightConstraint =
[itemView.heightAnchor constraintEqualToConstant:barHeight];
// This priority is set to low to avoid conflict of constraints due to the itemView's height
// being the same as the bar.
itemViewHeightConstraint.priority = UILayoutPriorityDefaultLow;
[self.itemViewHeightConstraints addObject:itemViewHeightConstraint];
NSLayoutConstraint *itemViewWidthConstraint =
[itemView.widthAnchor constraintEqualToConstant:kDefaultVerticalLayoutWidth];
// This priority is set to low to avoid conflict of constraints due to the itemView's width
// being the same as the bar.
itemViewWidthConstraint.priority = UILayoutPriorityDefaultLow;
[self.itemViewWidthConstraints addObject:itemViewWidthConstraint];
}
self.selectedBarItem = nil;
[NSLayoutConstraint activateConstraints:self.itemViewHeightConstraints];
[self loadConstraints];
[self addObserversToTabBarItemsForBarItems];
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (void)setSelectedBarItem:(nullable MDCBottomNavigationBarItem *)selectedBarItem {
[self setSelectedBarItem:selectedBarItem animated:NO];
}
- (void)setSelectedBarItem:(nullable MDCBottomNavigationBarItem *)selectedBarItem
animated:(BOOL)animated {
if (_selectedBarItem == selectedBarItem) {
return;
}
_selectedBarItem = selectedBarItem;
for (NSUInteger i = 0; i < self.barItems.count; i++) {
MDCBottomNavigationBarItem *barItem = self.barItems[i];
MDCBottomNavigationItemView *itemView = self.itemViews[i];
if (selectedBarItem == barItem) {
[itemView setSelected:YES animated:animated];
} else {
[itemView setSelected:NO animated:animated];
}
}
}
// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to
// setBarItems.
- (void)setItems:(NSArray<UITabBarItem *> *)items {
if ([_items isEqual:items] || _items == items || _barItems.count > 0) {
return;
}
// If clients report conflicting gesture recognizers please see proposed solution in the
// internal document: go/mdc-ios-bottomnavigation-largecontentvieweritem
[self addInteraction:[[UILargeContentViewerInteraction alloc] initWithDelegate:self]];
// Remove existing item views from the bottom navigation so it can be repopulated with new items.
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
[itemView removeFromSuperview];
}
[self.itemViews removeAllObjects];
[self.itemViewHeightConstraints removeAllObjects];
[self.itemViewWidthConstraints removeAllObjects];
[self removeObserversFromTabBarItems];
_items = [items copy];
CGFloat barHeight = [self calculateBarHeight];
for (NSUInteger i = 0; i < items.count; i++) {
MDCBottomNavigationItemView *itemView =
[[MDCBottomNavigationItemView alloc] initWithFrame:CGRectZero];
itemView.rippleTouchController.delegate = self;
itemView.selected = NO;
itemView.displayTitleInVerticalLayout = self.displayItemTitlesInVerticalLayout;
itemView.enableVerticalLayout = self.enableVerticalLayout;
[self configureTitleStateForItemView:itemView];
[self configureItemView:itemView withItem:items[i] appearance:self.itemBadgeAppearance];
[itemView.button addTarget:self
action:@selector(didTouchUpInsideButton:)
forControlEvents:UIControlEventTouchUpInside];
[self.itemViews addObject:itemView];
[self.itemsLayoutView addArrangedSubview:itemView];
itemView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *itemViewHeightConstraint =
[itemView.heightAnchor constraintEqualToConstant:barHeight];
// This priority is set to low to avoid conflict of constraints due to the itemView's height
// being the same as the bar.
itemViewHeightConstraint.priority = UILayoutPriorityDefaultLow;
[self.itemViewHeightConstraints addObject:itemViewHeightConstraint];
NSLayoutConstraint *itemViewWidthConstraint =
[itemView.widthAnchor constraintEqualToConstant:kDefaultVerticalLayoutWidth];
// This priority is set to low to avoid conflict of constraints due to the itemView's width
// being the same as the bar.
itemViewWidthConstraint.priority = UILayoutPriorityDefaultLow;
[self.itemViewWidthConstraints addObject:itemViewWidthConstraint];
}
self.selectedItem = nil;
[NSLayoutConstraint activateConstraints:self.itemViewHeightConstraints];
[self loadConstraints];
[self addObserversToTabBarItems];
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to
// setBarItems.
- (void)setSelectedItem:(nullable UITabBarItem *)selectedItem {
[self setSelectedItem:selectedItem animated:NO];
}
// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to
// setBarItems.
- (void)setSelectedItem:(UITabBarItem *)selectedItem animated:(BOOL)animated {
if (_selectedItem == selectedItem) {
return;
}
_selectedItem = selectedItem;
for (NSUInteger i = 0; i < self.items.count; i++) {
UITabBarItem *item = self.items[i];
MDCBottomNavigationItemView *itemView = self.itemViews[i];
if (selectedItem == item) {
[itemView setSelected:YES animated:animated];
} else {
[itemView setSelected:NO animated:animated];
}
}
}
- (void)setItemsContentVerticalMargin:(CGFloat)itemsContentsVerticalMargin {
if (MDCCGFloatEqual(_itemsContentVerticalMargin, itemsContentsVerticalMargin)) {
return;
}
_itemsContentVerticalMargin = itemsContentsVerticalMargin;
for (NSUInteger i = 0; i < [self itemCount]; i++) {
MDCBottomNavigationItemView *itemView = self.itemViews[i];
itemView.contentVerticalMargin = itemsContentsVerticalMargin;
}
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (void)setItemsContentHorizontalMargin:(CGFloat)itemsContentHorizontalMargin {
if (MDCCGFloatEqual(_itemsContentHorizontalMargin, itemsContentHorizontalMargin)) {
return;
}
_itemsContentHorizontalMargin = itemsContentHorizontalMargin;
for (NSUInteger i = 0; i < [self itemCount]; i++) {
MDCBottomNavigationItemView *itemView = self.itemViews[i];
itemView.contentHorizontalMargin = itemsContentHorizontalMargin;
}
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (void)setItemsHorizontalPadding:(CGFloat)itemsHorizontalPadding {
if (MDCCGFloatEqual(_itemsHorizontalPadding, itemsHorizontalPadding)) {
return;
}
_itemsHorizontalPadding = itemsHorizontalPadding;
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (void)setTruncatesLongTitles:(BOOL)truncatesLongTitles {
_truncatesLongTitles = truncatesLongTitles;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.truncatesTitle = truncatesLongTitles;
[itemView setNeedsLayout];
}
[self setNeedsLayout];
}
- (void)setSelectedItemTintColor:(UIColor *)selectedItemTintColor {
_selectedItemTintColor = selectedItemTintColor;
_selectedItemTitleColor = selectedItemTintColor;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.selectedItemTintColor = selectedItemTintColor;
}
}
- (void)setUnselectedItemTintColor:(UIColor *)unselectedItemTintColor {
_unselectedItemTintColor = unselectedItemTintColor;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.unselectedItemTintColor = unselectedItemTintColor;
}
}
- (void)setSelectedItemTitleColor:(UIColor *)selectedItemTitleColor {
_selectedItemTitleColor = selectedItemTitleColor;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.selectedItemTitleColor = selectedItemTitleColor;
}
}
- (void)setTitleVisibility:(MDCBottomNavigationBarTitleVisibility)titleVisibility {
_titleVisibility = titleVisibility;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.titleVisibility = titleVisibility;
}
}
- (void)setItemTitleFont:(UIFont *)itemTitleFont {
_itemTitleFont = itemTitleFont;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.itemTitleFont = itemTitleFont;
}
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (void)setTitlesNumberOfLines:(NSInteger)titlesNumberOfLines {
_titlesNumberOfLines = titlesNumberOfLines;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.titleNumberOfLines = titlesNumberOfLines;
}
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (void)setBarTintColor:(nullable UIColor *)barTintColor {
_barTintColor = barTintColor;
self.barView.backgroundColor = barTintColor;
}
- (void)setBackgroundColor:(nullable UIColor *)backgroundColor {
self.barView.backgroundColor = backgroundColor;
}
- (nullable UIColor *)backgroundColor {
return self.barView.backgroundColor;
}
- (void)setBackgroundBlurEffectStyle:(UIBlurEffectStyle)backgroundBlurEffectStyle {
if (_backgroundBlurEffectStyle == backgroundBlurEffectStyle) {
return;
}
_backgroundBlurEffectStyle = backgroundBlurEffectStyle;
if (self.blurEffectView) {
self.blurEffectView.effect = [UIBlurEffect effectWithStyle:_backgroundBlurEffectStyle];
}
}
- (void)setBackgroundBlurEnabled:(BOOL)backgroundBlurEnabled {
if (_backgroundBlurEnabled == backgroundBlurEnabled) {
return;
}
_backgroundBlurEnabled = backgroundBlurEnabled;
if (_backgroundBlurEnabled & !self.blurEffectView) {
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:_backgroundBlurEffectStyle];
self.blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
self.blurEffectView.hidden = !_backgroundBlurEnabled;
self.blurEffectView.autoresizingMask =
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
[self insertSubview:self.blurEffectView atIndex:0]; // Needs to always be at the bottom
self.blurEffectView.frame = CGRectStandardize(self.bounds);
} else if (self.blurEffectView) {
self.blurEffectView.hidden = !_backgroundBlurEnabled;
}
}
- (void)setAlignment:(MDCBottomNavigationBarAlignment)alignment {
if (_alignment == alignment) {
return;
}
_alignment = alignment;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
[self configureTitleStateForItemView:itemView];
}
[self recalculateBarHeightAndUpdateLayout];
}
- (void)setShowsSelectionIndicator:(BOOL)showsSelectionIndicator {
if (showsSelectionIndicator == _showsSelectionIndicator) {
return;
}
_showsSelectionIndicator = showsSelectionIndicator;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.showsSelectionIndicator = _showsSelectionIndicator;
}
}
- (void)setSelectionIndicatorColor:(UIColor *)selectionIndicatorColor {
_selectionIndicatorColor = selectionIndicatorColor;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.selectionIndicatorColor = _selectionIndicatorColor;
}
}
- (void)setSelectionIndicatorSize:(CGSize)selectionIndicatorSize {
_selectionIndicatorSize = selectionIndicatorSize;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.selectionIndicatorSize = _selectionIndicatorSize;
}
}
#pragma mark - MDCRippleTouchControllerDelegate methods
- (BOOL)rippleTouchController:(MDCRippleTouchController *)rippleTouchController
shouldProcessRippleTouchesAtTouchLocation:(CGPoint)location {
return self.rippleEnabled;
}
#pragma mark - MDCElevation
- (CGFloat)mdc_currentElevation {
return self.elevation;
}
- (MDCShadowsCollection *)shadowsCollection {
if (!_shadowsCollection) {
_shadowsCollection = MDCShadowsCollectionDefault();
}
return _shadowsCollection;
}
- (void)setShadowsCollection:(nullable MDCShadowsCollection *)shadowsCollection {
_shadowsCollection = shadowsCollection;
[self updateShadow];
}
- (void)cancelRippleInItemView:(MDCBottomNavigationItemView *)itemView animated:(BOOL)animated {
if (self.isRippleEnabled) {
if (animated) {
[itemView.rippleTouchController.rippleView beginRippleTouchUpAnimated:YES completion:nil];
} else {
[itemView.rippleTouchController.rippleView cancelAllRipplesAnimated:NO completion:nil];
}
}
}
- (void)beginRippleInItemView:(MDCBottomNavigationItemView *)itemView animated:(BOOL)animated {
if (self.isRippleEnabled) {
[itemView.rippleTouchController.rippleView beginRippleTouchDownAtPoint:itemView.center
animated:animated
completion:nil];
}
}
#pragma mark - UILargeContentViewerInteractionDelegate
#if MDC_AVAILABLE_SDK_IOS(13_0)
- (nullable id<UILargeContentViewerItem>)largeContentViewerInteraction:
(UILargeContentViewerInteraction *)interaction
itemAtPoint:(CGPoint)point
NS_AVAILABLE_IOS(13_0) {
MDCBottomNavigationItemView *lastItemView =
(MDCBottomNavigationItemView *)self.lastLargeContentViewerItem;
if (!CGRectContainsPoint(self.itemsLayoutView.frame, point)) {
// The touch has wandered outside of the view. Clear the ripple and do not display the
// content viewer.
if (lastItemView) {
[self cancelRippleInItemView:lastItemView animated:NO];
}
self.lastLargeContentViewerItem = nil;
return nil;
}
MDCBottomNavigationItemView *itemView = [self itemViewForPoint:point];
if (!itemView) {
// The touch is still within the navigation bar. Return the last seen item view.
return self.lastLargeContentViewerItem;
}
if (lastItemView != itemView) {
if (lastItemView) {
[self cancelRippleInItemView:lastItemView animated:NO];
}
// Only start ripple if it's not the first touch down of the long press
if (self.isLargeContentLongPressInProgress) {
[self beginRippleInItemView:itemView animated:NO];
}
self.lastLargeContentViewerItem = itemView;
}
self.isLargeContentLongPressInProgress = YES;
return itemView;
}
- (void)largeContentViewerInteraction:(UILargeContentViewerInteraction *)interaction
didEndOnItem:(nullable id<UILargeContentViewerItem>)item
atPoint:(CGPoint)point NS_AVAILABLE_IOS(13_0) {
if (self.lastLargeContentViewerItem) {
MDCBottomNavigationItemView *lastItemView =
(MDCBottomNavigationItemView *)self.lastLargeContentViewerItem;
[self cancelRippleInItemView:lastItemView animated:YES];
[self didTouchUpInsideButton:lastItemView.button];
}
self.lastLargeContentViewerItem = nil;
self.isLargeContentLongPressInProgress = NO;
}
#endif // MDC_AVAILABLE_SDK_IOS(13_0)
#ifdef __IPHONE_13_4
#pragma mark - UIPointerInteractionDelegate
- (nullable UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction
styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) {
MDCBottomNavigationItemView *bottomNavigationView = interaction.view;
if (![bottomNavigationView isKindOfClass:[MDCBottomNavigationItemView class]]) {
return nil;
}
UITargetedPreview *targetedPreview = [[UITargetedPreview alloc] initWithView:interaction.view];
UIPointerEffect *highlightEffect = [UIPointerHighlightEffect effectWithPreview:targetedPreview];
CGRect hoverRect =
[bottomNavigationView convertRect:[bottomNavigationView pointerEffectHighlightRect]
toView:self.itemsLayoutView];
UIPointerShape *shape = [UIPointerShape shapeWithRoundedRect:hoverRect];
return [UIPointerStyle styleWithEffect:highlightEffect shape:shape];
}
#endif
#pragma mark - Performant Shadow Toggle
+ (void)setEnablePerformantShadow:(BOOL)enable {
gEnablePerformantShadow = enable;
}
+ (BOOL)enablePerformantShadow {
return gEnablePerformantShadow;
}
#pragma mark - Configuring the ripple appearance
- (void)setRippleColor:(nullable UIColor *)rippleColor {
_rippleColor = rippleColor;
for (NSUInteger i = 0; i < [self itemCount]; ++i) {
MDCBottomNavigationItemView *itemView = self.itemViews[i];
itemView.rippleColor = _rippleColor;
}
}
#pragma mark - Configuring the visual appearance for all badges
- (void)setItemBadgeAppearance:(MDCBadgeAppearance *)itemBadgeAppearance {
_itemBadgeAppearance = [itemBadgeAppearance copy];
// Setting default values if `nil` is received for the appearance properties.
// The default value for the textColor is `white` and the default value for the font is
// `systemFontOfSize:8`.
if (!_itemBadgeAppearance.textColor) {
_itemBadgeAppearance.textColor = [UIColor whiteColor];
}
if (!_itemBadgeAppearance.font) {
_itemBadgeAppearance.font = [UIFont systemFontOfSize:kBadgeFontSize];
}
for (NSUInteger i = 0; i < [self itemCount]; ++i) {
MDCBottomNavigationItemView *itemView = self.itemViews[i];
if (self.barItems.count > 0 && self.barItems[i].badgeAppearance != nil) {
itemView.badgeAppearance = self.barItems[i].badgeAppearance;
} else {
itemView.badgeAppearance = _itemBadgeAppearance;
}
}
}
- (void)setItemBadgeHorizontalOffset:(CGFloat)itemBadgeHorizontalOffset {
_itemBadgeHorizontalOffset = itemBadgeHorizontalOffset;
for (NSUInteger i = 0; i < [self itemCount]; ++i) {
MDCBottomNavigationItemView *itemView = self.itemViews[i];
itemView.badgeHorizontalOffset = itemBadgeHorizontalOffset;
}
}
// TODO(b/244765238): Remove branching layout logic after GM3 migrations
// Assume that setting itemBadgeHorizontalOffset to any value other than its default (0) should
// activate the new anchored/GM3 layout, and assume that anyone applying GM3 branding for Bottom
// Navigation will use a non-zero offset.
- (BOOL)shouldUseAnchoredLayout {
return _itemBadgeHorizontalOffset != 0;
}
- (BOOL)itemViewsShouldAlwaysHideTitles {
return _titleVisibility == MDCBottomNavigationBarTitleVisibilityNever;
}
- (BOOL)barHeightShouldShrinkBasedOnTraitCollection:(UITraitCollection *)traitCollection {
return traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact ||
[self itemViewsShouldAlwaysHideTitles];
}
- (void)configureTitleStateForItemView:(MDCBottomNavigationItemView *)itemView {
if ([self shouldUseAnchoredLayout]) {
itemView.titleBelowIcon = ![self itemViewsShouldAlwaysHideTitles];
} else {
itemView.titleBelowIcon = self.isTitleBelowIcon;
}
}
- (void)setEnableSquareImages:(BOOL)enableSquareImages {
_enableSquareImages = enableSquareImages;
for (MDCBottomNavigationItemView *itemView in self.itemViews) {
itemView.enableSquareImages = enableSquareImages;
}
}
@end
NS_ASSUME_NONNULL_END