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.
- package/README.md +56 -0
- package/android/build.gradle +100 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewLayoutSupport.java +450 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewMediaItem.java +36 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewModule.kt +43 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewPagerAdapter.java +140 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementGeometry.java +25 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementNames.java +39 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementState.java +25 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSourceViewRegistry.java +115 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewTransitionSnapshotView.java +60 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewVideoPlayerView.java +338 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewView.kt +305 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerOverlayView.java +636 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerPageView.java +357 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerTransitionController.java +223 -0
- package/android/src/main/res/layout/native_image_preview_player_view.xml +9 -0
- package/dist/all.d.ts +2 -0
- package/dist/all.d.ts.map +1 -1
- package/dist/all.js +24 -22
- package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts +3 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.js +99 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts +4 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +150 -0
- package/dist/ui/NativeImagePreview/context.d.ts +14 -0
- package/dist/ui/NativeImagePreview/context.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/context.js +38 -0
- package/dist/ui/NativeImagePreview/index.d.ts +3 -0
- package/dist/ui/NativeImagePreview/index.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/index.js +5 -0
- package/dist/ui/NativeImagePreview/types.d.ts +68 -0
- package/dist/ui/NativeImagePreview/types.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/types.js +2 -0
- package/dist/ui/NativeImagePreview/utils.d.ts +8 -0
- package/dist/ui/NativeImagePreview/utils.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/utils.js +83 -0
- package/dist/ui/NativeImagePreview/web/geometry.d.ts +20 -0
- package/dist/ui/NativeImagePreview/web/geometry.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/geometry.js +121 -0
- package/dist/ui/NativeImagePreview/web/media.d.ts +6 -0
- package/dist/ui/NativeImagePreview/web/media.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/media.js +90 -0
- package/dist/ui/NativeImagePreview/web/styles.css +137 -0
- package/dist/ui/NativeImagePreview/web/transition.d.ts +15 -0
- package/dist/ui/NativeImagePreview/web/transition.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/transition.js +77 -0
- package/dist/ui/NativeImagePreview/web/types.d.ts +74 -0
- package/dist/ui/NativeImagePreview/web/types.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/types.js +3 -0
- package/dist/ui/NativeImagePreview/web/viewer.d.ts +104 -0
- package/dist/ui/NativeImagePreview/web/viewer.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/viewer.js +894 -0
- package/expo-module.config.json +11 -0
- package/ios/NativeImagePreview/NativeImagePreviewImageLoader.swift +73 -0
- package/ios/NativeImagePreview/NativeImagePreviewImageViewInteraction.swift +245 -0
- package/ios/NativeImagePreview/NativeImagePreviewLayoutSupport.swift +143 -0
- package/ios/NativeImagePreview/NativeImagePreviewMedia.swift +28 -0
- package/ios/NativeImagePreview/NativeImagePreviewSourceViewRegistry.swift +80 -0
- package/ios/NativeImagePreview/NativeImagePreviewVideoPlayerView.swift +437 -0
- package/ios/NativeImagePreview/NativeImagePreviewViewerConfiguration.swift +51 -0
- package/ios/NativeImagePreview/NativeImagePreviewViewerController.swift +745 -0
- package/ios/NativeImagePreview/NativeImagePreviewViewerPageView.swift +623 -0
- package/ios/NativeImagePreview/NativeImagePreviewViewerTransitionCoordinator.swift +312 -0
- package/ios/NativeImagePreviewModule.swift +46 -0
- package/ios/NativeImagePreviewView.swift +252 -0
- package/package.json +16 -2
- package/src/all.ts +18 -0
- package/src/services/ImagePreviewService/README.md +19 -0
- package/src/types.d.ts +1 -0
- package/src/ui/NativeImagePreview/NativeImagePreview.tsx +146 -0
- package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +201 -0
- package/src/ui/NativeImagePreview/README.md +111 -0
- package/src/ui/NativeImagePreview/context.tsx +22 -0
- package/src/ui/NativeImagePreview/index.tsx +18 -0
- package/src/ui/NativeImagePreview/types.ts +94 -0
- package/src/ui/NativeImagePreview/utils.ts +108 -0
- package/src/ui/NativeImagePreview/web/geometry.ts +134 -0
- package/src/ui/NativeImagePreview/web/media.ts +91 -0
- package/src/ui/NativeImagePreview/web/styles.css +137 -0
- package/src/ui/NativeImagePreview/web/transition.ts +111 -0
- package/src/ui/NativeImagePreview/web/types.ts +88 -0
- package/src/ui/NativeImagePreview/web/viewer.ts +1024 -0
- 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
|
+
}
|