From f44dae632cd78b5f6b112a1405db090903ff2dda Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 24 Nov 2024 18:19:25 -0500 Subject: [PATCH] Improve gallery transitions when source/dest aspect ratio don't match See #520 --- .../GalleryDismissAnimationController.swift | 55 +++++++++++-- ...lleryPresentationAnimationController.swift | 78 +++++++++++++++++-- 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissAnimationController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissAnimationController.swift index 6fb3fe51..e09d3788 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissAnimationController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissAnimationController.swift @@ -65,14 +65,53 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans from.view.frame = container.bounds container.addSubview(from.view) - + + let contentContainer = UIView() + contentContainer.layer.masksToBounds = true + contentContainer.frame = destFrameInContainer + container.addSubview(contentContainer) + let content = itemViewController.takeContent() content.view.translatesAutoresizingMaskIntoConstraints = true content.view.layer.masksToBounds = true - container.addSubview(content.view) - - content.view.frame = destFrameInContainer + content.view.transform = .identity content.view.layer.opacity = 1 + content.view.frame = contentContainer.bounds + contentContainer.addSubview(content.view) + + let sourceAspectRatio: CGFloat = if sourceFrameInContainer.height > 0 { + sourceFrameInContainer.width / sourceFrameInContainer.height + } else { + 0 + } + let destAspectRatio: CGFloat = if destFrameInContainer.height > 0 { + destFrameInContainer.width / destFrameInContainer.height + } else { + 0 + } + let sourceSizeWithDestAspectRatioCenteredInContentContainer: CGRect + if 0.001 < abs(sourceAspectRatio - destAspectRatio) { + // asepct ratios are effectively equal + sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(origin: .zero, size: sourceFrameInContainer.size) + } else if sourceAspectRatio < destAspectRatio { + // source aspect ratio is narrow/taller than dest + let width = sourceFrameInContainer.height * destAspectRatio + sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect( + x: -(width - sourceFrameInContainer.width) / 2, + y: 0, + width: width, + height: sourceFrameInContainer.height + ) + } else { + // source aspect ratio is wider/shorter than dest + let height = sourceFrameInContainer.width / destAspectRatio + sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect( + x: 0, + y: -(height - sourceFrameInContainer.height) / 2, + width: sourceFrameInContainer.width, + height: height + ) + } container.layoutIfNeeded() @@ -80,7 +119,7 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans 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 + // very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the spring's 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 @@ -104,13 +143,17 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans if appliedSourceToDestTransform { self.sourceView.transform = origSourceTransform } - content.view.frame = sourceFrameInContainer + + contentContainer.frame = sourceFrameInContainer + content.view.frame = sourceSizeWithDestAspectRatioCenteredInContentContainer content.view.layer.opacity = 0 itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false) } animator.addCompletion { _ in + // Having dismissed, we don't need to undo any of the changes to the content VC. + transitionContext.completeTransition(true) } diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift index ebd26f72..94d7eb78 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift @@ -56,21 +56,70 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated sourceToDestTransform = nil } + // Grab these before taking the content out and changing the transform. + let origContentTransform = itemViewController.content.view.transform + let origContentFrame = itemViewController.content.view.frame + + // The content container provides the clipping for the content view, + // which, in case the source/dest aspect ratios don't match, makes + // it look like the content is expanding out from the source rect. + let contentContainer = UIView() + contentContainer.layer.masksToBounds = true + container.insertSubview(contentContainer, belowSubview: to.view) let content = itemViewController.takeContent() content.view.translatesAutoresizingMaskIntoConstraints = true - container.insertSubview(content.view, belowSubview: to.view) + content.view.transform = .identity + // The fade-in makes the aspect ratio handling look a little bit worse, + // but papers over the z-index change and potential corner radius change. + content.view.layer.opacity = 0 + contentContainer.addSubview(content.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) + container.insertSubview(dimmingView, belowSubview: contentContainer) to.view.backgroundColor = nil to.view.layer.opacity = 0 - content.view.frame = sourceFrameInContainer - content.view.layer.opacity = 0 + + contentContainer.frame = sourceFrameInContainer + + let sourceAspectRatio: CGFloat = if sourceFrameInContainer.height > 0 { + sourceFrameInContainer.width / sourceFrameInContainer.height + } else { + 0 + } + let destAspectRatio: CGFloat = if destFrameInContainer.height > 0 { + destFrameInContainer.width / destFrameInContainer.height + } else { + 0 + } + let sourceSizeWithDestAspectRatioCenteredInContentContainer: CGRect + if 0.001 < abs(sourceAspectRatio - destAspectRatio) { + // asepct ratios are effectively equal + sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(origin: .zero, size: sourceFrameInContainer.size) + } else if sourceAspectRatio < destAspectRatio { + // source aspect ratio is narrow/taller than dest + let width = sourceFrameInContainer.height * destAspectRatio + sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect( + x: -(width - sourceFrameInContainer.width) / 2, + y: 0, + width: width, + height: sourceFrameInContainer.height + ) + } else { + // source aspect ratio is wider/shorter than dest + let height = sourceFrameInContainer.width / destAspectRatio + sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect( + x: 0, + y: -(height - sourceFrameInContainer.height) / 2, + width: sourceFrameInContainer.width, + height: height + ) + } + content.view.frame = sourceSizeWithDestAspectRatioCenteredInContentContainer container.layoutIfNeeded() @@ -78,8 +127,14 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: 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) + // less bounce on bigger screens + let spring = if UIDevice.current.userInterfaceIdiom == .pad { + // roughly equivalent to duration: 0.35, bounce: 0.2 + UISpringTimingParameters(mass: 1, stiffness: 322, damping: 28, initialVelocity: .zero) + } else { + // roughly equivalent to duration: 0.35, bounce: 0.3 + UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero) + } let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring) animator.addAnimations { @@ -87,9 +142,10 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated to.view.layer.opacity = 1 - content.view.frame = destFrameInContainer + contentContainer.frame = destFrameInContainer + content.view.frame = contentContainer.bounds content.view.layer.opacity = 1 - + itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false) if let sourceToDestTransform { @@ -98,6 +154,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated } animator.addCompletion { _ in + contentContainer.removeFromSuperview() dimmingView.removeFromSuperview() to.view.backgroundColor = .black @@ -106,6 +163,11 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated self.sourceView.transform = origSourceTransform } + // Reset the properties we changed before re-adding the content to the scroll view. + // (I would expect UIScrollView to effectively do this itself, but w/e.) + content.view.transform = origContentTransform + content.view.frame = origContentFrame + itemViewController.addContent() transitionContext.completeTransition(true)