diff --git a/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.h b/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.h index e0a318929..76118b32e 100644 --- a/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.h +++ b/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.h @@ -13,6 +13,25 @@ // limitations under the License. #import +#import "MDCBottomDrawerState.h" + +@class MDCBottomDrawerPresentationController; + +/** + Delegate for MDCBottomSheetPresentationController. + */ +@protocol MDCBottomDrawerPresentationControllerDelegate +/** + This method is called when the bottom drawer will change its presented state to one of the + MDCBottomDrawerState states. + + @param presentationController presentation controller of the bottom drawer + @param drawerState the drawer's state + */ +- (void)bottomDrawerWillChangeState: + (nonnull MDCBottomDrawerPresentationController *)presentationController + drawerState:(MDCBottomDrawerState)drawerState; +@end /** The presentation controller to use for presenting an MDC bottom drawer. @@ -27,4 +46,9 @@ */ @property(nonatomic, weak, nullable) UIScrollView *trackingScrollView; +/** + Delegate to tell the presenter when the drawer will change state. + */ +@property(nonatomic, weak, nullable) id delegate; + @end diff --git a/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.m b/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.m index fdd0168f2..f935140bd 100644 --- a/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.m +++ b/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.m @@ -21,7 +21,8 @@ static UIColor *DrawerOverlayBackgroundColor(void) { return [UIColor colorWithWhite:0 alpha:0.4f]; } -@interface MDCBottomDrawerPresentationController () +@interface MDCBottomDrawerPresentationController () /** A semi-transparent scrim view that darkens the visible main view when the drawer is displayed. @@ -37,6 +38,8 @@ static UIColor *DrawerOverlayBackgroundColor(void) { @implementation MDCBottomDrawerPresentationController +@synthesize delegate; + - (UIView *)presentedView { if ([self.presentedViewController isKindOfClass:[MDCBottomDrawerViewController class]]) { return super.presentedView; @@ -63,11 +66,13 @@ static UIColor *DrawerOverlayBackgroundColor(void) { bottomDrawerViewController.contentViewController; bottomDrawerContainerViewController.headerViewController = bottomDrawerViewController.headerViewController; + self.delegate = bottomDrawerViewController; } 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 = DrawerOverlayBackgroundColor(); @@ -153,4 +158,15 @@ static UIColor *DrawerOverlayBackgroundColor(void) { shouldReceiveTouch:touch]; } +#pragma mark - MDCBottomDrawerContainerViewControllerDelegate + +- (void)bottomDrawerContainerViewControllerWillChangeState: + (MDCBottomDrawerContainerViewController *)containerViewController + drawerState:(MDCBottomDrawerState)drawerState { + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(bottomDrawerWillChangeState:drawerState:)]) { + [strongDelegate bottomDrawerWillChangeState:self drawerState:drawerState]; + } +} + @end diff --git a/components/NavigationDrawer/src/MDCBottomDrawerState.h b/components/NavigationDrawer/src/MDCBottomDrawerState.h new file mode 100644 index 000000000..040f22c20 --- /dev/null +++ b/components/NavigationDrawer/src/MDCBottomDrawerState.h @@ -0,0 +1,32 @@ +// 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 + +/** + The MDCBottomDrawerState enum provides the different possible states the bottom drawer can be in. + There are 2 different states for the bottom drawer: + + - MDCBottomDrawerStateCollapsed: This state is reached when the bottom drawer is collapsed + (can be expanded to present more content), but is not taking up the entire screen. + - MDCBottomDrawerStateExpanded: This state is reached when the bottom drawer is fully expanded + (displaying the entire content), but is not taking up the entire screen. + - MDCBottomDrawerStateFullScreen: This state is reached when the bottom drawer + is in full screen. + */ +typedef NS_ENUM(NSUInteger, MDCBottomDrawerState) { + MDCBottomDrawerStateCollapsed = 0, + MDCBottomDrawerStateExpanded = 1, + MDCBottomDrawerStateFullScreen = 2, +}; diff --git a/components/NavigationDrawer/src/MDCBottomDrawerViewController.h b/components/NavigationDrawer/src/MDCBottomDrawerViewController.h index 4c7231280..0c775504b 100644 --- a/components/NavigationDrawer/src/MDCBottomDrawerViewController.h +++ b/components/NavigationDrawer/src/MDCBottomDrawerViewController.h @@ -13,13 +13,16 @@ // limitations under the License. #import +#import "MDCBottomDrawerPresentationController.h" +#import "MDCBottomDrawerState.h" @protocol MDCBottomDrawerHeader; /** View controller for containing a Google Material bottom drawer. */ -@interface MDCBottomDrawerViewController : UIViewController +@interface MDCBottomDrawerViewController + : UIViewController /** The main content displayed by the drawer. @@ -41,4 +44,9 @@ */ @property(nonatomic, weak, nullable) UIScrollView *trackingScrollView; +/** + The current state of the bottom drawer. + */ +@property(nonatomic, readonly) MDCBottomDrawerState drawerState; + @end diff --git a/components/NavigationDrawer/src/MDCBottomDrawerViewController.m b/components/NavigationDrawer/src/MDCBottomDrawerViewController.m index 81ca06c16..5804b4c2f 100644 --- a/components/NavigationDrawer/src/MDCBottomDrawerViewController.m +++ b/components/NavigationDrawer/src/MDCBottomDrawerViewController.m @@ -65,4 +65,10 @@ return YES; } +- (void)bottomDrawerWillChangeState: + (nonnull MDCBottomDrawerPresentationController *)presentationController + drawerState:(MDCBottomDrawerState)drawerState { + _drawerState = drawerState; +} + @end diff --git a/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.h b/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.h index c3837a482..924cdc321 100644 --- a/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.h +++ b/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.h @@ -13,8 +13,27 @@ // limitations under the License. #import +#import "MDCBottomDrawerState.h" +@class MDCBottomDrawerContainerViewController; @protocol MDCBottomDrawerHeader; +@protocol MDCBottomDrawerContainerViewControllerDelegate; + +/** + Delegate for MDCBottomDrawerContainerViewController. + */ +@protocol MDCBottomDrawerContainerViewControllerDelegate +/** + This method is called when the bottom drawer will change its presented state to one of the + MDCBottomDrawerState states. + + @param containerViewController the container view controller of the bottom drawer. + @param drawerState the drawer's state. + */ +- (void)bottomDrawerContainerViewControllerWillChangeState: + (nonnull MDCBottomDrawerContainerViewController *)containerViewController + drawerState:(MDCBottomDrawerState)drawerState; +@end /** View controller for containing a Google Material bottom drawer. Used internally only. @@ -61,4 +80,14 @@ // Whether the drawer is currently animating its presentation. @property(nonatomic) BOOL animatingPresentation; +/** + Delegate to tell the presentation controller when the drawer will change state. + */ +@property(nonatomic, weak, nullable) id delegate; + +/** + The current state of the bottom drawer. + */ +@property(nonatomic, readonly) MDCBottomDrawerState drawerState; + @end diff --git a/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.m b/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.m index 43fba7d87..2b3039d7f 100644 --- a/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.m +++ b/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.m @@ -137,6 +137,9 @@ static UIColor *DrawerShadowColor(void) { // The top header bottom shadow layer. @property(nonatomic) MDCShadowLayer *headerShadowLayer; +// The current bottom drawer state. +@property(nonatomic) MDCBottomDrawerState drawerState; + @end @implementation MDCBottomDrawerContainerViewController { @@ -162,6 +165,7 @@ static UIColor *DrawerShadowColor(void) { _maskLayer = [[MDCBottomDrawerHeaderMask alloc] initWithMaximumCornerRadius:kDefaultHeaderCornerRadius minimumCornerRadius:0]; + _drawerState = MDCBottomDrawerStateCollapsed; } return self; } @@ -319,6 +323,22 @@ static UIColor *DrawerShadowColor(void) { [self.scrollView removeObserver:self forKeyPath:kContentOffsetKeyPath]; } +- (void)setDrawerState:(MDCBottomDrawerState)drawerState { + if (drawerState != _drawerState) { + _drawerState = drawerState; + [self.delegate bottomDrawerContainerViewControllerWillChangeState:self drawerState:drawerState]; + } +} + +- (void)updateDrawerState:(CGFloat)transitionPercentage { + if (transitionPercentage >= 1.f - kEpsilon) { + self.drawerState = self.contentReachesFullscreen ? MDCBottomDrawerStateFullScreen + : MDCBottomDrawerStateExpanded; + } else { + self.drawerState = MDCBottomDrawerStateCollapsed; + } +} + #pragma mark UIViewController - (void)viewDidLoad { @@ -475,6 +495,7 @@ static UIColor *DrawerShadowColor(void) { distance:self.headerAnimationDistance]; CGFloat headerTransitionToTop = contentOffset.y >= self.transitionCompleteContentOffset ? 1.f : transitionPercentage; + [self updateDrawerState:transitionPercentage]; [_maskLayer animateWithPercentage:1.f - transitionPercentage]; self.currentlyFullscreen = self.contentReachesFullscreen && headerTransitionToTop >= 1.f; CGFloat fullscreenHeaderHeight = @@ -669,6 +690,13 @@ static UIColor *DrawerShadowColor(void) { CGFloat scrollingDistance = _contentHeaderTopInset + contentHeaderHeight + contentHeight; _contentHeightSurplus = scrollingDistance - containerHeight; + if ([self shouldPresentFullScreen]) { + self.drawerState = MDCBottomDrawerStateFullScreen; + } else if (_contentHeightSurplus <= 0) { + self.drawerState = MDCBottomDrawerStateExpanded; + } else { + self.drawerState = MDCBottomDrawerStateCollapsed; + } if (addedContentHeight < kEpsilon && (_contentHeaderTopInset > _contentHeightSurplus) && (_contentHeaderTopInset - _contentHeightSurplus < self.addedContentHeightThreshold)) { CGFloat addedContentheight = _contentHeaderTopInset - _contentHeightSurplus; diff --git a/components/NavigationDrawer/tests/unit/MDCNavigationDrawerScrollViewTests.m b/components/NavigationDrawer/tests/unit/MDCNavigationDrawerScrollViewTests.m index 27ebce16d..cfb0ec160 100644 --- a/components/NavigationDrawer/tests/unit/MDCNavigationDrawerScrollViewTests.m +++ b/components/NavigationDrawer/tests/unit/MDCNavigationDrawerScrollViewTests.m @@ -17,6 +17,27 @@ #import "../../src/private/MDCBottomDrawerContainerViewController.h" #import "MDCNavigationDrawerFakes.h" +@interface MDCBottomDrawerDelegateTest + : UIViewController +@property(nonatomic, assign) BOOL delegateWasCalled; +@end + +@implementation MDCBottomDrawerDelegateTest + +- (instancetype)init { + self = [super init]; + if (self) { + _delegateWasCalled = NO; + } + return self; +} +- (void)bottomDrawerWillChangeState:(MDCBottomDrawerPresentationController *)presentationController + drawerState:(MDCBottomDrawerState)drawerState { + _delegateWasCalled = YES; +} + +@end + @interface MDCBottomDrawerContainerViewController (ScrollViewTests) @property(nonatomic) BOOL scrollViewObserved; @@ -27,13 +48,25 @@ @property(nonatomic, readonly) CGRect presentingViewBounds; @property(nonatomic, readonly) CGFloat contentHeightSurplus; @property(nonatomic, readonly) BOOL contentScrollsToReveal; +@property(nonatomic) MDCBottomDrawerState drawerState; +@property(nullable, nonatomic, readonly) UIPresentationController *presentationController; - (void)cacheLayoutCalculations; +- (void)updateDrawerState:(CGFloat)transitionPercentage; +@end +@interface MDCBottomDrawerPresentationController (ScrollViewTests) < + MDCBottomDrawerContainerViewControllerDelegate> +@property(nonatomic) MDCBottomDrawerContainerViewController *bottomDrawerContainerViewController; +@property(nonatomic, weak, nullable) id delegate; @end @interface MDCNavigationDrawerScrollViewTests : XCTestCase @property(nonatomic, strong, nullable) UIScrollView *fakeScrollView; @property(nonatomic, strong, nullable) MDCBottomDrawerContainerViewController *fakeBottomDrawer; +@property(nonatomic, strong, nullable) MDCBottomDrawerViewController *drawerViewController; +@property(nonatomic, strong, nullable) + MDCBottomDrawerPresentationController *presentationController; +@property(nonatomic, strong, nullable) MDCBottomDrawerDelegateTest *delegateTest; @end @implementation MDCNavigationDrawerScrollViewTests @@ -48,6 +81,12 @@ _fakeBottomDrawer = [[MDCBottomDrawerContainerViewController alloc] initWithOriginalPresentingViewController:fakeViewController trackingScrollView:_fakeScrollView]; + _drawerViewController = [[MDCBottomDrawerViewController alloc] init]; + _drawerViewController.contentViewController = fakeViewController; + _presentationController = [[MDCBottomDrawerPresentationController alloc] + initWithPresentedViewController:_drawerViewController + presentingViewController:nil]; + _delegateTest = [[MDCBottomDrawerDelegateTest alloc] init]; } - (void)tearDown { @@ -317,4 +356,76 @@ XCTAssertTrue(self.fakeBottomDrawer.contentScrollsToReveal); } +- (void)testBottomDrawerStateCollapsed { + CGSize fakePreferredContentSize = CGSizeMake(200, 1000); + MDCNavigationDrawerFakeHeaderViewController *fakeHeader = + [[MDCNavigationDrawerFakeHeaderViewController alloc] init]; + fakeHeader.preferredContentSize = fakePreferredContentSize; + self.fakeBottomDrawer.headerViewController = fakeHeader; + self.fakeBottomDrawer.contentViewController = + [[MDCNavigationDrawerFakeTableViewController alloc] init]; + + // When + self.fakeBottomDrawer.contentViewController.preferredContentSize = CGSizeMake(200, 1500); + [self.fakeBottomDrawer cacheLayoutCalculations]; + + // Then + XCTAssertEqual(self.fakeBottomDrawer.drawerState, MDCBottomDrawerStateCollapsed); +} + +- (void)testBottomDrawerStateExpanded { + CGSize fakePreferredContentSize = CGSizeMake(200, 100); + MDCNavigationDrawerFakeHeaderViewController *fakeHeader = + [[MDCNavigationDrawerFakeHeaderViewController alloc] init]; + fakeHeader.preferredContentSize = fakePreferredContentSize; + self.fakeBottomDrawer.headerViewController = fakeHeader; + self.fakeBottomDrawer.contentViewController = + [[MDCNavigationDrawerFakeTableViewController alloc] init]; + + // When + self.fakeBottomDrawer.contentViewController.preferredContentSize = CGSizeMake(200, 200); + [self.fakeBottomDrawer cacheLayoutCalculations]; + + // Then + XCTAssertEqual(self.fakeBottomDrawer.drawerState, MDCBottomDrawerStateExpanded); +} + +- (void)testBottomDrawerStateFullScreen { + CGSize fakePreferredContentSize = CGSizeMake(200, 2000); + MDCNavigationDrawerFakeHeaderViewController *fakeHeader = + [[MDCNavigationDrawerFakeHeaderViewController alloc] init]; + fakeHeader.preferredContentSize = fakePreferredContentSize; + self.fakeBottomDrawer.headerViewController = fakeHeader; + self.fakeBottomDrawer.contentViewController = + [[MDCNavigationDrawerFakeTableViewController alloc] init]; + + // When + [self.fakeBottomDrawer cacheLayoutCalculations]; + [self.fakeBottomDrawer updateDrawerState:1.f]; + + // Then + XCTAssertEqual(self.fakeBottomDrawer.drawerState, MDCBottomDrawerStateFullScreen); +} + +- (void)testBottomDrawerStateCallback { + CGSize fakePreferredContentSize = CGSizeMake(200, 1000); + MDCNavigationDrawerFakeHeaderViewController *fakeHeader = + [[MDCNavigationDrawerFakeHeaderViewController alloc] init]; + fakeHeader.preferredContentSize = fakePreferredContentSize; + self.fakeBottomDrawer.headerViewController = fakeHeader; + self.fakeBottomDrawer.contentViewController = + [[MDCNavigationDrawerFakeTableViewController alloc] init]; + + // When + self.fakeBottomDrawer.contentViewController.preferredContentSize = CGSizeMake(200, 1500); + [self.fakeBottomDrawer cacheLayoutCalculations]; + self.presentationController.delegate = self.delegateTest; + self.presentationController.bottomDrawerContainerViewController = self.fakeBottomDrawer; + self.fakeBottomDrawer.delegate = self.presentationController; + self.fakeBottomDrawer.drawerState = MDCBottomDrawerStateExpanded; + + // Then + XCTAssertEqual(self.delegateTest.delegateWasCalled, YES); +} + @end