|
|
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
import SessionUIKit
|
|
|
|
|
|
|
|
class MediaDismissAnimationController: NSObject {
|
|
|
|
private let mediaItem: Media
|
|
|
|
public let interactionController: MediaInteractiveDismiss?
|
|
|
|
|
|
|
|
var fromView: UIView?
|
|
|
|
var transitionView: UIView?
|
|
|
|
var fromTransitionalOverlayView: UIView?
|
|
|
|
var toTransitionalOverlayView: UIView?
|
|
|
|
var fromMediaFrame: CGRect?
|
|
|
|
var pendingCompletion: (() -> ())?
|
|
|
|
|
|
|
|
init(galleryItem: MediaGalleryViewModel.Item, interactionController: MediaInteractiveDismiss? = nil) {
|
|
|
|
self.mediaItem = .gallery(galleryItem)
|
|
|
|
self.interactionController = interactionController
|
|
|
|
}
|
|
|
|
|
|
|
|
init(image: UIImage, interactionController: MediaInteractiveDismiss? = nil) {
|
|
|
|
self.mediaItem = .image(image)
|
|
|
|
self.interactionController = interactionController
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning {
|
|
|
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
|
|
return 0.3
|
|
|
|
}
|
|
|
|
|
|
|
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
|
|
let containerView = transitionContext.containerView
|
|
|
|
let fromContextProvider: MediaPresentationContextProvider
|
|
|
|
let toContextProvider: MediaPresentationContextProvider
|
|
|
|
|
|
|
|
guard let fromVC: UIViewController = transitionContext.viewController(forKey: .from) else {
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
guard let toVC: UIViewController = transitionContext.viewController(forKey: .to) else {
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch fromVC {
|
|
|
|
case let contextProvider as MediaPresentationContextProvider:
|
|
|
|
fromContextProvider = contextProvider
|
|
|
|
|
|
|
|
case let topBannerController as TopBannerController:
|
|
|
|
guard
|
|
|
|
let firstChild: UIViewController = topBannerController.children.first,
|
|
|
|
let navController: UINavigationController = firstChild as? UINavigationController,
|
|
|
|
let contextProvider = navController.topViewController as? MediaPresentationContextProvider
|
|
|
|
else {
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fromContextProvider = contextProvider
|
|
|
|
|
|
|
|
case let navController as UINavigationController:
|
|
|
|
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fromContextProvider = contextProvider
|
|
|
|
|
|
|
|
default:
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch toVC {
|
|
|
|
case let contextProvider as MediaPresentationContextProvider:
|
|
|
|
toVC.view.layoutIfNeeded()
|
|
|
|
toContextProvider = contextProvider
|
|
|
|
|
|
|
|
case let topBannerController as TopBannerController:
|
|
|
|
guard
|
|
|
|
let firstChild: UIViewController = topBannerController.children.first,
|
|
|
|
let navController: UINavigationController = firstChild as? UINavigationController,
|
|
|
|
let contextProvider = navController.topViewController as? MediaPresentationContextProvider
|
|
|
|
else {
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
toVC.view.layoutIfNeeded()
|
|
|
|
toContextProvider = contextProvider
|
|
|
|
|
|
|
|
case let navController as UINavigationController:
|
|
|
|
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
toVC.view.layoutIfNeeded()
|
|
|
|
toContextProvider = contextProvider
|
|
|
|
|
|
|
|
default:
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let fromMediaContext: MediaPresentationContext = fromContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView) else {
|
|
|
|
transitionContext.completeTransition(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let presentationImage: UIImage = mediaItem.image else {
|
|
|
|
transitionContext.completeTransition(true)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// fromView will be nil if doing a presentation, in which case we don't want to add the view -
|
|
|
|
// it will automatically be added to the view hierarchy, in front of the VC we're presenting from
|
|
|
|
if let fromView: UIView = transitionContext.view(forKey: .from) {
|
|
|
|
self.fromView = fromView
|
|
|
|
containerView.addSubview(fromView)
|
|
|
|
}
|
|
|
|
|
|
|
|
// toView will be nil if doing a modal dismiss, in which case we don't want to add the view -
|
|
|
|
// it's already in the view hierarchy, behind the VC we're dismissing.
|
|
|
|
if let toView: UIView = transitionContext.view(forKey: .to) {
|
|
|
|
containerView.insertSubview(toView, at: 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
let toMediaContext: MediaPresentationContext? = toContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView)
|
|
|
|
let duration: CGFloat = transitionDuration(using: transitionContext)
|
|
|
|
|
|
|
|
fromMediaContext.mediaView.alpha = 0
|
|
|
|
toMediaContext?.mediaView.alpha = 0
|
|
|
|
|
|
|
|
let transitionView = UIImageView(image: presentationImage)
|
|
|
|
transitionView.frame = fromMediaContext.presentationFrame
|
|
|
|
transitionView.contentMode = MediaView.contentMode
|
|
|
|
transitionView.layer.masksToBounds = true
|
|
|
|
transitionView.layer.cornerRadius = fromMediaContext.cornerRadius
|
|
|
|
transitionView.layer.maskedCorners = (toMediaContext?.cornerMask ?? fromMediaContext.cornerMask)
|
|
|
|
containerView.addSubview(transitionView)
|
|
|
|
|
|
|
|
// Add any UI elements which should appear above the media view
|
|
|
|
self.fromTransitionalOverlayView = {
|
|
|
|
guard let (overlayView, overlayViewFrame) = fromContextProvider.snapshotOverlayView(in: containerView) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
overlayView.frame = overlayViewFrame
|
|
|
|
containerView.addSubview(overlayView)
|
|
|
|
|
|
|
|
return overlayView
|
|
|
|
}()
|
|
|
|
self.toTransitionalOverlayView = { [weak self] in
|
|
|
|
guard let (overlayView, overlayViewFrame) = toContextProvider.snapshotOverlayView(in: containerView) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only fade in the 'toTransitionalOverlayView' if it's bigger than the origin
|
|
|
|
// one (makes it look cleaner as you don't get the crossfade effect)
|
|
|
|
if (self?.fromTransitionalOverlayView?.frame.size.height ?? 0) > overlayViewFrame.height {
|
|
|
|
overlayView.alpha = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
overlayView.frame = overlayViewFrame
|
|
|
|
|
|
|
|
if let fromTransitionalOverlayView = self?.fromTransitionalOverlayView {
|
|
|
|
containerView.insertSubview(overlayView, belowSubview: fromTransitionalOverlayView)
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
containerView.addSubview(overlayView)
|
|
|
|
}
|
|
|
|
|
|
|
|
return overlayView
|
|
|
|
}()
|
|
|
|
|
|
|
|
self.transitionView = transitionView
|
|
|
|
self.fromMediaFrame = transitionView.frame
|
|
|
|
|
|
|
|
self.pendingCompletion = {
|
|
|
|
let destinationFromAlpha: CGFloat
|
|
|
|
let destinationFrame: CGRect
|
|
|
|
let destinationCornerRadius: CGFloat
|
|
|
|
|
|
|
|
if transitionContext.transitionWasCancelled {
|
|
|
|
destinationFromAlpha = 1
|
|
|
|
destinationFrame = fromMediaContext.presentationFrame
|
|
|
|
destinationCornerRadius = fromMediaContext.cornerRadius
|
|
|
|
}
|
|
|
|
else if let toMediaContext: MediaPresentationContext = toMediaContext {
|
|
|
|
destinationFromAlpha = 0
|
|
|
|
destinationFrame = toMediaContext.presentationFrame
|
|
|
|
destinationCornerRadius = toMediaContext.cornerRadius
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// `toMediaContext` can be nil if the target item is scrolled off of the
|
|
|
|
// contextProvider's screen, so we synthesize a context to dismiss the item
|
|
|
|
// off screen
|
|
|
|
destinationFromAlpha = 0
|
|
|
|
destinationFrame = fromMediaContext.presentationFrame
|
|
|
|
.offsetBy(dx: 0, dy: (containerView.bounds.height * 2))
|
|
|
|
destinationCornerRadius = fromMediaContext.cornerRadius
|
|
|
|
}
|
|
|
|
|
|
|
|
UIView.animate(
|
|
|
|
withDuration: duration,
|
|
|
|
delay: 0,
|
|
|
|
options: [.beginFromCurrentState, .curveEaseInOut],
|
|
|
|
animations: { [weak self] in
|
|
|
|
self?.fromTransitionalOverlayView?.alpha = destinationFromAlpha
|
|
|
|
self?.fromView?.alpha = destinationFromAlpha
|
|
|
|
self?.toTransitionalOverlayView?.alpha = (1.0 - destinationFromAlpha)
|
|
|
|
transitionView.frame = destinationFrame
|
|
|
|
transitionView.layer.cornerRadius = destinationCornerRadius
|
|
|
|
},
|
|
|
|
completion: { [weak self] _ in
|
|
|
|
self?.fromView?.alpha = 1
|
|
|
|
fromMediaContext.mediaView.alpha = 1
|
|
|
|
toMediaContext?.mediaView.alpha = 1
|
|
|
|
transitionView.removeFromSuperview()
|
|
|
|
self?.fromTransitionalOverlayView?.removeFromSuperview()
|
|
|
|
self?.toTransitionalOverlayView?.removeFromSuperview()
|
|
|
|
|
|
|
|
if transitionContext.transitionWasCancelled {
|
|
|
|
// The "to" view will be nil if we're doing a modal dismiss, in which case
|
|
|
|
// we wouldn't want to remove the toView.
|
|
|
|
transitionContext.view(forKey: .to)?.removeFromSuperview()
|
|
|
|
|
|
|
|
// Note: We shouldn't need to do this but for some reason it's not
|
|
|
|
// automatically getting re-enabled so we manually enable it
|
|
|
|
transitionContext.view(forKey: .from)?.isUserInteractionEnabled = true
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
transitionContext.view(forKey: .from)?.removeFromSuperview()
|
|
|
|
|
|
|
|
// Note: We shouldn't need to do this but for some reason it's not
|
|
|
|
// automatically getting re-enabled so we manually enable it
|
|
|
|
transitionContext.view(forKey: .to)?.isUserInteractionEnabled = true
|
|
|
|
}
|
|
|
|
|
|
|
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// The interactive transition will call the 'pendingCompletion' when it completes so don't call it here
|
|
|
|
guard !transitionContext.isInteractive else { return }
|
|
|
|
|
|
|
|
self.pendingCompletion?()
|
|
|
|
self.pendingCompletion = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension MediaDismissAnimationController: InteractiveDismissDelegate {
|
|
|
|
func interactiveDismissUpdate(_ interactiveDismiss: UIPercentDrivenInteractiveTransition, didChangeTouchOffset offset: CGPoint) {
|
|
|
|
guard let transitionView: UIView = transitionView else { return } // Transition hasn't started yet
|
|
|
|
guard let fromMediaFrame: CGRect = fromMediaFrame else { return }
|
|
|
|
|
|
|
|
fromView?.alpha = (1.0 - interactiveDismiss.percentComplete)
|
|
|
|
transitionView.center = fromMediaFrame.offsetBy(dx: offset.x, dy: offset.y).center
|
|
|
|
}
|
|
|
|
|
|
|
|
func interactiveDismissDidFinish(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) {
|
|
|
|
self.pendingCompletion?()
|
|
|
|
self.pendingCompletion = nil
|
|
|
|
}
|
|
|
|
}
|