Improve gallery transitions when source/dest aspect ratio don't match
See #520
This commit is contained in:
parent
0dcb67c44e
commit
f44dae632c
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue