zkit-ui 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +56 -0
  2. package/android/build.gradle +100 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewLayoutSupport.java +450 -0
  5. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewMediaItem.java +36 -0
  6. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewModule.kt +43 -0
  7. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewPagerAdapter.java +140 -0
  8. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementGeometry.java +25 -0
  9. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementNames.java +39 -0
  10. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementState.java +25 -0
  11. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSourceViewRegistry.java +115 -0
  12. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewTransitionSnapshotView.java +60 -0
  13. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewVideoPlayerView.java +338 -0
  14. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewView.kt +305 -0
  15. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerOverlayView.java +636 -0
  16. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerPageView.java +357 -0
  17. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerTransitionController.java +223 -0
  18. package/android/src/main/res/layout/native_image_preview_player_view.xml +9 -0
  19. package/dist/all.d.ts +2 -0
  20. package/dist/all.d.ts.map +1 -1
  21. package/dist/all.js +24 -22
  22. package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts +3 -0
  23. package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts.map +1 -0
  24. package/dist/ui/NativeImagePreview/NativeImagePreview.js +99 -0
  25. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts +4 -0
  26. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -0
  27. package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +150 -0
  28. package/dist/ui/NativeImagePreview/context.d.ts +14 -0
  29. package/dist/ui/NativeImagePreview/context.d.ts.map +1 -0
  30. package/dist/ui/NativeImagePreview/context.js +38 -0
  31. package/dist/ui/NativeImagePreview/index.d.ts +3 -0
  32. package/dist/ui/NativeImagePreview/index.d.ts.map +1 -0
  33. package/dist/ui/NativeImagePreview/index.js +5 -0
  34. package/dist/ui/NativeImagePreview/types.d.ts +68 -0
  35. package/dist/ui/NativeImagePreview/types.d.ts.map +1 -0
  36. package/dist/ui/NativeImagePreview/types.js +2 -0
  37. package/dist/ui/NativeImagePreview/utils.d.ts +8 -0
  38. package/dist/ui/NativeImagePreview/utils.d.ts.map +1 -0
  39. package/dist/ui/NativeImagePreview/utils.js +83 -0
  40. package/dist/ui/NativeImagePreview/web/geometry.d.ts +20 -0
  41. package/dist/ui/NativeImagePreview/web/geometry.d.ts.map +1 -0
  42. package/dist/ui/NativeImagePreview/web/geometry.js +121 -0
  43. package/dist/ui/NativeImagePreview/web/media.d.ts +6 -0
  44. package/dist/ui/NativeImagePreview/web/media.d.ts.map +1 -0
  45. package/dist/ui/NativeImagePreview/web/media.js +90 -0
  46. package/dist/ui/NativeImagePreview/web/styles.css +137 -0
  47. package/dist/ui/NativeImagePreview/web/transition.d.ts +15 -0
  48. package/dist/ui/NativeImagePreview/web/transition.d.ts.map +1 -0
  49. package/dist/ui/NativeImagePreview/web/transition.js +77 -0
  50. package/dist/ui/NativeImagePreview/web/types.d.ts +74 -0
  51. package/dist/ui/NativeImagePreview/web/types.d.ts.map +1 -0
  52. package/dist/ui/NativeImagePreview/web/types.js +3 -0
  53. package/dist/ui/NativeImagePreview/web/viewer.d.ts +104 -0
  54. package/dist/ui/NativeImagePreview/web/viewer.d.ts.map +1 -0
  55. package/dist/ui/NativeImagePreview/web/viewer.js +894 -0
  56. package/expo-module.config.json +11 -0
  57. package/ios/NativeImagePreview/NativeImagePreviewImageLoader.swift +73 -0
  58. package/ios/NativeImagePreview/NativeImagePreviewImageViewInteraction.swift +245 -0
  59. package/ios/NativeImagePreview/NativeImagePreviewLayoutSupport.swift +143 -0
  60. package/ios/NativeImagePreview/NativeImagePreviewMedia.swift +28 -0
  61. package/ios/NativeImagePreview/NativeImagePreviewSourceViewRegistry.swift +80 -0
  62. package/ios/NativeImagePreview/NativeImagePreviewVideoPlayerView.swift +437 -0
  63. package/ios/NativeImagePreview/NativeImagePreviewViewerConfiguration.swift +51 -0
  64. package/ios/NativeImagePreview/NativeImagePreviewViewerController.swift +745 -0
  65. package/ios/NativeImagePreview/NativeImagePreviewViewerPageView.swift +623 -0
  66. package/ios/NativeImagePreview/NativeImagePreviewViewerTransitionCoordinator.swift +312 -0
  67. package/ios/NativeImagePreviewModule.swift +46 -0
  68. package/ios/NativeImagePreviewView.swift +252 -0
  69. package/package.json +16 -2
  70. package/src/all.ts +18 -0
  71. package/src/services/ImagePreviewService/README.md +19 -0
  72. package/src/types.d.ts +1 -0
  73. package/src/ui/NativeImagePreview/NativeImagePreview.tsx +146 -0
  74. package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +201 -0
  75. package/src/ui/NativeImagePreview/README.md +111 -0
  76. package/src/ui/NativeImagePreview/context.tsx +22 -0
  77. package/src/ui/NativeImagePreview/index.tsx +18 -0
  78. package/src/ui/NativeImagePreview/types.ts +94 -0
  79. package/src/ui/NativeImagePreview/utils.ts +108 -0
  80. package/src/ui/NativeImagePreview/web/geometry.ts +134 -0
  81. package/src/ui/NativeImagePreview/web/media.ts +91 -0
  82. package/src/ui/NativeImagePreview/web/styles.css +137 -0
  83. package/src/ui/NativeImagePreview/web/transition.ts +111 -0
  84. package/src/ui/NativeImagePreview/web/types.ts +88 -0
  85. package/src/ui/NativeImagePreview/web/viewer.ts +1024 -0
  86. package/zkit-ui.podspec +13 -1
