zkit-ui 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +56 -0
  2. package/android/build.gradle +100 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewLayoutSupport.java +450 -0
  5. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewMediaItem.java +36 -0
  6. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewModule.kt +43 -0
  7. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewPagerAdapter.java +140 -0
  8. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementGeometry.java +25 -0
  9. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementNames.java +39 -0
  10. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementState.java +25 -0
  11. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSourceViewRegistry.java +115 -0
  12. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewTransitionSnapshotView.java +60 -0
  13. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewVideoPlayerView.java +338 -0
  14. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewView.kt +305 -0
  15. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerOverlayView.java +636 -0
  16. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerPageView.java +357 -0
  17. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerTransitionController.java +223 -0
  18. package/android/src/main/res/layout/native_image_preview_player_view.xml +9 -0
  19. package/dist/all.d.ts +2 -0
  20. package/dist/all.d.ts.map +1 -1
  21. package/dist/all.js +24 -22
  22. package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts +3 -0
  23. package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts.map +1 -0
  24. package/dist/ui/NativeImagePreview/NativeImagePreview.js +99 -0
  25. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts +4 -0
  26. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -0
  27. package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +150 -0
  28. package/dist/ui/NativeImagePreview/context.d.ts +14 -0
  29. package/dist/ui/NativeImagePreview/context.d.ts.map +1 -0
  30. package/dist/ui/NativeImagePreview/context.js +38 -0
  31. package/dist/ui/NativeImagePreview/index.d.ts +3 -0
  32. package/dist/ui/NativeImagePreview/index.d.ts.map +1 -0
  33. package/dist/ui/NativeImagePreview/index.js +5 -0
  34. package/dist/ui/NativeImagePreview/types.d.ts +68 -0
  35. package/dist/ui/NativeImagePreview/types.d.ts.map +1 -0
  36. package/dist/ui/NativeImagePreview/types.js +2 -0
  37. package/dist/ui/NativeImagePreview/utils.d.ts +8 -0
  38. package/dist/ui/NativeImagePreview/utils.d.ts.map +1 -0
  39. package/dist/ui/NativeImagePreview/utils.js +83 -0
  40. package/dist/ui/NativeImagePreview/web/geometry.d.ts +20 -0
  41. package/dist/ui/NativeImagePreview/web/geometry.d.ts.map +1 -0
  42. package/dist/ui/NativeImagePreview/web/geometry.js +121 -0
  43. package/dist/ui/NativeImagePreview/web/media.d.ts +6 -0
  44. package/dist/ui/NativeImagePreview/web/media.d.ts.map +1 -0
  45. package/dist/ui/NativeImagePreview/web/media.js +90 -0
  46. package/dist/ui/NativeImagePreview/web/styles.css +137 -0
  47. package/dist/ui/NativeImagePreview/web/transition.d.ts +15 -0
  48. package/dist/ui/NativeImagePreview/web/transition.d.ts.map +1 -0
  49. package/dist/ui/NativeImagePreview/web/transition.js +77 -0
  50. package/dist/ui/NativeImagePreview/web/types.d.ts +74 -0
  51. package/dist/ui/NativeImagePreview/web/types.d.ts.map +1 -0
  52. package/dist/ui/NativeImagePreview/web/types.js +3 -0
  53. package/dist/ui/NativeImagePreview/web/viewer.d.ts +104 -0
  54. package/dist/ui/NativeImagePreview/web/viewer.d.ts.map +1 -0
  55. package/dist/ui/NativeImagePreview/web/viewer.js +894 -0
  56. package/expo-module.config.json +11 -0
  57. package/ios/NativeImagePreview/NativeImagePreviewImageLoader.swift +73 -0
  58. package/ios/NativeImagePreview/NativeImagePreviewImageViewInteraction.swift +245 -0
  59. package/ios/NativeImagePreview/NativeImagePreviewLayoutSupport.swift +143 -0
  60. package/ios/NativeImagePreview/NativeImagePreviewMedia.swift +28 -0
  61. package/ios/NativeImagePreview/NativeImagePreviewSourceViewRegistry.swift +80 -0
  62. package/ios/NativeImagePreview/NativeImagePreviewVideoPlayerView.swift +437 -0
  63. package/ios/NativeImagePreview/NativeImagePreviewViewerConfiguration.swift +51 -0
  64. package/ios/NativeImagePreview/NativeImagePreviewViewerController.swift +745 -0
  65. package/ios/NativeImagePreview/NativeImagePreviewViewerPageView.swift +623 -0
  66. package/ios/NativeImagePreview/NativeImagePreviewViewerTransitionCoordinator.swift +312 -0
  67. package/ios/NativeImagePreviewModule.swift +46 -0
  68. package/ios/NativeImagePreviewView.swift +252 -0
  69. package/package.json +16 -2
  70. package/src/all.ts +18 -0
  71. package/src/services/ImagePreviewService/README.md +19 -0
  72. package/src/types.d.ts +1 -0
  73. package/src/ui/NativeImagePreview/NativeImagePreview.tsx +146 -0
  74. package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +201 -0
  75. package/src/ui/NativeImagePreview/README.md +111 -0
  76. package/src/ui/NativeImagePreview/context.tsx +22 -0
  77. package/src/ui/NativeImagePreview/index.tsx +18 -0
  78. package/src/ui/NativeImagePreview/types.ts +94 -0
  79. package/src/ui/NativeImagePreview/utils.ts +108 -0
  80. package/src/ui/NativeImagePreview/web/geometry.ts +134 -0
  81. package/src/ui/NativeImagePreview/web/media.ts +91 -0
  82. package/src/ui/NativeImagePreview/web/styles.css +137 -0
  83. package/src/ui/NativeImagePreview/web/transition.ts +111 -0
  84. package/src/ui/NativeImagePreview/web/types.ts +88 -0
  85. package/src/ui/NativeImagePreview/web/viewer.ts +1024 -0
  86. package/zkit-ui.podspec +13 -1
