Skip to content

Commit

Permalink
[NavDrawer] Allow touch events to propagate to delegate for MDCBottom…
Browse files Browse the repository at this point in the history
…NavigationDrawer (material-components#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.
  • Loading branch information
hiediutley authored and yarneo committed Sep 25, 2019
1 parent 9dfd404 commit 8df9fcd
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () <UIGestureRecognizerDelegate,
MDCBottomDrawerContainerViewControllerDelegate>

Expand Down Expand Up @@ -55,6 +74,7 @@ - (instancetype)initWithPresentedViewController:(UIViewController *)presentedVie
_maximumInitialDrawerHeight = 0;
_drawerShadowColor = [UIColor.blackColor colorWithAlphaComponent:(CGFloat)0.2];
_elevation = MDCShadowElevationNavDrawer;
_shouldAutoDismissOnTap = YES;
}
return self;
}
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -184,19 +204,20 @@ - (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];
if (!completed) {
[self.scrimView removeFromSuperview];
[self.topHandle removeFromSuperview];
}

[self.delegate bottomDrawerPresentTransitionDidEnd:self];
}

Expand Down
17 changes: 17 additions & 0 deletions components/NavigationDrawer/src/MDCBottomDrawerViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions components/NavigationDrawer/src/MDCBottomDrawerViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,6 +70,8 @@ - (void)commonMDCBottomDrawerViewControllerInit {
_mdc_overrideBaseElevation = -1;
_isDrawerClosed = YES;
_lastOffset = NSNotFound;
_shouldAutoDismissOnTap = YES;
_shouldForwardTouchEvents = NO;
}

- (void)viewWillLayoutSubviews {
Expand Down Expand Up @@ -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]]) {
Expand All @@ -224,6 +238,21 @@ - (void)setShouldAlwaysExpandHeader:(BOOL)shouldAlwaysExpandHeader {
}
}

- (void)setDelegate:(id<MDCBottomDrawerViewControllerDelegate>)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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

/**
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit 8df9fcd

Please sign in to comment.