zkit-ui 2.0.3 → 2.0.5
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/GaleriaModule.kt +43 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GaleriaView.kt +305 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryLayoutSupport.java +450 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryMediaItem.java +36 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryPagerAdapter.java +140 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementGeometry.java +25 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementNames.java +39 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementState.java +25 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySourceViewRegistry.java +115 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryTransitionSnapshotView.java +60 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryVideoPlayerView.java +338 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerOverlayView.java +636 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerPageView.java +357 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerTransitionController.java +223 -0
- package/android/src/main/res/layout/galeria_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 +3 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +56 -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 +66 -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 +82 -0
- package/expo-module.config.json +11 -0
- package/ios/GaleriaModule.swift +46 -0
- package/ios/GaleriaView.swift +253 -0
- package/ios/GalleryViewer/GaleriaImageLoader.swift +73 -0
- package/ios/GalleryViewer/GaleriaImageViewInteraction.swift +245 -0
- package/ios/GalleryViewer/GaleriaLayoutSupport.swift +143 -0
- package/ios/GalleryViewer/GaleriaMedia.swift +28 -0
- package/ios/GalleryViewer/GaleriaSourceViewRegistry.swift +80 -0
- package/ios/GalleryViewer/GaleriaVideoPlayerView.swift +437 -0
- package/ios/GalleryViewer/GaleriaViewerConfiguration.swift +51 -0
- package/ios/GalleryViewer/GaleriaViewerController.swift +745 -0
- package/ios/GalleryViewer/GaleriaViewerPageView.swift +623 -0
- package/ios/GalleryViewer/GaleriaViewerTransitionCoordinator.swift +312 -0
- package/package.json +21 -13
- package/src/all.ts +18 -0
- package/src/services/ImagePreviewService/README.md +19 -0
- package/src/ui/NativeImagePreview/NativeImagePreview.tsx +146 -0
- package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +37 -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 +92 -0
- package/src/ui/NativeImagePreview/utils.ts +107 -0
- package/zkit-ui.podspec +13 -1
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
private final class GaleriaTransitionSnapshotView: UIView {
|
|
4
|
+
private let imageView = UIImageView()
|
|
5
|
+
|
|
6
|
+
init(image: UIImage) {
|
|
7
|
+
super.init(frame: .zero)
|
|
8
|
+
backgroundColor = .clear
|
|
9
|
+
clipsToBounds = true
|
|
10
|
+
isUserInteractionEnabled = false
|
|
11
|
+
|
|
12
|
+
imageView.image = image
|
|
13
|
+
imageView.backgroundColor = .clear
|
|
14
|
+
imageView.contentMode = .scaleToFill
|
|
15
|
+
addSubview(imageView)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
required init?(coder: NSCoder) {
|
|
19
|
+
fatalError("init(coder:) has not been implemented")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func updateImage(_ image: UIImage) {
|
|
23
|
+
imageView.image = image
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func applyContentFrame(_ frame: CGRect) {
|
|
27
|
+
imageView.frame = frame
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
final class GaleriaViewerTransitionCoordinator {
|
|
32
|
+
private weak var containerView: UIView?
|
|
33
|
+
private weak var hiddenAnchorView: UIView?
|
|
34
|
+
private weak var hiddenPageView: GaleriaViewerPageView?
|
|
35
|
+
private var activeSnapshotView: UIView?
|
|
36
|
+
|
|
37
|
+
init(containerView: UIView) {
|
|
38
|
+
self.containerView = containerView
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static func aspectFitRect(for imageSize: CGSize, in bounds: CGRect) -> CGRect {
|
|
42
|
+
guard imageSize.width > 0, imageSize.height > 0 else { return bounds }
|
|
43
|
+
|
|
44
|
+
let widthRatio = bounds.width / imageSize.width
|
|
45
|
+
let heightRatio = bounds.height / imageSize.height
|
|
46
|
+
let scale = min(widthRatio, heightRatio)
|
|
47
|
+
let fittedSize = CGSize(
|
|
48
|
+
width: imageSize.width * scale,
|
|
49
|
+
height: imageSize.height * scale
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return CGRect(
|
|
53
|
+
x: bounds.midX - fittedSize.width * 0.5,
|
|
54
|
+
y: bounds.midY - fittedSize.height * 0.5,
|
|
55
|
+
width: fittedSize.width,
|
|
56
|
+
height: fittedSize.height
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func performOpenTransition(
|
|
61
|
+
from sourceView: UIImageView?,
|
|
62
|
+
to pageView: GaleriaViewerPageView?,
|
|
63
|
+
backgroundView: UIView,
|
|
64
|
+
contentView: UIView,
|
|
65
|
+
completion: @escaping () -> Void
|
|
66
|
+
) {
|
|
67
|
+
guard let containerView = containerView else {
|
|
68
|
+
completion()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
restoreHiddenViews()
|
|
73
|
+
activeSnapshotView?.removeFromSuperview()
|
|
74
|
+
activeSnapshotView = nil
|
|
75
|
+
|
|
76
|
+
backgroundView.alpha = 0
|
|
77
|
+
contentView.alpha = 0
|
|
78
|
+
|
|
79
|
+
flushLayoutForSharedElementSampling(of: sourceView)
|
|
80
|
+
guard let sourceView = sourceView, let sourceState = sourceView.galeriaSharedElementState() else {
|
|
81
|
+
animateSimpleAppearance(
|
|
82
|
+
backgroundView: backgroundView,
|
|
83
|
+
contentView: contentView,
|
|
84
|
+
completion: completion
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let snapshotView = makeSnapshot(from: sourceState)
|
|
90
|
+
apply(sourceState.geometry, to: snapshotView)
|
|
91
|
+
containerView.addSubview(snapshotView)
|
|
92
|
+
activeSnapshotView = snapshotView
|
|
93
|
+
|
|
94
|
+
hiddenAnchorView = sourceView
|
|
95
|
+
sourceView.alpha = 0
|
|
96
|
+
pageView?.setMediaHidden(true)
|
|
97
|
+
hiddenPageView = pageView
|
|
98
|
+
|
|
99
|
+
let animateIntoPage: (Bool) -> Void = { [weak self] isReady in
|
|
100
|
+
guard let self = self, let containerView = self.containerView else {
|
|
101
|
+
completion()
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let windowBounds = containerView.window?.bounds ?? UIScreen.main.bounds
|
|
106
|
+
let targetGeometry: GaleriaSharedElementGeometry
|
|
107
|
+
if isReady, let pageState = pageView?.sharedElementState() {
|
|
108
|
+
targetGeometry = pageState.geometry
|
|
109
|
+
} else if let pageView = pageView {
|
|
110
|
+
targetGeometry = pageView.defaultTransitionGeometry(in: windowBounds)
|
|
111
|
+
} else {
|
|
112
|
+
targetGeometry = self.defaultGeometry(for: sourceState.image.size, in: windowBounds)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
UIView.animate(
|
|
116
|
+
withDuration: 0.32,
|
|
117
|
+
delay: 0,
|
|
118
|
+
usingSpringWithDamping: 0.94,
|
|
119
|
+
initialSpringVelocity: 0.08,
|
|
120
|
+
options: [.curveEaseInOut, .allowUserInteraction]
|
|
121
|
+
) {
|
|
122
|
+
backgroundView.alpha = 1
|
|
123
|
+
self.apply(targetGeometry, to: snapshotView)
|
|
124
|
+
} completion: { _ in
|
|
125
|
+
self.restoreHiddenViews()
|
|
126
|
+
contentView.alpha = 1
|
|
127
|
+
snapshotView.removeFromSuperview()
|
|
128
|
+
self.activeSnapshotView = nil
|
|
129
|
+
completion()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
guard let pageView = pageView else {
|
|
134
|
+
animateIntoPage(false)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
waitUntilReady(pageView: pageView, timeout: 0.42) { ready in
|
|
139
|
+
animateIntoPage(ready)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func performCloseTransition(
|
|
144
|
+
from pageView: GaleriaViewerPageView?,
|
|
145
|
+
to anchorView: UIImageView?,
|
|
146
|
+
backgroundView: UIView,
|
|
147
|
+
contentView: UIView,
|
|
148
|
+
completion: @escaping () -> Void
|
|
149
|
+
) {
|
|
150
|
+
guard let containerView = containerView else {
|
|
151
|
+
completion()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
restoreHiddenViews()
|
|
156
|
+
activeSnapshotView?.removeFromSuperview()
|
|
157
|
+
activeSnapshotView = nil
|
|
158
|
+
|
|
159
|
+
guard
|
|
160
|
+
let pageView,
|
|
161
|
+
let pageState = pageView.sharedElementState()
|
|
162
|
+
else {
|
|
163
|
+
animateSimpleDismissal(
|
|
164
|
+
backgroundView: backgroundView,
|
|
165
|
+
contentView: contentView,
|
|
166
|
+
completion: completion
|
|
167
|
+
)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let snapshotView = makeSnapshot(from: pageState)
|
|
172
|
+
apply(pageState.geometry, to: snapshotView)
|
|
173
|
+
containerView.addSubview(snapshotView)
|
|
174
|
+
activeSnapshotView = snapshotView
|
|
175
|
+
|
|
176
|
+
hiddenPageView = pageView
|
|
177
|
+
pageView.setMediaHidden(true)
|
|
178
|
+
|
|
179
|
+
var targetGeometry: GaleriaSharedElementGeometry?
|
|
180
|
+
flushLayoutForSharedElementSampling(of: anchorView)
|
|
181
|
+
if let anchorView = anchorView, let anchorState = anchorView.galeriaSharedElementState() {
|
|
182
|
+
hiddenAnchorView = anchorView
|
|
183
|
+
anchorView.alpha = 0
|
|
184
|
+
targetGeometry = anchorState.geometry
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
UIView.animate(
|
|
188
|
+
withDuration: 0.24,
|
|
189
|
+
delay: 0,
|
|
190
|
+
options: [.curveEaseInOut, .allowUserInteraction]
|
|
191
|
+
) {
|
|
192
|
+
backgroundView.alpha = 0
|
|
193
|
+
contentView.alpha = 0
|
|
194
|
+
if let targetGeometry = targetGeometry {
|
|
195
|
+
self.apply(targetGeometry, to: snapshotView)
|
|
196
|
+
} else {
|
|
197
|
+
snapshotView.alpha = 0
|
|
198
|
+
snapshotView.transform = CGAffineTransform(scaleX: 0.92, y: 0.92)
|
|
199
|
+
}
|
|
200
|
+
} completion: { [weak self] _ in
|
|
201
|
+
self?.restoreHiddenViews()
|
|
202
|
+
snapshotView.removeFromSuperview()
|
|
203
|
+
self?.activeSnapshotView = nil
|
|
204
|
+
completion()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func animateSimpleAppearance(
|
|
209
|
+
backgroundView: UIView,
|
|
210
|
+
contentView: UIView,
|
|
211
|
+
completion: @escaping () -> Void
|
|
212
|
+
) {
|
|
213
|
+
UIView.animate(
|
|
214
|
+
withDuration: 0.18,
|
|
215
|
+
delay: 0,
|
|
216
|
+
options: [.curveEaseOut, .allowUserInteraction]
|
|
217
|
+
) {
|
|
218
|
+
backgroundView.alpha = 1
|
|
219
|
+
contentView.alpha = 1
|
|
220
|
+
} completion: { _ in
|
|
221
|
+
completion()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func animateSimpleDismissal(
|
|
226
|
+
backgroundView: UIView,
|
|
227
|
+
contentView: UIView,
|
|
228
|
+
completion: @escaping () -> Void
|
|
229
|
+
) {
|
|
230
|
+
UIView.animate(
|
|
231
|
+
withDuration: 0.18,
|
|
232
|
+
delay: 0,
|
|
233
|
+
options: [.curveEaseIn, .allowUserInteraction]
|
|
234
|
+
) {
|
|
235
|
+
backgroundView.alpha = 0
|
|
236
|
+
contentView.alpha = 0
|
|
237
|
+
} completion: { _ in
|
|
238
|
+
completion()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private func waitUntilReady(
|
|
243
|
+
pageView: GaleriaViewerPageView,
|
|
244
|
+
timeout: TimeInterval,
|
|
245
|
+
completion: @escaping (Bool) -> Void
|
|
246
|
+
) {
|
|
247
|
+
let deadline = CACurrentMediaTime() + timeout
|
|
248
|
+
var lastStableState: GaleriaSharedElementState?
|
|
249
|
+
var stableFrameCount = 0
|
|
250
|
+
|
|
251
|
+
func poll() {
|
|
252
|
+
pageView.layoutIfNeeded()
|
|
253
|
+
if let currentState = pageView.sharedElementState() {
|
|
254
|
+
if let lastStableState = lastStableState, lastStableState.approximatelyEquals(currentState) {
|
|
255
|
+
stableFrameCount += 1
|
|
256
|
+
} else {
|
|
257
|
+
lastStableState = currentState
|
|
258
|
+
stableFrameCount = 1
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if stableFrameCount >= 2 {
|
|
262
|
+
completion(true)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if CACurrentMediaTime() >= deadline {
|
|
267
|
+
completion(false)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.016, execute: poll)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
poll()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private func frameInContainer(fromWindowFrame windowFrame: CGRect) -> CGRect {
|
|
277
|
+
guard let containerView = containerView else { return windowFrame }
|
|
278
|
+
return containerView.convert(windowFrame, from: nil)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private func flushLayoutForSharedElementSampling(of imageView: UIImageView?) {
|
|
282
|
+
imageView?.superview?.layoutIfNeeded()
|
|
283
|
+
imageView?.layoutIfNeeded()
|
|
284
|
+
containerView?.layoutIfNeeded()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private func makeSnapshot(from state: GaleriaSharedElementState) -> GaleriaTransitionSnapshotView {
|
|
288
|
+
GaleriaTransitionSnapshotView(image: state.image)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private func apply(_ geometry: GaleriaSharedElementGeometry, to snapshotView: GaleriaTransitionSnapshotView) {
|
|
292
|
+
snapshotView.frame = frameInContainer(fromWindowFrame: geometry.visibleFrameInWindow)
|
|
293
|
+
snapshotView.applyContentFrame(geometry.contentFrameInVisibleBounds)
|
|
294
|
+
snapshotView.layer.cornerRadius = geometry.cornerRadius
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private func defaultGeometry(for imageSize: CGSize, in windowBounds: CGRect) -> GaleriaSharedElementGeometry {
|
|
298
|
+
let visibleFrame = Self.aspectFitRect(for: imageSize, in: windowBounds)
|
|
299
|
+
return GaleriaSharedElementGeometry(
|
|
300
|
+
visibleFrameInWindow: visibleFrame,
|
|
301
|
+
contentFrameInVisibleBounds: CGRect(origin: .zero, size: visibleFrame.size),
|
|
302
|
+
cornerRadius: 0
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private func restoreHiddenViews() {
|
|
307
|
+
hiddenAnchorView?.alpha = 1
|
|
308
|
+
hiddenAnchorView = nil
|
|
309
|
+
hiddenPageView?.setMediaHidden(false)
|
|
310
|
+
hiddenPageView = nil
|
|
311
|
+
}
|
|
312
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zkit-ui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -87,6 +87,11 @@
|
|
|
87
87
|
"react-native": "./dist/ui/LinkedScroll/index.js",
|
|
88
88
|
"default": "./dist/ui/LinkedScroll/index.js"
|
|
89
89
|
},
|
|
90
|
+
"./native-image-preview": {
|
|
91
|
+
"types": "./dist/ui/NativeImagePreview/index.d.ts",
|
|
92
|
+
"react-native": "./dist/ui/NativeImagePreview/index.js",
|
|
93
|
+
"default": "./dist/ui/NativeImagePreview/index.js"
|
|
94
|
+
},
|
|
90
95
|
"./loading-spinner": {
|
|
91
96
|
"types": "./dist/ui/LoadingSpinner/index.d.ts",
|
|
92
97
|
"react-native": "./dist/ui/LoadingSpinner/index.js",
|
|
@@ -176,7 +181,9 @@
|
|
|
176
181
|
"url": "https://github.com/Concur-max/zkit/issues"
|
|
177
182
|
},
|
|
178
183
|
"files": [
|
|
184
|
+
"android",
|
|
179
185
|
"dist",
|
|
186
|
+
"expo-module.config.json",
|
|
180
187
|
"src",
|
|
181
188
|
"ios",
|
|
182
189
|
"react-native.config.js",
|
|
@@ -186,24 +193,18 @@
|
|
|
186
193
|
"publishConfig": {
|
|
187
194
|
"registry": "https://registry.npmjs.org/"
|
|
188
195
|
},
|
|
189
|
-
"scripts": {
|
|
190
|
-
"build": "node scripts/build.cjs",
|
|
191
|
-
"verify:entrypoints": "node scripts/verify-entrypoints.cjs",
|
|
192
|
-
"verify": "pnpm run build && pnpm run verify:entrypoints",
|
|
193
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
194
|
-
"prepack": "pnpm run build",
|
|
195
|
-
"prepublishOnly": "pnpm run build"
|
|
196
|
-
},
|
|
197
196
|
"dependencies": {
|
|
198
197
|
"@vant/area-data": "^2.1.0",
|
|
199
198
|
"dayjs": "^1.11.13",
|
|
199
|
+
"react-native-is-edge-to-edge": "^1.1.6",
|
|
200
200
|
"react-native-toast-message": "^2.3.3",
|
|
201
|
-
"zkit-tools": "
|
|
201
|
+
"zkit-tools": "1.0.1"
|
|
202
202
|
},
|
|
203
203
|
"peerDependencies": {
|
|
204
204
|
"@expo/vector-icons": "*",
|
|
205
205
|
"@lodev09/react-native-true-sheet": "*",
|
|
206
206
|
"@shopify/flash-list": "*",
|
|
207
|
+
"expo": "*",
|
|
207
208
|
"expo-image": "*",
|
|
208
209
|
"expo-linear-gradient": "*",
|
|
209
210
|
"lottie-react-native": "*",
|
|
@@ -218,10 +219,10 @@
|
|
|
218
219
|
},
|
|
219
220
|
"devDependencies": {
|
|
220
221
|
"@expo/vector-icons": "^15.1.1",
|
|
221
|
-
"@lodev09/react-native-true-sheet": "workspace:*",
|
|
222
222
|
"@shopify/flash-list": "^2.3.1",
|
|
223
223
|
"@types/react": "~19.1.17",
|
|
224
224
|
"dayjs": "^1.11.13",
|
|
225
|
+
"expo": "~54.0.31",
|
|
225
226
|
"expo-image": "~3.0.11",
|
|
226
227
|
"expo-linear-gradient": "~15.0.8",
|
|
227
228
|
"lottie-react-native": "7.3.2",
|
|
@@ -234,6 +235,13 @@
|
|
|
234
235
|
"react-native-svg": "^15.12.1",
|
|
235
236
|
"react-native-worklets": "0.5.1",
|
|
236
237
|
"sonner-native": "^0.22.2",
|
|
237
|
-
"typescript": "~5.9.3"
|
|
238
|
+
"typescript": "~5.9.3",
|
|
239
|
+
"@lodev09/react-native-true-sheet": "3.10.0"
|
|
240
|
+
},
|
|
241
|
+
"scripts": {
|
|
242
|
+
"build": "node scripts/build.cjs",
|
|
243
|
+
"verify:entrypoints": "node scripts/verify-entrypoints.cjs",
|
|
244
|
+
"verify": "pnpm run build && pnpm run verify:entrypoints",
|
|
245
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
238
246
|
}
|
|
239
|
-
}
|
|
247
|
+
}
|
package/src/all.ts
CHANGED
|
@@ -238,6 +238,24 @@ export type {
|
|
|
238
238
|
LinkedScrollSectionRenderContext,
|
|
239
239
|
LinkedScrollValue,
|
|
240
240
|
} from './ui/LinkedScroll/index';
|
|
241
|
+
export { NativeImagePreview } from './ui/NativeImagePreview/index';
|
|
242
|
+
export type {
|
|
243
|
+
NativeImagePreviewChangeEvent,
|
|
244
|
+
NativeImagePreviewChangeEventPayload,
|
|
245
|
+
NativeImagePreviewChangeMeta,
|
|
246
|
+
NativeImagePreviewChangeReason,
|
|
247
|
+
NativeImagePreviewColorScheme,
|
|
248
|
+
NativeImagePreviewComponent,
|
|
249
|
+
NativeImagePreviewItem,
|
|
250
|
+
NativeImagePreviewItemDescriptor,
|
|
251
|
+
NativeImagePreviewItemProps,
|
|
252
|
+
NativeImagePreviewMediaType,
|
|
253
|
+
NativeImagePreviewNativeProps,
|
|
254
|
+
NativeImagePreviewProps,
|
|
255
|
+
NativeImagePreviewResolvedItem,
|
|
256
|
+
NativeImagePreviewResolvedColorScheme,
|
|
257
|
+
NativeImagePreviewSource,
|
|
258
|
+
} from './ui/NativeImagePreview/index';
|
|
241
259
|
export { ThemeProvider } from './theme/ThemeProvider';
|
|
242
260
|
export { useTheme } from './theme/useTheme';
|
|
243
261
|
export type { Theme, ThemeOverride } from './theme/types';
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`ImagePreview` 是全屏图片预览基础件,支持 iOS / Android / Web 的统一声明式使用,也提供 `imagePreview.open()` 作为全局命令式入口。
|
|
4
4
|
|
|
5
|
+
如果需要从真实缩略图原生共享转场到全屏 viewer,请使用 `NativeImagePreview`。`ImagePreview` 适合普通命令式预览,`NativeImagePreview` 适合相册九宫格、瀑布流和商品图集这类有稳定图片锚点的场景。
|
|
6
|
+
|
|
5
7
|
## 设计取舍
|
|
6
8
|
|
|
7
9
|
- 核心是声明式组件:`open/defaultOpen` 控制显隐,`value/defaultValue/onChange` 控制当前图片索引。
|
|
@@ -60,6 +62,23 @@ console.log(result.reason, result.value);
|
|
|
60
62
|
- `renderHeader/renderFooter/renderOverlay`:自定义顶部、底部和覆盖层内容。
|
|
61
63
|
- `onClose`:关闭动画结束后触发,返回最终索引、图片和关闭原因。
|
|
62
64
|
|
|
65
|
+
## NativeImagePreview 关系
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { NativeImagePreview } from 'zkit-ui/native-image-preview';
|
|
69
|
+
|
|
70
|
+
<NativeImagePreview items={items} colorScheme="dark">
|
|
71
|
+
<NativeImagePreview.Item index={0}>
|
|
72
|
+
<Image source={thumbnailSource} />
|
|
73
|
+
</NativeImagePreview.Item>
|
|
74
|
+
</NativeImagePreview>;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
两者不要混用成同一个本地封装:
|
|
78
|
+
|
|
79
|
+
- `ImagePreview`:普通全屏预览,支持声明式 `open/value` 和命令式 `imagePreview.open()`。
|
|
80
|
+
- `NativeImagePreview`:原生共享转场预览,需要包住真实缩略图节点,iOS / Android 走原生 viewer。
|
|
81
|
+
|
|
63
82
|
## 手动确认点
|
|
64
83
|
|
|
65
84
|
- Android:Modal 返回键、下滑关闭后的状态回收、超长图/大图内存表现。
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Platform, View, useColorScheme } from 'react-native';
|
|
4
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
5
|
+
import {
|
|
6
|
+
controlEdgeToEdgeValues,
|
|
7
|
+
isEdgeToEdge,
|
|
8
|
+
} from 'react-native-is-edge-to-edge';
|
|
9
|
+
import { NativeImagePreviewContext } from './context';
|
|
10
|
+
import type {
|
|
11
|
+
NativeImagePreviewChangeEvent,
|
|
12
|
+
NativeImagePreviewComponent,
|
|
13
|
+
NativeImagePreviewItemProps,
|
|
14
|
+
NativeImagePreviewNativeItem,
|
|
15
|
+
NativeImagePreviewProps,
|
|
16
|
+
} from './types';
|
|
17
|
+
import {
|
|
18
|
+
clampPreviewIndex,
|
|
19
|
+
normalizeNativeImagePreviewItems,
|
|
20
|
+
resolveNativeImagePreviewColorScheme,
|
|
21
|
+
toNativePreviewItems,
|
|
22
|
+
} from './utils';
|
|
23
|
+
|
|
24
|
+
type NativePreviewViewProps = {
|
|
25
|
+
children: React.ReactElement;
|
|
26
|
+
index: number;
|
|
27
|
+
style?: StyleProp<ViewStyle>;
|
|
28
|
+
galleryId: string;
|
|
29
|
+
items: readonly NativeImagePreviewNativeItem[];
|
|
30
|
+
theme: 'dark' | 'light';
|
|
31
|
+
edgeToEdge: boolean;
|
|
32
|
+
closeIconName?: string;
|
|
33
|
+
onIndexChange?: (event: NativeImagePreviewChangeEvent) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const NativePreviewView = requireNativeView<NativePreviewViewProps>(
|
|
37
|
+
'ZKitNativeImagePreview'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const ANDROID_EDGE_TO_EDGE = Platform.OS === 'android' ? isEdgeToEdge() : false;
|
|
41
|
+
|
|
42
|
+
function createGalleryId() {
|
|
43
|
+
return `zkit-native-image-preview-${Date.now().toString(36)}-${Math.random()
|
|
44
|
+
.toString(36)
|
|
45
|
+
.slice(2, 10)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const NativeImagePreviewRoot = React.memo(function NativeImagePreview({
|
|
49
|
+
children,
|
|
50
|
+
colorScheme = 'dark',
|
|
51
|
+
disabled = false,
|
|
52
|
+
edgeToEdge = false,
|
|
53
|
+
items,
|
|
54
|
+
nativeProps,
|
|
55
|
+
onChange,
|
|
56
|
+
}: NativeImagePreviewProps) {
|
|
57
|
+
const systemColorScheme = useColorScheme();
|
|
58
|
+
const galleryIdRef = React.useRef<string | null>(null);
|
|
59
|
+
if (galleryIdRef.current == null) {
|
|
60
|
+
galleryIdRef.current = createGalleryId();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const resolvedItems = React.useMemo(
|
|
64
|
+
() => normalizeNativeImagePreviewItems(items),
|
|
65
|
+
[items]
|
|
66
|
+
);
|
|
67
|
+
const nativeItems = React.useMemo(
|
|
68
|
+
() => toNativePreviewItems(resolvedItems),
|
|
69
|
+
[resolvedItems]
|
|
70
|
+
);
|
|
71
|
+
const resolvedColorScheme = resolveNativeImagePreviewColorScheme(colorScheme, systemColorScheme);
|
|
72
|
+
const contextValue = React.useMemo(
|
|
73
|
+
() => ({
|
|
74
|
+
galleryId: galleryIdRef.current!,
|
|
75
|
+
items: resolvedItems,
|
|
76
|
+
nativeItems,
|
|
77
|
+
colorScheme: resolvedColorScheme,
|
|
78
|
+
disabled,
|
|
79
|
+
edgeToEdge,
|
|
80
|
+
nativeProps,
|
|
81
|
+
onChange,
|
|
82
|
+
}),
|
|
83
|
+
[disabled, edgeToEdge, nativeItems, nativeProps, onChange, resolvedColorScheme, resolvedItems]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<NativeImagePreviewContext.Provider value={contextValue}>
|
|
88
|
+
{children}
|
|
89
|
+
</NativeImagePreviewContext.Provider>
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const NativeImagePreviewItem = React.memo(function NativeImagePreviewItem({
|
|
94
|
+
children,
|
|
95
|
+
disabled = false,
|
|
96
|
+
index,
|
|
97
|
+
onChange,
|
|
98
|
+
style,
|
|
99
|
+
}: NativeImagePreviewItemProps) {
|
|
100
|
+
const context = React.useContext(NativeImagePreviewContext);
|
|
101
|
+
|
|
102
|
+
const handleIndexChange = React.useCallback(
|
|
103
|
+
(event: NativeImagePreviewChangeEvent) => {
|
|
104
|
+
if (!context) return;
|
|
105
|
+
|
|
106
|
+
const value = event.nativeEvent.currentIndex;
|
|
107
|
+
const meta = {
|
|
108
|
+
reason: 'swipe' as const,
|
|
109
|
+
item: context.items[value] ?? null,
|
|
110
|
+
nativeEvent: event,
|
|
111
|
+
};
|
|
112
|
+
context.onChange?.(value, meta);
|
|
113
|
+
onChange?.(value, meta);
|
|
114
|
+
},
|
|
115
|
+
[context, onChange]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!context || context.disabled || disabled || context.nativeItems.length === 0) {
|
|
119
|
+
return <View style={style}>{children}</View>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const resolvedEdgeToEdge = ANDROID_EDGE_TO_EDGE || context.edgeToEdge;
|
|
123
|
+
|
|
124
|
+
if (__DEV__ && Platform.OS === 'android') {
|
|
125
|
+
controlEdgeToEdgeValues({ edgeToEdge: resolvedEdgeToEdge });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<NativePreviewView
|
|
130
|
+
closeIconName={context.nativeProps?.iosCloseIconName}
|
|
131
|
+
edgeToEdge={resolvedEdgeToEdge}
|
|
132
|
+
galleryId={context.galleryId}
|
|
133
|
+
index={clampPreviewIndex(index, context.nativeItems.length)}
|
|
134
|
+
items={context.nativeItems}
|
|
135
|
+
onIndexChange={handleIndexChange}
|
|
136
|
+
style={style}
|
|
137
|
+
theme={context.colorScheme}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</NativePreviewView>
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export const NativeImagePreview = Object.assign(NativeImagePreviewRoot, {
|
|
145
|
+
Item: NativeImagePreviewItem,
|
|
146
|
+
}) as NativeImagePreviewComponent;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
NativeImagePreviewComponent,
|
|
5
|
+
NativeImagePreviewItemProps,
|
|
6
|
+
NativeImagePreviewProps,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
let didWarnUnsupportedWeb = false;
|
|
10
|
+
|
|
11
|
+
function warnUnsupportedWeb() {
|
|
12
|
+
if (!__DEV__ || didWarnUnsupportedWeb) return;
|
|
13
|
+
|
|
14
|
+
didWarnUnsupportedWeb = true;
|
|
15
|
+
console.warn(
|
|
16
|
+
'[zkit-ui][NativeImagePreview] Native shared-transition preview is only available in iOS and Android native builds.'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const NativeImagePreviewRoot = React.memo(function NativeImagePreview({
|
|
21
|
+
children,
|
|
22
|
+
}: NativeImagePreviewProps) {
|
|
23
|
+
React.useEffect(warnUnsupportedWeb, []);
|
|
24
|
+
|
|
25
|
+
return <>{children}</>;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const NativeImagePreviewItem = React.memo(function NativeImagePreviewItem({
|
|
29
|
+
children,
|
|
30
|
+
style,
|
|
31
|
+
}: NativeImagePreviewItemProps) {
|
|
32
|
+
return <View style={style}>{children}</View>;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const NativeImagePreview = Object.assign(NativeImagePreviewRoot, {
|
|
36
|
+
Item: NativeImagePreviewItem,
|
|
37
|
+
}) as NativeImagePreviewComponent;
|