forked from shadowfacts/Tusker
142 lines
6.2 KiB
Swift
142 lines
6.2 KiB
Swift
//
|
|
// GalleryDismissAnimationController.swift
|
|
// GalleryVC
|
|
//
|
|
// Created by Shadowfacts on 3/1/24.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
|
private let sourceView: UIView
|
|
private let interactiveTranslation: CGPoint?
|
|
private let interactiveVelocity: CGPoint?
|
|
|
|
init(sourceView: UIView, interactiveTranslation: CGPoint?, interactiveVelocity: CGPoint?) {
|
|
self.sourceView = sourceView
|
|
self.interactiveTranslation = interactiveTranslation
|
|
self.interactiveVelocity = interactiveVelocity
|
|
}
|
|
|
|
func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval {
|
|
return 0.3
|
|
}
|
|
|
|
func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) {
|
|
guard let to = transitionContext.viewController(forKey: .to),
|
|
let from = transitionContext.viewController(forKey: .from) as? GalleryViewController else {
|
|
fatalError()
|
|
}
|
|
|
|
let itemViewController = from.currentItemViewController
|
|
|
|
if !itemViewController.content.canAnimateFromSourceView || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) {
|
|
animateCrossFadeTransition(using: transitionContext)
|
|
return
|
|
}
|
|
|
|
let container = transitionContext.containerView
|
|
|
|
// Moving `to.view` to the container is necessary when the presenting VC (i.e., `to`)
|
|
// is in the window's root presentation.
|
|
// But it breaks when the gallery is presented from a sheet-presented VC--in which case
|
|
// `to.view` is already in the view hierarchy at this point; and adding it to the
|
|
// container causees it to be removed when the transition completes.
|
|
if to.view.superview == nil {
|
|
to.view.frame = container.bounds
|
|
container.addSubview(to.view)
|
|
}
|
|
|
|
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
|
|
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
|
|
|
|
let origSourceTransform = sourceView.transform
|
|
let appliedSourceToDestTransform: Bool
|
|
if destFrameInContainer.width > 0 && destFrameInContainer.height > 0 {
|
|
appliedSourceToDestTransform = true
|
|
let scale = min(destFrameInContainer.width / sourceFrameInContainer.width, destFrameInContainer.height / sourceFrameInContainer.height)
|
|
let sourceToDestTransform = origSourceTransform
|
|
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
|
|
.scaledBy(x: scale, y: scale)
|
|
sourceView.transform = sourceToDestTransform
|
|
} else {
|
|
appliedSourceToDestTransform = false
|
|
}
|
|
|
|
from.view.frame = container.bounds
|
|
container.addSubview(from.view)
|
|
|
|
let content = itemViewController.takeContent()
|
|
content.view.translatesAutoresizingMaskIntoConstraints = true
|
|
content.view.layer.masksToBounds = true
|
|
container.addSubview(content.view)
|
|
|
|
content.view.frame = destFrameInContainer
|
|
content.view.layer.opacity = 1
|
|
|
|
container.layoutIfNeeded()
|
|
|
|
let duration = self.transitionDuration(using: transitionContext)
|
|
var initialVelocity: CGVector
|
|
if let interactiveVelocity,
|
|
let interactiveTranslation,
|
|
// very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the springs initial undershoot
|
|
sqrt(pow(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100,
|
|
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
|
|
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
|
|
let yDistance = sourceFrameInContainer.midY - destFrameInContainer.midY
|
|
initialVelocity = CGVector(
|
|
dx: xDistance == 0 ? 0 : interactiveVelocity.x / xDistance,
|
|
dy: yDistance == 0 ? 0 : interactiveVelocity.y / yDistance
|
|
)
|
|
} else {
|
|
initialVelocity = .zero
|
|
}
|
|
initialVelocity.dx = max(-10, min(10, initialVelocity.dx))
|
|
initialVelocity.dy = max(-10, min(10, initialVelocity.dy))
|
|
// no bounce for the dismiss animation
|
|
let spring = UISpringTimingParameters(mass: 1, stiffness: 439, damping: 42, initialVelocity: initialVelocity)
|
|
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
|
|
|
|
animator.addAnimations {
|
|
from.view.layer.opacity = 0
|
|
|
|
if appliedSourceToDestTransform {
|
|
self.sourceView.transform = origSourceTransform
|
|
}
|
|
content.view.frame = sourceFrameInContainer
|
|
content.view.layer.opacity = 0
|
|
|
|
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
|
}
|
|
|
|
animator.addCompletion { _ in
|
|
transitionContext.completeTransition(true)
|
|
}
|
|
|
|
animator.startAnimation()
|
|
}
|
|
|
|
private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
guard let fromVC = transitionContext.viewController(forKey: .from),
|
|
let toVC = transitionContext.viewController(forKey: .to) else {
|
|
return
|
|
}
|
|
|
|
toVC.view.frame = transitionContext.containerView.bounds
|
|
fromVC.view.frame = transitionContext.containerView.bounds
|
|
transitionContext.containerView.addSubview(toVC.view)
|
|
transitionContext.containerView.addSubview(fromVC.view)
|
|
|
|
let duration = transitionDuration(using: transitionContext)
|
|
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
|
|
animator.addAnimations {
|
|
fromVC.view.alpha = 0
|
|
}
|
|
animator.addCompletion { _ in
|
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
}
|
|
animator.startAnimation()
|
|
}
|
|
}
|