[NavigationDrawer] Added a state system to the Nav Drawer (#5520)

Context:
To allow us to create APIs for our users to set different appearances based on the different states the drawer can be in, we need to create an initial state system.

The Problem:
Without defining a state for our drawer, we won't be able to differentiate between different presentations the drawer may be in, and then alter the drawer's appearance effectively.

The Fix:
Provide a state enum as part of MDCBottomDrawerController, that is read only, and is set using a delegate that is initially set by the internal implementation.

Testing:
Unit Tests + Tested on an iPhone X and iPhone 7 with smaller and bigger preferredContentSize to imitate different states.

Related Bugs:
Closes #5524
This commit is contained in:
Yarden Eitan 2018-10-30 13:27:17 +02:00 committed by GitHub
parent bfa674da94
commit a8d3794de3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 256 additions and 2 deletions

View File

@ -13,6 +13,25 @@
// limitations under the License.
#import <UIKit/UIKit.h>
#import "MDCBottomDrawerState.h"
@class MDCBottomDrawerPresentationController;
/**
Delegate for MDCBottomSheetPresentationController.
*/
@protocol MDCBottomDrawerPresentationControllerDelegate <UIAdaptivePresentationControllerDelegate>
/**
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<MDCBottomDrawerPresentationControllerDelegate> delegate;
@end

View File

@ -21,7 +21,8 @@ static UIColor *DrawerOverlayBackgroundColor(void) {
return [UIColor colorWithWhite:0 alpha:0.4f];
}
@interface MDCBottomDrawerPresentationController () <UIGestureRecognizerDelegate>
@interface MDCBottomDrawerPresentationController () <UIGestureRecognizerDelegate,
MDCBottomDrawerContainerViewControllerDelegate>
/**
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<MDCBottomDrawerPresentationControllerDelegate> strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(bottomDrawerWillChangeState:drawerState:)]) {
[strongDelegate bottomDrawerWillChangeState:self drawerState:drawerState];
}
}
@end

View File

@ -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 <Foundation/Foundation.h>
/**
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,
};

View File

@ -13,13 +13,16 @@
// limitations under the License.
#import <UIKit/UIKit.h>
#import "MDCBottomDrawerPresentationController.h"
#import "MDCBottomDrawerState.h"
@protocol MDCBottomDrawerHeader;
/**
View controller for containing a Google Material bottom drawer.
*/
@interface MDCBottomDrawerViewController : UIViewController
@interface MDCBottomDrawerViewController
: UIViewController <MDCBottomDrawerPresentationControllerDelegate>
/**
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

View File

@ -65,4 +65,10 @@
return YES;
}
- (void)bottomDrawerWillChangeState:
(nonnull MDCBottomDrawerPresentationController *)presentationController
drawerState:(MDCBottomDrawerState)drawerState {
_drawerState = drawerState;
}
@end

View File

@ -13,8 +13,27 @@
// limitations under the License.
#import <UIKit/UIKit.h>
#import "MDCBottomDrawerState.h"
@class MDCBottomDrawerContainerViewController;
@protocol MDCBottomDrawerHeader;
@protocol MDCBottomDrawerContainerViewControllerDelegate;
/**
Delegate for MDCBottomDrawerContainerViewController.
*/
@protocol MDCBottomDrawerContainerViewControllerDelegate <NSObject>
/**
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<MDCBottomDrawerContainerViewControllerDelegate> delegate;
/**
The current state of the bottom drawer.
*/
@property(nonatomic, readonly) MDCBottomDrawerState drawerState;
@end

View File

@ -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;

View File

@ -17,6 +17,27 @@
#import "../../src/private/MDCBottomDrawerContainerViewController.h"
#import "MDCNavigationDrawerFakes.h"
@interface MDCBottomDrawerDelegateTest
: UIViewController <MDCBottomDrawerPresentationControllerDelegate>
@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<MDCBottomDrawerPresentationControllerDelegate> 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