// // 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, 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, 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, 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, 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?) -> 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 } }