Zoom transition
This commit is contained in:
parent
9bb7ae0609
commit
8de227d780
|
@ -22,6 +22,10 @@
|
|||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */; };
|
||||
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */; };
|
||||
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */; };
|
||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5D2540DE3A00B1EBEF /* ZoomAnimator.swift */; };
|
||||
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5E2540DE3A00B1EBEF /* ZoomDismissalInteractionController.swift */; };
|
||||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */; };
|
||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */; };
|
||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||
D0A3C2F725390A9700739F88 /* AppPreferences+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A3C2F625390A9700739F88 /* AppPreferences+Extensions.swift */; };
|
||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||
|
@ -122,6 +126,10 @@
|
|||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePageViewController.swift; sourceTree = "<group>"; };
|
||||
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageNavigationController.swift; sourceTree = "<group>"; };
|
||||
D08B8D5D2540DE3A00B1EBEF /* ZoomAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomAnimator.swift; sourceTree = "<group>"; };
|
||||
D08B8D5E2540DE3A00B1EBEF /* ZoomDismissalInteractionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomDismissalInteractionController.swift; sourceTree = "<group>"; };
|
||||
D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomTransitionController.swift; sourceTree = "<group>"; };
|
||||
D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomAnimatableView.swift; sourceTree = "<group>"; };
|
||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||
D0A3C2F625390A9700739F88 /* AppPreferences+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPreferences+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||
|
@ -234,6 +242,7 @@
|
|||
D0C7D41D24F76169001EBDBB /* Supporting Files */,
|
||||
D0C7D45324F76169001EBDBB /* System */,
|
||||
D0666A2224C677B400F3F04B /* Tests */,
|
||||
D08B8D5C2540DDFC00B1EBEF /* Transitions */,
|
||||
D0C7D43024F76169001EBDBB /* View Controllers */,
|
||||
D0E2C1CF24FD8BA400854680 /* ViewModels */,
|
||||
D0C7D42024F76169001EBDBB /* Views */,
|
||||
|
@ -278,6 +287,17 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D08B8D5C2540DDFC00B1EBEF /* Transitions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */,
|
||||
D08B8D5D2540DE3A00B1EBEF /* ZoomAnimator.swift */,
|
||||
D08B8D5E2540DE3A00B1EBEF /* ZoomDismissalInteractionController.swift */,
|
||||
D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */,
|
||||
);
|
||||
path = Transitions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -569,12 +589,14 @@
|
|||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
||||
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */,
|
||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
||||
D0BEB1F324F8EE8C001B0F04 /* StatusAttachmentView.swift in Sources */,
|
||||
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
|
||||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
|
||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||
|
@ -586,6 +608,8 @@
|
|||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
|
||||
protocol ZoomAnimatableView {
|
||||
func transitionView() -> UIView
|
||||
func frame(inView view: UIView) -> CGRect
|
||||
}
|
||||
|
||||
extension UIImageView: ZoomAnimatableView {
|
||||
func transitionView() -> UIView {
|
||||
let transitionView = UIImageView(image: image)
|
||||
|
||||
transitionView.contentMode = .scaleAspectFill
|
||||
transitionView.clipsToBounds = true
|
||||
|
||||
return transitionView
|
||||
}
|
||||
|
||||
func frame(inView view: UIView) -> CGRect {
|
||||
guard let image = image else { return .zero }
|
||||
|
||||
return AVMakeRect(aspectRatio: image.size, insideRect: view.frame)
|
||||
}
|
||||
}
|
||||
|
||||
extension PlayerView: ZoomAnimatableView {
|
||||
func transitionView() -> UIView {
|
||||
let transitionView = PlayerView()
|
||||
|
||||
transitionView.videoGravity = .resizeAspectFill
|
||||
transitionView.player = player
|
||||
|
||||
return transitionView
|
||||
}
|
||||
|
||||
func frame(inView view: UIView) -> CGRect {
|
||||
guard let item = player?.currentItem else { return .zero }
|
||||
|
||||
return AVMakeRect(aspectRatio: item.presentationSize, insideRect: view.frame)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol ZoomAnimatorDelegate: class {
|
||||
func transitionWillStartWith(zoomAnimator: ZoomAnimator)
|
||||
func transitionDidEndWith(zoomAnimator: ZoomAnimator)
|
||||
func referenceView(for zoomAnimator: ZoomAnimator) -> UIView?
|
||||
func referenceViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect?
|
||||
}
|
||||
|
||||
class ZoomAnimator: NSObject {
|
||||
weak var fromDelegate: ZoomAnimatorDelegate?
|
||||
weak var toDelegate: ZoomAnimatorDelegate?
|
||||
|
||||
var transitionView: UIView?
|
||||
var isPresenting = true
|
||||
}
|
||||
|
||||
extension ZoomAnimator: UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
isPresenting ? .defaultAnimationDuration : .shortAnimationDuration
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
fromDelegate?.transitionWillStartWith(zoomAnimator: self)
|
||||
toDelegate?.transitionWillStartWith(zoomAnimator: self)
|
||||
|
||||
if isPresenting {
|
||||
animateZoomInTransition(using: transitionContext)
|
||||
} else {
|
||||
animateZoomOutTransition(using: transitionContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ZoomAnimator {
|
||||
private func animateZoomInTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
guard
|
||||
let toVC = transitionContext.viewController(forKey: .to),
|
||||
let fromVC = transitionContext.viewController(forKey: .from),
|
||||
let fromReferenceView = fromDelegate?.referenceView(for: self),
|
||||
let toReferenceView = toDelegate?.referenceView(for: self),
|
||||
let fromReferenceViewFrame = fromDelegate?.referenceViewFrameInTransitioningView(for: self)
|
||||
else { return }
|
||||
|
||||
toVC.view.alpha = 0
|
||||
toReferenceView.isHidden = true
|
||||
transitionContext.containerView.addSubview(toVC.view)
|
||||
|
||||
if transitionView == nil, let transitionView = (fromReferenceView as? ZoomAnimatableView)?.transitionView() {
|
||||
transitionView.frame = fromReferenceViewFrame
|
||||
self.transitionView = transitionView
|
||||
transitionContext.containerView.addSubview(transitionView)
|
||||
}
|
||||
|
||||
fromReferenceView.isHidden = true
|
||||
|
||||
let finalTransitionSize = (fromReferenceView as? ZoomAnimatableView)?.frame(inView: toVC.view) ?? .zero
|
||||
|
||||
UIView.animate(
|
||||
withDuration: transitionDuration(using: transitionContext),
|
||||
delay: 0,
|
||||
usingSpringWithDamping: 0.8,
|
||||
initialSpringVelocity: 0,
|
||||
options: [.transitionCrossDissolve]) {
|
||||
self.transitionView?.frame = finalTransitionSize
|
||||
toVC.view.alpha = 1.0
|
||||
fromVC.tabBarController?.tabBar.alpha = 0
|
||||
} completion: { _ in
|
||||
self.transitionView?.removeFromSuperview()
|
||||
toReferenceView.isHidden = false
|
||||
fromReferenceView.isHidden = false
|
||||
|
||||
self.transitionView = nil
|
||||
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
self.toDelegate?.transitionDidEndWith(zoomAnimator: self)
|
||||
self.fromDelegate?.transitionDidEndWith(zoomAnimator: self)
|
||||
}
|
||||
}
|
||||
|
||||
private func animateZoomOutTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
let containerView = transitionContext.containerView
|
||||
|
||||
guard
|
||||
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
|
||||
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
|
||||
let fromReferenceView = fromDelegate?.referenceView(for: self),
|
||||
let fromReferenceViewFrame = fromDelegate?.referenceViewFrameInTransitioningView(for: self)
|
||||
else { return }
|
||||
|
||||
let toReferenceView = toDelegate?.referenceView(for: self)
|
||||
let toReferenceViewFrame = toDelegate?.referenceViewFrameInTransitioningView(for: self)
|
||||
|
||||
toReferenceView?.isHidden = true
|
||||
|
||||
if transitionView == nil, let transitionView = (fromReferenceView as? ZoomAnimatableView)?.transitionView() {
|
||||
transitionView.frame = fromReferenceViewFrame
|
||||
self.transitionView = transitionView
|
||||
containerView.addSubview(transitionView)
|
||||
}
|
||||
|
||||
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
|
||||
fromReferenceView.isHidden = true
|
||||
|
||||
UIView.animate(
|
||||
withDuration: transitionDuration(using: transitionContext)) {
|
||||
fromVC.view.alpha = 0
|
||||
|
||||
if let toReferenceViewFrame = toReferenceViewFrame {
|
||||
self.transitionView?.frame = toReferenceViewFrame
|
||||
} else {
|
||||
self.transitionView?.alpha = 0
|
||||
}
|
||||
|
||||
toVC.tabBarController?.tabBar.alpha = 1
|
||||
} completion: { _ in
|
||||
self.transitionView?.removeFromSuperview()
|
||||
toReferenceView?.isHidden = false
|
||||
fromReferenceView.isHidden = false
|
||||
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
self.toDelegate?.transitionDidEndWith(zoomAnimator: self)
|
||||
self.fromDelegate?.transitionDidEndWith(zoomAnimator: self)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZoomDismissalInteractionController: NSObject {
|
||||
var transitionContext: UIViewControllerContextTransitioning?
|
||||
var animator: UIViewControllerAnimatedTransitioning?
|
||||
|
||||
var fromReferenceViewFrame: CGRect?
|
||||
var toReferenceViewFrame: CGRect?
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
func didPanWith(gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let transitionContext = self.transitionContext,
|
||||
let animator = self.animator as? ZoomAnimator,
|
||||
let transitionView = animator.transitionView,
|
||||
let fromVC = transitionContext.viewController(forKey: .from),
|
||||
let toVC = transitionContext.viewController(forKey: .to),
|
||||
let fromReferenceView = animator.fromDelegate?.referenceView(for: animator),
|
||||
let fromReferenceViewFrame = self.fromReferenceViewFrame
|
||||
else { return }
|
||||
|
||||
let toReferenceView = animator.toDelegate?.referenceView(for: animator)
|
||||
|
||||
fromReferenceView.isHidden = true
|
||||
|
||||
let anchorPoint = CGPoint(x: fromReferenceViewFrame.midX, y: fromReferenceViewFrame.midY)
|
||||
let dismissThreshold = fromReferenceViewFrame.height / 8
|
||||
let translatedPoint = gestureRecognizer.translation(in: fromReferenceView)
|
||||
|
||||
let backgroundAlpha = backgroundAlphaFor(view: fromVC.view, withPanningVerticalDelta: translatedPoint.y)
|
||||
let scale = scaleFor(view: fromVC.view, withPanningVerticalDelta: translatedPoint.y)
|
||||
|
||||
fromVC.view.alpha = backgroundAlpha
|
||||
|
||||
transitionView.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
let newCenter = CGPoint(
|
||||
x: anchorPoint.x + translatedPoint.x,
|
||||
y: anchorPoint.y + translatedPoint.y - transitionView.frame.height * (1 - scale) / 2.0)
|
||||
transitionView.center = newCenter
|
||||
|
||||
toReferenceView?.isHidden = true
|
||||
|
||||
transitionContext.updateInteractiveTransition(1 - scale)
|
||||
|
||||
toVC.tabBarController?.tabBar.alpha = 1 - backgroundAlpha
|
||||
|
||||
if gestureRecognizer.state == .ended {
|
||||
if abs(anchorPoint.y - newCenter.y) < dismissThreshold {
|
||||
// cancel
|
||||
UIView.animate(
|
||||
withDuration: 0.5,
|
||||
delay: 0,
|
||||
usingSpringWithDamping: 0.9,
|
||||
initialSpringVelocity: 0,
|
||||
options: []) {
|
||||
transitionView.frame = fromReferenceViewFrame
|
||||
fromVC.view.alpha = 1.0
|
||||
toVC.tabBarController?.tabBar.alpha = 0
|
||||
} completion: { _ in
|
||||
toReferenceView?.isHidden = false
|
||||
fromReferenceView.isHidden = false
|
||||
transitionView.removeFromSuperview()
|
||||
animator.transitionView = nil
|
||||
transitionContext.cancelInteractiveTransition()
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
animator.toDelegate?.transitionDidEndWith(zoomAnimator: animator)
|
||||
animator.fromDelegate?.transitionDidEndWith(zoomAnimator: animator)
|
||||
self.transitionContext = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// start animation
|
||||
UIView.animate(
|
||||
withDuration: .shortAnimationDuration) {
|
||||
fromVC.view.alpha = 0
|
||||
|
||||
if let toReferenceViewFrame = self.toReferenceViewFrame {
|
||||
transitionView.frame = toReferenceViewFrame
|
||||
} else {
|
||||
transitionView.alpha = 0
|
||||
}
|
||||
|
||||
toVC.tabBarController?.tabBar.alpha = 1
|
||||
} completion: { _ in
|
||||
transitionView.removeFromSuperview()
|
||||
toReferenceView?.isHidden = false
|
||||
fromReferenceView.isHidden = false
|
||||
|
||||
self.transitionContext?.finishInteractiveTransition()
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
animator.toDelegate?.transitionDidEndWith(zoomAnimator: animator)
|
||||
animator.fromDelegate?.transitionDidEndWith(zoomAnimator: animator)
|
||||
self.transitionContext = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func backgroundAlphaFor(view: UIView, withPanningVerticalDelta verticalDelta: CGFloat) -> CGFloat {
|
||||
let startingAlpha: CGFloat = 1.0
|
||||
let finalAlpha: CGFloat = 0.0
|
||||
let totalAvailableAlpha = startingAlpha - finalAlpha
|
||||
|
||||
let maximumDelta = view.bounds.height / 4.0
|
||||
let deltaAsPercentageOfMaximun = min(abs(verticalDelta) / maximumDelta, 1.0)
|
||||
|
||||
return startingAlpha - (deltaAsPercentageOfMaximun * totalAvailableAlpha)
|
||||
}
|
||||
|
||||
func scaleFor(view: UIView, withPanningVerticalDelta verticalDelta: CGFloat) -> CGFloat {
|
||||
let startingScale: CGFloat = 1.0
|
||||
let finalScale: CGFloat = 0.5
|
||||
let totalAvailableScale = startingScale - finalScale
|
||||
|
||||
let maximumDelta = view.bounds.height / 2.0
|
||||
let deltaAsPercentageOfMaximun = min(abs(verticalDelta) / maximumDelta, 1.0)
|
||||
|
||||
return startingScale - (deltaAsPercentageOfMaximun * totalAvailableScale)
|
||||
}
|
||||
}
|
||||
|
||||
extension ZoomDismissalInteractionController: UIViewControllerInteractiveTransitioning {
|
||||
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
|
||||
guard let animator = animator as? ZoomAnimator else { return }
|
||||
|
||||
animator.fromDelegate?.transitionWillStartWith(zoomAnimator: animator)
|
||||
animator.toDelegate?.transitionWillStartWith(zoomAnimator: animator)
|
||||
|
||||
self.transitionContext = transitionContext
|
||||
|
||||
let containerView = transitionContext.containerView
|
||||
|
||||
guard
|
||||
let fromVC = transitionContext.viewController(forKey: .from),
|
||||
let toVC = transitionContext.viewController(forKey: .to),
|
||||
let fromReferenceViewFrame = animator.fromDelegate?.referenceViewFrameInTransitioningView(for: animator),
|
||||
let fromReferenceView = animator.fromDelegate?.referenceView(for: animator)
|
||||
else { return }
|
||||
|
||||
self.fromReferenceViewFrame = fromReferenceViewFrame
|
||||
toReferenceViewFrame = animator.toDelegate?.referenceViewFrameInTransitioningView(for: animator)
|
||||
|
||||
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
|
||||
|
||||
if animator.transitionView == nil,
|
||||
let transitionView = (fromReferenceView as? ZoomAnimatableView)?.transitionView() {
|
||||
transitionView.frame = fromReferenceViewFrame
|
||||
animator.transitionView = transitionView
|
||||
containerView.addSubview(transitionView)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZoomTransitionController: NSObject {
|
||||
var isInteractive = false
|
||||
|
||||
weak var fromDelegate: ZoomAnimatorDelegate?
|
||||
weak var toDelegate: ZoomAnimatorDelegate?
|
||||
|
||||
private let animator = ZoomAnimator()
|
||||
private let interactionController = ZoomDismissalInteractionController()
|
||||
|
||||
func didPanWith(gestureRecognizer: UIPanGestureRecognizer) {
|
||||
interactionController.didPanWith(gestureRecognizer: gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
extension ZoomTransitionController: UIViewControllerTransitioningDelegate {
|
||||
func animationController(
|
||||
forPresented presented: UIViewController,
|
||||
presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
presentingAnimator()
|
||||
}
|
||||
|
||||
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
dismissingAnimator()
|
||||
}
|
||||
|
||||
func interactionControllerForDismissal(
|
||||
using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
||||
interactionController(animator: animator)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ZoomTransitionController: UINavigationControllerDelegate {
|
||||
func navigationController(_ navigationController: UINavigationController,
|
||||
animationControllerFor operation: UINavigationController.Operation,
|
||||
from fromVC: UIViewController,
|
||||
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
operation == .push ? presentingAnimator() : dismissingAnimator()
|
||||
}
|
||||
|
||||
func navigationController(
|
||||
_ navigationController: UINavigationController,
|
||||
interactionControllerFor animationController: UIViewControllerAnimatedTransitioning)
|
||||
-> UIViewControllerInteractiveTransitioning? {
|
||||
interactionController(animator: animator)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ZoomTransitionController {
|
||||
private func presentingAnimator() -> UIViewControllerAnimatedTransitioning {
|
||||
animator.isPresenting = true
|
||||
animator.fromDelegate = fromDelegate
|
||||
animator.toDelegate = toDelegate
|
||||
|
||||
return animator
|
||||
}
|
||||
|
||||
private func dismissingAnimator() -> UIViewControllerAnimatedTransitioning {
|
||||
animator.isPresenting = false
|
||||
let tmp = fromDelegate
|
||||
animator.fromDelegate = toDelegate
|
||||
animator.toDelegate = tmp
|
||||
|
||||
return animator
|
||||
}
|
||||
|
||||
private func interactionController(
|
||||
animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
||||
guard isInteractive else { return nil }
|
||||
|
||||
interactionController.animator = animator
|
||||
|
||||
return interactionController
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
|
||||
class ImageNavigationController: UINavigationController {
|
||||
let transitionController = ZoomTransitionController()
|
||||
|
||||
private let imagePageViewController: ImagePageViewController
|
||||
|
||||
init(imagePageViewController: ImagePageViewController) {
|
||||
|
@ -21,5 +24,91 @@ class ImageNavigationController: UINavigationController {
|
|||
|
||||
hidesBarsOnTap = true
|
||||
modalPresentationStyle = .fullScreen
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(
|
||||
target: self,
|
||||
action: #selector(didPanWith(gestureRecognizer:)))
|
||||
|
||||
panGestureRecognizer.delegate = self
|
||||
view.addGestureRecognizer(panGestureRecognizer)
|
||||
|
||||
transitioningDelegate = transitionController
|
||||
transitionController.toDelegate = self
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageNavigationController {
|
||||
var currentViewController: ImageViewController? {
|
||||
imagePageViewController.viewControllers?.first as? ImageViewController
|
||||
}
|
||||
|
||||
@objc func didPanWith(gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let currentViewController = currentViewController else { return }
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
currentViewController.scrollView.isScrollEnabled = false
|
||||
transitionController.isInteractive = true
|
||||
|
||||
presentingViewController?.dismiss(animated: true)
|
||||
case .ended:
|
||||
if transitionController.isInteractive {
|
||||
currentViewController.scrollView.isScrollEnabled = true
|
||||
transitionController.isInteractive = false
|
||||
transitionController.didPanWith(gestureRecognizer: gestureRecognizer)
|
||||
}
|
||||
default:
|
||||
if transitionController.isInteractive {
|
||||
transitionController.didPanWith(gestureRecognizer: gestureRecognizer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageNavigationController: ZoomAnimatorDelegate {
|
||||
func transitionWillStartWith(zoomAnimator: ZoomAnimator) {
|
||||
|
||||
}
|
||||
|
||||
func transitionDidEndWith(zoomAnimator: ZoomAnimator) {
|
||||
|
||||
}
|
||||
|
||||
func referenceView(for zoomAnimator: ZoomAnimator) -> UIView? {
|
||||
if currentViewController?.playerView.player != nil {
|
||||
return currentViewController?.playerView
|
||||
} else {
|
||||
return currentViewController?.imageView
|
||||
}
|
||||
}
|
||||
|
||||
func referenceViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? {
|
||||
guard let currentViewController = currentViewController else { return .zero }
|
||||
|
||||
let rect: CGRect
|
||||
|
||||
if let image = currentViewController.imageView.image {
|
||||
rect = AVMakeRect(aspectRatio: image.size, insideRect: currentViewController.imageView.frame)
|
||||
} else if let item = currentViewController.playerView.player?.currentItem {
|
||||
rect = AVMakeRect(aspectRatio: item.presentationSize, insideRect: currentViewController.playerView.frame)
|
||||
} else {
|
||||
return .zero
|
||||
}
|
||||
|
||||
return currentViewController.scrollView.convert(rect, to: currentViewController.view)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageNavigationController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if
|
||||
let currentViewController = currentViewController,
|
||||
otherGestureRecognizer == currentViewController.scrollView.panGestureRecognizer,
|
||||
currentViewController.scrollView.contentOffset.y == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import UIKit
|
|||
import ViewModels
|
||||
|
||||
class ImageViewController: UIViewController {
|
||||
let scrollView = UIScrollView()
|
||||
let imageView = AnimatedImageView()
|
||||
let playerView = PlayerView()
|
||||
|
||||
private let viewModel: AttachmentViewModel
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentView = UIView()
|
||||
private let imageView = AnimatedImageView()
|
||||
private let playerView = PlayerView()
|
||||
private let descriptionBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||
private let descriptionTextView = UITextView()
|
||||
|
||||
|
@ -93,6 +94,7 @@ class ImageViewController: UIViewController {
|
|||
|
||||
switch viewModel.attachment.type {
|
||||
case .image:
|
||||
imageView.tag = viewModel.tag
|
||||
playerView.isHidden = true
|
||||
imageView.isHidden = false
|
||||
imageView.kf.indicatorType = .activity
|
||||
|
@ -111,6 +113,7 @@ class ImageViewController: UIViewController {
|
|||
options: [.keepCurrentImageWhileLoading])
|
||||
})
|
||||
case .gifv:
|
||||
playerView.tag = viewModel.tag
|
||||
playerView.isHidden = false
|
||||
imageView.isHidden = true
|
||||
let player = PlayerCache.shared.player(url: viewModel.attachment.url)
|
||||
|
|
|
@ -13,6 +13,7 @@ class TableViewController: UITableViewController {
|
|||
private let webfingerIndicatorView = WebfingerIndicatorView()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
|
||||
private var transitionViewTag = -1
|
||||
|
||||
private lazy var dataSource: TableViewDataSource = {
|
||||
.init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:))
|
||||
|
@ -181,10 +182,36 @@ extension TableViewController: AVPlayerViewControllerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
private extension TableViewController {
|
||||
static let autoplayViews = [PlayerView](repeating: .init(), count: 4)
|
||||
static var visibleVideoURLs = Set<URL>()
|
||||
extension TableViewController: ZoomAnimatorDelegate {
|
||||
func transitionWillStartWith(zoomAnimator: ZoomAnimator) {
|
||||
view.layoutIfNeeded()
|
||||
|
||||
guard let imageViewController = (presentedViewController as? ImageNavigationController)?.currentViewController
|
||||
else { return }
|
||||
|
||||
if imageViewController.playerView.tag != 0 {
|
||||
transitionViewTag = imageViewController.playerView.tag
|
||||
} else if imageViewController.imageView.tag != 0 {
|
||||
transitionViewTag = imageViewController.imageView.tag
|
||||
}
|
||||
}
|
||||
|
||||
func transitionDidEndWith(zoomAnimator: ZoomAnimator) {
|
||||
|
||||
}
|
||||
|
||||
func referenceView(for zoomAnimator: ZoomAnimator) -> UIView? {
|
||||
tableView.visibleCells.compactMap { $0.viewWithTag(transitionViewTag) }.first
|
||||
}
|
||||
|
||||
func referenceViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? {
|
||||
guard let referenceView = referenceView(for: zoomAnimator) else { return nil }
|
||||
|
||||
return tabBarController?.view.convert(referenceView.frame, from: referenceView.superview)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TableViewController {
|
||||
var visibleLoadMoreViews: [LoadMoreView] {
|
||||
tableView.visibleCells.compactMap { $0.contentView as? LoadMoreView }
|
||||
}
|
||||
|
@ -315,6 +342,9 @@ private extension TableViewController {
|
|||
statusViewModel: statusViewModel)
|
||||
let imageNavigationController = ImageNavigationController(imagePageViewController: imagePageViewController)
|
||||
|
||||
imageNavigationController.transitionController.fromDelegate = self
|
||||
transitionViewTag = attachmentViewModel.tag
|
||||
|
||||
present(imageNavigationController, animated: true)
|
||||
case .unknown:
|
||||
break
|
||||
|
|
|
@ -6,12 +6,19 @@ import Mastodon
|
|||
public struct AttachmentViewModel {
|
||||
public let attachment: Attachment
|
||||
|
||||
init(attachment: Attachment) {
|
||||
private let status: Status
|
||||
|
||||
init(attachment: Attachment, status: Status) {
|
||||
self.attachment = attachment
|
||||
self.status = status
|
||||
}
|
||||
}
|
||||
|
||||
public extension AttachmentViewModel {
|
||||
var tag: Int {
|
||||
attachment.id.appending(status.id).hashValue
|
||||
}
|
||||
|
||||
var aspectRatio: Double? {
|
||||
if
|
||||
let info = attachment.meta?.original,
|
||||
|
|
|
@ -40,7 +40,7 @@ public struct StatusViewModel: CollectionItemViewModel {
|
|||
: statusService.status.account.displayName
|
||||
rebloggedByDisplayNameEmoji = statusService.status.account.emojis
|
||||
attachmentViewModels = statusService.status.displayStatus.mediaAttachments
|
||||
.map(AttachmentViewModel.init(attachment:))
|
||||
.map { AttachmentViewModel(attachment: $0, status: statusService.status) }
|
||||
pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? []
|
||||
pollEmoji = statusService.status.displayStatus.poll?.emojis ?? []
|
||||
events = eventsSubject.eraseToAnyPublisher()
|
||||
|
|
|
@ -14,8 +14,12 @@ final class StatusAttachmentView: UIView {
|
|||
didSet {
|
||||
if playing {
|
||||
play()
|
||||
imageView.tag = 0
|
||||
playerView.tag = viewModel.tag
|
||||
} else {
|
||||
stop()
|
||||
imageView.tag = viewModel.tag
|
||||
playerView.tag = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +83,7 @@ private extension StatusAttachmentView {
|
|||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
imageView.tag = viewModel.tag
|
||||
|
||||
let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||
let playView = UIVisualEffectView(effect: blurEffect)
|
||||
|
|
Loading…
Reference in New Issue