Hide large image source view during expand/shrink animation

This commit is contained in:
Shadowfacts 2020-03-25 23:10:48 -04:00
parent 80cf1850dd
commit fcab6818b0
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 104 additions and 44 deletions

View File

@ -224,6 +224,7 @@
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; }; D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; }; D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; }; D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; }; D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; }; D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
@ -512,6 +513,7 @@
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; }; D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; }; D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; }; D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; }; D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; }; D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
@ -1213,6 +1215,7 @@
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */, D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
D6F1F84E2193B9BE00F5FE67 /* Caching */, D6F1F84E2193B9BE00F5FE67 /* Caching */,
D6757A7A2157E00100721E32 /* XCallbackURL */, D6757A7A2157E00100721E32 /* XCallbackURL */,
D62D241E217AA46B005076CC /* Shortcuts */, D62D241E217AA46B005076CC /* Shortcuts */,
@ -1707,6 +1710,7 @@
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */, D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */, D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */,
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */, D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */, D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */, D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,

View File

@ -13,7 +13,7 @@ import AVKit
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController { class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController {
let attachments: [Attachment] let attachments: [Attachment]
let sourcesInfo: [LargeImageViewController.SourceInfo?] let sourceViews: WeakArray<UIImageView>
let startIndex: Int let startIndex: Int
let pages: [UIViewController] let pages: [UIViewController]
@ -26,12 +26,13 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
return index return index
} }
var animationSourceInfo: LargeImageViewController.SourceInfo? { sourcesInfo[currentIndex] } var animationSourceView: UIImageView? { sourceViews[currentIndex] }
var animationImage: UIImage? { var animationImage: UIImage? {
if let sourceImage = sourcesInfo[currentIndex]?.image { if let page = pages[currentIndex] as? LoadingLargeImageViewController,
return sourceImage let image = page.largeImageVC?.image {
return image
} else { } else {
return (pages[currentIndex] as? LoadingLargeImageViewController)?.largeImageVC?.image return animationSourceView?.image
} }
} }
var animationGifData: Data? { var animationGifData: Data? {
@ -59,9 +60,9 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
} }
} }
init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) { init(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) {
self.attachments = attachments self.attachments = attachments
self.sourcesInfo = sourcesInfo self.sourceViews = WeakArray(sourceViews)
self.startIndex = startIndex self.startIndex = startIndex
self.pages = attachments.map { self.pages = attachments.map {

View File

@ -11,10 +11,8 @@ import Gifu
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController { class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
typealias SourceInfo = (image: UIImage?, frame: CGRect, cornerRadius: CGFloat) weak var animationSourceView: UIImageView?
var animationImage: UIImage? { image ?? animationSourceView?.image }
var animationSourceInfo: SourceInfo?
var animationImage: UIImage? { animationSourceInfo?.image ?? image }
var animationGifData: Data? { gifData } var animationGifData: Data? { gifData }
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
@ -59,10 +57,10 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
return !controlsVisible return !controlsVisible
} }
init(image: UIImage, description: String?, sourceInfo: SourceInfo?) { init(image: UIImage, description: String?, sourceView: UIImageView?) {
self.image = image self.image = image
self.imageDescription = description self.imageDescription = description
self.animationSourceInfo = sourceInfo self.animationSourceView = sourceView
super.init(nibName: "LargeImageViewController", bundle: nil) super.init(nibName: "LargeImageViewController", bundle: nil)

View File

@ -35,8 +35,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
var shrinkGestureEnabled = true var shrinkGestureEnabled = true
var animationSourceInfo: LargeImageViewController.SourceInfo? weak var animationSourceView: UIImageView?
var animationImage: UIImage? { animationSourceInfo?.image ?? largeImageVC?.image } var animationImage: UIImage? { largeImageVC?.image ?? animationSourceView?.image }
var animationGifData: Data? { largeImageVC?.gifData } var animationGifData: Data? { largeImageVC?.gifData }
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
@ -108,7 +108,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
func createLargeImage(data: Data) { func createLargeImage(data: Data) {
guard let image = UIImage(data: data) else { return } guard let image = UIImage(data: data) else { return }
largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceInfo: nil) largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceView: animationSourceView)
largeImageVC!.initialControlsVisible = initialControlsVisible largeImageVC!.initialControlsVisible = initialControlsVisible
largeImageVC!.shrinkGestureEnabled = false largeImageVC!.shrinkGestureEnabled = false
if url.pathExtension == "gif" { if url.pathExtension == "gif" {

View File

@ -10,12 +10,30 @@ import UIKit
import Gifu import Gifu
protocol LargeImageAnimatableViewController: UIViewController { protocol LargeImageAnimatableViewController: UIViewController {
var animationSourceInfo: LargeImageViewController.SourceInfo? { get } var animationSourceView: UIImageView? { get }
var animationImage: UIImage? { get } var animationImage: UIImage? { get }
var animationGifData: Data? { get } var animationGifData: Data? { get }
var dismissInteractionController: LargeImageInteractionController? { get } var dismissInteractionController: LargeImageInteractionController? { get }
} }
extension LargeImageAnimatableViewController {
func sourceViewFrame(in coordinateSpace: UIView) -> CGRect? {
guard let sourceView = animationSourceView else { return nil }
var sourceFrame = sourceView.convert(sourceView.bounds, to: coordinateSpace)
if let scrollView = coordinateSpace as? UIScrollView {
let scale = scrollView.zoomScale
let width = sourceFrame.width * scale
let height = sourceFrame.height * scale
let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX
let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY
sourceFrame = CGRect(x: x, y: y, width: width, height: height)
}
return sourceFrame
}
}
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning { class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
@ -32,12 +50,16 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
containerView.addSubview(toVC.view) containerView.addSubview(toVC.view)
let finalVCFrame = transitionContext.finalFrame(for: toVC) let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let sourceInfo = toVC.animationSourceInfo, guard let sourceView = toVC.animationSourceView,
let sourceFrame = toVC.sourceViewFrame(in: fromVC.view),
let image = toVC.animationImage else { let image = toVC.animationImage else {
toVC.view.frame = finalVCFrame toVC.view.frame = finalVCFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return return
} }
// use alpha, becaus isHidden makes stack views re-layout
sourceView.alpha = 0
var finalFrameSize = finalVCFrame.inset(by: fromVC.view.safeAreaInsets).size var finalFrameSize = finalVCFrame.inset(by: fromVC.view.safeAreaInsets).size
let newWidth = finalFrameSize.width / image.size.width let newWidth = finalFrameSize.width / image.size.width
@ -49,13 +71,14 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
} }
let finalFrame = CGRect(origin: CGPoint(x: finalVCFrame.midX - finalFrameSize.width / 2, y: finalVCFrame.midY - finalFrameSize.height / 2), size: finalFrameSize) let finalFrame = CGRect(origin: CGPoint(x: finalVCFrame.midX - finalFrameSize.width / 2, y: finalVCFrame.midY - finalFrameSize.height / 2), size: finalFrameSize)
let imageView = GIFImageView(frame: sourceInfo.frame) let imageView = GIFImageView(frame: sourceFrame)
imageView.image = image imageView.image = image
if let gifData = toVC.animationGifData { if let gifData = toVC.animationGifData {
imageView.animate(withGIFData: gifData) imageView.animate(withGIFData: gifData)
} }
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = sourceInfo.cornerRadius imageView.layer.cornerRadius = sourceView.layer.cornerRadius
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
let blackView = UIView(frame: finalVCFrame) let blackView = UIView(frame: finalVCFrame)
@ -84,6 +107,9 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
fromVC.view.isHidden = false fromVC.view.isHidden = false
blackView.removeFromSuperview() blackView.removeFromSuperview()
imageView.removeFromSuperview() imageView.removeFromSuperview()
sourceView.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}) })
} }

