Files
2024-03-28 19:39:04 -04:00

567 lines
18 KiB
Objective-C

/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2016, Sam Falconer.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKSignatureView.h"
#import "ORKSelectionTitleLabel.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
@protocol ORKSignatureGestureRecognizerDelegate <NSObject>
- (void)gestureTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)gestureTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)gestureTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
@optional
- (void)gestureTouchesHaveEndedWithTimeInterval;
@end
@interface ORKSignatureGestureRecognizer : UIGestureRecognizer
@property (nonatomic, weak) id<ORKSignatureGestureRecognizerDelegate> eventDelegate;
@property (nonatomic, strong) NSTimer *endEditingTimer;
- (void)cancelAutoScrollTimer;
@end
static const CGFloat TopToSigningLineRatio = 0.7;
static const CGFloat TouchesEndedTimeInterval = 2.0;
@implementation ORKSignatureGestureRecognizer
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.endEditingTimer invalidate];
if (touches.count > 1 || self.numberOfTouches > 1) {
for (UITouch *touch in touches) {
[self ignoreTouch:touch forEvent:event];
}
} else {
self.state = UIGestureRecognizerStateBegan;
[self.eventDelegate gestureTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self.endEditingTimer invalidate];
[self.eventDelegate gestureTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
self.state = UIGestureRecognizerStateEnded;
[self.eventDelegate gestureTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event];
self.endEditingTimer = [NSTimer scheduledTimerWithTimeInterval:TouchesEndedTimeInterval target:self selector:@selector(timerDidEnd) userInfo:nil repeats:NO];
}
- (void)timerDidEnd {
[self.eventDelegate gestureTouchesHaveEndedWithTimeInterval];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
(void)touches; (void)event;
self.state = UIGestureRecognizerStateFailed;
}
- (BOOL)shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
// Prioritize over scrollView's pan gesture recognizer and swipe gesture recognizer
if ([otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]
|| [otherGestureRecognizer isKindOfClass:[UISwipeGestureRecognizer class]]) {
return YES;
}
return NO;
}
- (void)cancelAutoScrollTimer {
[self.endEditingTimer invalidate];
}
@end
static const CGFloat PointMinDistance = 5;
static const CGFloat PointMinDistanceSquared = PointMinDistance * PointMinDistance;
static const CGFloat DefaultLineWidth = 1;
static const CGFloat DefaultLineWidthVariation = 3;
static const CGFloat MaxPressureForStrokeVelocity = 9;
static const CGFloat LineWidthStepValue = 0.25f;
@interface ORKSignatureView () <ORKSignatureGestureRecognizerDelegate> {
CGPoint currentPoint;
CGPoint previousPoint1;
CGPoint previousPoint2;
// Pressure scale based on if using force or speed of stroke.
CGFloat minPressure;
CGFloat maxPressure;
// Time used only to calculate speed when force isn't available on the device.
NSTimeInterval previousTouchTime;
}
@property (nonatomic, strong) UIBezierPath *currentPath;
@property (nonatomic, strong) NSMutableArray *pathArray;
@property (nonatomic, strong) NSArray *backgroundLines;
@property (nonatomic) BOOL setWidth;
@end
@implementation ORKSignatureView {
NSLayoutConstraint *_heightConstraint;
NSLayoutConstraint *_widthConstraint;
CGSize _signatureSize;
}
+ (void)initialize {
if (self == [ORKSignatureView class]) {
if ([[ORKSignatureView appearance] backgroundColor] == nil) {
[[ORKSignatureView appearance] setBackgroundColor:ORKColor(ORKBackgroundColorKey)];
}
}
}
- (instancetype)init {
self = [super init];
if (self) {
self.setWidth = YES;
[self commonInit];
}
return self;
}
- (instancetype)initWithoutDefaultWidth {
self = [super init];
if (self) {
self.setWidth = NO;
[self commonInit];
}
return self;
}
- (void)commonInit {
_lineWidth = DefaultLineWidth;
_lineWidthVariation = DefaultLineWidthVariation;
self.layer.borderColor = [[UIColor separatorColor] CGColor];
self.layer.borderWidth = 1.0;
self.layer.cornerRadius = 10.0;
self.clipsToBounds = YES;
[self makeSignatureGestureRecognizer];
[self setUpConstraints];
}
- (void)willMoveToWindow:(UIWindow *)newWindow {
[super willMoveToWindow:newWindow];
[self updateConstraintConstantsForWindow:newWindow];
}
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
_heightConstraint.constant = ORKGetMetricForWindow(ORKScreenMetricSignatureViewHeight, window);
if (self.setWidth) {
_widthConstraint.constant = ORKWidthForSignatureView(window);
}
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray new];
_heightConstraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0]; // constant set in updateConstraintConstantsForWindow:
[constraints addObject:_heightConstraint];
if (self.setWidth) {
_widthConstraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0]; // constant set in updateConstraintConstantsForWindow:
[constraints addObject:_widthConstraint];
}
[NSLayoutConstraint activateConstraints:constraints];
[self updateConstraintConstantsForWindow:self.window];
}
- (void)updateConstraints {
[self updateConstraintConstantsForWindow:self.window];
[super updateConstraints];
}
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
[self setNeedsDisplay];
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self setNeedsDisplay];
}
- (UIBezierPath *)pathWithRoundedStyle {
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineCapStyle = kCGLineCapRound;
path.lineWidth = self.lineWidth;
path.lineJoinStyle = kCGLineJoinRound;
return path;
}
- (void)makeSignatureGestureRecognizer {
if (nil == _signatureGestureRecognizer) {
_signatureGestureRecognizer = [ORKSignatureGestureRecognizer new];
((ORKSignatureGestureRecognizer *)_signatureGestureRecognizer).eventDelegate = self;
[self addGestureRecognizer:_signatureGestureRecognizer];
}
}
- (UIColor *)lineColor {
if (_lineColor == nil) {
_lineColor = [UIColor labelColor];
}
return _lineColor;
}
- (NSMutableArray *)pathArray {
if (_pathArray == nil) {
_pathArray = [NSMutableArray new];
}
return _pathArray;
}
- (CGFloat)placeholderPoint {
CGFloat height = self.bounds.size.height;
CGFloat y1 = height * TopToSigningLineRatio;
UIFont *font = [ORKSelectionTitleLabel defaultFont];
return y1 - 5 - font.pointSize + font.descender;
}
- (NSArray *)backgroundLines {
if (_backgroundLines == nil) {
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
UIBezierPath *path = [UIBezierPath bezierPath];
{
CGFloat x1 = 0;
CGFloat x2 = width;
CGFloat y1 = height * TopToSigningLineRatio;
CGFloat y2 = height * TopToSigningLineRatio;
[path moveToPoint:CGPointMake(x1, y1)];
[path addLineToPoint:CGPointMake(x2, y2)];
}
_backgroundLines = @[path];
}
return _backgroundLines;
}
#pragma mark Touch Event Handlers
- (BOOL)isForceTouchAvailable {
static BOOL isAvailable;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
isAvailable = NO;
if ([self.traitCollection respondsToSelector:@selector(forceTouchCapability)] &&
self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
isAvailable = YES;
}
});
return isAvailable;
}
- (BOOL)isTouchTypeStylus:(UITouch*)touch {
BOOL isStylus = NO;
if ([touch respondsToSelector:@selector(type)] && touch.type == UITouchTypeStylus) {
isStylus = YES;
}
return isStylus;
}
- (void)gestureTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
self.currentPath = [self pathWithRoundedStyle];
// Trigger full redraw - whether there's a path has changed
[self setNeedsDisplay];
previousPoint1 = [touch previousLocationInView:self];
previousPoint2 = [touch previousLocationInView:self];
currentPoint = [touch locationInView:self];
if ([self isForceTouchAvailable] || [self isTouchTypeStylus:touch]) {
// This is a scale based on true force on the screen.
minPressure = 0.f;
maxPressure = [touch maximumPossibleForce] / 2.f;
}
else {
// This is a scale based on the speed of the stroke
// (scaled down logarithmically).
minPressure = 0.f;
maxPressure = MaxPressureForStrokeVelocity;
previousTouchTime = touch.timestamp;
}
[self.currentPath moveToPoint:currentPoint];
[self.currentPath addArcWithCenter:currentPoint radius:0.1 startAngle:0.0 endAngle:2.0 * M_PI clockwise:YES];
[self gestureTouchesMoved:touches withEvent:event];
}
static CGPoint mmid_Point(CGPoint p1, CGPoint p2) {
return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
}
- (void)gestureTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
//check if the point is farther than min dist from previous
CGFloat dx = point.x - currentPoint.x;
CGFloat dy = point.y - currentPoint.y;
CGFloat distanceSquared = (dx * dx + dy * dy);
if (distanceSquared < PointMinDistanceSquared) {
return;
}
// Will be assigned a real value on all devices.
CGFloat pressure;
if ([self isForceTouchAvailable] || [self isTouchTypeStylus:touch]) {
// If the device supports Force Touch, or is using a stylus, use it.
pressure = [touch force];
} else {
// If not, use a heuristic based on the speed of
// the stroke. Scale this speed logarithmically to
// require very slow touches to max out the line width.
// This value can become negative because of how it is
// inverted. It will be clamped right below.
pressure = maxPressure - log((sqrt(distanceSquared) /
MAX(0.0001, event.timestamp - previousTouchTime)));
previousTouchTime = event.timestamp;
}
// Clamp the pressure value to between the allowed min and max.
pressure = MAX(minPressure, pressure);
pressure = MIN(maxPressure, pressure);
CGFloat previousLineWidth = self.currentPath.lineWidth;
CGFloat proposedLineWidth = ((pressure - minPressure) *
self.lineWidthVariation /
(maxPressure - minPressure))
+ self.lineWidth;
// Only step the line width up and down by a set value.
// This prevents the line looking jagged, and adding excessive
// separate line segments.
if (ABS(previousLineWidth - proposedLineWidth) >= LineWidthStepValue) {
CGFloat lineWidth = previousLineWidth;
if (proposedLineWidth > previousLineWidth) {
lineWidth = previousLineWidth + LineWidthStepValue;
}
else if (proposedLineWidth < previousLineWidth) {
lineWidth = previousLineWidth - LineWidthStepValue;
}
[self commitCurrentPath];
self.currentPath = [self pathWithRoundedStyle];
self.currentPath.lineWidth = lineWidth;
CGPoint previousMid2 = mmid_Point(currentPoint, previousPoint1);
[self.currentPath moveToPoint:previousMid2];
}
previousPoint2 = previousPoint1;
previousPoint1 = [touch previousLocationInView:self];
currentPoint = [touch locationInView:self];
CGPoint mid1 = mmid_Point(previousPoint1, previousPoint2);
CGPoint mid2 = mmid_Point(currentPoint, previousPoint1);
UIBezierPath *subPath = [UIBezierPath bezierPath];
[subPath moveToPoint:mid1];
[subPath addQuadCurveToPoint:mid2 controlPoint:previousPoint1];
CGRect bounds = CGPathGetBoundingBox(subPath.CGPath);
[self.currentPath addQuadCurveToPoint:mid2 controlPoint:previousPoint1];
CGRect drawBox = bounds;
drawBox.origin.x -= self.currentPath.lineWidth * 2.0;
drawBox.origin.y -= self.currentPath.lineWidth * 2.0;
drawBox.size.width += self.currentPath.lineWidth * 4.0;
drawBox.size.height += self.currentPath.lineWidth * 4.0;
[self setNeedsDisplayInRect:drawBox];
}
- (void)gestureTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self commitCurrentPath];
}
- (void)gestureTouchesHaveEndedWithTimeInterval {
if (self.delegate && [self.delegate respondsToSelector:@selector(signatureViewDidEndEditingWithTimeInterval)]) {
[self.delegate signatureViewDidEndEditingWithTimeInterval];
}
}
- (void)commitCurrentPath {
CGRect rect = self.currentPath.bounds;
if (CGSizeEqualToSize(rect.size, CGSizeZero)) {
return;
}
[self.pathArray addObject:self.currentPath];
[self.delegate signatureViewDidEditImage:self];
}
- (void)drawRect:(CGRect)rect {
UIColor *fillColor;
fillColor = [UIColor systemBackgroundColor];
[fillColor setFill];
CGContextFillRect(UIGraphicsGetCurrentContext(), rect);
for (UIBezierPath *path in self.pathArray) {
[self.lineColor setStroke];
[path stroke];
}
[self.lineColor setStroke];
[self.currentPath stroke];
_signatureSize = (self.bounds.size.width == 0 || self.bounds.size.height == 0) ? CGSizeMake(200, 200) :
self.bounds.size;
}
- (NSArray <UIBezierPath *> *)signaturePath {
return [self.pathArray copy];
}
- (void)setSignaturePath:(NSArray<UIBezierPath *> *)signaturePath {
if (signaturePath) {
_pathArray = [signaturePath mutableCopy];
[self setNeedsDisplay];
}
}
- (UIImage *)signatureImage {
UIGraphicsBeginImageContext(_signatureSize);
for (UIBezierPath *path in self.pathArray) {
[[UIColor blackColor] setStroke];
[path stroke];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
- (BOOL)signatureExists {
return self.pathArray.count > 0;
}
- (void)clear {
if (self.pathArray.count > 0) {
if (self.currentPath != nil) {
self.currentPath = [self pathWithRoundedStyle];
}
[self.pathArray removeAllObjects];
[self setNeedsDisplayInRect:self.bounds];
}
}
- (void)cancelAutoScrollTimer {
[(ORKSignatureGestureRecognizer *)_signatureGestureRecognizer cancelAutoScrollTimer];
}
- (void)setEnabled:(BOOL)enabled {
_enabled = enabled;
[self setUserInteractionEnabled:enabled];
}
#pragma mark - Accessibility
- (BOOL)isAccessibilityElement {
return YES;
}
- (NSString *)accessibilityLabel {
return ORKLocalizedString(@"AX_SIGNVIEW_LABEL", @"comment");
}
- (NSString *)accessibilityValue {
return (self.signatureExists ? ORKLocalizedString(@"AX_SIGNVIEW_SIGNED", @"comment") : ORKLocalizedString(@"AX_SIGNVIEW_UNSIGNED", @"comment"));
}
- (NSString *)accessibilityHint {
return ORKLocalizedString(@"AX_SIGNVIEW_HINT", @"comment");
}
- (UIAccessibilityTraits)accessibilityTraits {
return [super accessibilityTraits] | UIAccessibilityTraitAllowsDirectInteraction;
}
@end