| // Copyright 2019-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 "MDCRippleTouchController.h" |
| |
| #import <CoreGraphics/CoreGraphics.h> |
| |
| #import "MDCRippleTouchControllerDelegate.h" |
| #import "MDCRippleView.h" |
| |
| @implementation MDCRippleTouchController { |
| BOOL _tapWentOutsideOfBounds; |
| BOOL _deferred; |
| |
| struct { |
| unsigned int rippleTouchControllerShouldProcessRippleTouchesAtTouchLocation : 1; |
| unsigned int rippleTouchControllerDidProcessRippleViewAtTouchLocation : 1; |
| unsigned int rippleTouchControllerInsertRippleViewIntoView : 1; |
| unsigned int rippleTouchControllerRippleViewAtTouchLocation : 1; |
| } _delegateFlags; |
| } |
| |
| @synthesize rippleView = _rippleView; |
| |
| - (instancetype)initWithView:(UIView *)view { |
| return [self initWithView:view deferred:NO]; |
| } |
| |
| - (nonnull instancetype)initWithView:(nonnull UIView *)view deferred:(BOOL)deferred { |
| self = [self init]; |
| if (self) { |
| _deferred = deferred; |
| if (deferred) { |
| [self attachGestureRecognizerToView:view]; |
| } else { |
| [self configureRippleWithView:view]; |
| } |
| } |
| return self; |
| } |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _gestureRecognizer = |
| [[UILongPressGestureRecognizer alloc] initWithTarget:self |
| action:@selector(handleRippleGesture:)]; |
| _gestureRecognizer.minimumPressDuration = 0; |
| _gestureRecognizer.delegate = self; |
| _gestureRecognizer.cancelsTouchesInView = NO; |
| _gestureRecognizer.delaysTouchesEnded = NO; |
| |
| _shouldProcessRippleWithScrollViewGestures = YES; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [_view removeGestureRecognizer:_gestureRecognizer]; |
| _gestureRecognizer.delegate = nil; |
| } |
| |
| - (MDCRippleView *)rippleView { |
| if (_rippleView == nil) { |
| _rippleView = [[MDCRippleView alloc] init]; |
| } |
| return _rippleView; |
| } |
| |
| - (void)setDelegate:(id<MDCRippleTouchControllerDelegate>)delegate { |
| _delegate = delegate; |
| |
| // The delegate's behavior - in terms of which optional methods are deemed to be |
| // implemented - is cached at assignment rather than inspected on each invocation. |
| _delegateFlags.rippleTouchControllerShouldProcessRippleTouchesAtTouchLocation = |
| [_delegate respondsToSelector:@selector(rippleTouchController: |
| shouldProcessRippleTouchesAtTouchLocation:)]; |
| _delegateFlags.rippleTouchControllerDidProcessRippleViewAtTouchLocation = [_delegate |
| respondsToSelector:@selector(rippleTouchController:didProcessRippleView:atTouchLocation:)]; |
| _delegateFlags.rippleTouchControllerInsertRippleViewIntoView = |
| [delegate respondsToSelector:@selector(rippleTouchController:insertRippleView:intoView:)]; |
| _delegateFlags.rippleTouchControllerRippleViewAtTouchLocation = |
| [_delegate respondsToSelector:@selector(rippleTouchController:rippleViewAtTouchLocation:)]; |
| } |
| |
| - (void)addRippleToView:(UIView *)view { |
| [self configureRippleWithView:view]; |
| } |
| |
| - (void)configureRippleWithView:(UIView *)view { |
| [self attachGestureRecognizerToView:view]; |
| [self insertRippleViewIntoView:view]; |
| } |
| |
| - (void)attachGestureRecognizerToView:(UIView *)view { |
| [_view removeGestureRecognizer:_gestureRecognizer]; |
| _view = view; |
| [_view addGestureRecognizer:_gestureRecognizer]; |
| } |
| |
| - (void)insertRippleViewIntoView:(UIView *)view { |
| if (!_delegateFlags.rippleTouchControllerRippleViewAtTouchLocation) { |
| // Insert the rippleView to the _view |
| MDCRippleView *rippleView = self.rippleView; |
| if (_delegateFlags.rippleTouchControllerInsertRippleViewIntoView) { |
| [_delegate rippleTouchController:self insertRippleView:rippleView intoView:view]; |
| } else { |
| [_view addSubview:rippleView]; |
| } |
| rippleView.frame = view.bounds; |
| } |
| } |
| |
| - (void)handleRippleGesture:(UILongPressGestureRecognizer *)recognizer { |
| CGPoint touchLocation = [recognizer locationInView:_view]; |
| |
| switch (recognizer.state) { |
| case UIGestureRecognizerStateBegan: { |
| if (_delegateFlags.rippleTouchControllerRippleViewAtTouchLocation) { |
| _rippleView = [_delegate rippleTouchController:self |
| rippleViewAtTouchLocation:touchLocation]; |
| if (!_rippleView) { |
| // If we find that a return isn't enough here, we may need to disable and then |
| // re-enable the recognizer so there are no side effects. |
| return; |
| } |
| } else { |
| MDCRippleView *rippleView = self.rippleView; |
| if (_deferred && rippleView.superview != _view) { |
| [self insertRippleViewIntoView:_view]; |
| } |
| } |
| |
| [_rippleView beginRippleTouchDownAtPoint:[recognizer locationInView:self.rippleView] |
| animated:YES |
| completion:nil]; |
| if (_delegateFlags.rippleTouchControllerDidProcessRippleViewAtTouchLocation) { |
| [_delegate rippleTouchController:self |
| didProcessRippleView:_rippleView |
| atTouchLocation:touchLocation]; |
| } |
| break; |
| } |
| case UIGestureRecognizerStatePossible: // Ignored |
| break; |
| case UIGestureRecognizerStateChanged: { |
| BOOL pointContainedinBounds = CGRectContainsPoint(_view.bounds, touchLocation); |
| if (pointContainedinBounds && _tapWentOutsideOfBounds) { |
| _tapWentOutsideOfBounds = NO; |
| [_rippleView fadeInRippleAnimated:YES completion:nil]; |
| } else if (!pointContainedinBounds && !_tapWentOutsideOfBounds) { |
| _tapWentOutsideOfBounds = YES; |
| [_rippleView fadeOutRippleAnimated:YES completion:nil]; |
| } |
| break; |
| } |
| case UIGestureRecognizerStateEnded: { |
| [_rippleView beginRippleTouchUpAnimated:YES completion:nil]; |
| break; |
| } |
| case UIGestureRecognizerStateCancelled: |
| case UIGestureRecognizerStateFailed: { |
| [_rippleView cancelAllRipplesAnimated:YES completion:nil]; |
| break; |
| } |
| } |
| } |
| |
| #pragma mark - UIGestureRecognizerDelegate |
| |
| - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer |
| shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { |
| if (!self.shouldProcessRippleWithScrollViewGestures && |
| [otherGestureRecognizer.view isKindOfClass:[UIScrollView class]] && |
| ![otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && |
| ![otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) { |
| return YES; |
| } |
| return NO; |
| } |
| |
| - (BOOL)gestureRecognizer:(__unused UIGestureRecognizer *)gestureRecognizer |
| shouldRecognizeSimultaneouslyWithGestureRecognizer:(__unused UIGestureRecognizer *)other { |
| // Subclasses can override this to prioritize another recognizer. |
| return YES; |
| } |
| |
| - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { |
| if (_delegateFlags.rippleTouchControllerShouldProcessRippleTouchesAtTouchLocation) { |
| CGPoint touchLocation = [gestureRecognizer locationInView:_view]; |
| return [_delegate rippleTouchController:self |
| shouldProcessRippleTouchesAtTouchLocation:touchLocation]; |
| } |
| return YES; |
| } |
| |
| @end |