diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift index 19581742..0e2f22e2 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift @@ -7,6 +7,10 @@ import UIKit +enum AdaptiveModalSnapAnimationConfig { + +}; + struct AdaptiveModalConfig { enum Direction { case horizontal; diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager+Helpers.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager+Helpers.swift index faec1193..c8235043 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager+Helpers.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager+Helpers.swift @@ -5,7 +5,7 @@ // Created by Dominic Go on 5/27/23. // -import Foundation +import UIKit extension AdaptiveModalManager { @@ -33,8 +33,8 @@ extension AdaptiveModalManager { let interpolatedValue = RNIAnimator.EasingFunctions.lerp( valueStart: rangeOutputEnd, - valueEnd: rangeOutputStart, - percent: percent + valueEnd : rangeOutputStart, + percent : percent ); let delta2 = interpolatedValue - rangeOutputEnd; @@ -81,4 +81,16 @@ extension AdaptiveModalManager { percent : progress ); }; + + static func computeFinalPosition( + position: CGFloat, + initialVelocity: CGFloat, + decelerationRate: CGFloat = UIScrollView.DecelerationRate.normal.rawValue + ) -> CGFloat { + let pointPerSecond = initialVelocity / 1000.0; + let accelerationRate = 1 - decelerationRate; + + let displacement = (pointPerSecond * decelerationRate) / accelerationRate; + return position + displacement; + }; }; diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift index b90a8543..0c86d126 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift @@ -70,11 +70,15 @@ class AdaptiveModalManager { var targetRectProvider: () -> CGRect; var currentSizeProvider: () -> CGSize; + let maxGestureVelocity: CGFloat = 20; + weak var targetView: UIView?; weak var modalView: UIView?; var gestureOffset: CGFloat?; - var gestureVelocity: CGFloat?; + var gestureVelocity: CGPoint?; + var gestureInitialPoint: CGPoint?; + var gesturePoint: CGPoint?; // MARK: - Computed Properties // --------------------------- @@ -106,6 +110,58 @@ class AdaptiveModalManager { self.modalConfig.snapPoints[self.currentSnapPointIndex]; }; + var gestureInitialVelocity: CGVector? { + guard let gestureInitialPoint = self.gestureInitialPoint, + let gestureFinalPoint = self.gesturePoint, + let gestureVelocity = self.gestureVelocity + else { return nil }; + + let gestureInitialCoord = gestureInitialPoint[keyPath: self.inputAxisKey]; + let gestureFinalCoord = gestureFinalPoint [keyPath: self.inputAxisKey]; + let gestureVelocityCoord = gestureVelocity [keyPath: self.inputAxisKey]; + + var velocity: CGFloat = 0; + let distance = gestureFinalCoord - gestureInitialCoord; + + if distance != 0 { + velocity = gestureVelocityCoord / distance; + }; + + velocity = velocity.clamped( + min: -self.maxGestureVelocity, + max: self.maxGestureVelocity + ); + + return CGVector(dx: velocity, dy: velocity); + }; + + /// Based on the gesture's velocity and it's current position, estimate + /// where would it eventually "stop" (i.e. it's final position) if it were to + /// decelerate over time + /// + var gestureFinalPoint: CGPoint? { + guard let gesturePoint = self.gesturePoint, + let gestureVelocity = self.gestureVelocity + else { return nil }; + + let gestureVelocityClamped = CGPoint( + x: gestureVelocity.x.clamped(minMax: self.maxGestureVelocity), + y: gestureVelocity.y.clamped(minMax: self.maxGestureVelocity) + ); + + let nextX = Self.computeFinalPosition( + position: gesturePoint.x, + initialVelocity: gestureVelocityClamped.x + ); + + let nextY = Self.computeFinalPosition( + position: gesturePoint.y, + initialVelocity: gestureVelocityClamped.y + ); + + return CGPoint(x: nextX, y: nextY); + }; + // MARK: - Init // ------------ @@ -139,10 +195,27 @@ class AdaptiveModalManager { func animateModal(toRect nextRect: CGRect) { guard let modalView = self.modalView else { return }; - let animator = UIViewPropertyAnimator( - duration: 0.2, - curve: .easeIn - ); + let animator: UIViewPropertyAnimator = { + if let gestureInitialVelocity = self.gestureInitialVelocity { + let springTiming = UISpringTimingParameters( + dampingRatio: 1, + initialVelocity: gestureInitialVelocity + ); + + // Move to animation config + let springAnimationSettlingTime: CGFloat = 0.4; + + return UIViewPropertyAnimator( + duration: springAnimationSettlingTime, + timingParameters: springTiming + ); + }; + + return UIViewPropertyAnimator( + duration: 0.2, + curve: .easeIn + ); + }(); animator.addAnimations { modalView.frame = nextRect; @@ -152,28 +225,17 @@ class AdaptiveModalManager { }; func getClosestSnapPoint( - forRect currentRect: CGRect - ) -> ( - snapPointIndex: Int, - snapPointConfig: AdaptiveModalSnapPointConfig, - computedRect: CGRect - ) { - return self.getClosestSnapPoint( - forGestureCoord: currentRect.origin[keyPath: self.inputAxisKey] - ); - }; - - func getClosestSnapPoint( - forGestureCoord: CGFloat + forGestureCoord gestureCoord: CGFloat ) -> ( snapPointIndex: Int, snapPointConfig: AdaptiveModalSnapPointConfig, computedRect: CGRect ) { let snapRects = self.computedSnapRects; + let gestureCoordAdj = gestureCoord + (self.gestureOffset ?? 0); let diffY = snapRects.map { - $0.origin[keyPath: self.inputAxisKey] - forGestureCoord; + $0.origin[keyPath: self.inputAxisKey] - gestureCoordAdj; }; let closestSnapPoint = diffY.enumerated().first { item in @@ -275,28 +337,32 @@ class AdaptiveModalManager { func notifyOnDragPanGesture(_ gesture: UIPanGestureRecognizer){ guard let modalView = self.modalView else { return }; - let gesturePoint = gesture.location(in: self.targetView); + let gesturePoint = gesture.location(in: self.targetView); + self.gesturePoint = gesturePoint; + let gestureVelocity = gesture.velocity(in: self.targetView); + self.gestureVelocity = gestureVelocity; let gestureVelocityCoord = gestureVelocity[keyPath: self.inputAxisKey]; switch gesture.state { case .began: + self.gestureInitialPoint = gesturePoint; break; case .cancelled, .ended: - self.gestureOffset = nil; - let nextSnapPointIndex: Int = { + let gestureFinalPoint = self.gestureFinalPoint ?? gesturePoint; + let closestSnapPoint = self.getClosestSnapPoint( - forRect: modalView.frame + forGestureCoord: gestureFinalPoint[keyPath: self.inputAxisKey] ); let lastIndex = self.modalConfig.snapPoints.count - 1; guard closestSnapPoint.snapPointIndex > 0, closestSnapPoint.snapPointIndex < lastIndex, - abs(gestureVelocityCoord) > 10 + abs(gestureVelocityCoord) > 100 else { return closestSnapPoint.snapPointIndex; }; @@ -314,6 +380,10 @@ class AdaptiveModalManager { self.animateModal(toRect: nextRect); self.currentSnapPointIndex = nextSnapPointIndex; + + self.gestureOffset = nil; + self.gestureInitialPoint = nil; + self.gestureVelocity = nil; break; case .changed: @@ -327,5 +397,4 @@ class AdaptiveModalManager { break; }; }; - }; diff --git a/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift b/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift index a83df4b6..6405efa1 100644 --- a/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift +++ b/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift @@ -8,9 +8,6 @@ import UIKit - - - class RNIDraggableTestViewController : UIViewController { lazy var modalManager = AdaptiveModalManager( @@ -91,6 +88,8 @@ class RNIDraggableTestViewController : UIViewController { }; @objc func onDragPanGestureView(_ sender: UIPanGestureRecognizer) { + print("onDragPanGestureView - velocity: \(sender.velocity(in: self.view))"); + self.modalManager.notifyOnDragPanGesture(sender); }; }; diff --git a/ios/src_library/Extensions/FloatingPoint+Clamping.swift b/ios/src_library/Extensions/FloatingPoint+Clamping.swift index 24666f1f..1c4f6422 100644 --- a/ios/src_library/Extensions/FloatingPoint+Clamping.swift +++ b/ios/src_library/Extensions/FloatingPoint+Clamping.swift @@ -21,4 +21,8 @@ extension FloatingPoint { return clampedValue; }; + + public func clamped(minMax: Self) -> Self { + self.clamped(min: -minMax, max: minMax); + }; };