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,745 @@
1
+ import UIKit
2
+
3
+ final class NativeImagePreviewViewerController: UIViewController {
4
+ private weak var dataSource: NativeImagePreviewDataSource?
5
+ private weak var initialSourceView: UIImageView?
6
+ private let imageLoader: NativeImagePreviewImageLoading
7
+ private let previewGroupId: String?
8
+ private let initialIndex: Int
9
+ private let configuration: NativeImagePreviewViewerConfiguration
10
+
11
+ private let backgroundView = UIView()
12
+ private let contentView = UIView()
13
+ private let collectionView: UICollectionView
14
+ private let navigationBar = UINavigationBar(frame: .zero)
15
+ private let navigationItemRef = UINavigationItem()
16
+
17
+ private var transitionCoordinatorRef: NativeImagePreviewViewerTransitionCoordinator?
18
+ private var hasPerformedOpenTransition = false
19
+ private var pendingDismissal = false
20
+ private var pendingInitialScroll = false
21
+
22
+ private var currentIndex = 0
23
+ private var pageCount = 0
24
+ private var horizontalPagingEnabled = true {
25
+ didSet {
26
+ collectionView.isScrollEnabled = horizontalPagingEnabled
27
+ }
28
+ }
29
+
30
+ private var verticalDismissLocked = false
31
+ private var verticalDismissNeedsReanchor = false
32
+ private var isRestoringDismissDrag = false
33
+ private var isDraggingToDismiss = false
34
+ private var dragStartPoint = CGPoint.zero
35
+ private var dragTargetView: UIView?
36
+ private weak var dragPageView: NativeImagePreviewViewerPageView?
37
+ private weak var hiddenActiveSourceView: UIImageView?
38
+ private var hiddenActiveSourcePreviousAlpha: CGFloat = 1
39
+
40
+ private lazy var panGestureRecognizer: UIPanGestureRecognizer = {
41
+ let recognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
42
+ recognizer.delegate = self
43
+ recognizer.cancelsTouchesInView = false
44
+ return recognizer
45
+ }()
46
+
47
+ private struct RightBarButtonConfiguration {
48
+ let image: UIImage?
49
+ let title: String?
50
+ let onTap: ((Int) -> Void)?
51
+ }
52
+
53
+ private lazy var theme = configuration.theme
54
+ private lazy var mediaContentMode = configuration.contentMode
55
+ private lazy var closeIcon = configuration.closeIcon ?? UIImage(systemName: "xmark")
56
+ private lazy var rightBarButtonConfiguration: RightBarButtonConfiguration? = {
57
+ switch configuration.rightBarButton {
58
+ case .title(let title, let onTap):
59
+ return RightBarButtonConfiguration(image: nil, title: title, onTap: onTap)
60
+ case .icon(let image, let onTap):
61
+ return RightBarButtonConfiguration(image: image, title: nil, onTap: onTap)
62
+ case .none:
63
+ return nil
64
+ }
65
+ }()
66
+
67
+ private var prefersDefaultVideoCloseButton: Bool {
68
+ configuration.closeIcon == nil
69
+ }
70
+
71
+ init(
72
+ sourceView: UIImageView,
73
+ dataSource: NativeImagePreviewDataSource?,
74
+ imageLoader: NativeImagePreviewImageLoading,
75
+ configuration: NativeImagePreviewViewerConfiguration = NativeImagePreviewViewerConfiguration(),
76
+ initialIndex: Int = 0,
77
+ previewGroupId: String? = nil
78
+ ) {
79
+ self.initialSourceView = sourceView
80
+ self.dataSource = dataSource
81
+ self.imageLoader = imageLoader
82
+ self.configuration = configuration
83
+ self.initialIndex = initialIndex
84
+ self.previewGroupId = previewGroupId
85
+
86
+ let layout = UICollectionViewFlowLayout()
87
+ layout.scrollDirection = .horizontal
88
+ layout.minimumLineSpacing = 0
89
+ layout.minimumInteritemSpacing = 0
90
+ layout.sectionInset = .zero
91
+ collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
92
+
93
+ super.init(nibName: nil, bundle: nil)
94
+ modalPresentationStyle = .overFullScreen
95
+ modalPresentationCapturesStatusBarAppearance = true
96
+ definesPresentationContext = true
97
+ }
98
+
99
+ required init?(coder: NSCoder) {
100
+ fatalError("init(coder:) has not been implemented")
101
+ }
102
+
103
+ override func viewDidLoad() {
104
+ super.viewDidLoad()
105
+ overrideUserInterfaceStyle = theme == .dark ? .dark : .light
106
+ setupUI()
107
+ currentIndex = clampedIndex(initialIndex)
108
+ pageCount = dataSource?.numberOfItems() ?? 0
109
+ collectionView.reloadData()
110
+ refreshNavigationItems()
111
+ setNavigationBarHidden(shouldHideNavigationBarForCurrentPage, animated: false)
112
+ }
113
+
114
+ override func viewDidLayoutSubviews() {
115
+ super.viewDidLayoutSubviews()
116
+
117
+ guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
118
+ if layout.itemSize != collectionView.bounds.size {
119
+ layout.itemSize = collectionView.bounds.size
120
+ layout.invalidateLayout()
121
+ scrollToIndex(currentIndex, animated: false)
122
+ } else if pendingInitialScroll {
123
+ scrollToIndex(currentIndex, animated: false)
124
+ pendingInitialScroll = false
125
+ }
126
+ }
127
+
128
+ override func viewDidAppear(_ animated: Bool) {
129
+ super.viewDidAppear(animated)
130
+ if hasPerformedOpenTransition == false {
131
+ hasPerformedOpenTransition = true
132
+ scrollToIndex(currentIndex, animated: false)
133
+ transitionCoordinatorRef = NativeImagePreviewViewerTransitionCoordinator(containerView: view)
134
+ performOpenTransition()
135
+ }
136
+ }
137
+
138
+ override var prefersStatusBarHidden: Bool {
139
+ true
140
+ }
141
+
142
+ override var preferredStatusBarStyle: UIStatusBarStyle {
143
+ theme == .dark ? .lightContent : .darkContent
144
+ }
145
+
146
+ deinit {
147
+ restoreHiddenActiveSourceView()
148
+ }
149
+
150
+ private var currentPageView: NativeImagePreviewViewerPageView? {
151
+ pageView(at: currentIndex)
152
+ }
153
+
154
+ private var shouldHideNavigationBarForCurrentPage: Bool {
155
+ guard let currentPageView = currentPageView else { return true }
156
+ guard currentPageView.isVideoPage else { return true }
157
+ return !currentPageView.areVideoControlsVisible
158
+ }
159
+
160
+ private func setupUI() {
161
+ view.backgroundColor = .clear
162
+ view.overrideUserInterfaceStyle = theme == .dark ? .dark : .light
163
+
164
+ backgroundView.backgroundColor = theme.backgroundColor
165
+ backgroundView.alpha = 0
166
+ view.addSubview(backgroundView)
167
+ backgroundView.pinToSuperviewEdges()
168
+
169
+ contentView.backgroundColor = .clear
170
+ contentView.alpha = 0
171
+ contentView.overrideUserInterfaceStyle = theme == .dark ? .dark : .light
172
+ view.addSubview(contentView)
173
+ contentView.pinToSuperviewEdges()
174
+
175
+ collectionView.backgroundColor = .clear
176
+ collectionView.alwaysBounceVertical = false
177
+ collectionView.alwaysBounceHorizontal = true
178
+ collectionView.showsHorizontalScrollIndicator = false
179
+ collectionView.showsVerticalScrollIndicator = false
180
+ collectionView.isPagingEnabled = true
181
+ collectionView.decelerationRate = .fast
182
+ collectionView.contentInsetAdjustmentBehavior = .never
183
+ collectionView.delegate = self
184
+ collectionView.dataSource = self
185
+ collectionView.register(NativeImagePreviewViewerPageCell.self, forCellWithReuseIdentifier: NativeImagePreviewViewerPageCell.reuseIdentifier)
186
+ contentView.addSubview(collectionView)
187
+ collectionView.pinToSuperviewEdges()
188
+ collectionView.addGestureRecognizer(panGestureRecognizer)
189
+
190
+ navigationBar.isTranslucent = true
191
+ navigationBar.setBackgroundImage(UIImage(), for: .default)
192
+ navigationBar.shadowImage = UIImage()
193
+ navigationBar.tintColor = theme.tintColor
194
+ navigationBar.overrideUserInterfaceStyle = theme == .dark ? .dark : .light
195
+ navigationBar.items = [navigationItemRef]
196
+ navigationBar.alpha = 0
197
+ navigationBar.isHidden = true
198
+ navigationBar.isUserInteractionEnabled = false
199
+ contentView.addSubview(navigationBar)
200
+ navigationBar.translatesAutoresizingMaskIntoConstraints = false
201
+ NSLayoutConstraint.activate([
202
+ navigationBar.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
203
+ navigationBar.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
204
+ navigationBar.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor),
205
+ ])
206
+
207
+ refreshNavigationItems()
208
+
209
+ if let rightBarButtonConfiguration = rightBarButtonConfiguration {
210
+ if let image = rightBarButtonConfiguration.image {
211
+ navigationItemRef.rightBarButtonItem = UIBarButtonItem(
212
+ image: image,
213
+ style: .plain,
214
+ target: self,
215
+ action: #selector(handleRightBarButton)
216
+ )
217
+ } else if let title = rightBarButtonConfiguration.title {
218
+ navigationItemRef.rightBarButtonItem = UIBarButtonItem(
219
+ title: title,
220
+ style: .plain,
221
+ target: self,
222
+ action: #selector(handleRightBarButton)
223
+ )
224
+ }
225
+ navigationItemRef.rightBarButtonItem?.tintColor = theme.tintColor
226
+ }
227
+ }
228
+
229
+ private func performOpenTransition() {
230
+ collectionView.layoutIfNeeded()
231
+ let pageView = currentPageView
232
+ refreshNavigationItems()
233
+ setNavigationBarHidden(shouldHideNavigationBarForCurrentPage, animated: false)
234
+ transitionCoordinatorRef?.performOpenTransition(
235
+ from: initialSourceView,
236
+ to: pageView,
237
+ backgroundView: backgroundView,
238
+ contentView: contentView
239
+ ) { [weak self] in
240
+ guard let self = self else { return }
241
+ self.setVideoRevealAllowedForVisiblePages(true)
242
+ self.pageView(at: self.currentIndex)?.setActive(true)
243
+ self.refreshNavigationItems()
244
+ self.setNavigationBarHidden(self.shouldHideNavigationBarForCurrentPage, animated: false)
245
+ self.hideActiveSourceViewForCurrentIndex()
246
+ self.configuration.onIndexChange?(self.currentIndex)
247
+ }
248
+ }
249
+
250
+ private func performDismissTransition(anchorOverride: UIImageView? = nil) {
251
+ guard pendingDismissal == false else { return }
252
+ pendingDismissal = true
253
+
254
+ let pageView = currentPageView
255
+ pageView?.prepareForReturnAnimation()
256
+
257
+ let anchorView = anchorOverride ?? anchorView(for: currentIndex)
258
+ transitionCoordinatorRef?.performCloseTransition(
259
+ from: pageView,
260
+ to: anchorView,
261
+ backgroundView: backgroundView,
262
+ contentView: contentView
263
+ ) { [weak self] in
264
+ self?.restoreHiddenActiveSourceView()
265
+ self?.dismiss(animated: false)
266
+ }
267
+ }
268
+
269
+ private func anchorView(for index: Int) -> UIImageView? {
270
+ if let previewGroupId = previewGroupId,
271
+ let sourceView = NativeImagePreviewSourceViewRegistry.shared.sourceView(for: previewGroupId, index: index) {
272
+ return sourceView
273
+ }
274
+ if index == initialIndex {
275
+ return initialSourceView
276
+ }
277
+ return nil
278
+ }
279
+
280
+ private func clampedIndex(_ index: Int) -> Int {
281
+ let count = dataSource?.numberOfItems() ?? 0
282
+ guard count > 0 else { return 0 }
283
+ return min(max(index, 0), count - 1)
284
+ }
285
+
286
+ private func pageView(at index: Int) -> NativeImagePreviewViewerPageView? {
287
+ let indexPath = IndexPath(item: index, section: 0)
288
+ return collectionView.cellForItem(at: indexPath).flatMap { $0 as? NativeImagePreviewViewerPageCell }?.pageView
289
+ }
290
+
291
+ private func hideActiveSourceViewForCurrentIndex() {
292
+ guard hasPerformedOpenTransition, pendingDismissal == false else { return }
293
+ let sourceView = anchorView(for: currentIndex)
294
+ if hiddenActiveSourceView !== sourceView {
295
+ restoreHiddenActiveSourceView()
296
+ hiddenActiveSourcePreviousAlpha = sourceView?.alpha ?? 1
297
+ hiddenActiveSourceView = sourceView
298
+ }
299
+ sourceView?.alpha = 0
300
+ }
301
+
302
+ private func restoreHiddenActiveSourceView() {
303
+ hiddenActiveSourceView?.alpha = hiddenActiveSourcePreviousAlpha
304
+ hiddenActiveSourceView = nil
305
+ hiddenActiveSourcePreviousAlpha = 1
306
+ }
307
+
308
+ private func scrollToIndex(_ index: Int, animated: Bool) {
309
+ guard pageCount > 0 else { return }
310
+ guard collectionView.bounds.width > 0 else {
311
+ pendingInitialScroll = true
312
+ return
313
+ }
314
+ let indexPath = IndexPath(item: clampedIndex(index), section: 0)
315
+ collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated)
316
+ }
317
+
318
+ private func updateCurrentIndexFromVisiblePage() {
319
+ let center = CGPoint(
320
+ x: collectionView.contentOffset.x + collectionView.bounds.width * 0.5,
321
+ y: collectionView.bounds.midY
322
+ )
323
+ if let indexPath = collectionView.indexPathForItem(at: center) {
324
+ setCurrentIndex(indexPath.item, notify: true)
325
+ }
326
+ }
327
+
328
+ private func setCurrentIndex(_ newIndex: Int, notify: Bool) {
329
+ let safeIndex = clampedIndex(newIndex)
330
+ guard safeIndex != currentIndex || notify == false else {
331
+ updateVisiblePageStates()
332
+ return
333
+ }
334
+
335
+ currentIndex = safeIndex
336
+ updateVisiblePageStates()
337
+ hideActiveSourceViewForCurrentIndex()
338
+ if notify {
339
+ configuration.onIndexChange?(currentIndex)
340
+ }
341
+ }
342
+
343
+ private func updateVisiblePageStates() {
344
+ for cell in collectionView.visibleCells {
345
+ guard let pageCell = cell as? NativeImagePreviewViewerPageCell else { continue }
346
+ let index = collectionView.indexPath(for: pageCell)?.item ?? -1
347
+ pageCell.pageView.setActive(index == currentIndex)
348
+ pageCell.pageView.setVideoRevealAllowed(index == currentIndex && hasPerformedOpenTransition)
349
+ if index != currentIndex {
350
+ pageCell.pageView.setVideoControlsVisible(false, animated: false)
351
+ }
352
+ }
353
+ refreshNavigationItems()
354
+ setNavigationBarHidden(shouldHideNavigationBarForCurrentPage, animated: true)
355
+ recalculateHorizontalPagingEnabled()
356
+ }
357
+
358
+ private func setVideoRevealAllowedForVisiblePages(_ allowed: Bool) {
359
+ for cell in collectionView.visibleCells {
360
+ guard let pageCell = cell as? NativeImagePreviewViewerPageCell else { continue }
361
+ let index = collectionView.indexPath(for: pageCell)?.item ?? -1
362
+ pageCell.pageView.setVideoRevealAllowed(allowed && index == currentIndex)
363
+ }
364
+ }
365
+
366
+ private func setNavigationBarHidden(_ hidden: Bool, animated: Bool) {
367
+ let shouldHide = hidden
368
+ let updates = {
369
+ self.navigationBar.alpha = shouldHide ? 0 : 1
370
+ }
371
+
372
+ guard animated else {
373
+ navigationBar.layer.removeAllAnimations()
374
+ updates()
375
+ navigationBar.isHidden = shouldHide
376
+ navigationBar.isUserInteractionEnabled = !shouldHide
377
+ return
378
+ }
379
+
380
+ navigationBar.layer.removeAllAnimations()
381
+ if shouldHide {
382
+ navigationBar.isUserInteractionEnabled = false
383
+ UIView.animate(
384
+ withDuration: 0.18,
385
+ delay: 0,
386
+ options: [.curveEaseInOut, .beginFromCurrentState],
387
+ animations: updates
388
+ ) { _ in
389
+ self.navigationBar.isHidden = true
390
+ }
391
+ return
392
+ }
393
+
394
+ navigationBar.isHidden = false
395
+ navigationBar.isUserInteractionEnabled = true
396
+ UIView.animate(
397
+ withDuration: 0.18,
398
+ delay: 0,
399
+ options: [.curveEaseInOut, .beginFromCurrentState],
400
+ animations: updates
401
+ )
402
+ }
403
+
404
+ private func recalculateHorizontalPagingEnabled() {
405
+ let canPageHorizontally = currentPageView?.canPageHorizontally ?? true
406
+ horizontalPagingEnabled = canPageHorizontally
407
+ && !isDraggingToDismiss
408
+ && !isRestoringDismissDrag
409
+ && !verticalDismissLocked
410
+ && !verticalDismissNeedsReanchor
411
+ }
412
+
413
+ private func refreshNavigationItems() {
414
+ let currentIsVideoPage = currentPageView?.isVideoPage == true
415
+ guard currentIsVideoPage else {
416
+ navigationItemRef.leftBarButtonItem = nil
417
+ return
418
+ }
419
+ let closeButtonItem: UIBarButtonItem
420
+ if currentIsVideoPage && prefersDefaultVideoCloseButton {
421
+ closeButtonItem = UIBarButtonItem(
422
+ barButtonSystemItem: .close,
423
+ target: self,
424
+ action: #selector(handleCloseButton)
425
+ )
426
+ } else {
427
+ closeButtonItem = UIBarButtonItem(
428
+ image: closeIcon,
429
+ style: .plain,
430
+ target: self,
431
+ action: #selector(handleCloseButton)
432
+ )
433
+ }
434
+ closeButtonItem.tintColor = theme.tintColor
435
+ navigationItemRef.leftBarButtonItem = closeButtonItem
436
+ }
437
+
438
+ private func beginVerticalDismissDrag() {
439
+ guard let pageView = currentPageView else { return }
440
+ isDraggingToDismiss = true
441
+ dragTargetView = pageView.dragTargetView
442
+ dragStartPoint = dragTargetView?.center ?? pageView.dragTargetView.center
443
+ dragPageView = pageView
444
+ setNavigationBarHidden(true, animated: true)
445
+ pageView.prepareForDismissDrag()
446
+ recalculateHorizontalPagingEnabled()
447
+ }
448
+
449
+ private func updateVerticalDismissDrag(_ recognizer: UIPanGestureRecognizer) {
450
+ guard let dragTargetView = dragTargetView else { return }
451
+ let translation = recognizer.translation(in: view)
452
+ let height = max(view.bounds.height, 1)
453
+ let progress = min(abs(translation.y) / height, 1)
454
+ let scale = max(0.84, 1 - progress * 0.16)
455
+
456
+ dragTargetView.center = CGPoint(
457
+ x: dragStartPoint.x + translation.x,
458
+ y: dragStartPoint.y + translation.y
459
+ )
460
+ dragTargetView.transform = CGAffineTransform(scaleX: scale, y: scale)
461
+ backgroundView.alpha = max(0, 1 - min(0.9, progress * 1.25))
462
+ }
463
+
464
+ private func finishVerticalDismissDrag(_ recognizer: UIPanGestureRecognizer) {
465
+ guard let dragTargetView = dragTargetView else {
466
+ isDraggingToDismiss = false
467
+ resetDragState()
468
+ recalculateHorizontalPagingEnabled()
469
+ return
470
+ }
471
+
472
+ let translation = recognizer.translation(in: view)
473
+ let velocity = recognizer.velocity(in: view)
474
+ let dismissDistance = view.bounds.height * 0.16
475
+ let quickFlingDistance = view.bounds.height * 0.06
476
+ let shouldDismissByDistance = abs(translation.y) > dismissDistance
477
+ let shouldDismissByFling = abs(translation.y) > quickFlingDistance && abs(velocity.y) > 1300
478
+
479
+ isDraggingToDismiss = false
480
+ recalculateHorizontalPagingEnabled()
481
+
482
+ if shouldDismissByDistance || shouldDismissByFling {
483
+ dragPageView?.prepareForReturnAnimation()
484
+ performDismissTransition()
485
+ resetDragState()
486
+ return
487
+ }
488
+
489
+ isRestoringDismissDrag = true
490
+ recalculateHorizontalPagingEnabled()
491
+ UIView.animate(
492
+ withDuration: 0.2,
493
+ delay: 0,
494
+ options: [.curveEaseOut, .allowUserInteraction]
495
+ ) {
496
+ dragTargetView.center = self.dragStartPoint
497
+ dragTargetView.transform = .identity
498
+ self.backgroundView.alpha = 1
499
+ } completion: { _ in
500
+ self.isRestoringDismissDrag = false
501
+ self.dragPageView?.restoreAfterDismissCancelled()
502
+ self.setNavigationBarHidden(self.shouldHideNavigationBarForCurrentPage, animated: false)
503
+ self.resetDragState()
504
+ self.recalculateHorizontalPagingEnabled()
505
+ }
506
+ }
507
+
508
+ private func cancelVerticalDismissDrag() {
509
+ guard let dragTargetView = dragTargetView else {
510
+ isDraggingToDismiss = false
511
+ resetDragState()
512
+ recalculateHorizontalPagingEnabled()
513
+ return
514
+ }
515
+
516
+ isDraggingToDismiss = false
517
+ isRestoringDismissDrag = true
518
+ recalculateHorizontalPagingEnabled()
519
+ UIView.animate(
520
+ withDuration: 0.2,
521
+ delay: 0,
522
+ options: [.curveEaseOut, .allowUserInteraction]
523
+ ) {
524
+ dragTargetView.center = self.dragStartPoint
525
+ dragTargetView.transform = .identity
526
+ self.backgroundView.alpha = 1
527
+ } completion: { _ in
528
+ self.isRestoringDismissDrag = false
529
+ self.dragPageView?.restoreAfterDismissCancelled()
530
+ self.setNavigationBarHidden(self.shouldHideNavigationBarForCurrentPage, animated: false)
531
+ self.resetDragState()
532
+ self.recalculateHorizontalPagingEnabled()
533
+ }
534
+ }
535
+
536
+ private func resetDragState() {
537
+ dragTargetView = nil
538
+ dragPageView = nil
539
+ }
540
+
541
+ @objc
542
+ private func handlePan(_ recognizer: UIPanGestureRecognizer) {
543
+ if pendingDismissal {
544
+ return
545
+ }
546
+
547
+ let location = recognizer.location(in: view.window)
548
+ switch recognizer.state {
549
+ case .began:
550
+ if verticalDismissNeedsReanchor {
551
+ verticalDismissNeedsReanchor = false
552
+ recalculateHorizontalPagingEnabled()
553
+ }
554
+ case .changed:
555
+ guard isRestoringDismissDrag == false else { return }
556
+ guard verticalDismissLocked == false else { return }
557
+ guard let pageView = currentPageView else { return }
558
+
559
+ if isDraggingToDismiss == false {
560
+ let translation = recognizer.translation(in: view)
561
+ let velocity = recognizer.velocity(in: view)
562
+ let isVerticalCandidate = abs(translation.y) > 8 && abs(translation.y) > abs(translation.x) * 1.02
563
+ if isVerticalCandidate && pageView.canBeginVerticalDismiss(at: location, velocity: velocity) {
564
+ beginVerticalDismissDrag()
565
+ }
566
+ }
567
+
568
+ if isDraggingToDismiss {
569
+ updateVerticalDismissDrag(recognizer)
570
+ }
571
+ case .ended:
572
+ if verticalDismissNeedsReanchor {
573
+ recalculateHorizontalPagingEnabled()
574
+ return
575
+ }
576
+ if isDraggingToDismiss {
577
+ finishVerticalDismissDrag(recognizer)
578
+ }
579
+ case .cancelled, .failed:
580
+ if isDraggingToDismiss {
581
+ cancelVerticalDismissDrag()
582
+ }
583
+ if verticalDismissNeedsReanchor {
584
+ recalculateHorizontalPagingEnabled()
585
+ }
586
+ default:
587
+ break
588
+ }
589
+ }
590
+
591
+ @objc
592
+ private func handleCloseButton() {
593
+ performDismissTransition()
594
+ }
595
+
596
+ @objc
597
+ private func handleRightBarButton() {
598
+ rightBarButtonConfiguration?.onTap?(currentIndex)
599
+ }
600
+ }
601
+
602
+ extension NativeImagePreviewViewerController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
603
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
604
+ pageCount = dataSource?.numberOfItems() ?? 0
605
+ return pageCount
606
+ }
607
+
608
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
609
+ guard let cell = collectionView.dequeueReusableCell(
610
+ withReuseIdentifier: NativeImagePreviewViewerPageCell.reuseIdentifier,
611
+ for: indexPath
612
+ ) as? NativeImagePreviewViewerPageCell else {
613
+ return UICollectionViewCell()
614
+ }
615
+
616
+ if let item = dataSource?.item(at: indexPath.item) {
617
+ cell.pageView.delegate = self
618
+ cell.pageView.configure(
619
+ index: indexPath.item,
620
+ item: item,
621
+ imageLoader: imageLoader,
622
+ mediaContentMode: mediaContentMode,
623
+ sourcePreviewImage: anchorView(for: indexPath.item)?.image
624
+ )
625
+ cell.pageView.setActive(indexPath.item == currentIndex)
626
+ cell.pageView.setVideoRevealAllowed(hasPerformedOpenTransition && indexPath.item == currentIndex)
627
+ }
628
+
629
+ return cell
630
+ }
631
+
632
+ func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
633
+ guard let pageCell = cell as? NativeImagePreviewViewerPageCell else { return }
634
+ pageCell.pageView.setActive(indexPath.item == currentIndex)
635
+ pageCell.pageView.setVideoRevealAllowed(hasPerformedOpenTransition && indexPath.item == currentIndex)
636
+ }
637
+
638
+ func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
639
+ guard let pageCell = cell as? NativeImagePreviewViewerPageCell else { return }
640
+ pageCell.pageView.setActive(false)
641
+ pageCell.pageView.setVideoRevealAllowed(false)
642
+ }
643
+
644
+ func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
645
+ updateCurrentIndexFromVisiblePage()
646
+ }
647
+
648
+ func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
649
+ updateCurrentIndexFromVisiblePage()
650
+ }
651
+
652
+ func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
653
+ if decelerate == false {
654
+ updateCurrentIndexFromVisiblePage()
655
+ }
656
+ }
657
+ }
658
+
659
+ extension NativeImagePreviewViewerController: UIGestureRecognizerDelegate {
660
+ func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
661
+ if gestureRecognizer === panGestureRecognizer {
662
+ recalculateHorizontalPagingEnabled()
663
+ if verticalDismissLocked || verticalDismissNeedsReanchor || isRestoringDismissDrag {
664
+ return false
665
+ }
666
+
667
+ guard let pageView = currentPageView else { return false }
668
+ let velocity = panGestureRecognizer.velocity(in: view)
669
+ let location = panGestureRecognizer.location(in: view.window)
670
+ return pageView.canBeginVerticalDismiss(at: location, velocity: velocity)
671
+ }
672
+
673
+ return true
674
+ }
675
+
676
+ func gestureRecognizer(
677
+ _ gestureRecognizer: UIGestureRecognizer,
678
+ shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
679
+ ) -> Bool {
680
+ gestureRecognizer === panGestureRecognizer
681
+ }
682
+ }
683
+
684
+ extension NativeImagePreviewViewerController: NativeImagePreviewViewerPageViewDelegate {
685
+ func nativeImagePreviewViewerPageViewDidRequestDismiss(_ pageView: NativeImagePreviewViewerPageView) {
686
+ guard pageView.index == currentIndex else { return }
687
+ performDismissTransition()
688
+ }
689
+
690
+ func nativeImagePreviewViewerPageViewDidToggleVideoChrome(_ pageView: NativeImagePreviewViewerPageView) {
691
+ guard pageView.index == currentIndex else { return }
692
+ _ = pageView.toggleVideoControls()
693
+ setNavigationBarHidden(shouldHideNavigationBarForCurrentPage, animated: true)
694
+ }
695
+
696
+ func nativeImagePreviewViewerPageView(_ pageView: NativeImagePreviewViewerPageView, setHorizontalPagingEnabled enabled: Bool) {
697
+ guard pageView.index == currentIndex else { return }
698
+ recalculateHorizontalPagingEnabled()
699
+ }
700
+
701
+ func nativeImagePreviewViewerPageViewDidBeginMultiTouch(_ pageView: NativeImagePreviewViewerPageView) {
702
+ guard pageView.index == currentIndex else { return }
703
+ verticalDismissLocked = true
704
+ verticalDismissNeedsReanchor = false
705
+ if isDraggingToDismiss {
706
+ cancelVerticalDismissDrag()
707
+ }
708
+ recalculateHorizontalPagingEnabled()
709
+ }
710
+
711
+ func nativeImagePreviewViewerPageViewDidEndMultiTouch(_ pageView: NativeImagePreviewViewerPageView) {
712
+ guard pageView.index == currentIndex else { return }
713
+ verticalDismissLocked = false
714
+ verticalDismissNeedsReanchor = true
715
+ recalculateHorizontalPagingEnabled()
716
+ }
717
+ }
718
+
719
+ private final class NativeImagePreviewViewerPageCell: UICollectionViewCell {
720
+ static let reuseIdentifier = "NativeImagePreviewViewerPageCell"
721
+
722
+ let pageView = NativeImagePreviewViewerPageView()
723
+
724
+ override init(frame: CGRect) {
725
+ super.init(frame: frame)
726
+ contentView.addSubview(pageView)
727
+ pageView.pinToSuperviewEdges()
728
+ }
729
+
730
+ required init?(coder: NSCoder) {
731
+ fatalError("init(coder:) has not been implemented")
732
+ }
733
+
734
+ override func prepareForReuse() {
735
+ super.prepareForReuse()
736
+ layer.removeAllAnimations()
737
+ contentView.layer.removeAllAnimations()
738
+ transform = .identity
739
+ contentView.transform = .identity
740
+ alpha = 1
741
+ contentView.alpha = 1
742
+ contentView.center = CGPoint(x: bounds.midX, y: bounds.midY)
743
+ pageView.prepareForReuse()
744
+ }
745
+ }