View File

@ -27,12 +27,15 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
return return
} }
guard let sourceInfo = fromVC.animationSourceInfo, guard let sourceView = fromVC.animationSourceView,
let sourceFrame = fromVC.sourceViewFrame(in: toVC.view),
let image = fromVC.animationImage else { let image = fromVC.animationImage else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return return
} }
// use alpha, becaus isHidden makes stack views re-layout
sourceView.alpha = 0
let containerView = transitionContext.containerView let containerView = transitionContext.containerView
@ -66,8 +69,9 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
let duration = transitionDuration(using: transitionContext) let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: { UIView.animate(withDuration: duration, animations: {
imageView.frame = sourceInfo.frame imageView.frame = sourceFrame
imageView.layer.cornerRadius = sourceInfo.cornerRadius imageView.layer.cornerRadius = sourceView.layer.cornerRadius
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
blackView.alpha = 0 blackView.alpha = 0
}, completion: { _ in }, completion: { _ in
blackView.removeFromSuperview() blackView.removeFromSuperview()
@ -75,6 +79,9 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
if transitionContext.transitionWasCancelled { if transitionContext.transitionWasCancelled {
toVC.view.removeFromSuperview() toVC.view.removeFromSuperview()
} }
sourceView.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}) })
} }

View File

@ -150,30 +150,15 @@ extension TuskerNavigationDelegate where Self: UIViewController {
vc.presentationController?.delegate = compose vc.presentationController?.delegate = compose
present(vc, animated: true) present(vc, animated: true)
} }
private func sourceViewInfo(_ sourceView: UIImageView?) -> LargeImageViewController.SourceInfo? {
guard let sourceView = sourceView else { return nil }
var sourceFrame = sourceView.convert(sourceView.bounds, to: view)
if let scrollView = view as? UIScrollView {
let scale = scrollView.zoomScale
let width = sourceFrame.width * scale
let height = sourceFrame.height * scale
let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX
let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY
sourceFrame = CGRect(x: x, y: y, width: width, height: height)
}
return (image: sourceView.image, frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius)
}
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController { func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: image, description: description, sourceInfo: sourceViewInfo(sourceView)) let vc = LargeImageViewController(image: image, description: description, sourceView: sourceView)
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }
func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController { func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceInfo: sourceViewInfo(sourceView)) let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceView: sourceView)
vc.transitioningDelegate = self vc.transitioningDelegate = self
vc.gifData = gifData vc.gifData = gifData
return vc return vc
@ -189,7 +174,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController { func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description) let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
vc.animationSourceInfo = sourceViewInfo(sourceView) vc.animationSourceView = sourceView
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }
@ -199,8 +184,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController { func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
let sourcesInfo = sourceViews.map(sourceViewInfo) let vc = GalleryViewController(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex)
let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex)
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }

40
Tusker/WeakArray.swift Normal file
View File

@ -0,0 +1,40 @@
//
// WeakArray.swift
// Tusker
//
// Created by Shadowfacts on 3/25/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import Foundation
fileprivate class WeakWrapper<T: AnyObject> {
weak var value: T?
init(_ value: T?) {
self.value = value
}
}
struct WeakArray<Element: AnyObject>: Collection {
private var array: [WeakWrapper<Element>]
var startIndex: Int { array.startIndex }
var endIndex: Int { array.endIndex }
init(_ elements: [Element]) {
array = elements.map { WeakWrapper($0) }
}
init(_ elements: [Element?]) {
array = elements.map { WeakWrapper($0) }
}
subscript(_ index: Int) -> Element? {
return array[index].value
}
func index(after i: Int) -> Int {
return array.index(after: i)
}
}