diff --git a/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.h b/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.h index 8c7cc2ffe33..34097fcb6b5 100644 --- a/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.h +++ b/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.h @@ -163,6 +163,24 @@ */ @property(nonatomic, assign) CGFloat maximumInitialDrawerHeight; +/** + A flag allowing clients to opt-out of the drawer closing when the user taps outside the content. + + @default YES The drawer should autohide on tap. + */ +@property(nonatomic, assign) BOOL shouldAutoDismissOnTap; + +/** + A flag allowing clients to opt-in to handling touch events. + + @default NO The drawer will not forward touch events. + + @discussion If set to YES and the delegate is an instance of @UIResponder, then the touch events + will be forwarded along to the delegate. Note: @shouldAutoDismissOnTap should also be set to NO + so that the events will propagate properly. + */ +@property(nonatomic, assign) BOOL shouldForwardTouchEvents; + /** A flag allowing clients to opt-in to the drawer adding additional height to the content to include the bottom safe area inset. This will remove the need for clients to calculate their content size diff --git a/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.m b/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.m index 70b6ded9ef7..15f0b38e456 100644 --- a/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.m +++ b/components/NavigationDrawer/src/MDCBottomDrawerPresentationController.m @@ -22,6 +22,25 @@ static CGFloat kTopHandleWidth = (CGFloat)24.0; static CGFloat kTopHandleTopMargin = (CGFloat)5.0; +/** + View that allows touches that aren't handled from within the view to be propagated up the + responder chain. This is used to allow forwarding of tap events from the scrim view through to + the delegate if that has been enabled on the VC. + */ +@interface MDCBottomDrawerScrimView : UIView +@end + +@implementation MDCBottomDrawerScrimView + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + // Allow unhandled touches to propagate along the responder chain and optionally be handled by + // the drawer delegate. + UIView *view = [super hitTest:point withEvent:event]; + return view == self ? nil : view; +} + +@end + @interface MDCBottomDrawerPresentationController () @@ -55,6 +74,7 @@ - (instancetype)initWithPresentedViewController:(UIViewController *)presentedVie _maximumInitialDrawerHeight = 0; _drawerShadowColor = [UIColor.blackColor colorWithAlphaComponent:(CGFloat)0.2]; _elevation = MDCShadowElevationNavDrawer; + _shouldAutoDismissOnTap = YES; } return self; } @@ -102,7 +122,7 @@ - (void)presentationTransitionWillBegin { self.bottomDrawerContainerViewController = bottomDrawerContainerViewController; self.bottomDrawerContainerViewController.delegate = self; - self.scrimView = [[UIView alloc] initWithFrame:self.containerView.bounds]; + self.scrimView = [[MDCBottomDrawerScrimView alloc] initWithFrame:self.containerView.bounds]; self.scrimView.backgroundColor = self.scrimColor ?: [UIColor colorWithWhite:0 alpha:(CGFloat)0.32]; self.scrimView.autoresizingMask = @@ -184,11 +204,13 @@ - (void)presentationTransitionWillBegin { } - (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; + if (self.shouldAutoDismissOnTap) { + // 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]; @@ -196,7 +218,6 @@ - (void)presentationTransitionDidEnd:(BOOL)completed { [self.scrimView removeFromSuperview]; [self.topHandle removeFromSuperview]; } - [self.delegate bottomDrawerPresentTransitionDidEnd:self]; } diff --git a/components/NavigationDrawer/src/MDCBottomDrawerViewController.h b/components/NavigationDrawer/src/MDCBottomDrawerViewController.h index e6b85a8923a..48cd4bfe799 100644 --- a/components/NavigationDrawer/src/MDCBottomDrawerViewController.h +++ b/components/NavigationDrawer/src/MDCBottomDrawerViewController.h @@ -86,6 +86,23 @@ */ @property(nonatomic, assign) CGFloat maximumInitialDrawerHeight; +/** + A flag allowing clients to opt-out of the drawer closing when the user taps outside the content. + + @default YES The drawer should autohide on tap. + */ +@property(nonatomic, assign) BOOL shouldAutoDismissOnTap; + +/** + A flag allowing clients to opt-in to handling touch events. + + @default NO The drawer will not forward touch events. + + @discussion If set to YES and the delegate is an instance of @UIResponder, then the touch events + will be forwarded along to the delegate. + */ +@property(nonatomic, assign) BOOL shouldForwardTouchEvents; + /** A flag allowing clients to opt-in to the drawer adding additional height to the content to include the bottom safe area inset. This will remove the need for clients to calculate their content size diff --git a/components/NavigationDrawer/src/MDCBottomDrawerViewController.m b/components/NavigationDrawer/src/MDCBottomDrawerViewController.m index 32a3884da1f..af40ef2b57a 100644 --- a/components/NavigationDrawer/src/MDCBottomDrawerViewController.m +++ b/components/NavigationDrawer/src/MDCBottomDrawerViewController.m @@ -34,6 +34,9 @@ @implementation MDCBottomDrawerViewController { // Used for tracking the presentation/dismissal animations. BOOL _isDrawerClosed; CGFloat _lastOffset; + + // Used for forwarding touch events if enabled. + __weak UIResponder *_cachedNextResponder; } @synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation; @@ -67,6 +70,8 @@ - (void)commonMDCBottomDrawerViewControllerInit { _mdc_overrideBaseElevation = -1; _isDrawerClosed = YES; _lastOffset = NSNotFound; + _shouldAutoDismissOnTap = YES; + _shouldForwardTouchEvents = NO; } - (void)viewWillLayoutSubviews { @@ -201,6 +206,15 @@ - (void)setMaximumInitialDrawerHeight:(CGFloat)maximumInitialDrawerHeight { } } +- (void)setShouldAutoDismissOnTap:(BOOL)shouldAutoDismissOnTap { + _shouldAutoDismissOnTap = shouldAutoDismissOnTap; + if ([self.presentationController isKindOfClass:[MDCBottomDrawerPresentationController class]]) { + MDCBottomDrawerPresentationController *bottomDrawerPresentationController = + (MDCBottomDrawerPresentationController *)self.presentationController; + bottomDrawerPresentationController.shouldAutoDismissOnTap = self.shouldAutoDismissOnTap; + } +} + - (void)setElevation:(MDCShadowElevation)elevation { _elevation = elevation; if ([self.presentationController isKindOfClass:[MDCBottomDrawerPresentationController class]]) { @@ -224,6 +238,21 @@ - (void)setShouldAlwaysExpandHeader:(BOOL)shouldAlwaysExpandHeader { } } +- (void)setDelegate:(id)delegate { + _delegate = delegate; + if ([delegate isKindOfClass:[UIResponder class]]) { + _cachedNextResponder = (UIResponder *)delegate; + } +} + +- (UIResponder *)nextResponder { + // Allow the delegate to opt-in to the responder chain to handle events. + if (self.shouldForwardTouchEvents && _cachedNextResponder) { + return _cachedNextResponder; + } + return [super nextResponder]; +} + - (CGFloat)mdc_currentElevation { return self.elevation; } diff --git a/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.m b/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.m index 41d22828121..23bc6a7d38e 100644 --- a/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.m +++ b/components/NavigationDrawer/src/private/MDCBottomDrawerContainerViewController.m @@ -52,6 +52,26 @@ - (MDCShadowLayer *)shadowLayer { } @end +/** + View that allows touches that aren't handled from within the view to be propagated up the + responder chain. This is used to allow forwarding of tap events from the scroll view through to + the delegate if that has been enabled on the VC. + */ + +@interface MDCBottomDrawerScrollView : UIScrollView +@end + +@implementation MDCBottomDrawerScrollView + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + // Allow unhandled touches to propagate along the responder chain and optionally be handled by + // the drawer delegate. + UIView *view = [super hitTest:point withEvent:event]; + return view == self ? nil : view; +} + +@end + @interface MDCBottomDrawerContainerViewController (LayoutCalculations) /** @@ -758,7 +778,7 @@ - (void)updateContentWithHeight:(CGFloat)height { - (UIScrollView *)scrollView { if (!_scrollView) { - _scrollView = [[UIScrollView alloc] init]; + _scrollView = [[MDCBottomDrawerScrollView alloc] init]; _scrollView.showsVerticalScrollIndicator = NO; _scrollView.alwaysBounceVertical = YES; _scrollView.backgroundColor = [UIColor clearColor]; diff --git a/components/NavigationDrawer/tests/unit/MDCNavigationDrawerScrollViewTests.m b/components/NavigationDrawer/tests/unit/MDCNavigationDrawerScrollViewTests.m index 20bfce52f79..fbf8db52f67 100644 --- a/components/NavigationDrawer/tests/unit/MDCNavigationDrawerScrollViewTests.m +++ b/components/NavigationDrawer/tests/unit/MDCNavigationDrawerScrollViewTests.m @@ -685,6 +685,20 @@ - (void)testSetTrackingScrollViewAfterSetScrimColor { drawerPresentationController.bottomDrawerContainerViewController.trackingScrollView); } +- (void)testSetShouldAutoDismissOnTapCorrectly { + MDCBottomDrawerPresentationController *drawerPresentationController = + (MDCBottomDrawerPresentationController *)self.drawerViewController.presentationController; + self.drawerViewController.shouldAutoDismissOnTap = YES; + XCTAssertTrue(drawerPresentationController.shouldAutoDismissOnTap); +} + +- (void)testSetShouldForwardTouchEventsCorrectly { + XCTAssertNil(self.drawerViewController.nextResponder); + self.drawerViewController.shouldForwardTouchEvents = YES; + XCTAssertEqualObjects(self.drawerViewController.delegate, + self.drawerViewController.nextResponder); +} + - (void)testBottomDrawerTopInset { // Given MDCNavigationDrawerFakeHeaderViewController *fakeHeader =