mirror of
https://github.com/material-components/material-components-ios.git
synced 2026-02-20 08:27:32 +08:00
[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:
parent
bfa674da94
commit
a8d3794de3
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
32
components/NavigationDrawer/src/MDCBottomDrawerState.h
Normal file
32
components/NavigationDrawer/src/MDCBottomDrawerState.h
Normal 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,
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -65,4 +65,10 @@
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)bottomDrawerWillChangeState:
|
||||
(nonnull MDCBottomDrawerPresentationController *)presentationController
|
||||
drawerState:(MDCBottomDrawerState)drawerState {
|
||||
_drawerState = drawerState;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user