@@ -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,8 +25,9 @@ final class PanGestureTarget {
2425 private weak var drawerConfiguration : DrawerConfiguration ?
2526 private weak var drawerPositionDelegate : DrawerPositionDelegate ?
2627 private var initialDrawerCenterLocation : CGPoint = . zero
27- var basePositionY : CGFloat !
2828 var topPositionY : CGFloat !
29+ var middlePositionY : CGFloat ?
30+ var bottomPositionY : CGFloat !
2931
3032 init ( canvasView: UIView ,
3133 drawerContainerView: UIView ,
@@ -41,8 +43,9 @@ final class PanGestureTarget {
4143 }
4244
4345 internal func refreshDrawerPositions( ) {
44- basePositionY = drawerConfiguration? . bottomPositionY ( for: canvasView. bounds. height) ?? 0
46+ bottomPositionY = drawerConfiguration? . bottomPositionY ( for: canvasView. bounds. height) ?? 0
4547 topPositionY = drawerConfiguration? . topPositionY ( for: canvasView. bounds. height) ?? 0
48+ middlePositionY = drawerConfiguration? . middlePositionY ( for: canvasView. bounds. height)
4649 }
4750
4851 deinit {
@@ -56,16 +59,13 @@ final class PanGestureTarget {
5659 performDrag ( recognizer: recognizer)
5760 } else if recognizer. state == . ended || recognizer. state == . cancelled {
5861 let velocity = recognizer. velocity ( in: canvasView)
59- if shouldFinishUp ( recognizer: recognizer) {
60- animate ( to: topPositionY, velocity: velocity)
61- } else {
62- animate ( to: basePositionY, velocity: velocity)
63- }
62+ let targetPositionY = self . targetPositionY ( for: velocity)
63+ animate ( to: targetPositionY, velocity: velocity)
6464 }
6565 }
6666
6767 private var isDraggedViewWithinAllowedArea : Bool {
68- return drawerContainerView. frame. minY > topPositionY && drawerContainerView. frame. minY < basePositionY
68+ return drawerContainerView. frame. minY > topPositionY && drawerContainerView. frame. minY < bottomPositionY
6969 }
7070
7171 private func performDrag( recognizer: UIPanGestureRecognizer ) {
@@ -84,16 +84,20 @@ final class PanGestureTarget {
8484
8585 private func updateDimming( ) {
8686 guard overDragAmount ( drawerContainerView. center) == 0 else { return }
87- let currentDistanceToBasePosition = basePositionY - drawerContainerView. frame. minY
88- let totalDraggableDistance = basePositionY - topPositionY
89- let movementPercentage = currentDistanceToBasePosition / totalDraggableDistance
87+ let currentDistanceToDimStartPosition = dimStartPosition - drawerContainerView. frame. minY
88+ let totalDraggableDistance = dimStartPosition - topPositionY
89+ let movementPercentage = currentDistanceToDimStartPosition / totalDraggableDistance
9090 dimmingView. alpha = movementPercentage * targetDimmingViewAlpha
9191 }
9292
93+ private var dimStartPosition : CGFloat {
94+ return middlePositionY ?? bottomPositionY
95+ }
96+
9397 private func overDragAmount( _ newCenter: CGPoint ) -> CGFloat {
9498 let newMinY = newCenter. y - drawerContainerView. frame. height/ 2
9599 let aboveTop = newMinY - topPositionY
96- let underBottom = newMinY - basePositionY
100+ let underBottom = newMinY - bottomPositionY
97101 if aboveTop < 0 {
98102 return aboveTop
99103 } else if underBottom > 0 {
@@ -103,23 +107,46 @@ final class PanGestureTarget {
103107 }
104108 }
105109
106- private func shouldFinishUp ( recognizer : UIPanGestureRecognizer ) -> Bool {
107- let velocity = recognizer . velocity ( in : canvasView )
110+ private func targetPositionY ( for velocity : CGPoint ) -> CGFloat {
111+ let isDraggingQuickly = abs ( velocity . y ) > skipMiddleVelocityThreshold
108112 let isDraggingSlowly = abs ( velocity. y) < bounceVelocityThreshold
109- if isDraggingSlowly {
110- 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)
111124 } else {
112- let isDraggingUp = velocity. y < 0
113- return isDraggingUp
125+ fatalError ( )
114126 }
115127 }
116128
117- private func isInUpperHalfOfMovement( ) -> Bool {
118- let allowedMovementMinY = topPositionY!
119- let allowedMovementMaxY = basePositionY!
120- let halfMovement = ( allowedMovementMaxY - allowedMovementMinY) / 2
121- let movementMidY = allowedMovementMinY + halfMovement
122- 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]
123150 }
124151
125152 private func animate( to minY: CGFloat , velocity: CGPoint ) {
@@ -135,6 +162,8 @@ final class PanGestureTarget {
135162 guard finished else { return }
136163 if self . isOnTopPosition {
137164 self . drawerPositionDelegate? . didMoveDrawerToTopPosition ( )
165+ } else if self . isOnMiddlePosition {
166+ self . drawerPositionDelegate? . didMoveDrawerToMiddlePosition ( )
138167 } else if self . isOnBasePosition {
139168 self . drawerPositionDelegate? . didMoveDrawerToBasePosition ( )
140169 }
@@ -158,6 +187,16 @@ final class PanGestureTarget {
158187 }
159188 }
160189
161- private var isOnTopPosition : Bool { return Int ( drawerContainerView. frame. minY) == Int ( topPositionY) }
162- 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+ }
163202}
0 commit comments