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.
Files changed (63) 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/GaleriaModule.kt +43 -0
  5. package/android/src/main/java/cn/fontree/zkit/ui/preview/GaleriaView.kt +305 -0
  6. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryLayoutSupport.java +450 -0
  7. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryMediaItem.java +36 -0
  8. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryPagerAdapter.java +140 -0
  9. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementGeometry.java +25 -0
  10. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementNames.java +39 -0
  11. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementState.java +25 -0
  12. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySourceViewRegistry.java +115 -0
  13. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryTransitionSnapshotView.java +60 -0
  14. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryVideoPlayerView.java +338 -0
  15. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerOverlayView.java +636 -0
  16. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerPageView.java +357 -0
  17. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerTransitionController.java +223 -0
  18. package/android/src/main/res/layout/galeria_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 +3 -0
  26. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -0
  27. package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +56 -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 +66 -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 +82 -0
  40. package/expo-module.config.json +11 -0
  41. package/ios/GaleriaModule.swift +46 -0
  42. package/ios/GaleriaView.swift +253 -0
  43. package/ios/GalleryViewer/GaleriaImageLoader.swift +73 -0
  44. package/ios/GalleryViewer/GaleriaImageViewInteraction.swift +245 -0
  45. package/ios/GalleryViewer/GaleriaLayoutSupport.swift +143 -0
  46. package/ios/GalleryViewer/GaleriaMedia.swift +28 -0
  47. package/ios/GalleryViewer/GaleriaSourceViewRegistry.swift +80 -0
  48. package/ios/GalleryViewer/GaleriaVideoPlayerView.swift +437 -0
  49. package/ios/GalleryViewer/GaleriaViewerConfiguration.swift +51 -0
  50. package/ios/GalleryViewer/GaleriaViewerController.swift +745 -0
  51. package/ios/GalleryViewer/GaleriaViewerPageView.swift +623 -0
  52. package/ios/GalleryViewer/GaleriaViewerTransitionCoordinator.swift +312 -0
  53. package/package.json +21 -13
  54. package/src/all.ts +18 -0
  55. package/src/services/ImagePreviewService/README.md +19 -0
  56. package/src/ui/NativeImagePreview/NativeImagePreview.tsx +146 -0
  57. package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +37 -0
  58. package/src/ui/NativeImagePreview/README.md +111 -0
  59. package/src/ui/NativeImagePreview/context.tsx +22 -0
  60. package/src/ui/NativeImagePreview/index.tsx +18 -0
  61. package/src/ui/NativeImagePreview/types.ts +92 -0
  62. package/src/ui/NativeImagePreview/utils.ts +107 -0
  63. 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",
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": "workspace:*"
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;