@@ -0,0 +1,11 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["NativeImagePreviewModule"],
5
+ "podspecPath": "zkit-ui.podspec",
6
+ "swiftModuleName": "zkit_ui"
7
+ },
8
+ "android": {
9
+ "modules": ["cn.fontree.zkit.ui.preview.NativeImagePreviewModule"]
10
+ }
11
+ }
@@ -0,0 +1,73 @@
1
+ import Foundation
2
+ import UIKit
3
+ #if canImport(SDWebImage)
4
+ import SDWebImage
5
+ #endif
6
+
7
+ public protocol NativeImagePreviewImageLoading {
8
+ func loadImage(
9
+ _ url: URL,
10
+ placeholder: UIImage?,
11
+ imageView: UIImageView,
12
+ completion: @escaping (_ image: UIImage?) -> Void
13
+ )
14
+ }
15
+
16
+ public struct NativeImagePreviewURLSessionImageLoader: NativeImagePreviewImageLoading {
17
+ public init() {}
18
+
19
+ public func loadImage(
20
+ _ url: URL,
21
+ placeholder: UIImage?,
22
+ imageView: UIImageView,
23
+ completion: @escaping (UIImage?) -> Void
24
+ ) {
25
+ if let placeholder = placeholder {
26
+ imageView.image = placeholder
27
+ }
28
+
29
+ DispatchQueue.global(qos: .background).async {
30
+ guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else {
31
+ completion(nil)
32
+ return
33
+ }
34
+
35
+ DispatchQueue.main.async {
36
+ imageView.image = image
37
+ completion(image)
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ #if canImport(SDWebImage)
44
+ struct NativeImagePreviewSDWebImageLoader: NativeImagePreviewImageLoading {
45
+ func loadImage(
46
+ _ url: URL,
47
+ placeholder: UIImage?,
48
+ imageView: UIImageView,
49
+ completion: @escaping (UIImage?) -> Void
50
+ ) {
51
+ imageView.sd_setImage(
52
+ with: url,
53
+ placeholderImage: placeholder,
54
+ options: [],
55
+ progress: nil
56
+ ) { image, _, _, _ in
57
+ DispatchQueue.main.async {
58
+ completion(image)
59
+ }
60
+ }
61
+ }
62
+ }
63
+ #endif
64
+
65
+ public enum NativeImagePreviewImageLoaderFactory {
66
+ public static func makeDefault() -> NativeImagePreviewImageLoading {
67
+ #if canImport(SDWebImage)
68
+ return NativeImagePreviewSDWebImageLoader()
69
+ #else
70
+ return NativeImagePreviewURLSessionImageLoader()
71
+ #endif
72
+ }
73
+ }
@@ -0,0 +1,245 @@
1
+ import ObjectiveC
2
+ import UIKit
3
+
4
+ extension UIImageView {
5
+ private struct NativeImagePreviewAnchorRegistration {
6
+ let previewGroupId: String
7
+ let index: Int
8
+ }
9
+
10
+ private final class NativeImagePreviewTapGestureRecognizer: UITapGestureRecognizer {
11
+ weak var fromViewController: UIViewController?
12
+ var dataSource: NativeImagePreviewDataSource?
13
+ var imageLoader: NativeImagePreviewImageLoading?
14
+ var initialIndex = 0
15
+ var previewGroupId: String?
16
+ var configuration = NativeImagePreviewViewerConfiguration()
17
+ }
18
+
19
+ private static var anchorRegistrationKey: UInt8 = 0
20
+ private static var gestureRecognizerKey: UInt8 = 0
21
+
22
+ private var topMostHostViewController: UIViewController? {
23
+ let keyWindow: UIWindow? = {
24
+ if #available(iOS 13.0, *) {
25
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
26
+ let activeScene = scenes.first(where: { $0.activationState == .foregroundActive }) ?? scenes.first
27
+ return activeScene?.windows.first(where: { $0.isKeyWindow }) ?? activeScene?.windows.first
28
+ } else {
29
+ return UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
30
+ }
31
+ }()
32
+ guard let rootViewController = keyWindow?.rootViewController else { return nil }
33
+ return rootViewController.presentedViewController ?? rootViewController
34
+ }
35
+
36
+ public func setupNativeImagePreviewViewer(
37
+ configuration: NativeImagePreviewViewerConfiguration = NativeImagePreviewViewerConfiguration(),
38
+ from viewController: UIViewController? = nil,
39
+ imageLoader: NativeImagePreviewImageLoading? = nil,
40
+ previewGroupId: String? = nil
41
+ ) {
42
+ configureNativeImagePreviewViewer(
43
+ dataSource: NativeImagePreviewArrayDataSource(items: [.image(image)]),
44
+ initialIndex: 0,
45
+ configuration: configuration,
46
+ from: viewController,
47
+ imageLoader: imageLoader,
48
+ previewGroupId: previewGroupId
49
+ )
50
+ }
51
+
52
+ public func setupNativeImagePreviewViewer(
53
+ url: URL,
54
+ initialIndex: Int = 0,
55
+ placeholder: UIImage? = nil,
56
+ configuration: NativeImagePreviewViewerConfiguration = NativeImagePreviewViewerConfiguration(),
57
+ from viewController: UIViewController? = nil,
58
+ imageLoader: NativeImagePreviewImageLoading? = nil,
59
+ previewGroupId: String? = nil
60
+ ) {
61
+ configureNativeImagePreviewViewer(
62
+ dataSource: NativeImagePreviewArrayDataSource(items: [.imageURL(url, placeholder: placeholder)]),
63
+ initialIndex: initialIndex,
64
+ configuration: configuration,
65
+ from: viewController,
66
+ imageLoader: imageLoader,
67
+ previewGroupId: previewGroupId
68
+ )
69
+ }
70
+
71
+ public func setupNativeImagePreviewViewer(
72
+ images: [UIImage],
73
+ initialIndex: Int = 0,
74
+ configuration: NativeImagePreviewViewerConfiguration = NativeImagePreviewViewerConfiguration(),
75
+ from viewController: UIViewController? = nil,
76
+ imageLoader: NativeImagePreviewImageLoading? = nil,
77
+ previewGroupId: String? = nil
78
+ ) {
79
+ let items = images.map { NativeImagePreviewMediaItem.image($0) }
80
+ configureNativeImagePreviewViewer(
81
+ dataSource: NativeImagePreviewArrayDataSource(items: items),
82
+ initialIndex: initialIndex,
83
+ configuration: configuration,
84
+ from: viewController,
85
+ imageLoader: imageLoader,
86
+ previewGroupId: previewGroupId
87
+ )
88
+ }
89
+
90
+ public func setupNativeImagePreviewViewer(
91
+ urls: [URL],
92
+ initialIndex: Int = 0,
93
+ placeholder: UIImage? = nil,
94
+ configuration: NativeImagePreviewViewerConfiguration = NativeImagePreviewViewerConfiguration(),
95
+ from viewController: UIViewController? = nil,
96
+ imageLoader: NativeImagePreviewImageLoading? = nil,
97
+ previewGroupId: String? = nil
98
+ ) {
99
+ let items = urls.map { NativeImagePreviewMediaItem.imageURL($0, placeholder: placeholder) }
100
+ configureNativeImagePreviewViewer(
101
+ dataSource: NativeImagePreviewArrayDataSource(items: items),
102
+ initialIndex: initialIndex,
103
+ configuration: configuration,
104
+ from: viewController,
105
+ imageLoader: imageLoader,
106
+ previewGroupId: previewGroupId
107
+ )
108
+ }
109
+
110
+ public func setupNativeImagePreviewViewer(
111
+ dataSource: NativeImagePreviewDataSource,
112
+ initialIndex: Int = 0,
113
+ configuration: NativeImagePreviewViewerConfiguration = NativeImagePreviewViewerConfiguration(),
114
+ from viewController: UIViewController? = nil,
115
+ imageLoader: NativeImagePreviewImageLoading? = nil,
116
+ previewGroupId: String? = nil
117
+ ) {
118
+ configureNativeImagePreviewViewer(
119
+ dataSource: dataSource,
120
+ initialIndex: initialIndex,
121
+ configuration: configuration,
122
+ from: viewController,
123
+ imageLoader: imageLoader,
124
+ previewGroupId: previewGroupId
125
+ )
126
+ }
127
+
128
+ func removeNativeImagePreviewViewerInteraction() {
129
+ if let recognizer = nativeImagePreviewTapGestureRecognizer {
130
+ removeGestureRecognizer(recognizer)
131
+ }
132
+ nativeImagePreviewTapGestureRecognizer = nil
133
+
134
+ if let registration = nativeImagePreviewAnchorRegistration {
135
+ NativeImagePreviewSourceViewRegistry.shared.unregister(
136
+ self,
137
+ previewGroupId: registration.previewGroupId,
138
+ index: registration.index
139
+ )
140
+ }
141
+ nativeImagePreviewAnchorRegistration = nil
142
+ }
143
+
144
+ private func configureNativeImagePreviewViewer(
145
+ dataSource: NativeImagePreviewDataSource?,
146
+ initialIndex: Int,
147
+ configuration: NativeImagePreviewViewerConfiguration,
148
+ from viewController: UIViewController?,
149
+ imageLoader: NativeImagePreviewImageLoading?,
150
+ previewGroupId: String?
151
+ ) {
152
+ let recognizer = nativeImagePreviewTapGestureRecognizer ?? {
153
+ let recognizer = NativeImagePreviewTapGestureRecognizer(target: self, action: #selector(showNativeImagePreviewViewer(_:)))
154
+ recognizer.numberOfTouchesRequired = 1
155
+ recognizer.numberOfTapsRequired = 1
156
+ nativeImagePreviewTapGestureRecognizer = recognizer
157
+ return recognizer
158
+ }()
159
+
160
+ isUserInteractionEnabled = true
161
+ clipsToBounds = true
162
+
163
+ recognizer.dataSource = dataSource
164
+ recognizer.imageLoader = imageLoader
165
+ recognizer.initialIndex = initialIndex
166
+ recognizer.previewGroupId = previewGroupId
167
+ recognizer.configuration = configuration
168
+ recognizer.fromViewController = viewController
169
+
170
+ if recognizer.view !== self {
171
+ addGestureRecognizer(recognizer)
172
+ }
173
+
174
+ updateNativeImagePreviewAnchorRegistration(previewGroupId: previewGroupId, index: initialIndex)
175
+ }
176
+
177
+ private var nativeImagePreviewTapGestureRecognizer: NativeImagePreviewTapGestureRecognizer? {
178
+ get {
179
+ objc_getAssociatedObject(self, &Self.gestureRecognizerKey) as? NativeImagePreviewTapGestureRecognizer
180
+ }
181
+ set {
182
+ objc_setAssociatedObject(
183
+ self,
184
+ &Self.gestureRecognizerKey,
185
+ newValue,
186
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
187
+ )
188
+ }
189
+ }
190
+
191
+ private var nativeImagePreviewAnchorRegistration: NativeImagePreviewAnchorRegistration? {
192
+ get {
193
+ objc_getAssociatedObject(self, &Self.anchorRegistrationKey) as? NativeImagePreviewAnchorRegistration
194
+ }
195
+ set {
196
+ objc_setAssociatedObject(
197
+ self,
198
+ &Self.anchorRegistrationKey,
199
+ newValue,
200
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
201
+ )
202
+ }
203
+ }
204
+
205
+ @objc
206
+ private func showNativeImagePreviewViewer(_ recognizer: NativeImagePreviewTapGestureRecognizer) {
207
+ guard let sourceView = recognizer.view as? UIImageView else { return }
208
+ let resolvedImageLoader = recognizer.imageLoader ?? NativeImagePreviewImageLoaderFactory.makeDefault()
209
+ let viewerController = NativeImagePreviewViewerController(
210
+ sourceView: sourceView,
211
+ dataSource: recognizer.dataSource,
212
+ imageLoader: resolvedImageLoader,
213
+ configuration: recognizer.configuration,
214
+ initialIndex: recognizer.initialIndex,
215
+ previewGroupId: recognizer.previewGroupId
216
+ )
217
+
218
+ let rootViewController = recognizer.fromViewController ?? topMostHostViewController
219
+ let presenter = rootViewController.flatMap(topMostPresentedController(from:))
220
+ presenter?.present(viewerController, animated: false)
221
+ }
222
+
223
+ private func topMostPresentedController(from rootViewController: UIViewController) -> UIViewController {
224
+ var controller = rootViewController
225
+ while let presented = controller.presentedViewController {
226
+ controller = presented
227
+ }
228
+ return controller
229
+ }
230
+
231
+ private func updateNativeImagePreviewAnchorRegistration(previewGroupId: String?, index: Int) {
232
+ if let existing = nativeImagePreviewAnchorRegistration {
233
+ NativeImagePreviewSourceViewRegistry.shared.unregister(
234
+ self,
235
+ previewGroupId: existing.previewGroupId,
236
+ index: existing.index
237
+ )
238
+ nativeImagePreviewAnchorRegistration = nil
239
+ }
240
+
241
+ guard let previewGroupId = previewGroupId, previewGroupId.isEmpty == false else { return }
242
+ NativeImagePreviewSourceViewRegistry.shared.register(self, previewGroupId: previewGroupId, index: index)
243
+ nativeImagePreviewAnchorRegistration = NativeImagePreviewAnchorRegistration(previewGroupId: previewGroupId, index: index)
244
+ }
245
+ }
@@ -0,0 +1,143 @@
1
+ import UIKit
2
+
3
+ struct NativeImagePreviewSharedElementGeometry {
4
+ let visibleFrameInWindow: CGRect
5
+ let contentFrameInVisibleBounds: CGRect
6
+ let cornerRadius: CGFloat
7
+ }
8
+
9
+ struct NativeImagePreviewSharedElementState {
10
+ let image: UIImage
11
+ let geometry: NativeImagePreviewSharedElementGeometry
12
+ }
13
+
14
+ extension CGRect {
15
+ func nativeImagePreviewPixelAligned(scale: CGFloat) -> CGRect {
16
+ guard scale > 0 else { return self }
17
+ return CGRect(
18
+ x: (origin.x * scale).rounded() / scale,
19
+ y: (origin.y * scale).rounded() / scale,
20
+ width: (size.width * scale).rounded() / scale,
21
+ height: (size.height * scale).rounded() / scale
22
+ )
23
+ }
24
+
25
+ func nativeImagePreviewApproximatelyEquals(_ other: CGRect, tolerance: CGFloat = 0.5) -> Bool {
26
+ abs(minX - other.minX) <= tolerance
27
+ && abs(minY - other.minY) <= tolerance
28
+ && abs(width - other.width) <= tolerance
29
+ && abs(height - other.height) <= tolerance
30
+ }
31
+ }
32
+
33
+ extension NativeImagePreviewSharedElementGeometry {
34
+ func nativeImagePreviewPixelAligned(scale: CGFloat) -> NativeImagePreviewSharedElementGeometry {
35
+ NativeImagePreviewSharedElementGeometry(
36
+ visibleFrameInWindow: visibleFrameInWindow.nativeImagePreviewPixelAligned(scale: scale),
37
+ contentFrameInVisibleBounds: contentFrameInVisibleBounds.nativeImagePreviewPixelAligned(scale: scale),
38
+ cornerRadius: (cornerRadius * scale).rounded() / scale
39
+ )
40
+ }
41
+
42
+ func approximatelyEquals(_ other: NativeImagePreviewSharedElementGeometry, tolerance: CGFloat = 0.5) -> Bool {
43
+ visibleFrameInWindow.nativeImagePreviewApproximatelyEquals(other.visibleFrameInWindow, tolerance: tolerance)
44
+ && contentFrameInVisibleBounds.nativeImagePreviewApproximatelyEquals(
45
+ other.contentFrameInVisibleBounds,
46
+ tolerance: tolerance
47
+ )
48
+ && abs(cornerRadius - other.cornerRadius) <= tolerance
49
+ }
50
+ }
51
+
52
+ extension NativeImagePreviewSharedElementState {
53
+ func approximatelyEquals(_ other: NativeImagePreviewSharedElementState, tolerance: CGFloat = 0.5) -> Bool {
54
+ geometry.approximatelyEquals(other.geometry, tolerance: tolerance)
55
+ && abs(image.size.width - other.image.size.width) <= tolerance
56
+ && abs(image.size.height - other.image.size.height) <= tolerance
57
+ }
58
+ }
59
+
60
+ extension UIView {
61
+ func pinToSuperviewEdges(
62
+ top: CGFloat = 0,
63
+ leading: CGFloat = 0,
64
+ trailing: CGFloat = 0,
65
+ bottom: CGFloat = 0
66
+ ) {
67
+ guard let superview = superview else { return }
68
+ translatesAutoresizingMaskIntoConstraints = false
69
+ NSLayoutConstraint.activate([
70
+ topAnchor.constraint(equalTo: superview.topAnchor, constant: top),
71
+ leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: leading),
72
+ trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: -trailing),
73
+ bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -bottom),
74
+ ])
75
+ }
76
+
77
+ func pinToSuperviewEdges(inset: CGFloat) {
78
+ pinToSuperviewEdges(top: inset, leading: inset, trailing: inset, bottom: inset)
79
+ }
80
+
81
+ func frameInWindow() -> CGRect {
82
+ convert(bounds, to: nil)
83
+ }
84
+ }
85
+
86
+ extension UIImageView {
87
+ func imageContentFrameInWindow() -> CGRect {
88
+ guard let image = image else { return frameInWindow() }
89
+ guard image.size.width > 0, image.size.height > 0 else { return frameInWindow() }
90
+ guard bounds.width > 0, bounds.height > 0 else { return frameInWindow() }
91
+
92
+ let widthRatio = bounds.width / image.size.width
93
+ let heightRatio = bounds.height / image.size.height
94
+
95
+ let scale: CGFloat
96
+ switch contentMode {
97
+ case .scaleAspectFill:
98
+ scale = max(widthRatio, heightRatio)
99
+ default:
100
+ scale = min(widthRatio, heightRatio)
101
+ }
102
+
103
+ let contentSize = CGSize(
104
+ width: image.size.width * scale,
105
+ height: image.size.height * scale
106
+ )
107
+ let contentFrame = CGRect(
108
+ x: (bounds.width - contentSize.width) * 0.5,
109
+ y: (bounds.height - contentSize.height) * 0.5,
110
+ width: contentSize.width,
111
+ height: contentSize.height
112
+ )
113
+ return convert(contentFrame, to: nil)
114
+ }
115
+
116
+ func nativeImagePreviewSharedElementState(clippingFrameInWindow: CGRect? = nil) -> NativeImagePreviewSharedElementState? {
117
+ guard let image = image else { return nil }
118
+ guard image.size.width > 0, image.size.height > 0 else { return nil }
119
+
120
+ let clippingFrame = clippingFrameInWindow ?? frameInWindow()
121
+ guard clippingFrame.width > 0, clippingFrame.height > 0 else { return nil }
122
+
123
+ let contentFrame = imageContentFrameInWindow()
124
+ var visibleFrame = clippingFrame.intersection(contentFrame)
125
+ if visibleFrame.isNull || visibleFrame.isEmpty {
126
+ visibleFrame = clippingFrame
127
+ }
128
+
129
+ let geometry = NativeImagePreviewSharedElementGeometry(
130
+ visibleFrameInWindow: visibleFrame,
131
+ contentFrameInVisibleBounds: contentFrame.offsetBy(
132
+ dx: -visibleFrame.minX,
133
+ dy: -visibleFrame.minY
134
+ ),
135
+ cornerRadius: (clipsToBounds || layer.masksToBounds) ? layer.cornerRadius : 0
136
+ )
137
+ let scale = window?.screen.scale ?? UIScreen.main.scale
138
+ return NativeImagePreviewSharedElementState(
139
+ image: image,
140
+ geometry: geometry.nativeImagePreviewPixelAligned(scale: scale)
141
+ )
142
+ }
143
+ }
@@ -0,0 +1,28 @@
1
+ import UIKit
2
+
3
+ public enum NativeImagePreviewMediaItem {
4
+ case image(UIImage?)
5
+ case imageURL(URL, placeholder: UIImage?)
6
+ case video(url: URL, poster: URL?)
7
+ }
8
+
9
+ public protocol NativeImagePreviewDataSource: AnyObject {
10
+ func numberOfItems() -> Int
11
+ func item(at index: Int) -> NativeImagePreviewMediaItem
12
+ }
13
+
14
+ public final class NativeImagePreviewArrayDataSource: NativeImagePreviewDataSource {
15
+ private let items: [NativeImagePreviewMediaItem]
16
+
17
+ public init(items: [NativeImagePreviewMediaItem]) {
18
+ self.items = items
19
+ }
20
+
21
+ public func numberOfItems() -> Int {
22
+ items.count
23
+ }
24
+
25
+ public func item(at index: Int) -> NativeImagePreviewMediaItem {
26
+ items[index]
27
+ }
28
+ }
@@ -0,0 +1,80 @@
1
+ import UIKit
2
+
3
+ final class NativeImagePreviewSourceViewRegistry {
4
+ static let shared = NativeImagePreviewSourceViewRegistry()
5
+
6
+ private final class WeakImageView {
7
+ weak var value: UIImageView?
8
+
9
+ init(_ value: UIImageView) {
10
+ self.value = value
11
+ }
12
+ }
13
+
14
+ private var anchors: [String: [Int: WeakImageView]] = [:]
15
+
16
+ private init() {}
17
+
18
+ func register(_ imageView: UIImageView, previewGroupId: String, index: Int) {
19
+ cleanup()
20
+ var previewGroupAnchors = anchors[previewGroupId] ?? [:]
21
+ previewGroupAnchors[index] = WeakImageView(imageView)
22
+ anchors[previewGroupId] = previewGroupAnchors
23
+ }
24
+
25
+ func unregister(_ imageView: UIImageView, previewGroupId: String?, index: Int?) {
26
+ cleanup()
27
+ if let previewGroupId = previewGroupId {
28
+ remove(imageView, from: previewGroupId, index: index)
29
+ } else {
30
+ for previewGroupId in Array(anchors.keys) {
31
+ remove(imageView, from: previewGroupId, index: nil)
32
+ }
33
+ }
34
+ cleanup()
35
+ }
36
+
37
+ func sourceView(for previewGroupId: String, index: Int) -> UIImageView? {
38
+ cleanup()
39
+ guard
40
+ let imageView = anchors[previewGroupId]?[index]?.value,
41
+ let window = imageView.window,
42
+ !imageView.isHidden
43
+ else {
44
+ return nil
45
+ }
46
+
47
+ let frameInWindow = imageView.frameInWindow()
48
+ guard !frameInWindow.isEmpty, frameInWindow.intersects(window.bounds) else {
49
+ return nil
50
+ }
51
+
52
+ return imageView
53
+ }
54
+
55
+ private func remove(_ imageView: UIImageView, from previewGroupId: String, index: Int?) {
56
+ guard var previewGroupAnchors = anchors[previewGroupId] else { return }
57
+
58
+ if let index = index {
59
+ if let registeredView = previewGroupAnchors[index]?.value, registeredView === imageView {
60
+ previewGroupAnchors.removeValue(forKey: index)
61
+ }
62
+ } else {
63
+ previewGroupAnchors = previewGroupAnchors.filter { _, weakImageView in
64
+ guard let registeredView = weakImageView.value else { return false }
65
+ return registeredView !== imageView
66
+ }
67
+ }
68
+
69
+ anchors[previewGroupId] = previewGroupAnchors.isEmpty ? nil : previewGroupAnchors
70
+ }
71
+
72
+ private func cleanup() {
73
+ anchors = anchors.compactMapValues { previewGroupAnchors in
74
+ let activeAnchors = previewGroupAnchors.filter { _, weakImageView in
75
+ weakImageView.value != nil
76
+ }
77
+ return activeAnchors.isEmpty ? nil : activeAnchors
78
+ }
79
+ }
80
+ }