blob: 2e8d0cdf7716d60c7c75406a455250954e190bbf [file] [log] [blame] [edit]
// 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 "MDCBottomAppBarView.h"
#import <MDFInternationalization/MDFInternationalization.h>
#import "private/MDCBottomAppBarAttributes.h"
#import "private/MDCBottomAppBarLayer.h"
#import "MaterialButtons.h"
#import "MaterialElevation.h"
#import "MaterialNavigationBar.h"
#import "MaterialShadowElevations.h"
#import "MaterialMath.h"
static NSString *kMDCBottomAppBarViewAnimKeyString = @"AnimKey";
static NSString *kMDCBottomAppBarViewPathString = @"path";
static NSString *kMDCBottomAppBarViewPositionString = @"position";
static const CGFloat kMDCBottomAppBarViewFloatingButtonCenterToNavigationBarTopOffset = 0;
static const CGFloat kMDCBottomAppBarViewFloatingButtonElevationPrimary = 6;
static const CGFloat kMDCBottomAppBarViewFloatingButtonElevationSecondary = 4;
@interface MDCBottomAppBarCutView : UIView
@end
@implementation MDCBottomAppBarCutView
// Allows touch events to pass through so MDCBottomAppBarController can handle touch events.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
return (view == self) ? nil : view;
}
@end
@interface MDCBottomAppBarView () <CAAnimationDelegate>
@property(nonatomic, assign) CGFloat bottomBarHeight;
@property(nonatomic, strong) MDCBottomAppBarCutView *cutView;
@property(nonatomic, strong) MDCBottomAppBarLayer *bottomBarLayer;
@property(nonatomic, strong) MDCNavigationBar *navBar;
@end
@implementation MDCBottomAppBarView
@synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation;
@synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonMDCBottomAppBarViewInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonMDCBottomAppBarViewInit];
}
return self;
}
- (void)commonMDCBottomAppBarViewInit {
self.cutView = [[MDCBottomAppBarCutView alloc] initWithFrame:self.bounds];
self.floatingButtonVerticalOffset =
kMDCBottomAppBarViewFloatingButtonCenterToNavigationBarTopOffset;
[self addSubview:self.cutView];
self.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleRightMargin);
[self addFloatingButton];
[self addBottomBarLayer];
[self addNavBar];
self.barTintColor = UIColor.whiteColor;
self.shadowColor = UIColor.blackColor;
_elevation = MDCShadowElevationBottomAppBar;
_mdc_overrideBaseElevation = -1;
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(UIViewNoIntrinsicMetric, kMDCBottomAppBarHeight);
}
- (void)addFloatingButton {
MDCFloatingButton *floatingButton = [[MDCFloatingButton alloc] init];
[self setFloatingButton:floatingButton];
[self setFloatingButtonPosition:MDCBottomAppBarFloatingButtonPositionCenter];
[self setFloatingButtonElevation:MDCBottomAppBarFloatingButtonElevationPrimary];
[self setFloatingButtonHidden:NO];
}
- (void)addNavBar {
_navBar = [[MDCNavigationBar alloc] initWithFrame:CGRectZero];
[self addSubview:_navBar];
_navBar.backgroundColor = [UIColor clearColor];
_navBar.tintColor = [UIColor blackColor];
_navBar.leadingBarItemsTintColor = UIColor.blackColor;
_navBar.trailingBarItemsTintColor = UIColor.blackColor;
}
- (void)addBottomBarLayer {
if (_bottomBarLayer) {
[_bottomBarLayer removeFromSuperlayer];
}
_bottomBarLayer = [MDCBottomAppBarLayer layer];
[_cutView.layer addSublayer:_bottomBarLayer];
}
- (void)renderPathBasedOnFloatingButtonVisibitlityAnimated:(BOOL)animated {
if (!self.floatingButtonHidden) {
[self cutBottomAppBarViewAnimated:animated];
} else {
[self healBottomAppBarViewAnimated:animated];
}
}
- (CGPoint)getFloatingButtonCenterPositionForAppBarWidth:(CGFloat)appBarWidth {
CGPoint floatingButtonPoint = CGPointZero;
CGFloat navigationBarMinY = CGRectGetMinY(self.navBar.frame);
floatingButtonPoint.y = MAX(0, navigationBarMinY - self.floatingButtonVerticalOffset);
UIEdgeInsets safeAreaInsets = UIEdgeInsetsZero;
if (@available(iOS 11.0, *)) {
safeAreaInsets = self.safeAreaInsets;
}
CGFloat leftCenter = kMDCBottomAppBarFloatingButtonPositionX + safeAreaInsets.left;
CGFloat rightCenter =
appBarWidth - kMDCBottomAppBarFloatingButtonPositionX - safeAreaInsets.right;
BOOL isRTL =
self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
switch (self.floatingButtonPosition) {
case MDCBottomAppBarFloatingButtonPositionLeading: {
floatingButtonPoint.x = isRTL ? rightCenter : leftCenter;
break;
}
case MDCBottomAppBarFloatingButtonPositionCenter: {
floatingButtonPoint.x = appBarWidth / 2;
break;
}
case MDCBottomAppBarFloatingButtonPositionTrailing: {
floatingButtonPoint.x = isRTL ? leftCenter : rightCenter;
break;
}
default:
break;
}
return floatingButtonPoint;
}
- (void)cutBottomAppBarViewAnimated:(BOOL)animated {
CGPathRef pathWithCut = [self.bottomBarLayer pathFromRect:self.bounds
floatingButton:self.floatingButton
navigationBarFrame:self.navBar.frame
shouldCut:YES];
if (animated) {
CABasicAnimation *pathAnimation =
[CABasicAnimation animationWithKeyPath:kMDCBottomAppBarViewPathString];
pathAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnimation.duration = kMDCFloatingButtonExitDuration;
pathAnimation.fromValue = (id)self.bottomBarLayer.presentationLayer.path;
pathAnimation.toValue = (__bridge id _Nullable)(pathWithCut);
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.delegate = self;
[pathAnimation setValue:kMDCBottomAppBarViewPathString
forKey:kMDCBottomAppBarViewAnimKeyString];
[self.bottomBarLayer addAnimation:pathAnimation forKey:kMDCBottomAppBarViewPathString];
} else {
self.bottomBarLayer.path = pathWithCut;
}
}
- (void)healBottomAppBarViewAnimated:(BOOL)animated {
CGPathRef pathWithoutCut = [self.bottomBarLayer pathFromRect:self.bounds
floatingButton:self.floatingButton
navigationBarFrame:self.navBar.frame
shouldCut:NO];
if (animated) {
CABasicAnimation *pathAnimation =
[CABasicAnimation animationWithKeyPath:kMDCBottomAppBarViewPathString];
pathAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnimation.duration = kMDCFloatingButtonEnterDuration;
pathAnimation.fromValue = (id)self.bottomBarLayer.presentationLayer.path;
pathAnimation.toValue = (__bridge id _Nullable)(pathWithoutCut);
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.delegate = self;
[pathAnimation setValue:kMDCBottomAppBarViewPathString
forKey:kMDCBottomAppBarViewAnimKeyString];
[self.bottomBarLayer addAnimation:pathAnimation forKey:kMDCBottomAppBarViewPathString];
} else {
self.bottomBarLayer.path = pathWithoutCut;
}
}
- (void)moveFloatingButtonCenterAnimated:(BOOL)animated {
CGPoint endPoint =
[self getFloatingButtonCenterPositionForAppBarWidth:CGRectGetWidth(self.bounds)];
if (animated) {
CABasicAnimation *animation =
[CABasicAnimation animationWithKeyPath:kMDCBottomAppBarViewPositionString];
animation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.duration = kMDCFloatingButtonExitDuration;
animation.fromValue = [NSValue valueWithCGPoint:self.floatingButton.center];
animation.toValue = [NSValue valueWithCGPoint:endPoint];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
animation.delegate = self;
[animation setValue:kMDCBottomAppBarViewPositionString
forKey:kMDCBottomAppBarViewAnimKeyString];
[self.floatingButton.layer addAnimation:animation forKey:kMDCBottomAppBarViewPositionString];
}
self.floatingButton.center = endPoint;
}
- (void)showBarButtonItemsWithFloatingButtonPosition:
(MDCBottomAppBarFloatingButtonPosition)floatingButtonPosition {
switch (floatingButtonPosition) {
case MDCBottomAppBarFloatingButtonPositionCenter:
[self.navBar setLeadingBarButtonItems:_leadingBarButtonItems];
[self.navBar setTrailingBarButtonItems:_trailingBarButtonItems];
break;
case MDCBottomAppBarFloatingButtonPositionLeading:
[self.navBar setLeadingBarButtonItems:nil];
[self.navBar setTrailingBarButtonItems:_trailingBarButtonItems];
break;
case MDCBottomAppBarFloatingButtonPositionTrailing:
[self.navBar setLeadingBarButtonItems:_leadingBarButtonItems];
[self.navBar setTrailingBarButtonItems:nil];
break;
default:
break;
}
}
#pragma mark - UIView overrides
- (void)layoutSubviews {
[super layoutSubviews];
CGRect navBarFrame =
CGRectMake(0, kMDCBottomAppBarNavigationViewYOffset, CGRectGetWidth(self.bounds),
kMDCBottomAppBarHeight - kMDCBottomAppBarNavigationViewYOffset);
self.navBar.frame = navBarFrame;
self.floatingButton.center =
[self getFloatingButtonCenterPositionForAppBarWidth:CGRectGetWidth(self.bounds)];
[self renderPathBasedOnFloatingButtonVisibitlityAnimated:NO];
self.bottomBarLayer.fillColor = self.barTintColor.CGColor;
self.bottomBarLayer.shadowColor = self.shadowColor.CGColor;
}
- (UIEdgeInsets)mdc_safeAreaInsets {
UIEdgeInsets insets = UIEdgeInsetsZero;
if (@available(iOS 11.0, *)) {
// Accommodate insets for iPhone X.
insets = self.safeAreaInsets;
}
return insets;
}
- (CGSize)sizeThatFits:(CGSize)size {
UIEdgeInsets insets = self.mdc_safeAreaInsets;
CGFloat heightWithInset = kMDCBottomAppBarHeight + insets.bottom;
CGSize insetSize = CGSizeMake(size.width, heightWithInset);
return insetSize;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// Make sure the floating button can always be tapped.
BOOL contains = CGRectContainsPoint(self.floatingButton.frame, point);
if (contains) {
return self.floatingButton;
}
UIView *view = [super hitTest:point withEvent:event];
// Only subviews can receive events.
return (view == self) ? nil : view;
}
#pragma mark - CAAnimationDelegate
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
if (flag) {
[self renderPathBasedOnFloatingButtonVisibitlityAnimated:NO];
NSString *animValueForKeyString = [animation valueForKey:kMDCBottomAppBarViewAnimKeyString];
if ([animValueForKeyString isEqualToString:kMDCBottomAppBarViewPathString]) {
[self.bottomBarLayer removeAnimationForKey:kMDCBottomAppBarViewPathString];
} else if ([animValueForKeyString isEqualToString:kMDCBottomAppBarViewPositionString]) {
[self.floatingButton.layer removeAnimationForKey:kMDCBottomAppBarViewPositionString];
}
}
}
#pragma mark - Setters
- (void)setElevation:(MDCShadowElevation)elevation {
if (MDCCGFloatEqual(elevation, _elevation)) {
return;
}
_elevation = elevation;
[self mdc_elevationDidChange];
}
- (void)setFloatingButton:(MDCFloatingButton *)floatingButton {
if (_floatingButton == floatingButton) {
return;
}
[_floatingButton removeFromSuperview];
_floatingButton = floatingButton;
_floatingButton.translatesAutoresizingMaskIntoConstraints = NO;
[_floatingButton sizeToFit];
}
- (void)setFloatingButtonElevation:(MDCBottomAppBarFloatingButtonElevation)floatingButtonElevation {
[self setFloatingButtonElevation:floatingButtonElevation animated:NO];
}
- (void)setFloatingButtonElevation:(MDCBottomAppBarFloatingButtonElevation)floatingButtonElevation
animated:(BOOL)animated {
if (_floatingButton.superview == self && _floatingButtonElevation == floatingButtonElevation) {
return;
}
_floatingButtonElevation = floatingButtonElevation;
CGFloat elevation = kMDCBottomAppBarViewFloatingButtonElevationPrimary;
NSInteger subViewIndex = 1;
if (floatingButtonElevation == MDCBottomAppBarFloatingButtonElevationSecondary) {
elevation = kMDCBottomAppBarViewFloatingButtonElevationSecondary;
subViewIndex = 0;
}
// Immediately move the button to the correct z-ordering so that the shadow clipping effect isn't
// as apparent. If we did this at the end of the animation, then the shadow would appear to
// suddenly clip at the end of the animation.
[self insertSubview:_floatingButton atIndex:subViewIndex];
[_floatingButton setElevation:elevation forState:UIControlStateNormal];
}
- (void)setFloatingButtonPosition:(MDCBottomAppBarFloatingButtonPosition)floatingButtonPosition {
[self setFloatingButtonPosition:floatingButtonPosition animated:NO];
}
- (void)setFloatingButtonPosition:(MDCBottomAppBarFloatingButtonPosition)floatingButtonPosition
animated:(BOOL)animated {
if (_floatingButtonPosition == floatingButtonPosition) {
return;
}
_floatingButtonPosition = floatingButtonPosition;
[self moveFloatingButtonCenterAnimated:animated];
[self renderPathBasedOnFloatingButtonVisibitlityAnimated:animated];
[self showBarButtonItemsWithFloatingButtonPosition:floatingButtonPosition];
}
- (void)setFloatingButtonHidden:(BOOL)floatingButtonHidden {
[self setFloatingButtonHidden:floatingButtonHidden animated:NO];
}
- (void)setFloatingButtonHidden:(BOOL)floatingButtonHidden animated:(BOOL)animated {
if (_floatingButtonHidden == floatingButtonHidden) {
return;
}
_floatingButtonHidden = floatingButtonHidden;
if (floatingButtonHidden) {
[self healBottomAppBarViewAnimated:animated];
[_floatingButton collapse:animated
completion:^{
self.floatingButton.hidden = YES;
}];
} else {
_floatingButton.hidden = NO;
[self cutBottomAppBarViewAnimated:animated];
[_floatingButton expand:animated completion:nil];
}
}
- (void)setLeadingBarButtonItems:(NSArray<UIBarButtonItem *> *)leadingBarButtonItems {
_leadingBarButtonItems = [leadingBarButtonItems copy];
[self showBarButtonItemsWithFloatingButtonPosition:self.floatingButtonPosition];
}
- (void)setTrailingBarButtonItems:(NSArray<UIBarButtonItem *> *)trailingBarButtonItems {
_trailingBarButtonItems = [trailingBarButtonItems copy];
[self showBarButtonItemsWithFloatingButtonPosition:self.floatingButtonPosition];
}
- (void)setBarTintColor:(UIColor *)barTintColor {
_barTintColor = barTintColor;
_bottomBarLayer.fillColor = barTintColor.CGColor;
}
- (void)setLeadingBarItemsTintColor:(UIColor *)leadingBarItemsTintColor {
NSParameterAssert(leadingBarItemsTintColor);
if (!leadingBarItemsTintColor) {
leadingBarItemsTintColor = UIColor.blackColor;
}
self.navBar.leadingBarItemsTintColor = leadingBarItemsTintColor;
}
- (UIColor *)leadingBarItemsTintColor {
return self.navBar.leadingBarItemsTintColor;
}
- (void)setTrailingBarItemsTintColor:(UIColor *)trailingBarItemsTintColor {
NSParameterAssert(trailingBarItemsTintColor);
if (!trailingBarItemsTintColor) {
trailingBarItemsTintColor = UIColor.blackColor;
}
self.navBar.trailingBarItemsTintColor = trailingBarItemsTintColor;
}
- (UIColor *)trailingBarItemsTintColor {
return self.navBar.trailingBarItemsTintColor;
}
- (void)setShadowColor:(UIColor *)shadowColor {
_shadowColor = shadowColor;
_bottomBarLayer.shadowColor = shadowColor.CGColor;
}
- (void)setRippleColor:(UIColor *)rippleColor {
_rippleColor = [rippleColor copy];
self.navBar.rippleColor = _rippleColor;
}
- (BOOL)enableRippleBehavior {
return self.navBar.enableRippleBehavior;
}
- (void)setEnableRippleBehavior:(BOOL)enableRippleBehavior {
self.navBar.enableRippleBehavior = enableRippleBehavior;
}
#pragma mark TraitCollection
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
}
#pragma mark - MDCElevation
- (CGFloat)mdc_currentElevation {
return self.elevation;
}
@end