package/README.md CHANGED
@@ -121,6 +121,7 @@ function Demo() {
121
121
  - Sheet:通用方向弹层(H5 bottom/top/left/right 自绘,iOS/Android bottom 内部走原生 TrueSheet)
122
122
  - LoadingService:全局加载 HUD 服务(handle 状态模型、Promise 绑定、Android 无 elevation 残影路径)
123
123
  - ImagePreview:全屏图片预览(声明式 open/value 状态模型 + 全局 imagePreview.open 服务)
124
+ - NativeImagePreview:原生共享转场图片预览(真实缩略图锚点 + 原生 viewer 手势链路)
124
125
  - Picker:底部滚轮选择器(单列/级联、确认提交、iOS 原生 wheel)
125
126
  - AddressCascader:省市区级联选择(内置中国数据)
126
127
  - DatePicker:年/月/日日期选择(空值、范围、精度与确认提交语义)
@@ -209,6 +210,61 @@ imagePreview.open({ images, index: 0 });
209
210
 
210
211
  `ImagePreview` 使用 `open/defaultOpen/onOpenChange` 管理显隐,使用 `value/defaultValue/onChange` 管理当前图片索引。全局 `imagePreview.open()` 返回 handle,可通过 `handle.result` 获取关闭原因和最终索引;`ZKitProvider` 已内置 `ImagePreviewProvider`。
211
212
 
213
+ ### NativeImagePreview 用法
214
+
215
+ ```tsx
216
+ import { Image, StyleSheet, View, type ImageSourcePropType } from 'react-native';
217
+ import { NativeImagePreview, type NativeImagePreviewItemDescriptor } from 'zkit-ui/native-image-preview';
218
+
219
+ const items: NativeImagePreviewItemDescriptor[] = [
220
+ { id: 'cover', source: require('./cover.png'), width: 1200, height: 900 },
221
+ { id: 'detail', source: 'https://example.com/detail.jpg' },
222
+ { id: 'clip', type: 'video', url: videoUrl, poster: require('./poster.png') },
223
+ ];
224
+
225
+ function getThumbnailSource(item: NativeImagePreviewItemDescriptor): ImageSourcePropType | undefined {
226
+ const source = item.poster ?? item.source;
227
+ return typeof source === 'string' ? { uri: source } : source;
228
+ }
229
+
230
+ export function DemoNativeImagePreview() {
231
+ return (
232
+ <NativeImagePreview items={items} colorScheme="dark" edgeToEdge>
233
+ <View style={styles.grid}>
234
+ {items.map((item, index) => {
235
+ const thumbnailSource = getThumbnailSource(item);
236
+ if (!thumbnailSource) return null;
237
+
238
+ return (
239
+ <View key={item.id ?? index} style={styles.tile}>
240
+ <NativeImagePreview.Item index={index} style={StyleSheet.absoluteFill}>
241
+ <Image source={thumbnailSource} style={styles.thumbnail} />
242
+ </NativeImagePreview.Item>
243
+ </View>
244
+ );
245
+ })}
246
+ </View>
247
+ </NativeImagePreview>
248
+ );
249
+ }
250
+
251
+ const styles = StyleSheet.create({
252
+ grid: {
253
+ flexDirection: 'row',
254
+ flexWrap: 'wrap',
255
+ },
256
+ tile: {
257
+ aspectRatio: 1,
258
+ width: '33.333%',
259
+ },
260
+ thumbnail: {
261
+ ...StyleSheet.absoluteFillObject,
262
+ },
263
+ });
264
+ ```
265
+
266
+ `NativeImagePreview` 不是 `ImagePreview` 的命令式替代品,而是面向真实缩略图锚点的共享转场组件。`items` 顺序必须和 `NativeImagePreview.Item index` 对齐,`Item` 内必须渲染真实图片节点;iOS / Android 走原生 viewer,原生模块未注册时不会降级到普通 `ImagePreview`,需要重建 native app;Web 走 H5 viewer,支持共享转场、缩放、滑动、视频播放和拖拽关闭。
267
+
212
268
  ### 目录与导出
213
269
 
214
270
  - 组件放在 `src/ui/`(一组件一文件为主;复杂组件可拆同名子文件)
@@ -0,0 +1,100 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+ apply plugin: 'maven-publish'
4
+
5
+ group = 'cn.fontree.zkit.ui'
6
+ version = '1.0.0'
7
+
8
+ buildscript {
9
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10
+ if (expoModulesCorePlugin.exists()) {
11
+ apply from: expoModulesCorePlugin
12
+ applyKotlinExpoModulesCorePlugin()
13
+ }
14
+
15
+ // Simple helper that allows the root project to override versions declared by this library.
16
+ ext.safeExtGet = { prop, fallback ->
17
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
18
+ }
19
+
20
+ // Ensures backward compatibility
21
+ ext.getKotlinVersion = {
22
+ if (ext.has("kotlinVersion")) {
23
+ ext.kotlinVersion()
24
+ } else {
25
+ ext.safeExtGet("kotlinVersion", "1.8.10")
26
+ }
27
+ }
28
+
29
+ repositories {
30
+ mavenCentral()
31
+ maven { url 'https://jitpack.io' }
32
+ }
33
+
34
+ dependencies {
35
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
36
+ }
37
+ }
38
+
39
+ afterEvaluate {
40
+ publishing {
41
+ publications {
42
+ release(MavenPublication) {
43
+ from components.release
44
+ }
45
+ }
46
+ repositories {
47
+ maven {
48
+ url = mavenLocal().url
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ android {
55
+ compileSdkVersion safeExtGet("compileSdkVersion", 33)
56
+
57
+ compileOptions {
58
+ sourceCompatibility JavaVersion.VERSION_17
59
+ targetCompatibility JavaVersion.VERSION_17
60
+ }
61
+
62
+ kotlinOptions {
63
+ jvmTarget = JavaVersion.VERSION_17.majorVersion
64
+ }
65
+
66
+ namespace "cn.fontree.zkit.ui.preview"
67
+ defaultConfig {
68
+ minSdkVersion safeExtGet("minSdkVersion", 21)
69
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
70
+ versionCode 1
71
+ versionName "1.0.0"
72
+ }
73
+ lintOptions {
74
+ abortOnError false
75
+ }
76
+ publishing {
77
+ singleVariant("release") {
78
+ withSourcesJar()
79
+ }
80
+ }
81
+ }
82
+
83
+ repositories {
84
+ mavenCentral()
85
+ }
86
+
87
+ dependencies {
88
+ def GLIDE_VERSION = "4.16.0"
89
+ implementation 'com.facebook.react:react-android'
90
+ implementation 'androidx.appcompat:appcompat:1.7.0'
91
+
92
+ implementation project(':expo-modules-core')
93
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
94
+ implementation "com.github.bumptech.glide:glide:${GLIDE_VERSION}"
95
+ implementation 'androidx.viewpager2:viewpager2:1.1.0'
96
+ implementation 'androidx.recyclerview:recyclerview:1.4.0'
97
+ implementation 'com.github.chrisbanes:PhotoView:2.3.0'
98
+ implementation 'androidx.media3:media3-exoplayer:1.5.1'
99
+ implementation 'androidx.media3:media3-ui:1.5.1'
100
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,450 @@
1
+ package cn.fontree.zkit.ui.preview;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.graphics.Canvas;
5
+ import android.graphics.Matrix;
6
+ import android.graphics.Rect;
7
+ import android.graphics.RectF;
8
+ import android.graphics.drawable.BitmapDrawable;
9
+ import android.graphics.drawable.Drawable;
10
+ import android.view.View;
11
+ import android.widget.ImageView;
12
+
13
+ import androidx.annotation.NonNull;
14
+ import androidx.annotation.Nullable;
15
+
16
+ import com.github.chrisbanes.photoview.PhotoView;
17
+
18
+ public final class NativeImagePreviewLayoutSupport {
19
+ private static final int MAX_TRANSITION_BITMAP_SIDE = 2048;
20
+
21
+ private NativeImagePreviewLayoutSupport() {
22
+ }
23
+
24
+ @Nullable
25
+ public static NativeImagePreviewSharedElementState captureImageViewState(@Nullable ImageView imageView) {
26
+ return captureImageViewState(imageView, null);
27
+ }
28
+
29
+ @Nullable
30
+ public static NativeImagePreviewSharedElementState captureImageViewState(@Nullable ImageView imageView, @Nullable RectF clippingFrameInWindow) {
31
+ if (imageView == null) {
32
+ return null;
33
+ }
34
+ Drawable drawable = imageView.getDrawable();
35
+ if (drawable == null || imageView.getWidth() <= 0 || imageView.getHeight() <= 0) {
36
+ return null;
37
+ }
38
+
39
+ RectF imageBoundsInWindow = viewBoundsOnScreen(imageView);
40
+ RectF clippingFrame = clippingFrameInWindow != null
41
+ ? new RectF(clippingFrameInWindow)
42
+ : visibleBoundsOnScreen(imageView);
43
+ if (clippingFrame.isEmpty()) {
44
+ clippingFrame = new RectF(imageBoundsInWindow);
45
+ }
46
+
47
+ RectF contentFrameInView = resolveDisplayedContentFrame(imageView, drawable);
48
+ if (contentFrameInView == null || contentFrameInView.isEmpty()) {
49
+ return null;
50
+ }
51
+
52
+ Drawable transitionDrawable = transitionDrawableForImageView(imageView);
53
+ if (transitionDrawable == null) {
54
+ return null;
55
+ }
56
+
57
+ RectF contentFrameInWindow = new RectF(contentFrameInView);
58
+ int[] location = new int[2];
59
+ imageView.getLocationOnScreen(location);
60
+ contentFrameInWindow.offset(location[0], location[1]);
61
+
62
+ RectF visibleFrameInWindow = new RectF();
63
+ boolean intersects = visibleFrameInWindow.setIntersect(clippingFrame, contentFrameInWindow);
64
+ if (!intersects || visibleFrameInWindow.isEmpty()) {
65
+ visibleFrameInWindow = new RectF(clippingFrame);
66
+ }
67
+
68
+ RectF contentFrameInVisibleBounds = new RectF(
69
+ contentFrameInWindow.left - visibleFrameInWindow.left,
70
+ contentFrameInWindow.top - visibleFrameInWindow.top,
71
+ contentFrameInWindow.right - visibleFrameInWindow.left,
72
+ contentFrameInWindow.bottom - visibleFrameInWindow.top
73
+ );
74
+
75
+ return new NativeImagePreviewSharedElementState(
76
+ transitionDrawable,
77
+ new NativeImagePreviewSharedElementGeometry(visibleFrameInWindow, contentFrameInVisibleBounds)
78
+ );
79
+ }
80
+
81
+ @Nullable
82
+ public static NativeImagePreviewSharedElementGeometry captureImageViewGeometry(@Nullable ImageView imageView, @Nullable Drawable geometryDrawable) {
83
+ if (imageView == null || imageView.getWidth() <= 0 || imageView.getHeight() <= 0) {
84
+ return null;
85
+ }
86
+
87
+ RectF imageBoundsInWindow = viewBoundsOnScreen(imageView);
88
+ RectF clippingFrame = visibleBoundsOnScreen(imageView);
89
+ if (clippingFrame.isEmpty()) {
90
+ clippingFrame = new RectF(imageBoundsInWindow);
91
+ }
92
+
93
+ RectF contentFrameInView = null;
94
+ Drawable drawable = geometryDrawable != null ? geometryDrawable : imageView.getDrawable();
95
+ if (drawable != null) {
96
+ contentFrameInView = resolveDisplayedContentFrame(imageView, drawable);
97
+ }
98
+ if (contentFrameInView == null || contentFrameInView.isEmpty()) {
99
+ contentFrameInView = new RectF(0f, 0f, imageView.getWidth(), imageView.getHeight());
100
+ }
101
+
102
+ RectF contentFrameInWindow = new RectF(contentFrameInView);
103
+ int[] location = new int[2];
104
+ imageView.getLocationOnScreen(location);
105
+ contentFrameInWindow.offset(location[0], location[1]);
106
+
107
+ RectF visibleFrameInWindow = new RectF();
108
+ boolean intersects = visibleFrameInWindow.setIntersect(clippingFrame, contentFrameInWindow);
109
+ if (!intersects || visibleFrameInWindow.isEmpty()) {
110
+ visibleFrameInWindow = new RectF(clippingFrame);
111
+ }
112
+
113
+ RectF contentFrameInVisibleBounds = new RectF(
114
+ contentFrameInWindow.left - visibleFrameInWindow.left,
115
+ contentFrameInWindow.top - visibleFrameInWindow.top,
116
+ contentFrameInWindow.right - visibleFrameInWindow.left,
117
+ contentFrameInWindow.bottom - visibleFrameInWindow.top
118
+ );
119
+
120
+ return new NativeImagePreviewSharedElementGeometry(visibleFrameInWindow, contentFrameInVisibleBounds);
121
+ }
122
+
123
+ @NonNull
124
+ public static RectF viewBoundsOnScreen(@NonNull View view) {
125
+ int[] location = new int[2];
126
+ view.getLocationOnScreen(location);
127
+ return new RectF(
128
+ location[0],
129
+ location[1],
130
+ location[0] + view.getWidth(),
131
+ location[1] + view.getHeight()
132
+ );
133
+ }
134
+
135
+ @NonNull
136
+ public static RectF visibleBoundsOnScreen(@NonNull View view) {
137
+ Rect localVisibleRect = new Rect();
138
+ if (!view.getLocalVisibleRect(localVisibleRect) || localVisibleRect.isEmpty()) {
139
+ return viewBoundsOnScreen(view);
140
+ }
141
+
142
+ int[] location = new int[2];
143
+ view.getLocationOnScreen(location);
144
+ return new RectF(
145
+ location[0] + localVisibleRect.left,
146
+ location[1] + localVisibleRect.top,
147
+ location[0] + localVisibleRect.right,
148
+ location[1] + localVisibleRect.bottom
149
+ );
150
+ }
151
+
152
+ @Nullable
153
+ public static Drawable cloneDrawable(@Nullable Drawable drawable, @NonNull View view) {
154
+ if (drawable == null) {
155
+ return null;
156
+ }
157
+ Drawable.ConstantState constantState = drawable.getConstantState();
158
+ if (constantState == null) {
159
+ return drawable.mutate();
160
+ }
161
+ return constantState.newDrawable(view.getResources()).mutate();
162
+ }
163
+
164
+ @Nullable
165
+ public static Drawable transitionDrawableForImageView(@Nullable ImageView imageView) {
166
+ if (imageView == null) {
167
+ return null;
168
+ }
169
+ Drawable draweeActualDrawable = resolveDraweeActualDrawable(imageView);
170
+ if (draweeActualDrawable != null) {
171
+ Drawable clonedActualDrawable = cloneDrawableFromConstantState(draweeActualDrawable, imageView);
172
+ if (clonedActualDrawable != null) {
173
+ return clonedActualDrawable;
174
+ }
175
+ Drawable bitmapCopy = bitmapDrawableCopy(draweeActualDrawable, imageView);
176
+ if (bitmapCopy != null) {
177
+ return bitmapCopy;
178
+ }
179
+ }
180
+ if (isDraweeRootDrawable(imageView.getDrawable())) {
181
+ return null;
182
+ }
183
+ return cloneDrawable(imageView.getDrawable(), imageView);
184
+ }
185
+
186
+ @Nullable
187
+ private static Drawable cloneDrawableFromConstantState(@Nullable Drawable drawable, @NonNull View view) {
188
+ if (drawable == null) {
189
+ return null;
190
+ }
191
+ Drawable.ConstantState constantState = drawable.getConstantState();
192
+ if (constantState == null) {
193
+ return null;
194
+ }
195
+ return constantState.newDrawable(view.getResources()).mutate();
196
+ }
197
+
198
+ @Nullable
199
+ private static Drawable bitmapDrawableCopy(@NonNull Drawable drawable, @NonNull View view) {
200
+ int intrinsicWidth = drawable.getIntrinsicWidth();
201
+ int intrinsicHeight = drawable.getIntrinsicHeight();
202
+ if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
203
+ Rect bounds = drawable.getBounds();
204
+ intrinsicWidth = bounds.width();
205
+ intrinsicHeight = bounds.height();
206
+ }
207
+ if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
208
+ return null;
209
+ }
210
+
211
+ float scale = Math.min(
212
+ 1f,
213
+ MAX_TRANSITION_BITMAP_SIDE / Math.max((float) intrinsicWidth, (float) intrinsicHeight)
214
+ );
215
+ int bitmapWidth = Math.max(1, Math.round(intrinsicWidth * scale));
216
+ int bitmapHeight = Math.max(1, Math.round(intrinsicHeight * scale));
217
+
218
+ Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
219
+ Canvas canvas = new Canvas(bitmap);
220
+ Rect oldBounds = new Rect(drawable.getBounds());
221
+ drawable.setBounds(0, 0, bitmapWidth, bitmapHeight);
222
+ drawable.draw(canvas);
223
+ drawable.setBounds(oldBounds);
224
+ return new BitmapDrawable(view.getResources(), bitmap);
225
+ }
226
+
227
+ @NonNull
228
+ public static NativeImagePreviewSharedElementGeometry defaultGeometryFor(@NonNull Drawable drawable, @NonNull RectF containerBounds) {
229
+ RectF visibleFrame = aspectFitRect(drawable, containerBounds);
230
+ return new NativeImagePreviewSharedElementGeometry(
231
+ visibleFrame,
232
+ new RectF(0f, 0f, visibleFrame.width(), visibleFrame.height())
233
+ );
234
+ }
235
+
236
+ @NonNull
237
+ public static NativeImagePreviewSharedElementGeometry defaultGeometryForAspectRatio(float width, float height, @NonNull RectF containerBounds) {
238
+ float safeWidth = Math.max(1f, width);
239
+ float safeHeight = Math.max(1f, height);
240
+ float widthScale = containerBounds.width() / safeWidth;
241
+ float heightScale = containerBounds.height() / safeHeight;
242
+ float scale = Math.min(widthScale, heightScale);
243
+ float targetWidth = safeWidth * scale;
244
+ float targetHeight = safeHeight * scale;
245
+ float left = containerBounds.left + (containerBounds.width() - targetWidth) * 0.5f;
246
+ float top = containerBounds.top + (containerBounds.height() - targetHeight) * 0.5f;
247
+ RectF visibleFrame = new RectF(left, top, left + targetWidth, top + targetHeight);
248
+ return new NativeImagePreviewSharedElementGeometry(
249
+ visibleFrame,
250
+ new RectF(0f, 0f, visibleFrame.width(), visibleFrame.height())
251
+ );
252
+ }
253
+
254
+ @NonNull
255
+ public static RectF aspectFitRect(@NonNull Drawable drawable, @NonNull RectF containerBounds) {
256
+ int intrinsicWidth = Math.max(1, drawable.getIntrinsicWidth());
257
+ int intrinsicHeight = Math.max(1, drawable.getIntrinsicHeight());
258
+ float widthScale = containerBounds.width() / intrinsicWidth;
259
+ float heightScale = containerBounds.height() / intrinsicHeight;
260
+ float scale = Math.min(widthScale, heightScale);
261
+ float width = intrinsicWidth * scale;
262
+ float height = intrinsicHeight * scale;
263
+ float left = containerBounds.left + (containerBounds.width() - width) * 0.5f;
264
+ float top = containerBounds.top + (containerBounds.height() - height) * 0.5f;
265
+ return new RectF(left, top, left + width, top + height);
266
+ }
267
+
268
+ @Nullable
269
+ private static RectF resolveDisplayedContentFrame(@NonNull ImageView imageView, @NonNull Drawable drawable) {
270
+ RectF viewBounds = new RectF(0f, 0f, imageView.getWidth(), imageView.getHeight());
271
+ if (viewBounds.isEmpty()) {
272
+ return null;
273
+ }
274
+
275
+ if (imageView instanceof PhotoView) {
276
+ RectF displayRect = ((PhotoView) imageView).getDisplayRect();
277
+ if (displayRect != null && !displayRect.isEmpty()) {
278
+ return new RectF(displayRect);
279
+ }
280
+ }
281
+
282
+ RectF draweeContentFrame = resolveDraweeActualContentFrame(imageView);
283
+ if (draweeContentFrame != null && !draweeContentFrame.isEmpty()) {
284
+ return draweeContentFrame;
285
+ }
286
+
287
+ int intrinsicWidth = drawable.getIntrinsicWidth();
288
+ int intrinsicHeight = drawable.getIntrinsicHeight();
289
+ if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
290
+ return new RectF(viewBounds);
291
+ }
292
+
293
+ ImageView.ScaleType scaleType = imageView.getScaleType();
294
+ if (scaleType == ImageView.ScaleType.MATRIX) {
295
+ RectF contentFrameInView = new RectF(0f, 0f, intrinsicWidth, intrinsicHeight);
296
+ Matrix imageMatrix = new Matrix(imageView.getImageMatrix());
297
+ imageMatrix.mapRect(contentFrameInView);
298
+ return contentFrameInView;
299
+ }
300
+
301
+ if (scaleType == ImageView.ScaleType.FIT_XY) {
302
+ return new RectF(viewBounds);
303
+ }
304
+
305
+ float widthScale = viewBounds.width() / intrinsicWidth;
306
+ float heightScale = viewBounds.height() / intrinsicHeight;
307
+ float scale;
308
+ float dx = 0f;
309
+ float dy = 0f;
310
+
311
+ switch (scaleType) {
312
+ case CENTER:
313
+ scale = 1f;
314
+ dx = (viewBounds.width() - intrinsicWidth) * 0.5f;
315
+ dy = (viewBounds.height() - intrinsicHeight) * 0.5f;
316
+ break;
317
+ case CENTER_CROP:
318
+ scale = Math.max(widthScale, heightScale);
319
+ dx = (viewBounds.width() - intrinsicWidth * scale) * 0.5f;
320
+ dy = (viewBounds.height() - intrinsicHeight * scale) * 0.5f;
321
+ break;
322
+ case CENTER_INSIDE:
323
+ scale = Math.min(1f, Math.min(widthScale, heightScale));
324
+ dx = (viewBounds.width() - intrinsicWidth * scale) * 0.5f;
325
+ dy = (viewBounds.height() - intrinsicHeight * scale) * 0.5f;
326
+ break;
327
+ case FIT_START:
328
+ scale = Math.min(widthScale, heightScale);
329
+ break;
330
+ case FIT_END:
331
+ scale = Math.min(widthScale, heightScale);
332
+ dx = viewBounds.width() - intrinsicWidth * scale;
333
+ dy = viewBounds.height() - intrinsicHeight * scale;
334
+ break;
335
+ case FIT_CENTER:
336
+ default:
337
+ scale = Math.min(widthScale, heightScale);
338
+ dx = (viewBounds.width() - intrinsicWidth * scale) * 0.5f;
339
+ dy = (viewBounds.height() - intrinsicHeight * scale) * 0.5f;
340
+ break;
341
+ }
342
+
343
+ float width = intrinsicWidth * scale;
344
+ float height = intrinsicHeight * scale;
345
+ return new RectF(dx, dy, dx + width, dy + height);
346
+ }
347
+
348
+ @Nullable
349
+ private static RectF resolveDraweeActualContentFrame(@NonNull ImageView imageView) {
350
+ try {
351
+ Object hierarchy = imageView.getClass().getMethod("getHierarchy").invoke(imageView);
352
+ if (hierarchy == null) {
353
+ return null;
354
+ }
355
+ RectF bounds = new RectF();
356
+ hierarchy.getClass().getMethod("getActualImageBounds", RectF.class).invoke(hierarchy, bounds);
357
+ if (bounds.width() <= 1f || bounds.height() <= 1f) {
358
+ return null;
359
+ }
360
+ return bounds;
361
+ } catch (Exception ignored) {
362
+ return null;
363
+ }
364
+ }
365
+
366
+ @Nullable
367
+ private static Drawable resolveDraweeActualDrawable(@NonNull ImageView imageView) {
368
+ try {
369
+ Object hierarchy = imageView.getClass().getMethod("getHierarchy").invoke(imageView);
370
+ if (hierarchy == null) {
371
+ return null;
372
+ }
373
+ Object hasImage = hierarchy.getClass().getMethod("hasImage").invoke(hierarchy);
374
+ if (!(hasImage instanceof Boolean) || !((Boolean) hasImage)) {
375
+ return null;
376
+ }
377
+ java.lang.reflect.Field actualImageWrapperField = hierarchy.getClass().getDeclaredField("mActualImageWrapper");
378
+ actualImageWrapperField.setAccessible(true);
379
+ Object actualImageWrapper = actualImageWrapperField.get(hierarchy);
380
+ if (actualImageWrapper == null) {
381
+ return null;
382
+ }
383
+ Object actualDrawable = actualImageWrapper.getClass().getMethod("getDrawable").invoke(actualImageWrapper);
384
+ if (actualDrawable instanceof Drawable) {
385
+ return (Drawable) actualDrawable;
386
+ }
387
+ } catch (Exception ignored) {
388
+ }
389
+
390
+ return findPositiveIntrinsicLeafDrawable(
391
+ imageView.getDrawable(),
392
+ new java.util.IdentityHashMap<>(),
393
+ 0
394
+ );
395
+ }
396
+
397
+ @Nullable
398
+ private static Drawable findPositiveIntrinsicLeafDrawable(
399
+ @Nullable Drawable drawable,
400
+ @NonNull java.util.IdentityHashMap<Drawable, Boolean> visited,
401
+ int depth
402
+ ) {
403
+ if (drawable == null || depth > 12 || visited.containsKey(drawable)) {
404
+ return null;
405
+ }
406
+ visited.put(drawable, true);
407
+
408
+ if (!isDraweeWrapperDrawable(drawable)
409
+ && drawable.getIntrinsicWidth() > 0
410
+ && drawable.getIntrinsicHeight() > 0) {
411
+ return drawable;
412
+ }
413
+
414
+ Drawable current = drawable.getCurrent();
415
+ if (current != null && current != drawable) {
416
+ Drawable found = findPositiveIntrinsicLeafDrawable(current, visited, depth + 1);
417
+ if (found != null) {
418
+ return found;
419
+ }
420
+ }
421
+
422
+ try {
423
+ Object count = drawable.getClass().getMethod("getNumberOfLayers").invoke(drawable);
424
+ if (count instanceof Integer) {
425
+ for (int i = ((Integer) count) - 1; i >= 0; i--) {
426
+ Object layer = drawable.getClass().getMethod("getDrawable", int.class).invoke(drawable, i);
427
+ if (layer instanceof Drawable) {
428
+ Drawable found = findPositiveIntrinsicLeafDrawable((Drawable) layer, visited, depth + 1);
429
+ if (found != null) {
430
+ return found;
431
+ }
432
+ }
433
+ }
434
+ }
435
+ } catch (Exception ignored) {
436
+ }
437
+
438
+ return null;
439
+ }
440
+
441
+ private static boolean isDraweeRootDrawable(@Nullable Drawable drawable) {
442
+ return drawable != null
443
+ && "com.facebook.drawee.generic.RootDrawable".equals(drawable.getClass().getName());
444
+ }
445
+
446
+ private static boolean isDraweeWrapperDrawable(@NonNull Drawable drawable) {
447
+ String className = drawable.getClass().getName();
448
+ return className.startsWith("com.facebook.drawee.");
449
+ }
450
+ }
@@ -0,0 +1,36 @@
1
+ package cn.fontree.zkit.ui.preview;
2
+
3
+ public class NativeImagePreviewMediaItem {
4
+ public enum MediaType {
5
+ IMAGE,
6
+ VIDEO
7
+ }
8
+
9
+ private final String id;
10
+ private final MediaType mediaType;
11
+ private final String sourceUrl;
12
+ private final String thumbnailUrl;
13
+
14
+ public NativeImagePreviewMediaItem(String id, MediaType mediaType, String sourceUrl, String thumbnailUrl) {
15
+ this.id = id;
16
+ this.mediaType = mediaType;
17
+ this.sourceUrl = sourceUrl;
18
+ this.thumbnailUrl = thumbnailUrl;
19
+ }
20
+
21
+ public String getId() {
22
+ return id;
23
+ }
24
+
25
+ public MediaType getMediaType() {
26
+ return mediaType;
27
+ }
28
+
29
+ public String getSourceUrl() {
30
+ return sourceUrl;
31
+ }
32
+
33
+ public String getThumbnailUrl() {
34
+ return thumbnailUrl;
35
+ }
36
+ }