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,437 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
final class NativeImagePreviewVideoPlayerView: UIView {
|
|
5
|
+
private var player: AVPlayer?
|
|
6
|
+
private var playerLayer: AVPlayerLayer?
|
|
7
|
+
private var timeObserver: Any?
|
|
8
|
+
private var endObserver: NSObjectProtocol?
|
|
9
|
+
private var readyForDisplayObservation: NSKeyValueObservation?
|
|
10
|
+
private var itemStatusObservation: NSKeyValueObservation?
|
|
11
|
+
private var likelyToKeepUpObservation: NSKeyValueObservation?
|
|
12
|
+
private var bufferFullObservation: NSKeyValueObservation?
|
|
13
|
+
private var playbackGeneration = 0
|
|
14
|
+
private var isScrubbing = false
|
|
15
|
+
private var wasPlayingBeforeScrub = false
|
|
16
|
+
private var hasReportedFirstFrame = false
|
|
17
|
+
private var isPrimingFirstFrame = false
|
|
18
|
+
private var controlsVisible = false
|
|
19
|
+
private var controlsContainerConstraints: [NSLayoutConstraint] = []
|
|
20
|
+
|
|
21
|
+
private let playButton = UIButton(type: .system)
|
|
22
|
+
private let slider = UISlider()
|
|
23
|
+
private let timeLabel = UILabel()
|
|
24
|
+
private let controlsContainer = UIView()
|
|
25
|
+
|
|
26
|
+
var onControlsInteractStart: (() -> Void)?
|
|
27
|
+
var onControlsInteractEnd: (() -> Void)?
|
|
28
|
+
var onFirstFrameReady: (() -> Void)?
|
|
29
|
+
|
|
30
|
+
var url: URL? {
|
|
31
|
+
didSet {
|
|
32
|
+
setupPlayer()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override init(frame: CGRect) {
|
|
37
|
+
super.init(frame: frame)
|
|
38
|
+
setupUI()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
required init?(coder: NSCoder) {
|
|
42
|
+
fatalError("init(coder:) has not been implemented")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override func layoutSubviews() {
|
|
46
|
+
super.layoutSubviews()
|
|
47
|
+
playerLayer?.frame = bounds
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
deinit {
|
|
51
|
+
tearDownPlayer()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func toggleControls() {
|
|
55
|
+
setControlsVisible(!controlsVisible, animated: true)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func resetToStart() {
|
|
59
|
+
player?.pause()
|
|
60
|
+
player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
61
|
+
slider.value = 0
|
|
62
|
+
updateTimeLabel(current: 0, duration: CMTimeGetSeconds(player?.currentItem?.duration ?? .zero))
|
|
63
|
+
updatePlayButton(isPlaying: false)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func play() {
|
|
67
|
+
if hasReportedFirstFrame == false {
|
|
68
|
+
primeFirstFrameIfNeeded()
|
|
69
|
+
}
|
|
70
|
+
player?.isMuted = false
|
|
71
|
+
player?.play()
|
|
72
|
+
updatePlayButton(isPlaying: true)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func pause() {
|
|
76
|
+
player?.pause()
|
|
77
|
+
if hasReportedFirstFrame == false {
|
|
78
|
+
isPrimingFirstFrame = false
|
|
79
|
+
}
|
|
80
|
+
updatePlayButton(isPlaying: false)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func setPlayerVisible(_ visible: Bool) {
|
|
84
|
+
CATransaction.begin()
|
|
85
|
+
CATransaction.setDisableActions(true)
|
|
86
|
+
playerLayer?.frame = bounds
|
|
87
|
+
playerLayer?.isHidden = !visible
|
|
88
|
+
playerLayer?.opacity = visible ? 1 : 0
|
|
89
|
+
CATransaction.commit()
|
|
90
|
+
if visible {
|
|
91
|
+
setNeedsLayout()
|
|
92
|
+
layoutIfNeeded()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func attachControls(to hostView: UIView) {
|
|
97
|
+
if controlsContainer.superview === hostView {
|
|
98
|
+
hostView.bringSubviewToFront(controlsContainer)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
NSLayoutConstraint.deactivate(controlsContainerConstraints)
|
|
103
|
+
controlsContainer.removeFromSuperview()
|
|
104
|
+
hostView.addSubview(controlsContainer)
|
|
105
|
+
controlsContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
106
|
+
controlsContainerConstraints = [
|
|
107
|
+
controlsContainer.bottomAnchor.constraint(equalTo: hostView.safeAreaLayoutGuide.bottomAnchor, constant: -20),
|
|
108
|
+
controlsContainer.leadingAnchor.constraint(equalTo: hostView.leadingAnchor, constant: 12),
|
|
109
|
+
controlsContainer.trailingAnchor.constraint(equalTo: hostView.trailingAnchor, constant: -12),
|
|
110
|
+
controlsContainer.heightAnchor.constraint(equalToConstant: 56),
|
|
111
|
+
]
|
|
112
|
+
NSLayoutConstraint.activate(controlsContainerConstraints)
|
|
113
|
+
hostView.bringSubviewToFront(controlsContainer)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
|
117
|
+
controlsVisible = visible
|
|
118
|
+
let animations = {
|
|
119
|
+
self.controlsContainer.alpha = visible ? 1 : 0
|
|
120
|
+
}
|
|
121
|
+
let completion: (Bool) -> Void = { _ in
|
|
122
|
+
self.controlsContainer.isUserInteractionEnabled = visible
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if animated {
|
|
126
|
+
UIView.animate(
|
|
127
|
+
withDuration: 0.22,
|
|
128
|
+
delay: 0,
|
|
129
|
+
options: [.curveEaseInOut, .beginFromCurrentState],
|
|
130
|
+
animations: animations,
|
|
131
|
+
completion: completion
|
|
132
|
+
)
|
|
133
|
+
} else {
|
|
134
|
+
animations()
|
|
135
|
+
completion(true)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func isTouchOnInteractiveControls(atWindowPoint point: CGPoint) -> Bool {
|
|
140
|
+
return expandedFrameForInteraction(of: controlsContainer, padding: 20)?.contains(point) == true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func isPointInsideInteractiveControls(_ point: CGPoint) -> Bool {
|
|
144
|
+
guard controlsVisible else { return false }
|
|
145
|
+
guard let hostView = controlsContainer.superview else { return false }
|
|
146
|
+
let pointInHost = hostView.convert(point, from: self)
|
|
147
|
+
if controlsContainer.frame.insetBy(dx: -20, dy: -20).contains(pointInHost) {
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func primeFirstFrameIfNeeded() {
|
|
154
|
+
guard let player = player else { return }
|
|
155
|
+
guard hasReportedFirstFrame == false, isPrimingFirstFrame == false else { return }
|
|
156
|
+
isPrimingFirstFrame = true
|
|
157
|
+
player.isMuted = true
|
|
158
|
+
player.play()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func resetForReuse() {
|
|
162
|
+
setControlsVisible(false, animated: false)
|
|
163
|
+
url = nil
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private func setupUI() {
|
|
167
|
+
backgroundColor = .clear
|
|
168
|
+
|
|
169
|
+
controlsContainer.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
|
170
|
+
controlsContainer.layer.cornerRadius = 12
|
|
171
|
+
controlsContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
172
|
+
controlsContainer.alpha = 0
|
|
173
|
+
|
|
174
|
+
playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
|
|
175
|
+
playButton.setPreferredSymbolConfiguration(
|
|
176
|
+
UIImage.SymbolConfiguration(pointSize: 20, weight: .bold),
|
|
177
|
+
forImageIn: .normal
|
|
178
|
+
)
|
|
179
|
+
playButton.tintColor = .white
|
|
180
|
+
playButton.translatesAutoresizingMaskIntoConstraints = false
|
|
181
|
+
playButton.addTarget(self, action: #selector(handlePlayButtonTouchDown), for: .touchDown)
|
|
182
|
+
playButton.addTarget(self, action: #selector(togglePlay), for: .touchUpInside)
|
|
183
|
+
playButton.addTarget(
|
|
184
|
+
self,
|
|
185
|
+
action: #selector(handleControlInteractionEnded),
|
|
186
|
+
for: [.touchUpInside, .touchUpOutside, .touchCancel]
|
|
187
|
+
)
|
|
188
|
+
controlsContainer.addSubview(playButton)
|
|
189
|
+
|
|
190
|
+
slider.minimumValue = 0
|
|
191
|
+
slider.maximumValue = 1
|
|
192
|
+
slider.value = 0
|
|
193
|
+
slider.tintColor = .white
|
|
194
|
+
slider.translatesAutoresizingMaskIntoConstraints = false
|
|
195
|
+
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
|
|
196
|
+
slider.addTarget(self, action: #selector(handleSliderTouchDown), for: .touchDown)
|
|
197
|
+
slider.addTarget(self, action: #selector(handleSliderTouchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel])
|
|
198
|
+
controlsContainer.addSubview(slider)
|
|
199
|
+
|
|
200
|
+
timeLabel.textColor = .white
|
|
201
|
+
timeLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
|
202
|
+
timeLabel.text = "00:00 / 00:00"
|
|
203
|
+
timeLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
204
|
+
controlsContainer.addSubview(timeLabel)
|
|
205
|
+
|
|
206
|
+
NSLayoutConstraint.activate([
|
|
207
|
+
playButton.leadingAnchor.constraint(equalTo: controlsContainer.leadingAnchor, constant: 10),
|
|
208
|
+
playButton.centerYAnchor.constraint(equalTo: controlsContainer.centerYAnchor),
|
|
209
|
+
playButton.widthAnchor.constraint(equalToConstant: 44),
|
|
210
|
+
playButton.heightAnchor.constraint(equalToConstant: 44),
|
|
211
|
+
|
|
212
|
+
slider.leadingAnchor.constraint(equalTo: playButton.trailingAnchor, constant: 10),
|
|
213
|
+
slider.trailingAnchor.constraint(equalTo: timeLabel.leadingAnchor, constant: -10),
|
|
214
|
+
slider.centerYAnchor.constraint(equalTo: controlsContainer.centerYAnchor),
|
|
215
|
+
|
|
216
|
+
timeLabel.trailingAnchor.constraint(equalTo: controlsContainer.trailingAnchor, constant: -12),
|
|
217
|
+
timeLabel.centerYAnchor.constraint(equalTo: controlsContainer.centerYAnchor),
|
|
218
|
+
])
|
|
219
|
+
|
|
220
|
+
attachControls(to: self)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private func setupPlayer() {
|
|
224
|
+
playbackGeneration += 1
|
|
225
|
+
let generation = playbackGeneration
|
|
226
|
+
tearDownPlayer()
|
|
227
|
+
guard let url = url else { return }
|
|
228
|
+
|
|
229
|
+
let item = AVPlayerItem(url: url)
|
|
230
|
+
player = AVPlayer(playerItem: item)
|
|
231
|
+
playerLayer = AVPlayerLayer(player: player)
|
|
232
|
+
playerLayer?.videoGravity = .resizeAspect
|
|
233
|
+
if let playerLayer = playerLayer {
|
|
234
|
+
layer.insertSublayer(playerLayer, at: 0)
|
|
235
|
+
}
|
|
236
|
+
playerLayer?.isHidden = true
|
|
237
|
+
player?.pause()
|
|
238
|
+
player?.isMuted = true
|
|
239
|
+
player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
240
|
+
hasReportedFirstFrame = false
|
|
241
|
+
isPrimingFirstFrame = false
|
|
242
|
+
slider.value = 0
|
|
243
|
+
updateTimeLabel(current: 0, duration: 0)
|
|
244
|
+
updatePlayButton(isPlaying: false)
|
|
245
|
+
|
|
246
|
+
let notifyFirstFrameReadyIfPossible: () -> Void = { [weak self] in
|
|
247
|
+
guard let self = self, self.playbackGeneration == generation, self.hasReportedFirstFrame == false else { return }
|
|
248
|
+
guard let currentItem = self.player?.currentItem else { return }
|
|
249
|
+
let isReadyToPlay = currentItem.status == .readyToPlay
|
|
250
|
+
let isLikelyToKeepUp = currentItem.isPlaybackLikelyToKeepUp || currentItem.isPlaybackBufferFull
|
|
251
|
+
let isReadyForDisplay = self.playerLayer?.isReadyForDisplay == true
|
|
252
|
+
guard isReadyToPlay, isLikelyToKeepUp, isReadyForDisplay else { return }
|
|
253
|
+
self.hasReportedFirstFrame = true
|
|
254
|
+
self.isPrimingFirstFrame = false
|
|
255
|
+
self.player?.pause()
|
|
256
|
+
self.player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
257
|
+
self.player?.isMuted = false
|
|
258
|
+
DispatchQueue.main.async {
|
|
259
|
+
guard self.playbackGeneration == generation else { return }
|
|
260
|
+
DispatchQueue.main.async {
|
|
261
|
+
guard self.playbackGeneration == generation else { return }
|
|
262
|
+
self.onFirstFrameReady?()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
itemStatusObservation = item.observe(\.status, options: [.new, .initial]) { _, _ in
|
|
268
|
+
notifyFirstFrameReadyIfPossible()
|
|
269
|
+
}
|
|
270
|
+
likelyToKeepUpObservation = item.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .initial]) { _, _ in
|
|
271
|
+
notifyFirstFrameReadyIfPossible()
|
|
272
|
+
}
|
|
273
|
+
bufferFullObservation = item.observe(\.isPlaybackBufferFull, options: [.new, .initial]) { _, _ in
|
|
274
|
+
notifyFirstFrameReadyIfPossible()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
readyForDisplayObservation = playerLayer?.observe(\.isReadyForDisplay, options: [.new, .initial]) { [weak self] _, change in
|
|
278
|
+
guard let self = self, self.playbackGeneration == generation, change.newValue == true else {
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
notifyFirstFrameReadyIfPossible()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
timeObserver = player?.addPeriodicTimeObserver(
|
|
285
|
+
forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
|
|
286
|
+
queue: .main
|
|
287
|
+
) { [weak self] time in
|
|
288
|
+
guard let self = self, self.playbackGeneration == generation, let item = self.player?.currentItem else { return }
|
|
289
|
+
let duration = CMTimeGetSeconds(item.duration)
|
|
290
|
+
let current = CMTimeGetSeconds(time)
|
|
291
|
+
if duration.isFinite && duration > 0, !self.isScrubbing {
|
|
292
|
+
self.slider.value = Float(current / duration)
|
|
293
|
+
}
|
|
294
|
+
self.updateTimeLabel(current: current, duration: duration)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
endObserver = NotificationCenter.default.addObserver(
|
|
298
|
+
forName: .AVPlayerItemDidPlayToEndTime,
|
|
299
|
+
object: player?.currentItem,
|
|
300
|
+
queue: .main
|
|
301
|
+
) { [weak self] _ in
|
|
302
|
+
guard let self = self, self.playbackGeneration == generation else { return }
|
|
303
|
+
self.resetToStart()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@objc
|
|
308
|
+
private func togglePlay() {
|
|
309
|
+
if hasReportedFirstFrame == false {
|
|
310
|
+
primeFirstFrameIfNeeded()
|
|
311
|
+
}
|
|
312
|
+
guard let player = player else { return }
|
|
313
|
+
if player.timeControlStatus == .playing {
|
|
314
|
+
player.pause()
|
|
315
|
+
updatePlayButton(isPlaying: false)
|
|
316
|
+
} else {
|
|
317
|
+
player.isMuted = false
|
|
318
|
+
player.play()
|
|
319
|
+
updatePlayButton(isPlaying: true)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@objc
|
|
324
|
+
private func sliderValueChanged() {
|
|
325
|
+
guard let player = player, let item = player.currentItem else { return }
|
|
326
|
+
let duration = CMTimeGetSeconds(item.duration)
|
|
327
|
+
guard duration.isFinite && duration > 0 else { return }
|
|
328
|
+
let targetTime = Double(slider.value) * duration
|
|
329
|
+
isScrubbing = true
|
|
330
|
+
player.seek(
|
|
331
|
+
to: CMTime(seconds: targetTime, preferredTimescale: 600),
|
|
332
|
+
toleranceBefore: .zero,
|
|
333
|
+
toleranceAfter: .zero
|
|
334
|
+
)
|
|
335
|
+
updateTimeLabel(current: targetTime, duration: duration)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private func formatTime(_ seconds: Double) -> String {
|
|
339
|
+
guard seconds.isFinite && seconds >= 0 else { return "--:--" }
|
|
340
|
+
let total = Int(seconds.rounded())
|
|
341
|
+
return String(format: "%02d:%02d", total / 60, total % 60)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private func updateTimeLabel(current: Double, duration: Double) {
|
|
345
|
+
timeLabel.text = "\(formatTime(current)) / \(formatTime(duration))"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private func updatePlayButton(isPlaying: Bool) {
|
|
349
|
+
playButton.setImage(UIImage(systemName: isPlaying ? "pause.fill" : "play.fill"), for: .normal)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private func expandedFrameForInteraction(of view: UIView, padding: CGFloat) -> CGRect? {
|
|
353
|
+
guard controlsVisible else { return nil }
|
|
354
|
+
guard view.alpha > 0.01, !view.bounds.isEmpty else { return nil }
|
|
355
|
+
return view.convert(view.bounds.insetBy(dx: -padding, dy: -padding), to: nil)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@objc
|
|
359
|
+
private func handleSliderTouchDown() {
|
|
360
|
+
guard let player = player else { return }
|
|
361
|
+
isScrubbing = true
|
|
362
|
+
wasPlayingBeforeScrub = player.timeControlStatus == .playing
|
|
363
|
+
player.pause()
|
|
364
|
+
onControlsInteractStart?()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
@objc
|
|
368
|
+
private func handleSliderTouchUp() {
|
|
369
|
+
guard let player = player, let item = player.currentItem else {
|
|
370
|
+
isScrubbing = false
|
|
371
|
+
onControlsInteractEnd?()
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
let duration = CMTimeGetSeconds(item.duration)
|
|
375
|
+
guard duration.isFinite && duration > 0 else {
|
|
376
|
+
isScrubbing = false
|
|
377
|
+
onControlsInteractEnd?()
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
let targetTime = Double(slider.value) * duration
|
|
381
|
+
player.seek(
|
|
382
|
+
to: CMTime(seconds: targetTime, preferredTimescale: 600),
|
|
383
|
+
toleranceBefore: .zero,
|
|
384
|
+
toleranceAfter: .zero
|
|
385
|
+
) { [weak self] _ in
|
|
386
|
+
DispatchQueue.main.async {
|
|
387
|
+
guard let self = self else { return }
|
|
388
|
+
self.isScrubbing = false
|
|
389
|
+
if self.wasPlayingBeforeScrub {
|
|
390
|
+
self.play()
|
|
391
|
+
} else {
|
|
392
|
+
self.pause()
|
|
393
|
+
}
|
|
394
|
+
self.onControlsInteractEnd?()
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@objc
|
|
400
|
+
private func handlePlayButtonTouchDown() {
|
|
401
|
+
onControlsInteractStart?()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@objc
|
|
405
|
+
private func handleControlInteractionEnded() {
|
|
406
|
+
onControlsInteractEnd?()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private func tearDownPlayer() {
|
|
410
|
+
if let timeObserver = timeObserver, let player = player {
|
|
411
|
+
player.removeTimeObserver(timeObserver)
|
|
412
|
+
}
|
|
413
|
+
if let endObserver = endObserver {
|
|
414
|
+
NotificationCenter.default.removeObserver(endObserver)
|
|
415
|
+
}
|
|
416
|
+
readyForDisplayObservation?.invalidate()
|
|
417
|
+
itemStatusObservation?.invalidate()
|
|
418
|
+
likelyToKeepUpObservation?.invalidate()
|
|
419
|
+
bufferFullObservation?.invalidate()
|
|
420
|
+
readyForDisplayObservation = nil
|
|
421
|
+
itemStatusObservation = nil
|
|
422
|
+
likelyToKeepUpObservation = nil
|
|
423
|
+
bufferFullObservation = nil
|
|
424
|
+
timeObserver = nil
|
|
425
|
+
endObserver = nil
|
|
426
|
+
|
|
427
|
+
player?.pause()
|
|
428
|
+
player = nil
|
|
429
|
+
playerLayer?.removeFromSuperlayer()
|
|
430
|
+
playerLayer = nil
|
|
431
|
+
|
|
432
|
+
isScrubbing = false
|
|
433
|
+
wasPlayingBeforeScrub = false
|
|
434
|
+
hasReportedFirstFrame = false
|
|
435
|
+
isPrimingFirstFrame = false
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
public enum NativeImagePreviewViewerTheme {
|
|
4
|
+
case light
|
|
5
|
+
case dark
|
|
6
|
+
|
|
7
|
+
var backgroundColor: UIColor {
|
|
8
|
+
switch self {
|
|
9
|
+
case .light:
|
|
10
|
+
return .white
|
|
11
|
+
case .dark:
|
|
12
|
+
return .black
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var tintColor: UIColor {
|
|
17
|
+
switch self {
|
|
18
|
+
case .light:
|
|
19
|
+
return .black
|
|
20
|
+
case .dark:
|
|
21
|
+
return .white
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public enum NativeImagePreviewViewerBarButton {
|
|
27
|
+
case title(String, onTap: ((Int) -> Void)?)
|
|
28
|
+
case icon(UIImage, onTap: ((Int) -> Void)?)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public struct NativeImagePreviewViewerConfiguration {
|
|
32
|
+
public var theme: NativeImagePreviewViewerTheme
|
|
33
|
+
public var contentMode: UIView.ContentMode
|
|
34
|
+
public var closeIcon: UIImage?
|
|
35
|
+
public var rightBarButton: NativeImagePreviewViewerBarButton?
|
|
36
|
+
public var onIndexChange: ((Int) -> Void)?
|
|
37
|
+
|
|
38
|
+
public init(
|
|
39
|
+
theme: NativeImagePreviewViewerTheme = .light,
|
|
40
|
+
contentMode: UIView.ContentMode = .scaleAspectFit,
|
|
41
|
+
closeIcon: UIImage? = nil,
|
|
42
|
+
rightBarButton: NativeImagePreviewViewerBarButton? = nil,
|
|
43
|
+
onIndexChange: ((Int) -> Void)? = nil
|
|
44
|
+
) {
|
|
45
|
+
self.theme = theme
|
|
46
|
+
self.contentMode = contentMode
|
|
47
|
+
self.closeIcon = closeIcon
|
|
48
|
+
self.rightBarButton = rightBarButton
|
|
49
|
+
self.onIndexChange = onIndexChange
|
|
50
|
+
}
|
|
51
|
+
}
|