feat: update preview present/dismiss transition style to pin-to-source rect
This commit is contained in:
parent
7d8ffd187a
commit
6f0b4354a7
|
@ -533,14 +533,51 @@ extension StatusProviderFacade {
|
|||
provider.status(for: cell, indexPath: nil)
|
||||
.sink { [weak provider] status in
|
||||
guard let provider = provider else { return }
|
||||
guard let status = status?.reblog ?? status else { return }
|
||||
guard let source = status else { return }
|
||||
|
||||
let status = source.reblog ?? source
|
||||
|
||||
let meta = MediaPreviewViewModel.StatusImagePreviewMeta(
|
||||
statusObjectID: status.objectID,
|
||||
initialIndex: index,
|
||||
preloadThumbnailImages: mosaicImageView.imageViews.map { $0.image }
|
||||
)
|
||||
let mediaPreviewViewModel = MediaPreviewViewModel(context: provider.context, meta: meta)
|
||||
let pushTransitionItem = MediaPreviewTransitionItem(
|
||||
source: .mosaic(mosaicImageView),
|
||||
previewableViewController: provider
|
||||
)
|
||||
pushTransitionItem.aspectRatio = {
|
||||
if let image = imageView.image {
|
||||
return image.size
|
||||
}
|
||||
guard let media = status.mediaAttachments?.sorted(by: { $0.index.compare($1.index) == .orderedAscending }) else { return nil }
|
||||
guard index < media.count else { return nil }
|
||||
let meta = media[index].meta
|
||||
guard let width = meta?.original?.width, let height = meta?.original?.height else { return nil }
|
||||
return CGSize(width: width, height: height)
|
||||
}()
|
||||
pushTransitionItem.sourceImageView = imageView
|
||||
pushTransitionItem.initialFrame = {
|
||||
let initialFrame = imageView.superview!.convert(imageView.frame, to: nil)
|
||||
assert(initialFrame != .zero)
|
||||
return initialFrame
|
||||
}()
|
||||
pushTransitionItem.image = {
|
||||
if let image = imageView.image {
|
||||
return image
|
||||
}
|
||||
if index < mosaicImageView.blurhashOverlayImageViews.count {
|
||||
return mosaicImageView.blurhashOverlayImageViews[index].image
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
let mediaPreviewViewModel = MediaPreviewViewModel(
|
||||
context: provider.context,
|
||||
meta: meta,
|
||||
pushTransitionItem: pushTransitionItem
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
provider.coordinator.present(scene: .mediaPreview(viewModel: mediaPreviewViewModel), from: provider, transition: .custom(transitioningDelegate: provider.mediaPreviewTransitionController))
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import Pageboy
|
|||
|
||||
final class MediaPreviewViewController: UIViewController, NeedsDependency {
|
||||
|
||||
static let closeButtonSize = CGSize(width: 30, height: 30)
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
@ -20,6 +22,23 @@ final class MediaPreviewViewController: UIViewController, NeedsDependency {
|
|||
|
||||
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||
let pagingViewConttroller = MediaPreviewPagingViewController()
|
||||
|
||||
let closeButtonBackground: UIVisualEffectView = {
|
||||
let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial))
|
||||
backgroundView.alpha = 0.9
|
||||
backgroundView.layer.masksToBounds = true
|
||||
backgroundView.layer.cornerRadius = MediaPreviewViewController.closeButtonSize.width * 0.5
|
||||
return backgroundView
|
||||
}()
|
||||
|
||||
let closeButtonBackgroundVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial)))
|
||||
|
||||
let closeButton: UIButton = {
|
||||
let button = HitTestExpandedButton()
|
||||
button.imageView?.tintColor = .label
|
||||
button.setImage(UIImage(systemName: "xmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .bold))!, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
@ -48,11 +67,57 @@ extension MediaPreviewViewController {
|
|||
])
|
||||
pagingViewConttroller.didMove(toParent: self)
|
||||
|
||||
closeButtonBackground.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(closeButtonBackground)
|
||||
NSLayoutConstraint.activate([
|
||||
closeButtonBackground.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 12),
|
||||
closeButtonBackground.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor)
|
||||
])
|
||||
closeButtonBackgroundVisualEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
closeButtonBackground.contentView.addSubview(closeButtonBackgroundVisualEffectView)
|
||||
|
||||
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
closeButtonBackgroundVisualEffectView.contentView.addSubview(closeButton)
|
||||
NSLayoutConstraint.activate([
|
||||
closeButton.topAnchor.constraint(equalTo: closeButtonBackgroundVisualEffectView.topAnchor),
|
||||
closeButton.leadingAnchor.constraint(equalTo: closeButtonBackgroundVisualEffectView.leadingAnchor),
|
||||
closeButtonBackgroundVisualEffectView.trailingAnchor.constraint(equalTo: closeButton.trailingAnchor),
|
||||
closeButtonBackgroundVisualEffectView.bottomAnchor.constraint(equalTo: closeButton.bottomAnchor),
|
||||
closeButton.heightAnchor.constraint(equalToConstant: MediaPreviewViewController.closeButtonSize.height).priority(.defaultHigh),
|
||||
closeButton.widthAnchor.constraint(equalToConstant: MediaPreviewViewController.closeButtonSize.width).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
viewModel.mediaPreviewImageViewControllerDelegate = self
|
||||
|
||||
pagingViewConttroller.interPageSpacing = 10
|
||||
pagingViewConttroller.delegate = self
|
||||
pagingViewConttroller.dataSource = viewModel
|
||||
|
||||
closeButton.addTarget(self, action: #selector(MediaPreviewViewController.closeButtonPressed(_:)), for: .touchUpInside)
|
||||
|
||||
// bind view model
|
||||
viewModel.currentPage
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] index in
|
||||
guard let self = self else { return }
|
||||
switch self.viewModel.pushTransitionItem.source {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
mosaicImageViewContainer.setImageViews(alpha: 1)
|
||||
mosaicImageViewContainer.setImageView(alpha: 0, index: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MediaPreviewViewController {
|
||||
|
||||
@objc private func closeButtonPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -61,20 +126,19 @@ extension MediaPreviewViewController {
|
|||
extension MediaPreviewViewController: MediaPreviewingViewController {
|
||||
|
||||
func isInteractiveDismissable() -> Bool {
|
||||
return true
|
||||
// if let mediaPreviewImageViewController = pagingViewConttroller.currentViewController as? MediaPreviewImageViewController {
|
||||
// let previewImageView = mediaPreviewImageViewController.previewImageView
|
||||
// // TODO: allow zooming pan dismiss
|
||||
// guard previewImageView.zoomScale == previewImageView.minimumZoomScale else {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// let safeAreaInsets = previewImageView.safeAreaInsets
|
||||
// let statusBarFrameHeight = view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||
// return previewImageView.contentOffset.y <= -(safeAreaInsets.top - statusBarFrameHeight)
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
if let mediaPreviewImageViewController = pagingViewConttroller.currentViewController as? MediaPreviewImageViewController {
|
||||
let previewImageView = mediaPreviewImageViewController.previewImageView
|
||||
// TODO: allow zooming pan dismiss
|
||||
guard previewImageView.zoomScale == previewImageView.minimumZoomScale else {
|
||||
return false
|
||||
}
|
||||
|
||||
let safeAreaInsets = previewImageView.safeAreaInsets
|
||||
let statusBarFrameHeight = view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||
return previewImageView.contentOffset.y <= -(safeAreaInsets.top - statusBarFrameHeight)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -107,6 +171,7 @@ extension MediaPreviewViewController: PageboyViewControllerDelegate {
|
|||
) {
|
||||
// update page control
|
||||
// pageControl.currentPage = index
|
||||
viewModel.currentPage.value = index
|
||||
}
|
||||
|
||||
func pageboyViewController(
|
||||
|
|
|
@ -17,11 +17,13 @@ final class MediaPreviewViewModel: NSObject {
|
|||
let context: AppContext
|
||||
let initialItem: PreviewItem
|
||||
weak var mediaPreviewImageViewControllerDelegate: MediaPreviewImageViewControllerDelegate?
|
||||
|
||||
let currentPage: CurrentValueSubject<Int, Never>
|
||||
|
||||
// output
|
||||
let pushTransitionItem: MediaPreviewTransitionItem
|
||||
let viewControllers: [UIViewController]
|
||||
|
||||
init(context: AppContext, meta: StatusImagePreviewMeta) {
|
||||
init(context: AppContext, meta: StatusImagePreviewMeta, pushTransitionItem: MediaPreviewTransitionItem) {
|
||||
self.context = context
|
||||
self.initialItem = .status(meta)
|
||||
var viewControllers: [UIViewController] = []
|
||||
|
@ -45,6 +47,8 @@ final class MediaPreviewViewModel: NSObject {
|
|||
}
|
||||
}
|
||||
self.viewControllers = viewControllers
|
||||
self.currentPage = CurrentValueSubject(meta.initialIndex)
|
||||
self.pushTransitionItem = pushTransitionItem
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ extension MediaPreviewImageView {
|
|||
isUserInteractionEnabled = true
|
||||
showsVerticalScrollIndicator = false
|
||||
showsHorizontalScrollIndicator = false
|
||||
|
||||
|
||||
bouncesZoom = true
|
||||
minimumZoomScale = 1.0
|
||||
maximumZoomScale = 4.0
|
||||
|
@ -139,6 +139,9 @@ extension MediaPreviewImageView: UIScrollViewDelegate {
|
|||
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
centerScrollViewContents()
|
||||
|
||||
// set bounce when zoom in
|
||||
alwaysBounceVertical = zoomScale > minimumZoomScale
|
||||
}
|
||||
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
|
|
|
@ -296,6 +296,25 @@ extension MosaicImageViewContainer {
|
|||
|
||||
}
|
||||
|
||||
// FIXME: set imageView source from blurhash and image
|
||||
extension MosaicImageViewContainer {
|
||||
|
||||
func setImageViews(alpha: CGFloat) {
|
||||
// blurhashOverlayImageViews.forEach { $0.alpha = alpha }
|
||||
imageViews.forEach { $0.alpha = alpha }
|
||||
}
|
||||
|
||||
func setImageView(alpha: CGFloat, index: Int) {
|
||||
// if index < blurhashOverlayImageViews.count {
|
||||
// blurhashOverlayImageViews[index].alpha = alpha
|
||||
// }
|
||||
if index < imageViews.count {
|
||||
imageViews[index].alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MosaicImageViewContainer {
|
||||
|
||||
@objc private func visualEffectViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||
|
|
|
@ -48,29 +48,35 @@ struct MosaicMeta {
|
|||
|
||||
func blurhashImagePublisher() -> AnyPublisher<UIImage?, Never> {
|
||||
return Future { promise in
|
||||
guard let blurhash = blurhash else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
|
||||
let imageSize: CGSize = {
|
||||
let aspectRadio = size.width / size.height
|
||||
if size.width > size.height {
|
||||
let width: CGFloat = MosaicMeta.edgeMaxLength
|
||||
let height = width / aspectRadio
|
||||
return CGSize(width: width, height: height)
|
||||
} else {
|
||||
let height: CGFloat = MosaicMeta.edgeMaxLength
|
||||
let width = height * aspectRadio
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
}()
|
||||
|
||||
workingQueue.async {
|
||||
let image = UIImage(blurHash: blurhash, size: imageSize)
|
||||
let image = self.blurhashImage()
|
||||
promise(.success(image))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func blurhashImage() -> UIImage? {
|
||||
guard let blurhash = blurhash else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let imageSize: CGSize = {
|
||||
let aspectRadio = size.width / size.height
|
||||
if size.width > size.height {
|
||||
let width: CGFloat = MosaicMeta.edgeMaxLength
|
||||
let height = width / aspectRadio
|
||||
return CGSize(width: width, height: height)
|
||||
} else {
|
||||
let height: CGFloat = MosaicMeta.edgeMaxLength
|
||||
let width = height * aspectRadio
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
}()
|
||||
|
||||
let image = UIImage(blurHash: blurhash, size: imageSize)
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import os.log
|
||||
import UIKit
|
||||
import func AVFoundation.AVMakeRect
|
||||
|
||||
final class MediaHostToMediaPreviewViewControllerAnimatedTransitioning: ViewControllerAnimatedTransitioning {
|
||||
|
||||
|
||||
let transitionItem: MediaPreviewTransitionItem
|
||||
let panGestureRecognizer: UIPanGestureRecognizer
|
||||
|
||||
|
@ -32,7 +32,6 @@ final class MediaHostToMediaPreviewViewControllerAnimatedTransitioning: ViewCont
|
|||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UIViewControllerAnimatedTransitioning
|
||||
extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
||||
|
||||
|
@ -51,19 +50,48 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
let toView = transitionContext.view(forKey: .to) else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
||||
let toViewEndFrame = transitionContext.finalFrame(for: toVC)
|
||||
toView.frame = toViewEndFrame
|
||||
toView.alpha = 0
|
||||
transitionContext.containerView.addSubview(toView)
|
||||
|
||||
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: curve)
|
||||
// set to image hidden
|
||||
toVC.pagingViewConttroller.view.alpha = 0
|
||||
// set from image hidden. update hidden when paging. seealso: `MediaPreviewViewController`
|
||||
switch transitionItem.source {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
mosaicImageViewContainer.setImageView(alpha: 0, index: toVC.viewModel.currentPage.value)
|
||||
}
|
||||
|
||||
// Set transition image view
|
||||
assert(transitionItem.initialFrame != nil)
|
||||
let initialFrame = transitionItem.initialFrame ?? toViewEndFrame
|
||||
let transitionTargetFrame: CGRect = {
|
||||
let aspectRatio = transitionItem.aspectRatio ?? CGSize(width: initialFrame.width, height: initialFrame.height)
|
||||
return AVMakeRect(aspectRatio: aspectRatio, insideRect: toView.bounds)
|
||||
}()
|
||||
let transitionImageView: UIImageView = {
|
||||
let imageView = UIImageView(frame: transitionContext.containerView.convert(initialFrame, from: nil))
|
||||
imageView.clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.isUserInteractionEnabled = false
|
||||
imageView.image = transitionItem.image
|
||||
return imageView
|
||||
}()
|
||||
transitionItem.targetFrame = transitionTargetFrame
|
||||
transitionItem.imageView = transitionImageView
|
||||
transitionContext.containerView.addSubview(transitionImageView)
|
||||
|
||||
let animator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: .zero)
|
||||
|
||||
animator.addAnimations {
|
||||
transitionImageView.frame = transitionTargetFrame
|
||||
toView.alpha = 1
|
||||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
toVC.pagingViewConttroller.view.alpha = 1
|
||||
transitionImageView.removeFromSuperview()
|
||||
transitionContext.completeTransition(position == .end)
|
||||
}
|
||||
|
||||
|
@ -72,17 +100,59 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
|
||||
private func popTransition(using transitionContext: UIViewControllerContextTransitioning, curve: UIView.AnimationCurve = .easeInOut) -> UIViewPropertyAnimator {
|
||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController,
|
||||
let fromView = transitionContext.view(forKey: .from) else {
|
||||
let fromView = transitionContext.view(forKey: .from),
|
||||
let mediaPreviewImageViewController = fromVC.pagingViewConttroller.currentViewController as? MediaPreviewImageViewController,
|
||||
let index = fromVC.pagingViewConttroller.currentIndex else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
// assert view hierarchy not change
|
||||
let toVC = transitionItem.previewableViewController
|
||||
let targetFrame = toVC.sourceFrame(transitionItem: transitionItem, index: index)
|
||||
|
||||
let imageView = mediaPreviewImageViewController.previewImageView.imageView
|
||||
let _snapshot: UIView? = {
|
||||
transitionItem.snapshotRaw = imageView
|
||||
let snapshot = imageView.snapshotView(afterScreenUpdates: false)
|
||||
snapshot?.clipsToBounds = true
|
||||
snapshot?.contentMode = .scaleAspectFill
|
||||
return snapshot
|
||||
}()
|
||||
guard let snapshot = _snapshot else {
|
||||
transitionContext.completeTransition(false)
|
||||
fatalError()
|
||||
}
|
||||
mediaPreviewImageViewController.view.insertSubview(snapshot, aboveSubview: mediaPreviewImageViewController.previewImageView)
|
||||
|
||||
snapshot.center = transitionContext.containerView.center
|
||||
|
||||
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: curve)
|
||||
transitionItem.imageView = imageView
|
||||
transitionItem.snapshotTransitioning = snapshot
|
||||
transitionItem.initialFrame = snapshot.frame
|
||||
transitionItem.targetFrame = targetFrame
|
||||
|
||||
// disable interaction
|
||||
fromVC.pagingViewConttroller.isUserInteractionEnabled = false
|
||||
|
||||
let animator = popInteractiveTransitionAnimator
|
||||
|
||||
self.transitionItem.snapshotRaw?.alpha = 0.0
|
||||
animator.addAnimations {
|
||||
fromView.alpha = 0
|
||||
if let targetFrame = targetFrame {
|
||||
self.transitionItem.snapshotTransitioning?.frame = targetFrame
|
||||
} else {
|
||||
fromView.alpha = 0
|
||||
}
|
||||
fromVC.closeButtonBackground.alpha = 0
|
||||
fromVC.visualEffectView.effect = nil
|
||||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
self.transitionItem.snapshotTransitioning?.removeFromSuperview()
|
||||
switch self.transitionItem.source {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
mosaicImageViewContainer.setImageViews(alpha: 1)
|
||||
}
|
||||
transitionContext.completeTransition(position == .end)
|
||||
}
|
||||
|
||||
|
@ -99,35 +169,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
|
||||
switch operation {
|
||||
case .pop:
|
||||
guard let mediaPreviewViewController = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController,
|
||||
let mediaPreviewImageViewController = mediaPreviewViewController.pagingViewConttroller.currentViewController as? MediaPreviewImageViewController else {
|
||||
transitionContext.completeTransition(false)
|
||||
return
|
||||
}
|
||||
|
||||
let imageView = mediaPreviewImageViewController.previewImageView.imageView
|
||||
let _snapshot: UIView? = {
|
||||
// if imageView.image == nil {
|
||||
// transitionItem.snapshotRaw = mediaPreviewImageViewController.progressBarView
|
||||
// return mediaPreviewImageViewController.progressBarView.snapshotView(afterScreenUpdates: false)
|
||||
// } else {
|
||||
transitionItem.snapshotRaw = imageView
|
||||
return imageView.snapshotView(afterScreenUpdates: false)
|
||||
// }
|
||||
}()
|
||||
guard let snapshot = _snapshot else {
|
||||
transitionContext.completeTransition(false)
|
||||
return
|
||||
}
|
||||
mediaPreviewImageViewController.view.insertSubview(snapshot, aboveSubview: mediaPreviewImageViewController.previewImageView)
|
||||
|
||||
snapshot.center = transitionContext.containerView.center
|
||||
|
||||
transitionItem.imageView = imageView
|
||||
transitionItem.snapshotTransitioning = snapshot
|
||||
transitionItem.initialFrame = snapshot.frame
|
||||
transitionItem.targetFrame = snapshot.frame
|
||||
|
||||
// Note: change item.imageView transform via pan gesture
|
||||
panGestureRecognizer.addTarget(self, action: #selector(MediaHostToMediaPreviewViewControllerAnimatedTransitioning.updatePanGestureInteractive(_:)))
|
||||
popInteractiveTransition(using: transitionContext)
|
||||
default:
|
||||
|
@ -138,27 +180,62 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
|
||||
private func popInteractiveTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController,
|
||||
let fromView = transitionContext.view(forKey: .from) else {
|
||||
let fromView = transitionContext.view(forKey: .from),
|
||||
let mediaPreviewImageViewController = fromVC.pagingViewConttroller.currentViewController as? MediaPreviewImageViewController,
|
||||
let index = fromVC.pagingViewConttroller.currentIndex else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
// assert view hierarchy not change
|
||||
let toVC = transitionItem.previewableViewController
|
||||
let targetFrame = toVC.sourceFrame(transitionItem: transitionItem, index: index)
|
||||
|
||||
let imageView = mediaPreviewImageViewController.previewImageView.imageView
|
||||
let _snapshot: UIView? = {
|
||||
transitionItem.snapshotRaw = imageView
|
||||
let snapshot = imageView.snapshotView(afterScreenUpdates: false)
|
||||
snapshot?.clipsToBounds = true
|
||||
snapshot?.contentMode = .scaleAspectFill
|
||||
return snapshot
|
||||
}()
|
||||
guard let snapshot = _snapshot else {
|
||||
transitionContext.completeTransition(false)
|
||||
return
|
||||
}
|
||||
mediaPreviewImageViewController.view.insertSubview(snapshot, aboveSubview: mediaPreviewImageViewController.previewImageView)
|
||||
|
||||
snapshot.center = transitionContext.containerView.center
|
||||
|
||||
transitionItem.imageView = imageView
|
||||
transitionItem.snapshotTransitioning = snapshot
|
||||
transitionItem.initialFrame = snapshot.frame
|
||||
transitionItem.targetFrame = targetFrame
|
||||
|
||||
// disable interaction
|
||||
fromVC.pagingViewConttroller.isUserInteractionEnabled = false
|
||||
|
||||
let animator = popInteractiveTransitionAnimator
|
||||
|
||||
let blurEffect = fromVC.visualEffectView.effect
|
||||
self.transitionItem.imageView?.isHidden = true
|
||||
self.transitionItem.snapshotRaw?.alpha = 0.0
|
||||
|
||||
animator.addAnimations {
|
||||
self.transitionItem.snapshotTransitioning?.alpha = 0.4
|
||||
// fromVC.mediaInfoDescriptionView.alpha = 0
|
||||
// fromVC.closeButtonBackground.alpha = 0
|
||||
// fromVC.pageControl.alpha = 0
|
||||
fromVC.closeButtonBackground.alpha = 0
|
||||
fromVC.visualEffectView.effect = nil
|
||||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
fromVC.pagingViewConttroller.isUserInteractionEnabled = true
|
||||
fromVC.closeButtonBackground.alpha = position == .end ? 0 : 1
|
||||
self.transitionItem.imageView?.isHidden = position == .end
|
||||
self.transitionItem.snapshotRaw?.alpha = position == .start ? 1.0 : 0.0
|
||||
self.transitionItem.snapshotTransitioning?.removeFromSuperview()
|
||||
if position == .end {
|
||||
switch self.transitionItem.source {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
mosaicImageViewContainer.setImageViews(alpha: 1)
|
||||
}
|
||||
}
|
||||
fromVC.visualEffectView.effect = position == .end ? nil : blurEffect
|
||||
transitionContext.completeTransition(position == .end)
|
||||
}
|
||||
|
@ -184,10 +261,10 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
case .ended, .cancelled:
|
||||
let targetPosition = completionPosition()
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: target position: %s", ((#file as NSString).lastPathComponent), #line, #function, targetPosition == .end ? "end" : "start")
|
||||
targetPosition == .end ? transitionContext.finishInteractiveTransition() : transitionContext.cancelInteractiveTransition()
|
||||
isTransitionContextFinish = true
|
||||
animate(targetPosition)
|
||||
|
||||
targetPosition == .end ? transitionContext.finishInteractiveTransition() : transitionContext.cancelInteractiveTransition()
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
@ -239,10 +316,17 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
|
||||
itemAnimator.addAnimations {
|
||||
if toPosition == .end {
|
||||
self.transitionItem.snapshotTransitioning?.alpha = 0
|
||||
if let targetFrame = self.transitionItem.targetFrame {
|
||||
self.transitionItem.snapshotTransitioning?.frame = targetFrame
|
||||
} else {
|
||||
self.transitionItem.snapshotTransitioning?.alpha = 0
|
||||
}
|
||||
} else {
|
||||
self.transitionItem.snapshotTransitioning?.alpha = 1
|
||||
self.transitionItem.snapshotTransitioning?.frame = self.transitionItem.initialFrame!
|
||||
if let initialFrame = self.transitionItem.initialFrame {
|
||||
self.transitionItem.snapshotTransitioning?.frame = initialFrame
|
||||
} else {
|
||||
self.transitionItem.snapshotTransitioning?.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,10 +74,9 @@ extension MediaPreviewTransitionController: UIViewControllerTransitioningDelegat
|
|||
self.mediaPreviewViewController = mediaPreviewViewController
|
||||
self.mediaPreviewViewController?.view.addGestureRecognizer(panGestureRecognizer)
|
||||
|
||||
let transitionItem = MediaPreviewTransitionItem(id: UUID())
|
||||
return MediaHostToMediaPreviewViewControllerAnimatedTransitioning(
|
||||
operation: .push,
|
||||
transitionItem: transitionItem,
|
||||
transitionItem: mediaPreviewViewController.viewModel.pushTransitionItem,
|
||||
panGestureRecognizer: panGestureRecognizer
|
||||
)
|
||||
}
|
||||
|
@ -92,10 +91,10 @@ extension MediaPreviewTransitionController: UIViewControllerTransitioningDelegat
|
|||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
let transitionItem = MediaPreviewTransitionItem(id: UUID())
|
||||
|
||||
return MediaHostToMediaPreviewViewControllerAnimatedTransitioning(
|
||||
operation: .pop,
|
||||
transitionItem: transitionItem,
|
||||
transitionItem: mediaPreviewViewController.viewModel.pushTransitionItem,
|
||||
panGestureRecognizer: panGestureRecognizer
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,21 +6,40 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
class MediaPreviewTransitionItem: Identifiable {
|
||||
|
||||
let id: UUID
|
||||
let source: Source
|
||||
var previewableViewController: MediaPreviewableViewController
|
||||
|
||||
// TODO:
|
||||
// source
|
||||
// value maybe invalid when preview paging
|
||||
var image: UIImage?
|
||||
var aspectRatio: CGSize?
|
||||
var initialFrame: CGRect? = nil
|
||||
var sourceImageView: UIImageView?
|
||||
|
||||
// target
|
||||
var targetFrame: CGRect? = nil
|
||||
|
||||
// transitioning
|
||||
var imageView: UIImageView?
|
||||
var snapshotRaw: UIView?
|
||||
var snapshotTransitioning: UIView?
|
||||
var initialFrame: CGRect? = nil
|
||||
var targetFrame: CGRect? = nil
|
||||
var touchOffset: CGVector = CGVector.zero
|
||||
|
||||
init(id: UUID) {
|
||||
init(id: UUID = UUID(), source: Source, previewableViewController: MediaPreviewableViewController) {
|
||||
self.id = id
|
||||
self.source = source
|
||||
self.previewableViewController = previewableViewController
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MediaPreviewTransitionItem {
|
||||
enum Source {
|
||||
case mosaic(MosaicImageViewContainer)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,20 @@
|
|||
// Created by MainasuK Cirno on 2021-4-28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol MediaPreviewableViewController: class {
|
||||
protocol MediaPreviewableViewController: AnyObject {
|
||||
var mediaPreviewTransitionController: MediaPreviewTransitionController { get }
|
||||
func sourceFrame(transitionItem: MediaPreviewTransitionItem, index: Int) -> CGRect?
|
||||
}
|
||||
|
||||
extension MediaPreviewableViewController {
|
||||
func sourceFrame(transitionItem: MediaPreviewTransitionItem, index: Int) -> CGRect? {
|
||||
switch transitionItem.source {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
guard index < mosaicImageViewContainer.imageViews.count else { return nil }
|
||||
let imageView = mosaicImageViewContainer.imageViews[index]
|
||||
return imageView.superview!.convert(imageView.frame, to: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
protocol MediaPreviewingViewController: class {
|
||||
protocol MediaPreviewingViewController: AnyObject {
|
||||
func isInteractiveDismissable() -> Bool
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue