@@ -8,6 +8,7 @@ final class PanGestureTarget {
88 when view lands at the target drawer position
99 */
1010 private let bounceVelocityThreshold : CGFloat = 100
11+ private let skipMiddleVelocityThreshold : CGFloat = 1500
1112 private let animationDuration : TimeInterval = 0.25
1213 private let dampedAnimationDuration : TimeInterval = 0.4
1314 private let springAnimationDamping : CGFloat = 0.75
@@ -24,9 +25,9 @@ final class PanGestureTarget {
2425 private weak var drawerConfiguration : DrawerConfiguration ?
2526 private weak var drawerPositionDelegate : DrawerPositionDelegate ?
2627 private var initialDrawerCenterLocation : CGPoint = . zero
27- private var bottomPositionHeight : CGFloat !
28- var basePositionY : CGFloat { return canvasView. frame. height - bottomPositionHeight }
2928 var topPositionY : CGFloat !
29+ var middlePositionY : CGFloat ?
30+ var bottomPositionY : CGFloat !
3031
3132 init ( canvasView: UIView ,
3233 drawerContainerView: UIView ,
@@ -42,8 +43,9 @@ final class PanGestureTarget {
4243 }
4344
4445 internal func refreshDrawerPositions( ) {
45- bottomPositionHeight = drawerConfiguration? . bottomPositionHeight ?? 0
46- topPositionY = drawerConfiguration? . topPositionY ( in: canvasView. bounds) ?? 0
46+ bottomPositionY = drawerConfiguration? . bottomPositionY ( for: canvasView. bounds. height) ?? 0
47+ topPositionY = drawerConfiguration? . topPositionY ( for: canvasView. bounds. height) ?? 0
48+ middlePositionY = drawerConfiguration? . middlePositionY ( for: canvasView. bounds. height)
4749 }
4850
4951 deinit {
@@ -57,16 +59,13 @@ final class PanGestureTarget {
5759 performDrag ( recognizer: recognizer)
5860 } else if recognizer. state == . ended || recognizer. state == . cancelled {
5961 let velocity = recognizer. velocity ( in: canvasView)
60- if shouldFinishUp ( recognizer: recognizer) {
61- animate ( to: topPositionY, velocity: velocity)
62- } else {
63- animate ( to: basePositionY, velocity: velocity)
64- }
62+ let targetPositionY = self . targetPositionY ( for: velocity)
63+ animate ( to: targetPositionY, velocity: velocity)
6564 }
6665 }
6766
6867 private var isDraggedViewWithinAllowedArea : Bool {
69- return drawerContainerView. frame. minY > topPositionY && drawerContainerView. frame. minY < basePositionY
68+ return drawerContainerView. frame. minY > topPositionY && drawerContainerView. frame. minY < bottomPositionY
7069 }
7170
7271 private func performDrag( recognizer: UIPanGestureRecognizer ) {
@@ -85,16 +84,20 @@ final class PanGestureTarget {
8584
8685 private func updateDimming( ) {
8786 guard overDragAmount ( drawerContainerView. center) == 0 else { return }
88- let currentDistanceToBasePosition = basePositionY - drawerContainerView. frame. minY
89- let totalDraggableDistance = basePositionY - topPositionY
90- let movementPercentage = currentDistanceToBasePosition / totalDraggableDistance
87+ let currentDistanceToDimStartPosition = dimStartPosition - drawerContainerView. frame. minY
88+ let totalDraggableDistance = dimStartPosition - topPositionY
89+ let movementPercentage = currentDistanceToDimStartPosition / totalDraggableDistance
9190 dimmingView. alpha = movementPercentage * targetDimmingViewAlpha
9291 }
9392
93+ private var dimStartPosition : CGFloat {
94+ return middlePositionY ?? bottomPositionY
95+ }
96+
9497 private func overDragAmount( _ newCenter: CGPoint ) -> CGFloat {
9598 let newMinY = newCenter. y - drawerContainerView. frame. height/ 2
9699 let aboveTop = newMinY - topPositionY
97- let underBottom = newMinY - basePositionY
100+ let underBottom = newMinY - bottomPositionY
98101 if aboveTop < 0 {
99102 return aboveTop
100103 } else if underBottom > 0 {
@@ -104,23 +107,46 @@ final class PanGestureTarget {
104107 }
105108 }
106109
107- private func shouldFinishUp ( recognizer : UIPanGestureRecognizer ) -> Bool {
108- let velocity = recognizer . velocity ( in : canvasView )
110+ private func targetPositionY ( for velocity : CGPoint ) -> CGFloat {
111+ let isDraggingQuickly = abs ( velocity . y ) > skipMiddleVelocityThreshold
109112 let isDraggingSlowly = abs ( velocity. y) < bounceVelocityThreshold
110- if isDraggingSlowly {
111- return isInUpperHalfOfMovement ( )
113+ let isDraggingUp = velocity. y < 0
114+ if isDraggingQuickly && isDraggingUp {
115+ return topPositionY
116+ } else if isDraggingQuickly && !isDraggingUp {
117+ return bottomPositionY
118+ } else if isDraggingSlowly {
119+ return nearestPosition ( searchStyle: . nearest)
120+ } else if isDraggingUp {
121+ return nearestPosition ( searchStyle: . higher)
122+ } else if !isDraggingUp {
123+ return nearestPosition ( searchStyle: . lower)
112124 } else {
113- let isDraggingUp = velocity. y < 0
114- return isDraggingUp
125+ fatalError ( )
115126 }
116127 }
117128
118- private func isInUpperHalfOfMovement( ) -> Bool {
119- let allowedMovementMinY = topPositionY!
120- let allowedMovementMaxY = basePositionY
121- let halfMovement = ( allowedMovementMaxY - allowedMovementMinY) / 2
122- let movementMidY = allowedMovementMinY + halfMovement
123- return drawerContainerView. frame. minY < movementMidY
129+ private enum NearestPositionSearchStyle {
130+ case nearest, higher, lower
131+ }
132+
133+ private func nearestPosition( searchStyle: NearestPositionSearchStyle ) -> CGFloat {
134+ let currentPosition = drawerContainerView. frame. minY
135+ switch searchStyle {
136+ case . nearest:
137+ return positionsArray. min ( by: { abs ( currentPosition - $0) < abs ( currentPosition - $1) } ) !
138+ case . higher:
139+ /* Keep in mind, that higher position means lower Y in UIView coordinate system */
140+ return positionsArray. sorted ( by: > ) . first ( where: { currentPosition - $0 > 0 } ) ?? topPositionY
141+ case . lower:
142+ /* Keep in mind, that lower position means higher Y in UIView coordinate system */
143+ return positionsArray. sorted ( by: < ) . first ( where: { $0 - currentPosition > 0 } ) ?? bottomPositionY
144+ }
145+ }
146+
147+ private var positionsArray : [ CGFloat ] {
148+ guard let middlePositionY = middlePositionY else { return [ bottomPositionY, topPositionY] }
149+ return [ bottomPositionY, middlePositionY, topPositionY]
124150 }
125151
126152 private func animate( to minY: CGFloat , velocity: CGPoint ) {
@@ -136,6 +162,8 @@ final class PanGestureTarget {
136162 guard finished else { return }
137163 if self . isOnTopPosition {
138164 self . drawerPositionDelegate? . didMoveDrawerToTopPosition ( )
165+ } else if self . isOnMiddlePosition {
166+ self . drawerPositionDelegate? . didMoveDrawerToMiddlePosition ( )
139167 } else if self . isOnBasePosition {
140168 self . drawerPositionDelegate? . didMoveDrawerToBasePosition ( )
141169 }
@@ -159,6 +187,16 @@ final class PanGestureTarget {
159187 }
160188 }
161189
162- private var isOnTopPosition : Bool { return Int ( drawerContainerView. frame. minY) == Int ( topPositionY) }
163- private var isOnBasePosition : Bool { return Int ( drawerContainerView. frame. minY) == Int ( basePositionY) }
190+ private var isOnTopPosition : Bool {
191+ return Int ( drawerContainerView. frame. minY) == Int ( topPositionY)
192+ }
193+
194+ private var isOnMiddlePosition : Bool {
195+ guard let middlePositionY = middlePositionY else { return false }
196+ return Int ( drawerContainerView. frame. minY) == Int ( middlePositionY)
197+ }
198+
199+ private var isOnBasePosition : Bool {
200+ return Int ( drawerContainerView. frame. minY) == Int ( bottomPositionY)
201+ }
164202}
0 commit comments