Improve gallery transitions when source/dest aspect ratio don't match

See #520
This commit is contained in:
Shadowfacts 2024-11-24 18:19:25 -05:00
parent 0dcb67c44e
commit f44dae632c
2 changed files with 119 additions and 14 deletions

View File

@ -65,14 +65,53 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
from.view.frame = container.bounds from.view.frame = container.bounds
container.addSubview(from.view) container.addSubview(from.view)
let contentContainer = UIView()
contentContainer.layer.masksToBounds = true
contentContainer.frame = destFrameInContainer
container.addSubview(contentContainer)
let content = itemViewController.takeContent() let content = itemViewController.takeContent()
content.view.translatesAutoresizingMaskIntoConstraints = true content.view.translatesAutoresizingMaskIntoConstraints = true
content.view.layer.masksToBounds = true content.view.layer.masksToBounds = true
container.addSubview(content.view) content.view.transform = .identity
content.view.frame = destFrameInContainer
content.view.layer.opacity = 1 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() container.layoutIfNeeded()
@ -80,7 +119,7 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
var initialVelocity: CGVector var initialVelocity: CGVector
if let interactiveVelocity, if let interactiveVelocity,
let interactiveTranslation, 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(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100,
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 { sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
@ -104,13 +143,17 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
if appliedSourceToDestTransform { if appliedSourceToDestTransform {
self.sourceView.transform = origSourceTransform self.sourceView.transform = origSourceTransform
} }
content.view.frame = sourceFrameInContainer
contentContainer.frame = sourceFrameInContainer
content.view.frame = sourceSizeWithDestAspectRatioCenteredInContentContainer
content.view.layer.opacity = 0 content.view.layer.opacity = 0
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false) itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
} }
animator.addCompletion { _ in animator.addCompletion { _ in
// Having dismissed, we don't need to undo any of the changes to the content VC.
transitionContext.completeTransition(true) transitionContext.completeTransition(true)
} }

View File

@ -56,21 +56,70 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
sourceToDestTransform = nil 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() let content = itemViewController.takeContent()
content.view.translatesAutoresizingMaskIntoConstraints = true 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. // Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content.
let dimmingView = UIView() let dimmingView = UIView()
dimmingView.backgroundColor = .black dimmingView.backgroundColor = .black
dimmingView.frame = container.bounds dimmingView.frame = container.bounds
dimmingView.layer.opacity = 0 dimmingView.layer.opacity = 0
container.insertSubview(dimmingView, belowSubview: content.view) container.insertSubview(dimmingView, belowSubview: contentContainer)
to.view.backgroundColor = nil to.view.backgroundColor = nil
to.view.layer.opacity = 0 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() container.layoutIfNeeded()
@ -78,8 +127,14 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false) itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
let duration = self.transitionDuration(using: transitionContext) let duration = self.transitionDuration(using: transitionContext)
// rougly equivalent to duration: 0.35, bounce: 0.3 // less bounce on bigger screens
let spring = UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero) 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) let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
animator.addAnimations { animator.addAnimations {
@ -87,9 +142,10 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
to.view.layer.opacity = 1 to.view.layer.opacity = 1
content.view.frame = destFrameInContainer contentContainer.frame = destFrameInContainer
content.view.frame = contentContainer.bounds
content.view.layer.opacity = 1 content.view.layer.opacity = 1
itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false) itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
if let sourceToDestTransform { if let sourceToDestTransform {
@ -98,6 +154,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
} }
animator.addCompletion { _ in animator.addCompletion { _ in
contentContainer.removeFromSuperview()
dimmingView.removeFromSuperview() dimmingView.removeFromSuperview()
to.view.backgroundColor = .black to.view.backgroundColor = .black
@ -106,6 +163,11 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
self.sourceView.transform = origSourceTransform 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() itemViewController.addContent()
transitionContext.completeTransition(true) transitionContext.completeTransition(true)