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,623 @@
1
+ import UIKit
2
+
3
+ protocol NativeImagePreviewViewerPageViewDelegate: AnyObject {
4
+ func nativeImagePreviewViewerPageViewDidRequestDismiss(_ pageView: NativeImagePreviewViewerPageView)
5
+ func nativeImagePreviewViewerPageViewDidToggleVideoChrome(_ pageView: NativeImagePreviewViewerPageView)
6
+ func nativeImagePreviewViewerPageView(_ pageView: NativeImagePreviewViewerPageView, setHorizontalPagingEnabled enabled: Bool)
7
+ func nativeImagePreviewViewerPageViewDidBeginMultiTouch(_ pageView: NativeImagePreviewViewerPageView)
8
+ func nativeImagePreviewViewerPageViewDidEndMultiTouch(_ pageView: NativeImagePreviewViewerPageView)
9
+ }
10
+
11
+ final class NativeImagePreviewViewerPageView: UIView {
12
+ weak var delegate: NativeImagePreviewViewerPageViewDelegate?
13
+
14
+ private(set) var index: Int = 0
15
+ private(set) var item: NativeImagePreviewMediaItem?
16
+
17
+ private let mediaContainer = UIView()
18
+ private let activityIndicator = UIActivityIndicatorView(style: .large)
19
+
20
+ private let imageScrollView = UIScrollView()
21
+ private let imageView = UIImageView()
22
+ private let sourcePreviewImageView = UIImageView()
23
+
24
+ private let videoContainer = UIView()
25
+ private let posterImageView = UIImageView()
26
+ private let videoPlayerView = NativeImagePreviewVideoPlayerView(frame: .zero)
27
+
28
+ private var imageLoader: NativeImagePreviewImageLoading?
29
+ private var mediaContentMode: UIView.ContentMode = .scaleAspectFit
30
+ private var loadGeneration = 0
31
+
32
+ private var active = false
33
+ private var videoRevealAllowed = false
34
+ private var videoFirstFrameReady = false
35
+ private var pinchInProgress = false
36
+ private var videoControlsVisible = false
37
+ private var videoControlsVisibleBeforeDismissDrag = false
38
+
39
+ private lazy var imageSingleTapGesture = UITapGestureRecognizer(
40
+ target: self,
41
+ action: #selector(handleImageSingleTap(_:))
42
+ )
43
+ private lazy var imageDoubleTapGesture = UITapGestureRecognizer(
44
+ target: self,
45
+ action: #selector(handleImageDoubleTap(_:))
46
+ )
47
+ private lazy var videoSingleTapGesture = UITapGestureRecognizer(
48
+ target: self,
49
+ action: #selector(handleVideoSingleTap(_:))
50
+ )
51
+
52
+ var isVideoPage: Bool {
53
+ if case .video = item {
54
+ return true
55
+ }
56
+ return false
57
+ }
58
+
59
+ var dragTargetView: UIView {
60
+ mediaContainer
61
+ }
62
+
63
+ var canPageHorizontally: Bool {
64
+ guard !isVideoPage else { return true }
65
+ return imageScrollView.zoomScale <= imageScrollView.minimumZoomScale + 0.01
66
+ }
67
+
68
+ var areVideoControlsVisible: Bool {
69
+ videoControlsVisible
70
+ }
71
+
72
+ var sharedElementView: UIImageView? {
73
+ if isVideoPage {
74
+ return posterImageView.image == nil ? nil : posterImageView
75
+ }
76
+ return imageView.image == nil ? nil : imageView
77
+ }
78
+
79
+ var isReadyForTransition: Bool {
80
+ guard let sharedElementView = sharedElementView else { return false }
81
+ guard sharedElementView.window != nil else { return false }
82
+ guard sharedElementView.bounds.width > 0, sharedElementView.bounds.height > 0 else { return false }
83
+ guard let image = sharedElementView.image else { return false }
84
+ return image.size.width > 0 && image.size.height > 0
85
+ }
86
+
87
+ override init(frame: CGRect) {
88
+ super.init(frame: frame)
89
+ setupUI()
90
+ }
91
+
92
+ required init?(coder: NSCoder) {
93
+ fatalError("init(coder:) has not been implemented")
94
+ }
95
+
96
+ deinit {
97
+ videoPlayerView.resetForReuse()
98
+ }
99
+
100
+ override func layoutSubviews() {
101
+ super.layoutSubviews()
102
+ layoutImageIfNeeded(resetZoom: false)
103
+ }
104
+
105
+ func configure(
106
+ index: Int,
107
+ item: NativeImagePreviewMediaItem,
108
+ imageLoader: NativeImagePreviewImageLoading,
109
+ mediaContentMode: UIView.ContentMode,
110
+ sourcePreviewImage: UIImage? = nil
111
+ ) {
112
+ self.index = index
113
+ self.item = item
114
+ self.imageLoader = imageLoader
115
+ self.mediaContentMode = normalizedContentMode(from: mediaContentMode)
116
+ loadGeneration += 1
117
+
118
+ activityIndicator.stopAnimating()
119
+ activityIndicator.isHidden = true
120
+
121
+ imageScrollView.zoomScale = 1
122
+ imageScrollView.minimumZoomScale = 1
123
+ imageScrollView.maximumZoomScale = 1
124
+ imageScrollView.contentSize = .zero
125
+ imageScrollView.contentOffset = .zero
126
+
127
+ imageView.image = nil
128
+ sourcePreviewImageView.image = nil
129
+ sourcePreviewImageView.isHidden = true
130
+ posterImageView.image = nil
131
+ videoPlayerView.resetForReuse()
132
+ videoFirstFrameReady = false
133
+ videoControlsVisible = false
134
+ videoControlsVisibleBeforeDismissDrag = false
135
+
136
+ imageScrollView.isHidden = true
137
+ videoContainer.isHidden = true
138
+ mediaContainer.alpha = 1
139
+ layer.removeAllAnimations()
140
+ mediaContainer.layer.removeAllAnimations()
141
+ imageScrollView.layer.removeAllAnimations()
142
+ videoContainer.layer.removeAllAnimations()
143
+ posterImageView.layer.removeAllAnimations()
144
+ imageView.layer.removeAllAnimations()
145
+ sourcePreviewImageView.layer.removeAllAnimations()
146
+ transform = .identity
147
+ alpha = 1
148
+ center = CGPoint(x: bounds.midX, y: bounds.midY)
149
+ mediaContainer.transform = .identity
150
+ mediaContainer.alpha = 1
151
+
152
+ switch item {
153
+ case .image(let image):
154
+ configureImagePage(image: image)
155
+ case .imageURL(let url, let placeholder):
156
+ configureImagePage(image: placeholder)
157
+ if placeholder == nil {
158
+ showSourcePreview(image: sourcePreviewImage)
159
+ }
160
+ loadImage(from: url, placeholder: placeholder, into: imageView)
161
+ case .video(let url, let poster):
162
+ configureVideoPage(url: url, poster: poster)
163
+ }
164
+ }
165
+
166
+ func prepareForReuse() {
167
+ loadGeneration += 1
168
+ active = false
169
+ videoRevealAllowed = false
170
+ videoFirstFrameReady = false
171
+ pinchInProgress = false
172
+ videoControlsVisible = false
173
+ videoControlsVisibleBeforeDismissDrag = false
174
+
175
+ imageScrollView.zoomScale = 1
176
+ imageScrollView.minimumZoomScale = 1
177
+ imageScrollView.maximumZoomScale = 1
178
+ imageScrollView.contentSize = .zero
179
+ imageScrollView.contentOffset = .zero
180
+
181
+ imageView.image = nil
182
+ sourcePreviewImageView.image = nil
183
+ sourcePreviewImageView.isHidden = true
184
+ posterImageView.image = nil
185
+ videoPlayerView.resetForReuse()
186
+ videoPlayerView.setControlsVisible(false, animated: false)
187
+ videoControlsVisible = false
188
+
189
+ mediaContainer.alpha = 1
190
+ imageScrollView.isHidden = true
191
+ videoContainer.isHidden = true
192
+ activityIndicator.stopAnimating()
193
+ activityIndicator.isHidden = true
194
+ layer.removeAllAnimations()
195
+ mediaContainer.layer.removeAllAnimations()
196
+ imageScrollView.layer.removeAllAnimations()
197
+ videoContainer.layer.removeAllAnimations()
198
+ posterImageView.layer.removeAllAnimations()
199
+ imageView.layer.removeAllAnimations()
200
+ sourcePreviewImageView.layer.removeAllAnimations()
201
+ transform = .identity
202
+ alpha = 1
203
+ center = CGPoint(x: bounds.midX, y: bounds.midY)
204
+ mediaContainer.transform = .identity
205
+ mediaContainer.alpha = 1
206
+ }
207
+
208
+ func setActive(_ active: Bool) {
209
+ self.active = active
210
+ updateVideoPlaybackIfNeeded()
211
+ }
212
+
213
+ func setVideoRevealAllowed(_ allowed: Bool) {
214
+ videoRevealAllowed = allowed
215
+ if !allowed {
216
+ setVideoControlsVisible(false, animated: false)
217
+ }
218
+ updateVideoPlaybackIfNeeded()
219
+ }
220
+
221
+ func setMediaHidden(_ hidden: Bool) {
222
+ mediaContainer.alpha = hidden ? 0 : 1
223
+ }
224
+
225
+ func sharedElementState() -> NativeImagePreviewSharedElementState? {
226
+ if isVideoPage {
227
+ return posterImageView.nativeImagePreviewSharedElementState()
228
+ }
229
+ return imageView.nativeImagePreviewSharedElementState(clippingFrameInWindow: imageScrollView.frameInWindow())
230
+ }
231
+
232
+ func defaultTransitionGeometry(in windowBounds: CGRect) -> NativeImagePreviewSharedElementGeometry {
233
+ let imageSize = sharedElementView?.image?.size
234
+ ?? imageView.image?.size
235
+ ?? posterImageView.image?.size
236
+ ?? CGSize(width: max(windowBounds.width * 0.72, 1), height: max(windowBounds.height * 0.42, 1))
237
+ let visibleFrame = NativeImagePreviewViewerTransitionCoordinator.aspectFitRect(for: imageSize, in: windowBounds)
238
+ return NativeImagePreviewSharedElementGeometry(
239
+ visibleFrameInWindow: visibleFrame,
240
+ contentFrameInVisibleBounds: CGRect(origin: .zero, size: visibleFrame.size),
241
+ cornerRadius: 0
242
+ )
243
+ }
244
+
245
+ func canBeginVerticalDismiss(at windowLocation: CGPoint, velocity: CGPoint) -> Bool {
246
+ guard abs(velocity.y) > abs(velocity.x) * 1.02 else { return false }
247
+ if isVideoPage {
248
+ return !videoPlayerView.isTouchOnInteractiveControls(atWindowPoint: windowLocation)
249
+ }
250
+ return canPageHorizontally
251
+ }
252
+
253
+ func prepareForDismissDrag() {
254
+ guard isVideoPage else { return }
255
+ videoControlsVisibleBeforeDismissDrag = videoControlsVisible
256
+ setVideoControlsVisible(false, animated: false)
257
+ posterImageView.alpha = 1
258
+ videoPlayerView.setPlayerVisible(false)
259
+ }
260
+
261
+ func restoreAfterDismissCancelled() {
262
+ guard isVideoPage else { return }
263
+ updateVideoPlaybackIfNeeded()
264
+ setVideoControlsVisible(videoControlsVisibleBeforeDismissDrag, animated: false)
265
+ videoControlsVisibleBeforeDismissDrag = false
266
+ }
267
+
268
+ func prepareForReturnAnimation() {
269
+ guard isVideoPage else { return }
270
+ videoControlsVisibleBeforeDismissDrag = false
271
+ setVideoControlsVisible(false, animated: false)
272
+ posterImageView.alpha = 1
273
+ videoPlayerView.setPlayerVisible(false)
274
+ }
275
+
276
+ func setVideoControlsVisible(_ visible: Bool, animated: Bool) {
277
+ guard isVideoPage else { return }
278
+ videoControlsVisible = visible
279
+ videoPlayerView.setControlsVisible(visible, animated: animated)
280
+ }
281
+
282
+ func toggleVideoControls() -> Bool {
283
+ guard isVideoPage else { return false }
284
+ let nextVisible = !videoControlsVisible
285
+ setVideoControlsVisible(nextVisible, animated: true)
286
+ return nextVisible
287
+ }
288
+
289
+ private func setupUI() {
290
+ backgroundColor = .clear
291
+
292
+ mediaContainer.backgroundColor = .clear
293
+ addSubview(mediaContainer)
294
+ mediaContainer.pinToSuperviewEdges()
295
+
296
+ imageScrollView.backgroundColor = .clear
297
+ imageScrollView.delegate = self
298
+ imageScrollView.showsHorizontalScrollIndicator = false
299
+ imageScrollView.showsVerticalScrollIndicator = false
300
+ imageScrollView.decelerationRate = .fast
301
+ imageScrollView.bouncesZoom = true
302
+ mediaContainer.addSubview(imageScrollView)
303
+ imageScrollView.pinToSuperviewEdges()
304
+
305
+ imageView.backgroundColor = .clear
306
+ imageView.isUserInteractionEnabled = true
307
+ imageScrollView.addSubview(imageView)
308
+
309
+ sourcePreviewImageView.backgroundColor = .clear
310
+ sourcePreviewImageView.isHidden = true
311
+ sourcePreviewImageView.isUserInteractionEnabled = false
312
+ mediaContainer.addSubview(sourcePreviewImageView)
313
+ sourcePreviewImageView.pinToSuperviewEdges()
314
+
315
+ imageSingleTapGesture.numberOfTapsRequired = 1
316
+ imageDoubleTapGesture.numberOfTapsRequired = 2
317
+ imageSingleTapGesture.require(toFail: imageDoubleTapGesture)
318
+ imageScrollView.addGestureRecognizer(imageSingleTapGesture)
319
+ imageScrollView.addGestureRecognizer(imageDoubleTapGesture)
320
+
321
+ if let pinchGestureRecognizer = imageScrollView.pinchGestureRecognizer {
322
+ pinchGestureRecognizer.addTarget(self, action: #selector(handlePinchState(_:)))
323
+ }
324
+
325
+ videoContainer.backgroundColor = .clear
326
+ videoContainer.isHidden = true
327
+ mediaContainer.addSubview(videoContainer)
328
+ videoContainer.pinToSuperviewEdges()
329
+
330
+ videoPlayerView.backgroundColor = .clear
331
+ videoContainer.addSubview(videoPlayerView)
332
+ videoPlayerView.pinToSuperviewEdges()
333
+ videoPlayerView.setPlayerVisible(false)
334
+ videoPlayerView.setControlsVisible(false, animated: false)
335
+ videoPlayerView.onFirstFrameReady = { [weak self] in
336
+ self?.handleVideoFirstFrameReady()
337
+ }
338
+ videoPlayerView.onControlsInteractStart = { [weak self] in
339
+ guard let self = self else { return }
340
+ self.delegate?.nativeImagePreviewViewerPageView(self, setHorizontalPagingEnabled: false)
341
+ }
342
+ videoPlayerView.onControlsInteractEnd = { [weak self] in
343
+ guard let self = self else { return }
344
+ self.delegate?.nativeImagePreviewViewerPageView(self, setHorizontalPagingEnabled: self.canPageHorizontally)
345
+ }
346
+
347
+ posterImageView.backgroundColor = .clear
348
+ posterImageView.contentMode = .scaleAspectFit
349
+ videoContainer.addSubview(posterImageView)
350
+ posterImageView.pinToSuperviewEdges()
351
+ videoPlayerView.attachControls(to: videoContainer)
352
+
353
+ videoSingleTapGesture.numberOfTapsRequired = 1
354
+ videoSingleTapGesture.delegate = self
355
+ videoContainer.addGestureRecognizer(videoSingleTapGesture)
356
+
357
+ activityIndicator.isHidden = true
358
+ activityIndicator.color = .white
359
+ addSubview(activityIndicator)
360
+ activityIndicator.translatesAutoresizingMaskIntoConstraints = false
361
+ NSLayoutConstraint.activate([
362
+ activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
363
+ activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),
364
+ ])
365
+ }
366
+
367
+ private func configureImagePage(image: UIImage?) {
368
+ imageScrollView.isHidden = false
369
+ videoContainer.isHidden = true
370
+ imageView.image = image
371
+ if image != nil {
372
+ layoutImageIfNeeded(resetZoom: true)
373
+ } else {
374
+ showLoading()
375
+ }
376
+ delegate?.nativeImagePreviewViewerPageView(self, setHorizontalPagingEnabled: canPageHorizontally)
377
+ }
378
+
379
+ private func configureVideoPage(url: URL, poster: URL?) {
380
+ imageScrollView.isHidden = true
381
+ videoContainer.isHidden = false
382
+
383
+ posterImageView.layer.removeAllAnimations()
384
+ posterImageView.alpha = 1
385
+ videoPlayerView.setPlayerVisible(false)
386
+ videoPlayerView.setControlsVisible(false, animated: false)
387
+ videoControlsVisible = false
388
+ videoControlsVisibleBeforeDismissDrag = false
389
+ videoPlayerView.url = url
390
+
391
+ if let poster = poster {
392
+ showLoading()
393
+ loadImage(from: poster, placeholder: nil, into: posterImageView)
394
+ } else {
395
+ posterImageView.image = nil
396
+ }
397
+
398
+ delegate?.nativeImagePreviewViewerPageView(self, setHorizontalPagingEnabled: true)
399
+ updateVideoPlaybackIfNeeded()
400
+ }
401
+
402
+ private func loadImage(from url: URL, placeholder: UIImage?, into imageView: UIImageView) {
403
+ let generation = loadGeneration
404
+ showLoading()
405
+ imageLoader?.loadImage(url, placeholder: placeholder, imageView: imageView) { [weak self] loadedImage in
406
+ guard let self = self else { return }
407
+ guard self.loadGeneration == generation else { return }
408
+ DispatchQueue.main.async {
409
+ guard self.loadGeneration == generation else { return }
410
+ self.hideLoading()
411
+ if imageView === self.imageView {
412
+ if loadedImage != nil {
413
+ self.hideSourcePreview()
414
+ }
415
+ self.layoutImageIfNeeded(resetZoom: true)
416
+ self.delegate?.nativeImagePreviewViewerPageView(self, setHorizontalPagingEnabled: self.canPageHorizontally)
417
+ }
418
+ }
419
+ }
420
+ }
421
+
422
+ private func showSourcePreview(image: UIImage?) {
423
+ guard let image = image else { return }
424
+ sourcePreviewImageView.contentMode = mediaContentMode
425
+ sourcePreviewImageView.image = image
426
+ sourcePreviewImageView.isHidden = false
427
+ }
428
+
429
+ private func hideSourcePreview() {
430
+ sourcePreviewImageView.isHidden = true
431
+ sourcePreviewImageView.image = nil
432
+ }
433
+
434
+ private func showLoading() {
435
+ activityIndicator.isHidden = false
436
+ activityIndicator.startAnimating()
437
+ }
438
+
439
+ private func hideLoading() {
440
+ activityIndicator.stopAnimating()
441
+ activityIndicator.isHidden = true
442
+ }
443
+
444
+ private func updateVideoPlaybackIfNeeded() {
445
+ guard isVideoPage else { return }
446
+ guard active else {
447
+ videoPlayerView.pause()
448
+ restoreVideoPoster()
449
+ return
450
+ }
451
+
452
+ if videoFirstFrameReady == false {
453
+ videoPlayerView.primeFirstFrameIfNeeded()
454
+ }
455
+
456
+ if videoRevealAllowed && videoFirstFrameReady {
457
+ revealVideoFromPoster()
458
+ return
459
+ }
460
+
461
+ if videoFirstFrameReady {
462
+ videoPlayerView.pause()
463
+ }
464
+ restoreVideoPoster()
465
+ }
466
+
467
+ private func restoreVideoPoster() {
468
+ posterImageView.layer.removeAllAnimations()
469
+ posterImageView.alpha = 1
470
+ videoPlayerView.setPlayerVisible(false)
471
+ }
472
+
473
+ private func revealVideoFromPoster() {
474
+ videoPlayerView.setPlayerVisible(true)
475
+ videoPlayerView.play()
476
+ posterImageView.layer.removeAllAnimations()
477
+ guard posterImageView.alpha > 0.01 else { return }
478
+ UIView.animate(
479
+ withDuration: 0.22,
480
+ delay: 0,
481
+ options: [.curveEaseInOut, .beginFromCurrentState]
482
+ ) {
483
+ self.posterImageView.alpha = 0
484
+ }
485
+ }
486
+
487
+ private func handleVideoFirstFrameReady() {
488
+ videoFirstFrameReady = true
489
+ hideLoading()
490
+ updateVideoPlaybackIfNeeded()
491
+ }
492
+
493
+ private func normalizedContentMode(from contentMode: UIView.ContentMode) -> UIView.ContentMode {
494
+ switch contentMode {
495
+ case .scaleAspectFill:
496
+ return .scaleAspectFill
497
+ default:
498
+ return .scaleAspectFit
499
+ }
500
+ }
501
+
502
+ private func layoutImageIfNeeded(resetZoom: Bool) {
503
+ guard !isVideoPage else { return }
504
+ guard bounds.width > 0, bounds.height > 0 else { return }
505
+ guard let image = imageView.image else { return }
506
+
507
+ let scrollBounds = imageScrollView.bounds.size
508
+ guard scrollBounds.width > 0, scrollBounds.height > 0 else { return }
509
+
510
+ guard image.size.width > 0, image.size.height > 0 else { return }
511
+
512
+ let widthRatio = scrollBounds.width / image.size.width
513
+ let heightRatio = scrollBounds.height / image.size.height
514
+ let minimumScale: CGFloat
515
+ switch mediaContentMode {
516
+ case .scaleAspectFill:
517
+ minimumScale = max(widthRatio, heightRatio)
518
+ default:
519
+ minimumScale = min(widthRatio, heightRatio)
520
+ }
521
+
522
+ let maximumScale = max(minimumScale * 4.0, 3.0)
523
+
524
+ imageScrollView.minimumZoomScale = minimumScale
525
+ imageScrollView.maximumZoomScale = maximumScale
526
+ if resetZoom || imageScrollView.zoomScale < minimumScale {
527
+ imageScrollView.zoomScale = minimumScale
528
+ }
529
+
530
+ let scaledSize = CGSize(
531
+ width: image.size.width * imageScrollView.zoomScale,
532
+ height: image.size.height * imageScrollView.zoomScale
533
+ )
534
+ let contentSize = CGSize(
535
+ width: max(scaledSize.width, scrollBounds.width),
536
+ height: max(scaledSize.height, scrollBounds.height)
537
+ )
538
+ imageScrollView.contentSize = contentSize
539
+ imageView.frame = CGRect(
540
+ x: max((contentSize.width - scaledSize.width) * 0.5, 0),
541
+ y: max((contentSize.height - scaledSize.height) * 0.5, 0),
542
+ width: scaledSize.width,
543
+ height: scaledSize.height
544
+ )
545
+ hideLoading()
546
+ }
547
+
548
+ private func zoomRect(for scale: CGFloat, centeredAt point: CGPoint) -> CGRect {
549
+ let width = imageScrollView.bounds.width / scale
550
+ let height = imageScrollView.bounds.height / scale
551
+ return CGRect(
552
+ x: point.x - (width * 0.5),
553
+ y: point.y - (height * 0.5),
554
+ width: width,
555
+ height: height
556
+ )
557
+ }
558
+
559
+ @objc
560
+ private func handleImageSingleTap(_ recognizer: UITapGestureRecognizer) {
561
+ delegate?.nativeImagePreviewViewerPageViewDidRequestDismiss(self)
562
+ }
563
+
564
+ @objc
565
+ private func handleImageDoubleTap(_ recognizer: UITapGestureRecognizer) {
566
+ guard !isVideoPage else { return }
567
+ let minimumScale = imageScrollView.minimumZoomScale
568
+ let maximumScale = imageScrollView.maximumZoomScale
569
+ let newScale: CGFloat
570
+ if imageScrollView.zoomScale <= minimumScale + 0.01 {
571
+ newScale = min(maximumScale, max(minimumScale * 2.4, 2.0))
572
+ } else {
573
+ newScale = minimumScale
574
+ }
575
+ let point = recognizer.location(in: imageView)
576
+ imageScrollView.zoom(to: zoomRect(for: newScale, centeredAt: point), animated: true)
577
+ }
578
+
579
+ @objc
580
+ private func handleVideoSingleTap(_ recognizer: UITapGestureRecognizer) {
581
+ delegate?.nativeImagePreviewViewerPageViewDidToggleVideoChrome(self)
582
+ }
583
+
584
+ @objc
585
+ private func handlePinchState(_ recognizer: UIPinchGestureRecognizer) {
586
+ switch recognizer.state {
587
+ case .began:
588
+ pinchInProgress = true
589
+ delegate?.nativeImagePreviewViewerPageViewDidBeginMultiTouch(self)
590
+ case .cancelled, .ended, .failed:
591
+ if pinchInProgress {
592
+ pinchInProgress = false
593
+ delegate?.nativeImagePreviewViewerPageViewDidEndMultiTouch(self)
594
+ delegate?.nativeImagePreviewViewerPageView(self, setHorizontalPagingEnabled: canPageHorizontally)
595
+ }
596
+ default:
597
+ break
598
+ }
599
+ }
600
+ }
601
+
602
+ extension NativeImagePreviewViewerPageView: UIScrollViewDelegate {
603
+ func viewForZooming(in scrollView: UIScrollView) -> UIView? {
604
+ isVideoPage ? nil : imageView
605
+ }
606
+
607
+ func scrollViewDidZoom(_ scrollView: UIScrollView) {
608
+ layoutImageIfNeeded(resetZoom: false)
609
+ delegate?.nativeImagePreviewViewerPageView(self, setHorizontalPagingEnabled: canPageHorizontally)
610
+ }
611
+
612
+ func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
613
+ delegate?.nativeImagePreviewViewerPageView(self, setHorizontalPagingEnabled: canPageHorizontally)
614
+ }
615
+ }
616
+
617
+ extension NativeImagePreviewViewerPageView: UIGestureRecognizerDelegate {
618
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
619
+ guard gestureRecognizer === videoSingleTapGesture else { return true }
620
+ let location = touch.location(in: videoPlayerView)
621
+ return !videoPlayerView.isPointInsideInteractiveControls(location)
622
+ }
623
+ }