forked from shadowfacts/Tusker
137 lines
5.6 KiB
Swift
137 lines
5.6 KiB
Swift
//
|
|
// GalleryPresentationAnimationController.swift
|
|
// GalleryVC
|
|
//
|
|
// Created by Shadowfacts on 12/28/23.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
|
private let sourceView: UIView
|
|
|
|
init(sourceView: UIView) {
|
|
self.sourceView = sourceView
|
|
}
|
|
|
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
return 0.4
|
|
}
|
|
|
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
|
|
fatalError()
|
|
}
|
|
|
|
let itemViewController = to.currentItemViewController
|
|
|
|
if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions {
|
|
animateCrossFadeTransition(using: transitionContext)
|
|
return
|
|
}
|
|
|
|
let container = transitionContext.containerView
|
|
to.view.frame = container.bounds
|
|
container.addSubview(to.view)
|
|
|
|
container.layoutIfNeeded()
|
|
// Make sure the zoom scale is updated before getting the content view frame, since it needs to take into account the correct transform.
|
|
itemViewController.updateZoomScale(resetZoom: true)
|
|
|
|
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
|
|
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
|
|
|
|
// Use a transformation to make the actual source view appear to move into the destination frame.
|
|
// Doing this while having the content view fade-in papers over the z-index change when
|
|
// there was something overlapping the source view.
|
|
let origSourceTransform = sourceView.transform
|
|
let sourceToDestTransform: CGAffineTransform?
|
|
if destFrameInContainer.width > 0 && destFrameInContainer.height > 0 {
|
|
// Scale evenly in both dimensions, to prevent the source view appearing to stretch/distort during the animation.
|
|
let scale = min(destFrameInContainer.width / sourceFrameInContainer.width, destFrameInContainer.height / sourceFrameInContainer.height)
|
|
sourceToDestTransform = origSourceTransform
|
|
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
|
|
.scaledBy(x: scale, y: scale)
|
|
} else {
|
|
sourceToDestTransform = nil
|
|
}
|
|
|
|
let content = itemViewController.takeContent()
|
|
content.view.translatesAutoresizingMaskIntoConstraints = true
|
|
container.insertSubview(content.view, belowSubview: to.view)
|
|
|
|
// Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content.
|
|
let dimmingView = UIView()
|
|
dimmingView.backgroundColor = .black
|
|
dimmingView.frame = container.bounds
|
|
dimmingView.layer.opacity = 0
|
|
container.insertSubview(dimmingView, belowSubview: content.view)
|
|
|
|
to.view.backgroundColor = nil
|
|
to.view.layer.opacity = 0
|
|
content.view.frame = sourceFrameInContainer
|
|
content.view.layer.opacity = 0
|
|
|
|
container.layoutIfNeeded()
|
|
|
|
// This needs to take place after the layout, so that the transform is correct.
|
|
itemViewController.setControlsVisible(false, animated: false)
|
|
|
|
let duration = self.transitionDuration(using: transitionContext)
|
|
// rougly equivalent to duration: 0.35, bounce: 0.3
|
|
let spring = UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero)
|
|
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
|
|
|
|
animator.addAnimations {
|
|
dimmingView.layer.opacity = 1
|
|
|
|
to.view.layer.opacity = 1
|
|
|
|
content.view.frame = destFrameInContainer
|
|
content.view.layer.opacity = 1
|
|
|
|
itemViewController.setControlsVisible(true, animated: false)
|
|
|
|
if let sourceToDestTransform {
|
|
self.sourceView.transform = sourceToDestTransform
|
|
}
|
|
}
|
|
|
|
animator.addCompletion { _ in
|
|
dimmingView.removeFromSuperview()
|
|
|
|
to.view.backgroundColor = .black
|
|
|
|
if sourceToDestTransform != nil {
|
|
self.sourceView.transform = origSourceTransform
|
|
}
|
|
|
|
itemViewController.addContent()
|
|
|
|
transitionContext.completeTransition(true)
|
|
}
|
|
|
|
animator.startAnimation()
|
|
}
|
|
|
|
private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
|
|
return
|
|
}
|
|
|
|
to.view.alpha = 0
|
|
to.view.frame = transitionContext.containerView.bounds
|
|
transitionContext.containerView.addSubview(to.view)
|
|
|
|
let duration = transitionDuration(using: transitionContext)
|
|
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
|
|
animator.addAnimations {
|
|
to.view.alpha = 1
|
|
}
|
|
animator.addCompletion { _ in
|
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
}
|
|
animator.startAnimation()
|
|
}
|
|
}
|