blob: 9ba425a08bd93975a6e8978f36912d2f31ca6b15 [file] [log] [blame] [edit]
// Copyright 2017-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 <UIKit/UIGestureRecognizerSubclass.h>
#import "MDCFeatureHighlightDismissGestureRecognizer.h"
#import "MDCFeatureHighlightView+Private.h"
#import "MaterialMath.h"
@interface MDCFeatureHighlightDismissGestureRecognizer ()
@property(nullable, nonatomic, readonly) MDCFeatureHighlightView *view;
@end
@implementation MDCFeatureHighlightDismissGestureRecognizer {
CGFloat _startProgress;
CGFloat _previousProgress;
NSTimeInterval _eventTimeStamp;
NSTimeInterval _previousEventTimeStamp;
BOOL _hasTouch;
}
@dynamic view;
- (void)reset {
[super reset];
_hasTouch = NO;
_velocity = 0;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSCAssert([self.view isKindOfClass:[MDCFeatureHighlightView class]],
@"%@ was attached to a view not of type MDCFeatureHighlightView", self);
if (_hasTouch) {
for (UITouch *touch in touches) {
[self ignoreTouch:touch forEvent:event];
}
return;
}
[super touchesBegan:touches withEvent:event];
_hasTouch = YES;
_startProgress = [self dismissPercentOfTouches:touches];
_progress = _previousProgress = 1;
_eventTimeStamp = _previousEventTimeStamp = event.timestamp;
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
// first touch that can be considered a pan
if (self.state == UIGestureRecognizerStatePossible && !MDCCGFloatEqual(_progress, 1.0)) {
self.state = UIGestureRecognizerStateBegan;
}
_previousEventTimeStamp = _eventTimeStamp;
_eventTimeStamp = event.timestamp;
NSTimeInterval deltaTime = _eventTimeStamp - _previousEventTimeStamp;
if (deltaTime > 0) {
_velocity = (CGFloat)((_progress - _previousProgress) / deltaTime);
}
[self updateState:touches];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
if (_hasTouch) {
self.state = UIGestureRecognizerStateEnded;
}
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
if (_hasTouch) {
self.state = UIGestureRecognizerStateCancelled;
}
}
- (void)updateState:(NSSet<UITouch *> *)touches {
_previousProgress = _progress;
CGFloat newProgress = [self dismissPercentOfTouches:touches];
if (newProgress < _startProgress) {
_startProgress = newProgress;
}
_progress = 1 - [self dismissPercentOfTouches:touches] + _startProgress;
_progress = MIN(1, MAX(0, _progress));
}
- (CGFloat)progressForTouchPosition:(CGPoint)touchPos {
CGPoint c1 = self.view.highlightCenter;
CGPoint c2 = self.view.highlightPoint;
CGFloat r1 = self.view.highlightRadius;
CGFloat r2 = 0;
CGPoint p = touchPos;
// Center and radius as paramaterized functions of t
// c(t) = c1 + (c2 - c1)t
// r(t) = r1 + (r2 - r1)t
// Radius in terms of distance from the center to the touch point
// r(t) = ||c(t) - p)||
// r(t)^2 = || c(t) - p ||^2
// r(t)^2 = (c(t).x - p.x)^2 + (c(t).y - p.y)^2
// (r1 + (r2 - r1)t)^2 = (c1.x + (c2.x - c1.x)t - p.x)^2 + (c1.y + (c2.y - c1.y)t - p.y)^2
// r1^2 + 2r1(r2 - r1)t + (r2 - r1)^2*t^2
// = c1.x^2 + 2c1.x(c2.x - c1.x)t - 2c1.x*p.x - 2(c2.x - c1.x)t*p.x + (c2.x - c1.x)^2t^2 + p.x^2
// + c1.y^2 + 2c1.y(c2.y - c1.y)t - 2c1.y*p.y - 2(c2.y - c1.y)t*p.y + (c2.y - c1.y)^2t^2 + p.y^2
// Moving everything to left side so that ... = 0
// r1^2 + 2r1(r2 - r1)t + (r2 - r1)^2*t^2
// - c1.x^2 - 2c1.x(c2.x - c1.x)t + 2c1.x*p.x + 2(c2.x - c1.x)t*p.x - (c2.x - c1.x)^2t^2 - p.x^2
// - c1.y^2 - 2c1.y(c2.y - c1.y)t + 2c1.y*p.y + 2(c2.y - c1.y)t*p.y - (c2.y - c1.y)^2t^2 - p.y^2
// = 0
// Now compute in the form at^2 + bt + c = 0
// a = (r2 - r1)^2 - (c2.x - c1.x)^2 - (c2.y - c1.y)^2
CGFloat a = pow(r2 - r1, 2) - pow(c2.x - c1.x, 2) - pow(c2.y - c1.y, 2);
// b = 2r1(r2 - r1) - 2c1.x(c2.x - c1.x) + 2(c2.x - c1.x)p.x - 2c1.y(c2.y - c1.y) + 2(c2.y -
// c1.y)p.y
CGFloat b = 2 * r1 * (r2 - r1) - 2 * c1.x * (c2.x - c1.x) + 2 * (c2.x - c1.x) * p.x -
2 * c1.y * (c2.y - c1.y) + 2 * (c2.y - c1.y) * p.y;
// c = r1^2 - c1.x^2 + 2c1.x*p.x - p.x^2 - c1.y^2 + 2c1.y*p.y - p.y^2
CGFloat c = pow(r1, 2) - pow(c1.x, 2) + 2 * c1.x * p.x - pow(p.x, 2) - pow(c1.y, 2) +
2 * c1.y * p.y - pow(p.y, 2);
// Apply the quadratic equation
CGFloat t = (-b - sqrt(b * b - 4 * a * c)) / (2 * a);
return MIN(1, MAX(0, t));
}
- (CGFloat)dismissPercentOfTouches:(NSSet<UITouch *> *)touches {
if (touches.count == 0) {
return 0.0;
}
CGFloat dismissSum = 0;
for (UITouch *touch in touches) {
CGPoint touchPos = [touch locationInView:self.view];
dismissSum += [self progressForTouchPosition:touchPos];
}
CGFloat progress = dismissSum / touches.count;
return MIN(1, MAX(0, progress));
}
@end