Last active
October 16, 2019 23:09
-
-
Save shaps80/a7092ee9c240604542b12f4a6d51b4ad to your computer and use it in GitHub Desktop.
Playback Gesture
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
/// A gesture optimised for use in video playback. Both delta-offset and target-delta based translations are supported | |
public final class PlaybackGestureRecognizer: LongPressGestureRecognizer { | |
/// Specifies the direction of the scrub | |
public enum Direction { | |
/// The user is scrubbed forward through time | |
case forward | |
/// The user is scrubbed backward through time | |
case backward | |
} | |
/// A type for representing a delta offset value | |
public enum Delta: Comparable { | |
case frameByFrame | |
case slowest | |
case slower | |
case slow | |
case medium | |
case fast | |
case faster | |
case fastest | |
case custom(framesPerSecond: CGFloat) | |
/// Returns the number of frames per second this delta represents. | |
public var framesPerSecond: CGFloat { | |
switch self { | |
case .frameByFrame: return 1 | |
case .slowest: return 2 | |
case .slower: return 5 | |
case .slow: return 10 | |
case .medium: return 20 | |
case .fast: return 30 | |
case .faster: return 60 | |
case .fastest: return 120 | |
case let .custom(framesPerSecond): return framesPerSecond | |
} | |
} | |
/// Represents the default set of delta's to be provided to the initializer | |
public static var defaults: [Delta] { | |
return [.frameByFrame, .slowest, .slower, .slow, .medium, .fast, .faster, .fastest] | |
} | |
public static func < (lhs: Delta, rhs: Delta) -> Bool { | |
return lhs.framesPerSecond < rhs.framesPerSecond | |
} | |
} | |
// The possible delta values to use for triggering a call to the closure | |
private let deltas: [Delta] | |
// The observer to notify about translation delta updates | |
private let directionObserver: ((Direction) -> Void)? | |
// The observer to nofify about delta updates | |
private let deltasObserver: ((Delta, Direction) -> Void)? | |
/// Initializes the gesture with an `observer` that will be notified for every `delta` change that occurs. | |
/// | |
/// E.g. if `delta=10`, the `observer` will be called for every 10pt change in `translation`. | |
/// | |
/// - Parameter delta: The delta that will trigger an update | |
/// - Parameter observer: The observer to be notified | |
public init(delta: Delta = .slow, observer: @escaping (Direction) -> Void) { | |
self.deltas = [delta] | |
self.directionObserver = observer | |
self.deltasObserver = nil | |
super.init(target: nil, action: nil) | |
addTarget(self, action: #selector(handle(_:))) | |
} | |
/// Initializes the gesture with an `observer` that will be notified whenever a `targetDelta` has been reached. | |
/// | |
/// E.g. if `deltas==[10, 20, 50]` , the `observer` will be notified when the `translation` reaches each of those targets. | |
/// | |
/// - Parameter deltas: The target delta's that will trigger an update | |
/// - Parameter observer: The observer that will be notified | |
public init(targetDeltas deltas: [Delta] = Delta.defaults, observer: @escaping (Delta, Direction) -> Void) { | |
// to sanitize the deltas, we remove negative values (including 0), sort them in ascending order and then reverse them. | |
self.deltas = deltas.filter { $0.framesPerSecond > 0 }.sorted().reversed() | |
self.deltasObserver = observer | |
self.directionObserver = nil | |
super.init(target: nil, action: nil) | |
addTarget(self, action: #selector(handle(_:))) | |
} | |
@objc private func handle(_ gesture: PlaybackGestureRecognizer) { | |
let translation = self.translation(in: view).x | |
switch gesture.state { | |
case .began: break | |
case .changed: | |
if deltas.count == 1 { | |
guard !(0...deltas[0].framesPerSecond).contains(translation) else { return } | |
directionObserver?(translation > 0 ? .forward : .backward) | |
setTranslation(.zero, in: gesture.view) | |
} else { | |
guard let delta = deltas.first(where: { abs(translation) > $0.framesPerSecond }) else { return } | |
deltasObserver?(delta, translation > 0 ? .forward : .backward) | |
} | |
default: break | |
} | |
} | |
} | |
/// A UILongPressGestureRecognizer with built-in support for translation | |
open class LongPressGestureRecognizer: UILongPressGestureRecognizer { | |
// The location of the initial touch | |
private var initialLocation: CGPoint = .zero | |
// The current translation | |
private var translation: CGPoint = .zero | |
/// The translation of the pan gesture in the coordinate system of the specified view. | |
/// - Parameter view: The view in whose coordinate system the translation of the pan gesture should be computed. If you want to adjust a view's location to keep it under the user's finger, request the translation in that view's superview's coordinate system. | |
/// | |
/// The x and y values report the total translation over time. They are not delta values from the last time that the translation was reported. Apply the translation value to the state of the view when the gesture is first recognized—do not concatenate the value each time the handler is called. | |
open func translation(in view: UIView?) -> CGPoint { | |
return self.view?.convert(translation, to: view) ?? translation | |
} | |
/// Sets the translation value in the coordinate system of the specified view. | |
/// - Parameter translation: A point that identifies the new translation value. | |
/// - Parameter view: A view in whose coordinate system the translation is to occur. | |
open func setTranslation(_ translation: CGPoint, in view: UIView?) { | |
initialLocation.x += self.translation.x | |
initialLocation.y += self.translation.y | |
self.translation = self.view?.convert(translation, to: view) ?? translation | |
} | |
} | |
extension LongPressGestureRecognizer { | |
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { | |
initialLocation = touches.first?.location(in: view) ?? .zero | |
translation = .zero | |
super.touchesBegan(touches, with: event) | |
} | |
open override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { | |
let location = touches.first?.location(in: view) ?? .zero | |
translation = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y) | |
super.touchesMoved(touches, with: event) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment