From 8df9fcdf33d5ead19668f364c6b048ff0bf043e0 Mon Sep 17 00:00:00 2001 From: Hiedi Utley Date: Wed, 25 Sep 2019 14:08:21 -0700 Subject: [PATCH] [NavDrawer] Allow touch events to propagate to delegate for MDCBottomNavigationDrawer (#8486) Allow the drawer to optionally forward touch events to the delegate for handling. This enables tap thru to the underlying VC if the client needs that behavior. An example use case would be when the client wants to present the drawer on top of a VC that has controls that they want to still be tappable. For example: A video player or a podcast player with play/pause controls. Then the client VC could still receive the tap event on the control and respond to that and close the drawer at the same time. This would allow the user to save a tap. --- .../MDCBottomDrawerPresentationController.h | 18 ++++++++++ .../MDCBottomDrawerPresentationController.m | 35 +++++++++++++++---- .../src/MDCBottomDrawerViewController.h | 17 +++++++++ .../src/MDCBottomDrawerViewController.m | 29 +++++++++++++++ .../MDCBottomDrawerContainerViewController.m | 22 +++++++++++- .../unit/MDCNavigationDrawerScrollViewTests.m | 14 ++++++++ 6 files changed, 127 insertions(+), 8 deletions(-) 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 =