|
|
|
//
|
|
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
|
|
|
|
public struct ImageEditorPinchState {
|
|
|
|
public let centroid: CGPoint
|
|
|
|
public let distance: CGFloat
|
|
|
|
public let angleRadians: CGFloat
|
|
|
|
|
|
|
|
init(centroid: CGPoint,
|
|
|
|
distance: CGFloat,
|
|
|
|
angleRadians: CGFloat) {
|
|
|
|
self.centroid = centroid
|
|
|
|
self.distance = distance
|
|
|
|
self.angleRadians = angleRadians
|
|
|
|
}
|
|
|
|
|
|
|
|
static var empty: ImageEditorPinchState {
|
|
|
|
return ImageEditorPinchState(centroid: .zero, distance: 1.0, angleRadians: 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This GR:
|
|
|
|
//
|
|
|
|
// * Tries to fail quickly to avoid conflicts with other GRs, especially pans/swipes.
|
|
|
|
// * Captures a bunch of useful "pinch state" that makes using this GR much easier
|
|
|
|
// than UIPinchGestureRecognizer.
|
|
|
|
public class ImageEditorPinchGestureRecognizer: UIGestureRecognizer {
|
|
|
|
|
|
|
|
public weak var referenceView: UIView?
|
|
|
|
|
|
|
|
public var pinchStateStart = ImageEditorPinchState.empty
|
|
|
|
|
|
|
|
public var pinchStateLast = ImageEditorPinchState.empty
|
|
|
|
|
|
|
|
// MARK: - Touch Handling
|
|
|
|
|
|
|
|
private var gestureBeganLocation: CGPoint?
|
|
|
|
|
|
|
|
private func failAndReset() {
|
|
|
|
state = .failed
|
|
|
|
gestureBeganLocation = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc
|
|
|
|
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
|
|
|
|
if state == .possible {
|
|
|
|
if gestureBeganLocation == nil {
|
|
|
|
gestureBeganLocation = centroid(forTouches: event.allTouches)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch touchState(for: event) {
|
|
|
|
case .possible:
|
|
|
|
// Do nothing
|
|
|
|
break
|
|
|
|
case .invalid:
|
|
|
|
failAndReset()
|
|
|
|
case .valid(let pinchState):
|
|
|
|
state = .began
|
|
|
|
pinchStateStart = pinchState
|
|
|
|
pinchStateLast = pinchState
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
failAndReset()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc
|
|
|
|
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
|
|
super.touchesMoved(touches, with: event)
|
|
|
|
|
|
|
|
switch state {
|
|
|
|
case .began, .changed:
|
|
|
|
switch touchState(for: event) {
|
|
|
|
case .possible:
|
|
|
|
if let gestureBeganLocation = gestureBeganLocation {
|
|
|
|
let location = centroid(forTouches: event.allTouches)
|
|
|
|
|
|
|
|
// If the initial touch moves too much without a second touch,
|
|
|
|
// this GR needs to fail - the gesture looks like a pan/swipe/etc.,
|
|
|
|
// not a pinch.
|
|
|
|
let distance = CGPointDistance(location, gestureBeganLocation)
|
|
|
|
let maxDistance: CGFloat = 10.0
|
|
|
|
guard distance <= maxDistance else {
|
|
|
|
failAndReset()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do nothing
|
|
|
|
break
|
|
|
|
case .invalid:
|
|
|
|
failAndReset()
|
|
|
|
case .valid(let pinchState):
|
|
|
|
state = .changed
|
|
|
|
pinchStateLast = pinchState
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
failAndReset()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc
|
|
|
|
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
|
|
super.touchesEnded(touches, with: event)
|
|
|
|
|
|
|
|
switch state {
|
|
|
|
case .began, .changed:
|
|
|
|
switch touchState(for: event) {
|
|
|
|
case .possible:
|
|
|
|
failAndReset()
|
|
|
|
case .invalid:
|
|
|
|
failAndReset()
|
|
|
|
case .valid(let pinchState):
|
|
|
|
state = .ended
|
|
|
|
pinchStateLast = pinchState
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
failAndReset()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc
|
|
|
|
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
|
|
super.touchesCancelled(touches, with: event)
|
|
|
|
|
|
|
|
state = .cancelled
|
|
|
|
}
|
|
|
|
|
|
|
|
public enum TouchState {
|
|
|
|
case possible
|
|
|
|
case valid(pinchState : ImageEditorPinchState)
|
|
|
|
case invalid
|
|
|
|
}
|
|
|
|
|
|
|
|
private func touchState(for event: UIEvent) -> TouchState {
|
|
|
|
guard let allTouches = event.allTouches else {
|
|
|
|
owsFailDebug("Missing allTouches")
|
|
|
|
return .invalid
|
|
|
|
}
|
|
|
|
// Note that we use _all_ touches.
|
|
|
|
if allTouches.count < 2 {
|
|
|
|
return .possible
|
|
|
|
}
|
|
|
|
guard let pinchState = pinchState() else {
|
|
|
|
return .invalid
|
|
|
|
}
|
|
|
|
return .valid(pinchState:pinchState)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func pinchState() -> ImageEditorPinchState? {
|
|
|
|
guard let referenceView = referenceView else {
|
|
|
|
owsFailDebug("Missing view")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
guard numberOfTouches == 2 else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// We need the touch locations _with a stable ordering_.
|
|
|
|
// The only way to ensure the ordering is to use location(ofTouch:in:).
|
|
|
|
let location0 = location(ofTouch: 0, in: referenceView)
|
|
|
|
let location1 = location(ofTouch: 1, in: referenceView)
|
|
|
|
|
|
|
|
let centroid = CGPointScale(CGPointAdd(location0, location1), 0.5)
|
|
|
|
let distance = CGPointDistance(location0, location1)
|
|
|
|
|
|
|
|
// The valence of the angle doesn't matter; we're only going to be using
|
|
|
|
// changes to the angle.
|
|
|
|
let delta = CGPointSubtract(location1, location0)
|
|
|
|
let angleRadians = atan2(delta.y, delta.x)
|
|
|
|
return ImageEditorPinchState(centroid: centroid,
|
|
|
|
distance: distance,
|
|
|
|
angleRadians: angleRadians)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func centroid(forTouches touches: Set<UITouch>?) -> CGPoint {
|
|
|
|
guard let view = self.view else {
|
|
|
|
owsFailDebug("Missing view")
|
|
|
|
return .zero
|
|
|
|
}
|
|
|
|
guard let touches = touches else {
|
|
|
|
return .zero
|
|
|
|
}
|
|
|
|
guard touches.count > 0 else {
|
|
|
|
return .zero
|
|
|
|
}
|
|
|
|
var sum = CGPoint.zero
|
|
|
|
for touch in touches {
|
|
|
|
let location = touch.location(in: view)
|
|
|
|
sum = CGPointAdd(sum, location)
|
|
|
|
}
|
|
|
|
|
|
|
|
let centroid = CGPointScale(sum, 1 / CGFloat(touches.count))
|
|
|
|
return centroid
|
|
|
|
}
|
|
|
|
}
|