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
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
|
+
}
|