From dbff13d9018514e67d54742cac1dd4e176ac2b30 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 24 Apr 2017 16:34:22 -0400 Subject: [PATCH] Add a resistance property to Draggable. Summary: This new property makes it possible to configure resistance behaviors on Draggable when the user moves beyond a given perimeter. This API is preferred over applying a rubberBand constraint to a Tossable interaction because it ensures that resistance is only applied to the draggable output, not the spring (which may re-read the position value and then incorrectly cause the position to jump to the doubly-resisted position). Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3097 --- src/interactions/Draggable.swift | 21 +++++++++++++ src/operators/rubberBanded.swift | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/interactions/Draggable.swift b/src/interactions/Draggable.swift index e56896f..ff882a7 100644 --- a/src/interactions/Draggable.swift +++ b/src/interactions/Draggable.swift @@ -34,6 +34,25 @@ import UIKit - `{ $0.yLocked(to: somePosition) }` */ public final class Draggable: Gesturable, Interaction, Togglable, Manipulation { + + /** + When a non-null resistance perimiter is provided, dragging beyond the perimeter will result in + resistance being applied to the position until the max length is reached. + */ + public let resistance = ( + /** + The region beyond which resistance should take effect, in absolute coordinates. + + If .null, no resistance will be applied to the drag position. + */ + perimeter: createProperty(withInitialValue: CGRect.null), + + /** + The maximum distance the drag position is able to move beyond the perimeter. + */ + maxLength: createProperty(withInitialValue: 48) + ) + /** A sub-interaction for writing the next gesture recognizer's final velocity to a property. @@ -63,6 +82,8 @@ public final class Draggable: Gesturable, Interaction, T if let applyConstraints = applyConstraints { stream = applyConstraints(stream) } + stream = stream.rubberBanded(outsideOf: resistance.perimeter, + maxLength: resistance.maxLength) runtime.connect(stream, to: position) } } diff --git a/src/operators/rubberBanded.swift b/src/operators/rubberBanded.swift index ca9b65f..839ac5b 100644 --- a/src/operators/rubberBanded.swift +++ b/src/operators/rubberBanded.swift @@ -33,13 +33,65 @@ extension MotionObservableConvertible where T == CGPoint { /** Applies resistance to values that fall outside of the given range. + + Does not modify the value if CGRect is .null. */ public func rubberBanded(outsideOf rect: CGRect, maxLength: CGFloat) -> MotionObservable { return _map(#function, args: [rect, maxLength]) { + guard rect != .null else { + return $0 + } + return CGPoint(x: rubberBand(value: $0.x, min: rect.minX, max: rect.maxX, bandLength: maxLength), y: rubberBand(value: $0.y, min: rect.minY, max: rect.maxY, bandLength: maxLength)) } } + + /** + Applies resistance to values that fall outside of the given range. + + Does not modify the value if CGRect is .null. + */ + public func rubberBanded(outsideOf rectStream: O1, maxLength maxLengthStream: O2) -> MotionObservable where O1: MotionObservableConvertible, O1.T == CGRect, O2: MotionObservableConvertible, O2.T == CGFloat { + var lastRect: CGRect? + var lastMaxLength: CGFloat? + var lastValue: CGPoint? + return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [rectStream, maxLengthStream]))) { observer in + + let checkAndEmit = { + guard let rect = lastRect, let maxLength = lastMaxLength, let value = lastValue else { + return + } + guard lastRect != .null else { + observer.next(value) + return + } + observer.next(CGPoint(x: rubberBand(value: value.x, min: rect.minX, max: rect.maxX, bandLength: maxLength), + y: rubberBand(value: value.y, min: rect.minY, max: rect.maxY, bandLength: maxLength))) + } + + let rectSubscription = rectStream.subscribeToValue { rect in + lastRect = rect + checkAndEmit() + } + + let maxLengthSubscription = maxLengthStream.subscribeToValue { maxLength in + lastMaxLength = maxLength + checkAndEmit() + } + + let upstreamSubscription = self.subscribeAndForward(to: observer) { value in + lastValue = value + checkAndEmit() + } + + return { + rectSubscription.unsubscribe() + maxLengthSubscription.unsubscribe() + upstreamSubscription.unsubscribe() + } + } + } } private func rubberBand(value: CGFloat, min: CGFloat, max: CGFloat, bandLength: CGFloat) -> CGFloat {