blob: 19f7bfbffeb41605d391a1a40c3d009f3b8628ef [file] [log] [blame] [edit]
// Copyright 2018-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 "MDCBottomDrawerPresentationController.h"
#import "MDCBottomDrawerViewController.h"
#import "MaterialPalettes.h"
#import "private/MDCBottomDrawerContainerViewController.h"
static CGFloat kTopHandleHeight = (CGFloat)2.0;
static CGFloat kTopHandleWidth = (CGFloat)24.0;
static CGFloat kTopHandleTopMargin = (CGFloat)5.0;
@interface MDCBottomDrawerPresentationController () <UIGestureRecognizerDelegate,
MDCBottomDrawerContainerViewControllerDelegate>
/**
A semi-transparent scrim view that darkens the visible main view when the drawer is displayed.
*/
@property(nonatomic) UIView *scrimView;
/**
The top handle view at the top of the drawer to provide a visual affordance for scrollability.
*/
@property(nonatomic) UIView *topHandle;
/**
The bottom drawer container view controller.
*/
@property(nonatomic) MDCBottomDrawerContainerViewController *bottomDrawerContainerViewController;
@end
@implementation MDCBottomDrawerPresentationController
@synthesize delegate;
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController
presentingViewController:(UIViewController *)presentingViewController {
self = [super initWithPresentedViewController:presentedViewController
presentingViewController:presentingViewController];
if (self) {
_topHandleHidden = YES;
}
return self;
}
- (UIView *)presentedView {
if ([self.presentedViewController isKindOfClass:[MDCBottomDrawerViewController class]]) {
return super.presentedView;
} else {
// Override the presentedView property getter to return our container view controller's
// view instead of the default.
return self.bottomDrawerContainerViewController.view;
}
}
- (void)presentationTransitionWillBegin {
MDCBottomDrawerContainerViewController *bottomDrawerContainerViewController =
[[MDCBottomDrawerContainerViewController alloc]
initWithOriginalPresentingViewController:self.presentingViewController
trackingScrollView:self.trackingScrollView];
if ([self.presentedViewController isKindOfClass:[MDCBottomDrawerViewController class]]) {
// If in fact the presentedViewController is an MDCBottomDrawerViewController,
// we then know there is a content and an (optional) header view controller.
// We pass those view controllers to the MDCBottomDrawerContainerViewController that
// consists of the drawer logic.
MDCBottomDrawerViewController *bottomDrawerViewController =
(MDCBottomDrawerViewController *)self.presentedViewController;
self.delegate = bottomDrawerViewController;
bottomDrawerContainerViewController.contentViewController =
bottomDrawerViewController.contentViewController;
bottomDrawerContainerViewController.headerViewController =
bottomDrawerViewController.headerViewController;
} else {
bottomDrawerContainerViewController.contentViewController = self.presentedViewController;
}
bottomDrawerContainerViewController.animatingPresentation = YES;
self.bottomDrawerContainerViewController = bottomDrawerContainerViewController;
self.bottomDrawerContainerViewController.delegate = self;
self.scrimView = [[UIView alloc] initWithFrame:self.containerView.bounds];
self.scrimView.backgroundColor =
self.scrimColor ?: [UIColor colorWithWhite:0 alpha:(CGFloat)0.32];
self.scrimView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.scrimView.accessibilityIdentifier = @"Close drawer";
self.scrimView.accessibilityTraits |= UIAccessibilityTraitButton;
[self.containerView addSubview:self.scrimView];
self.topHandle =
[[UIView alloc] initWithFrame:CGRectMake(0, 0, kTopHandleWidth, kTopHandleHeight)];
self.topHandle.layer.cornerRadius = kTopHandleHeight * (CGFloat)0.5;
self.topHandle.backgroundColor = self.topHandleColor ?: MDCPalette.greyPalette.tint300;
self.topHandle.hidden = self.topHandleHidden;
self.topHandle.translatesAutoresizingMaskIntoConstraints = NO;
UIView *handleSuperview = nil;
if (bottomDrawerContainerViewController.headerViewController) {
handleSuperview = bottomDrawerContainerViewController.headerViewController.view;
} else {
handleSuperview = bottomDrawerContainerViewController.contentViewController.view;
}
[handleSuperview addSubview:self.topHandle];
[NSLayoutConstraint constraintWithItem:self.topHandle
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:handleSuperview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:kTopHandleTopMargin]
.active = YES;
[NSLayoutConstraint constraintWithItem:self.topHandle
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:handleSuperview
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0]
.active = YES;
[NSLayoutConstraint constraintWithItem:self.topHandle
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:kTopHandleWidth]
.active = YES;
[NSLayoutConstraint constraintWithItem:self.topHandle
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:kTopHandleHeight]
.active = YES;
if ([self.presentedViewController isKindOfClass:[MDCBottomDrawerViewController class]]) {
[self.presentedView addSubview:self.bottomDrawerContainerViewController.view];
[self.presentedViewController addChildViewController:self.bottomDrawerContainerViewController];
} else {
[self.containerView addSubview:self.bottomDrawerContainerViewController.view];
}
id<UIViewControllerTransitionCoordinator> transitionCoordinator =
[[self presentingViewController] transitionCoordinator];
// Fade in the scrim view during the transition.
self.scrimView.alpha = 0.0;
[transitionCoordinator
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.scrimView.alpha = 1.0;
}
completion:nil];
}
- (void)presentationTransitionDidEnd:(BOOL)completed {
// Set up the tap recognizer to dimiss the drawer by.
UITapGestureRecognizer *tapGestureRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideDrawer)];
[self.containerView addGestureRecognizer:tapGestureRecognizer];
tapGestureRecognizer.delegate = self;
self.bottomDrawerContainerViewController.animatingPresentation = NO;
[self.bottomDrawerContainerViewController.view setNeedsLayout];
if (!completed) {
[self.scrimView removeFromSuperview];
[self.topHandle removeFromSuperview];
}
}
- (void)dismissalTransitionWillBegin {
id<UIViewControllerTransitionCoordinator> transitionCoordinator =
[[self presentingViewController] transitionCoordinator];
[transitionCoordinator
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.scrimView.alpha = 0.0;
}
completion:nil];
}
- (void)dismissalTransitionDidEnd:(BOOL)completed {
if (completed) {
if ([self.presentedViewController isKindOfClass:[MDCBottomDrawerViewController class]]) {
CGRect newFrame = CGRectStandardize(
self.bottomDrawerContainerViewController.contentViewController.view.frame);
newFrame.size.height -= self.bottomDrawerContainerViewController.addedHeight;
self.bottomDrawerContainerViewController.contentViewController.view.frame = newFrame;
[self.bottomDrawerContainerViewController removeFromParentViewController];
}
[self.scrimView removeFromSuperview];
[self.topHandle removeFromSuperview];
}
}
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container {
[super preferredContentSizeDidChangeForChildContentContainer:container];
[self.bottomDrawerContainerViewController.view layoutIfNeeded];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.bottomDrawerContainerViewController viewWillTransitionToSize:size
withTransitionCoordinator:coordinator];
}
- (void)setScrimColor:(UIColor *)scrimColor {
_scrimColor = scrimColor;
self.scrimView.backgroundColor = scrimColor;
}
- (void)setTopHandleHidden:(BOOL)topHandleHidden {
_topHandleHidden = topHandleHidden;
self.topHandle.hidden = topHandleHidden;
}
- (void)setTopHandleColor:(UIColor *)topHandleColor {
_topHandleColor = topHandleColor;
self.topHandle.backgroundColor = topHandleColor;
}
- (BOOL)contentReachesFullscreen {
return self.bottomDrawerContainerViewController.contentReachesFullscreen;
}
#pragma mark - Private
- (void)hideDrawer {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch {
return [self.bottomDrawerContainerViewController gestureRecognizer:gestureRecognizer
shouldReceiveTouch:touch];
}
#pragma mark - MDCBottomDrawerContainerViewControllerDelegate
- (void)bottomDrawerContainerViewControllerWillChangeState:
(MDCBottomDrawerContainerViewController *)containerViewController
drawerState:(MDCBottomDrawerState)drawerState {
id<MDCBottomDrawerPresentationControllerDelegate> strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(bottomDrawerWillChangeState:drawerState:)]) {
[strongDelegate bottomDrawerWillChangeState:self drawerState:drawerState];
}
}
- (void)bottomDrawerContainerViewControllerTopTransitionRatio:
(MDCBottomDrawerContainerViewController *)containerViewController
transitionRatio:(CGFloat)transitionRatio {
id<MDCBottomDrawerPresentationControllerDelegate> strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(bottomDrawerTopTransitionRatio:
transitionRatio:)]) {
[strongDelegate bottomDrawerTopTransitionRatio:self transitionRatio:transitionRatio];
self.topHandle.alpha = (CGFloat)1.0 - transitionRatio;
}
}
- (void)setContentOffsetY:(CGFloat)contentOffsetY animated:(BOOL)animated {
[self.bottomDrawerContainerViewController setContentOffsetY:contentOffsetY animated:animated];
}
- (void)expandToFullscreenWithDuration:(CGFloat)duration
completion:(void (^__nullable)(BOOL finished))completion {
[self.bottomDrawerContainerViewController expandToFullscreenWithDuration:duration
completion:completion];
}
@end