/* * Copyright @ 2017-present Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class DragGestureController { var insets: UIEdgeInsets = UIEdgeInsets.zero private var frameBeforeDragging: CGRect = CGRect.zero private weak var view: UIView? private lazy var panGesture: UIPanGestureRecognizer = { return UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:))) }() func startDragListener(inView view: UIView) { self.view = view view.addGestureRecognizer(panGesture) panGesture.isEnabled = true } func stopDragListener() { panGesture.isEnabled = false view?.removeGestureRecognizer(panGesture) view = nil } @objc private func handlePan(gesture: UIPanGestureRecognizer) { guard let view = self.view else { return } let translation = gesture.translation(in: view.superview) let velocity = gesture.velocity(in: view.superview) var frame = frameBeforeDragging switch gesture.state { case .began: frameBeforeDragging = view.frame case .changed: frame.origin.x = floor(frame.origin.x + translation.x) frame.origin.y = floor(frame.origin.y + translation.y) view.frame = frame case .ended: let currentPos = view.frame.origin let finalPos = calculateFinalPosition() let distance = CGPoint(x: currentPos.x - finalPos.x, y: currentPos.y - finalPos.y) let distanceMagnitude = magnitude(vector: distance) let velocityMagnitude = magnitude(vector: velocity) let animationDuration = 0.5 let initialSpringVelocity = velocityMagnitude / distanceMagnitude / CGFloat(animationDuration) frame.origin = CGPoint(x: finalPos.x, y: finalPos.y) UIView.animate(withDuration: animationDuration, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: initialSpringVelocity, options: .curveLinear, animations: { view.frame = frame }, completion: nil) default: break } } private func calculateFinalPosition() -> CGPoint { guard let view = self.view, let bounds = view.superview?.frame else { return CGPoint.zero } let currentSize = view.frame.size let adjustedBounds = UIEdgeInsetsInsetRect(bounds, insets) let threshold: CGFloat = 20.0 let velocity = panGesture.velocity(in: view.superview) let location = panGesture.location(in: view.superview) let goLeft: Bool if fabs(velocity.x) > threshold { goLeft = velocity.x < -threshold } else { goLeft = location.x < bounds.midX } let goUp: Bool if fabs(velocity.y) > threshold { goUp = velocity.y < -threshold } else { goUp = location.y < bounds.midY } let finalPosX: CGFloat = goLeft ? adjustedBounds.origin.x : bounds.size.width - insets.right - currentSize.width let finalPosY: CGFloat = goUp ? adjustedBounds.origin.y : bounds.size.height - insets.bottom - currentSize.height return CGPoint(x: finalPosX, y: finalPosY) } private func magnitude(vector: CGPoint) -> CGFloat { return sqrt(pow(vector.x, 2) + pow(vector.y, 2)) } }