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/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewVideoPlayerView.java
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
package cn.fontree.zkit.ui.preview;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.res.ColorStateList;
|
|
5
|
+
import android.graphics.Color;
|
|
6
|
+
import android.graphics.Rect;
|
|
7
|
+
import android.graphics.drawable.Drawable;
|
|
8
|
+
import android.net.Uri;
|
|
9
|
+
import android.util.TypedValue;
|
|
10
|
+
import android.view.Gravity;
|
|
11
|
+
import android.view.LayoutInflater;
|
|
12
|
+
import android.view.View;
|
|
13
|
+
import android.view.ViewGroup;
|
|
14
|
+
import android.widget.FrameLayout;
|
|
15
|
+
import android.widget.ImageView;
|
|
16
|
+
|
|
17
|
+
import androidx.annotation.NonNull;
|
|
18
|
+
import androidx.annotation.Nullable;
|
|
19
|
+
import androidx.appcompat.content.res.AppCompatResources;
|
|
20
|
+
import androidx.appcompat.widget.AppCompatImageButton;
|
|
21
|
+
import androidx.media3.common.MediaItem;
|
|
22
|
+
import androidx.media3.common.Player;
|
|
23
|
+
import androidx.media3.exoplayer.ExoPlayer;
|
|
24
|
+
import androidx.media3.ui.PlayerControlView;
|
|
25
|
+
import androidx.media3.ui.PlayerView;
|
|
26
|
+
|
|
27
|
+
import com.bumptech.glide.Glide;
|
|
28
|
+
import com.bumptech.glide.load.engine.GlideException;
|
|
29
|
+
import com.bumptech.glide.request.RequestListener;
|
|
30
|
+
import com.bumptech.glide.request.target.Target;
|
|
31
|
+
|
|
32
|
+
public final class NativeImagePreviewVideoPlayerView extends FrameLayout {
|
|
33
|
+
public interface Listener {
|
|
34
|
+
void onCloseRequested();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private final ImageView posterImageView;
|
|
38
|
+
private final PlayerView playerView;
|
|
39
|
+
private final AppCompatImageButton closeButton;
|
|
40
|
+
@Nullable
|
|
41
|
+
private Runnable contentReadyListener;
|
|
42
|
+
@Nullable
|
|
43
|
+
private NativeImagePreviewMediaItem item;
|
|
44
|
+
@Nullable
|
|
45
|
+
private ExoPlayer player;
|
|
46
|
+
@Nullable
|
|
47
|
+
private String preparedSourceUrl;
|
|
48
|
+
@Nullable
|
|
49
|
+
private Listener listener;
|
|
50
|
+
private boolean firstFrameReady;
|
|
51
|
+
private boolean active;
|
|
52
|
+
|
|
53
|
+
private final Player.Listener playerListener = new Player.Listener() {
|
|
54
|
+
@Override
|
|
55
|
+
public void onRenderedFirstFrame() {
|
|
56
|
+
firstFrameReady = true;
|
|
57
|
+
if (active) {
|
|
58
|
+
revealPlayer();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
public NativeImagePreviewVideoPlayerView(@NonNull Context context) {
|
|
64
|
+
super(context);
|
|
65
|
+
setLayoutParams(new LayoutParams(
|
|
66
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
67
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
68
|
+
));
|
|
69
|
+
setBackgroundColor(Color.TRANSPARENT);
|
|
70
|
+
|
|
71
|
+
posterImageView = new ImageView(context);
|
|
72
|
+
posterImageView.setLayoutParams(new LayoutParams(
|
|
73
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
74
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
75
|
+
));
|
|
76
|
+
posterImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
|
77
|
+
posterImageView.setBackgroundColor(Color.TRANSPARENT);
|
|
78
|
+
addView(posterImageView);
|
|
79
|
+
|
|
80
|
+
playerView = (PlayerView) LayoutInflater.from(context)
|
|
81
|
+
.inflate(R.layout.native_image_preview_player_view, this, false);
|
|
82
|
+
playerView.setUseController(true);
|
|
83
|
+
playerView.setControllerAutoShow(true);
|
|
84
|
+
playerView.setControllerHideOnTouch(true);
|
|
85
|
+
playerView.setShutterBackgroundColor(Color.TRANSPARENT);
|
|
86
|
+
playerView.setBackgroundColor(Color.TRANSPARENT);
|
|
87
|
+
playerView.setShowPreviousButton(false);
|
|
88
|
+
playerView.setShowNextButton(false);
|
|
89
|
+
playerView.setShowRewindButton(false);
|
|
90
|
+
playerView.setShowFastForwardButton(false);
|
|
91
|
+
playerView.setShowSubtitleButton(false);
|
|
92
|
+
playerView.setShowVrButton(false);
|
|
93
|
+
playerView.setShowShuffleButton(false);
|
|
94
|
+
playerView.setAlpha(0f);
|
|
95
|
+
playerView.setVisibility(VISIBLE);
|
|
96
|
+
addView(playerView);
|
|
97
|
+
hideControllerView(androidx.media3.ui.R.id.exo_settings);
|
|
98
|
+
hideControllerView(androidx.media3.ui.R.id.exo_overflow_show);
|
|
99
|
+
hideControllerView(androidx.media3.ui.R.id.exo_overflow_hide);
|
|
100
|
+
|
|
101
|
+
closeButton = new AppCompatImageButton(context);
|
|
102
|
+
LayoutParams closeLayoutParams = new LayoutParams(dp(48), dp(48));
|
|
103
|
+
closeLayoutParams.gravity = Gravity.TOP | Gravity.START;
|
|
104
|
+
closeLayoutParams.topMargin = dp(14);
|
|
105
|
+
closeLayoutParams.leftMargin = dp(10);
|
|
106
|
+
closeButton.setLayoutParams(closeLayoutParams);
|
|
107
|
+
closeButton.setImageDrawable(
|
|
108
|
+
AppCompatResources.getDrawable(context, androidx.appcompat.R.drawable.abc_ic_clear_material)
|
|
109
|
+
);
|
|
110
|
+
closeButton.setBackgroundResource(resolveSelectableItemBackgroundBorderless());
|
|
111
|
+
closeButton.setImageTintList(ColorStateList.valueOf(Color.WHITE));
|
|
112
|
+
closeButton.setPadding(dp(12), dp(12), dp(12), dp(12));
|
|
113
|
+
closeButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
|
114
|
+
closeButton.setVisibility(GONE);
|
|
115
|
+
closeButton.setOnClickListener(v -> {
|
|
116
|
+
if (listener != null) {
|
|
117
|
+
listener.onCloseRequested();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
addView(closeButton);
|
|
121
|
+
|
|
122
|
+
playerView.setControllerVisibilityListener((PlayerControlView.VisibilityListener) visibility -> {
|
|
123
|
+
if (firstFrameReady) {
|
|
124
|
+
closeButton.setVisibility(visibility == View.VISIBLE ? View.VISIBLE : View.GONE);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public void setListener(@Nullable Listener listener) {
|
|
130
|
+
this.listener = listener;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public void setOnContentReadyListener(@Nullable Runnable contentReadyListener) {
|
|
134
|
+
this.contentReadyListener = contentReadyListener;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public void bind(@NonNull NativeImagePreviewMediaItem item) {
|
|
138
|
+
this.item = item;
|
|
139
|
+
firstFrameReady = false;
|
|
140
|
+
playerView.animate().cancel();
|
|
141
|
+
playerView.setAlpha(0f);
|
|
142
|
+
playerView.setVisibility(VISIBLE);
|
|
143
|
+
closeButton.setVisibility(GONE);
|
|
144
|
+
posterImageView.animate().cancel();
|
|
145
|
+
posterImageView.setAlpha(1f);
|
|
146
|
+
posterImageView.setVisibility(VISIBLE);
|
|
147
|
+
Glide.with(posterImageView).clear(posterImageView);
|
|
148
|
+
posterImageView.setImageDrawable(null);
|
|
149
|
+
Glide.with(posterImageView)
|
|
150
|
+
.load(item.getThumbnailUrl())
|
|
151
|
+
.dontAnimate()
|
|
152
|
+
.listener(new RequestListener<Drawable>() {
|
|
153
|
+
@Override
|
|
154
|
+
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
|
155
|
+
notifyContentReady();
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Override
|
|
160
|
+
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
|
|
161
|
+
notifyContentReady();
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.into(posterImageView);
|
|
166
|
+
if (active) {
|
|
167
|
+
ensurePlayerPrepared(item.getSourceUrl());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public void setActive(boolean active) {
|
|
172
|
+
this.active = active;
|
|
173
|
+
if (item == null) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (active) {
|
|
177
|
+
ensurePlayerPrepared(item.getSourceUrl());
|
|
178
|
+
if (player != null) {
|
|
179
|
+
player.play();
|
|
180
|
+
}
|
|
181
|
+
if (firstFrameReady) {
|
|
182
|
+
revealPlayer();
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (player != null) {
|
|
186
|
+
player.pause();
|
|
187
|
+
}
|
|
188
|
+
closeButton.setVisibility(GONE);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public void setMediaHidden(boolean hidden) {
|
|
193
|
+
setAlpha(hidden ? 0f : 1f);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Nullable
|
|
197
|
+
public NativeImagePreviewSharedElementState sharedElementState() {
|
|
198
|
+
return NativeImagePreviewLayoutSupport.captureImageViewState(posterImageView);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Nullable
|
|
202
|
+
public Drawable getPosterDrawable() {
|
|
203
|
+
return posterImageView.getDrawable();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public void prepareForReturnTransition() {
|
|
207
|
+
playerView.animate().cancel();
|
|
208
|
+
posterImageView.animate().cancel();
|
|
209
|
+
posterImageView.setAlpha(1f);
|
|
210
|
+
posterImageView.setVisibility(VISIBLE);
|
|
211
|
+
playerView.setAlpha(0f);
|
|
212
|
+
playerView.setVisibility(VISIBLE);
|
|
213
|
+
closeButton.setVisibility(GONE);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
public void restoreAfterDismissCancelled() {
|
|
217
|
+
if (!firstFrameReady) {
|
|
218
|
+
prepareForReturnTransition();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
playerView.setVisibility(VISIBLE);
|
|
222
|
+
playerView.setAlpha(1f);
|
|
223
|
+
posterImageView.setVisibility(VISIBLE);
|
|
224
|
+
posterImageView.setAlpha(0f);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public boolean isTouchOnInteractiveControls(float rawX, float rawY) {
|
|
228
|
+
return isTouchInsideView(playerView.findViewById(androidx.media3.ui.R.id.exo_progress), rawX, rawY, dp(24))
|
|
229
|
+
|| isTouchInsideView(closeButton, rawX, rawY, dp(12));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public void release() {
|
|
233
|
+
Glide.with(posterImageView.getContext().getApplicationContext()).clear(posterImageView);
|
|
234
|
+
posterImageView.setImageDrawable(null);
|
|
235
|
+
posterImageView.animate().cancel();
|
|
236
|
+
posterImageView.setAlpha(1f);
|
|
237
|
+
posterImageView.setVisibility(VISIBLE);
|
|
238
|
+
closeButton.setVisibility(GONE);
|
|
239
|
+
if (player != null) {
|
|
240
|
+
player.removeListener(playerListener);
|
|
241
|
+
player.release();
|
|
242
|
+
player = null;
|
|
243
|
+
playerView.setPlayer(null);
|
|
244
|
+
}
|
|
245
|
+
playerView.animate().cancel();
|
|
246
|
+
playerView.setAlpha(0f);
|
|
247
|
+
playerView.setVisibility(VISIBLE);
|
|
248
|
+
preparedSourceUrl = null;
|
|
249
|
+
firstFrameReady = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private void ensurePlayerPrepared(@NonNull String sourceUrl) {
|
|
253
|
+
if (player == null) {
|
|
254
|
+
player = new ExoPlayer.Builder(getContext()).build();
|
|
255
|
+
player.addListener(playerListener);
|
|
256
|
+
playerView.setPlayer(player);
|
|
257
|
+
}
|
|
258
|
+
if (sourceUrl.equals(preparedSourceUrl)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
firstFrameReady = false;
|
|
262
|
+
playerView.animate().cancel();
|
|
263
|
+
playerView.setAlpha(0f);
|
|
264
|
+
playerView.setVisibility(VISIBLE);
|
|
265
|
+
closeButton.setVisibility(GONE);
|
|
266
|
+
player.setMediaItem(MediaItem.fromUri(Uri.parse(sourceUrl)));
|
|
267
|
+
player.prepare();
|
|
268
|
+
preparedSourceUrl = sourceUrl;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private void revealPlayer() {
|
|
272
|
+
playerView.setVisibility(VISIBLE);
|
|
273
|
+
playerView.animate().cancel();
|
|
274
|
+
playerView.setAlpha(0f);
|
|
275
|
+
posterImageView.animate().cancel();
|
|
276
|
+
posterImageView.setVisibility(VISIBLE);
|
|
277
|
+
posterImageView.setAlpha(1f);
|
|
278
|
+
playerView.animate()
|
|
279
|
+
.alpha(1f)
|
|
280
|
+
.setDuration(180L)
|
|
281
|
+
.start();
|
|
282
|
+
posterImageView.animate()
|
|
283
|
+
.alpha(0f)
|
|
284
|
+
.setDuration(220L)
|
|
285
|
+
.withEndAction(() -> {
|
|
286
|
+
if (playerView.isControllerFullyVisible()) {
|
|
287
|
+
closeButton.setVisibility(VISIBLE);
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
.start();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private void notifyContentReady() {
|
|
294
|
+
if (contentReadyListener != null) {
|
|
295
|
+
contentReadyListener.run();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private void hideControllerView(int viewId) {
|
|
300
|
+
View target = playerView.findViewById(viewId);
|
|
301
|
+
if (target != null) {
|
|
302
|
+
target.setVisibility(GONE);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private boolean isTouchInsideView(@Nullable View target, float rawX, float rawY, int extraPaddingPx) {
|
|
307
|
+
if (target == null || !target.isShown() || target.getWidth() <= 0 || target.getHeight() <= 0) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
int[] location = new int[2];
|
|
311
|
+
target.getLocationOnScreen(location);
|
|
312
|
+
Rect hitRect = new Rect(
|
|
313
|
+
location[0] - extraPaddingPx,
|
|
314
|
+
location[1] - extraPaddingPx,
|
|
315
|
+
location[0] + target.getWidth() + extraPaddingPx,
|
|
316
|
+
location[1] + target.getHeight() + extraPaddingPx
|
|
317
|
+
);
|
|
318
|
+
return hitRect.contains(Math.round(rawX), Math.round(rawY));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private int resolveSelectableItemBackgroundBorderless() {
|
|
322
|
+
TypedValue outValue = new TypedValue();
|
|
323
|
+
boolean resolved = getContext().getTheme().resolveAttribute(
|
|
324
|
+
android.R.attr.selectableItemBackgroundBorderless,
|
|
325
|
+
outValue,
|
|
326
|
+
true
|
|
327
|
+
);
|
|
328
|
+
return resolved ? outValue.resourceId : 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private int dp(int value) {
|
|
332
|
+
return (int) TypedValue.applyDimension(
|
|
333
|
+
TypedValue.COMPLEX_UNIT_DIP,
|
|
334
|
+
value,
|
|
335
|
+
getResources().getDisplayMetrics()
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
package cn.fontree.zkit.ui.preview
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.ContextWrapper
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.widget.ImageView
|
|
9
|
+
import androidx.activity.ComponentActivity
|
|
10
|
+
import androidx.activity.OnBackPressedCallback
|
|
11
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
12
|
+
|
|
13
|
+
enum class Theme(val value: String) {
|
|
14
|
+
Dark("dark"),
|
|
15
|
+
Light("light")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class NativeImagePreviewView(context: Context) : ViewGroup(context) {
|
|
19
|
+
var urls: Array<String>? = null
|
|
20
|
+
var items: List<Map<String, Any?>> = emptyList()
|
|
21
|
+
var theme: Theme = Theme.Dark
|
|
22
|
+
var initialIndex: Int = 0
|
|
23
|
+
set(value) {
|
|
24
|
+
field = value
|
|
25
|
+
bindSourceImageView()
|
|
26
|
+
}
|
|
27
|
+
var previewGroupId: String? = null
|
|
28
|
+
set(value) {
|
|
29
|
+
field = value
|
|
30
|
+
bindSourceImageView()
|
|
31
|
+
}
|
|
32
|
+
var disableHiddenOriginalImage = false
|
|
33
|
+
var edgeToEdge = false
|
|
34
|
+
var transitionOffsetY: Int? = null
|
|
35
|
+
var transitionOffsetX: Int? = 0
|
|
36
|
+
val onIndexChange by EventDispatcher()
|
|
37
|
+
|
|
38
|
+
private var sourceImageView: ImageView? = null
|
|
39
|
+
private var observedSourceImageView: ImageView? = null
|
|
40
|
+
private var sourceImageLayoutListener: View.OnLayoutChangeListener? = null
|
|
41
|
+
private var sourceImageAttachListener: View.OnAttachStateChangeListener? = null
|
|
42
|
+
private var overlayView: NativeImagePreviewViewerOverlayView? = null
|
|
43
|
+
private var overlayBackCallback: OnBackPressedCallback? = null
|
|
44
|
+
|
|
45
|
+
override fun onAttachedToWindow() {
|
|
46
|
+
super.onAttachedToWindow()
|
|
47
|
+
bindSourceImageView()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
51
|
+
for (i in 0 until childCount) {
|
|
52
|
+
val child = getChildAt(i)
|
|
53
|
+
child.layout(0, 0, width, height)
|
|
54
|
+
}
|
|
55
|
+
bindSourceImageView()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun onDetachedFromWindow() {
|
|
59
|
+
overlayView?.dismissImmediately()
|
|
60
|
+
overlayView = null
|
|
61
|
+
overlayBackCallback?.remove()
|
|
62
|
+
overlayBackCallback = null
|
|
63
|
+
super.onDetachedFromWindow()
|
|
64
|
+
unregisterSourceImageView()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private fun scopedPreviewGroupId(): String? = previewGroupId?.takeIf { it.isNotBlank() }
|
|
68
|
+
|
|
69
|
+
private fun transitionNameForIndex(index: Int): String {
|
|
70
|
+
return NativeImagePreviewSharedElementNames.forIndex(scopedPreviewGroupId(), index)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private fun transitionNameForItem(item: NativeImagePreviewMediaItem): String {
|
|
74
|
+
return NativeImagePreviewSharedElementNames.forItem(scopedPreviewGroupId(), item)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private fun bindSourceImageView() {
|
|
78
|
+
val imageView = findImageView(this)
|
|
79
|
+
if (imageView == null) {
|
|
80
|
+
unregisterSourceImageView()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
val previousSourceImageView = sourceImageView
|
|
85
|
+
if (previousSourceImageView != null && previousSourceImageView !== imageView) {
|
|
86
|
+
clearSourceImageObserver()
|
|
87
|
+
if (!isSourceImageViewUsable(previousSourceImageView)) {
|
|
88
|
+
NativeImagePreviewSourceViewRegistry.unregisterView(previousSourceImageView)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
sourceImageView = imageView
|
|
93
|
+
imageView.setOnClickListener {
|
|
94
|
+
openViewer(imageView)
|
|
95
|
+
}
|
|
96
|
+
registerSourceImageViewWhenReady(imageView)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun openViewer(sourceView: ImageView) {
|
|
100
|
+
val mediaItems = buildMediaItems()
|
|
101
|
+
if (mediaItems.isEmpty()) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
if (overlayView != null) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
val safeIndex = initialIndex.coerceIn(0, mediaItems.lastIndex)
|
|
108
|
+
val activity = getActivity(context) ?: return
|
|
109
|
+
val overlayHost = activity.window.decorView as? ViewGroup
|
|
110
|
+
?: activity.findViewById<ViewGroup>(android.R.id.content)
|
|
111
|
+
?: return
|
|
112
|
+
val sourceKey = transitionNameForItem(mediaItems[safeIndex])
|
|
113
|
+
NativeImagePreviewSourceViewRegistry.register(sourceKey, sourceView)
|
|
114
|
+
val overlay = NativeImagePreviewViewerOverlayView(
|
|
115
|
+
activity,
|
|
116
|
+
mediaItems,
|
|
117
|
+
safeIndex,
|
|
118
|
+
theme == Theme.Light,
|
|
119
|
+
scopedPreviewGroupId(),
|
|
120
|
+
object : NativeImagePreviewViewerOverlayView.Listener {
|
|
121
|
+
override fun onOverlayDismissed() {
|
|
122
|
+
if (overlayView != null) {
|
|
123
|
+
overlayView = null
|
|
124
|
+
}
|
|
125
|
+
overlayBackCallback?.remove()
|
|
126
|
+
overlayBackCallback = null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override fun onOverlayIndexChange(index: Int) {
|
|
130
|
+
onIndexChange(mapOf("currentIndex" to index))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
overlayView = overlay
|
|
135
|
+
overlayHost.addView(overlay)
|
|
136
|
+
if (activity is ComponentActivity) {
|
|
137
|
+
overlayBackCallback?.remove()
|
|
138
|
+
overlayBackCallback = object : OnBackPressedCallback(true) {
|
|
139
|
+
override fun handleOnBackPressed() {
|
|
140
|
+
overlayView?.requestClose()
|
|
141
|
+
}
|
|
142
|
+
}.also { callback ->
|
|
143
|
+
activity.onBackPressedDispatcher.addCallback(callback)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private fun buildMediaItems(): List<NativeImagePreviewMediaItem> {
|
|
149
|
+
if (items.isNotEmpty()) {
|
|
150
|
+
return items.mapIndexedNotNull { index, data ->
|
|
151
|
+
val type = data["type"] as? String ?: return@mapIndexedNotNull null
|
|
152
|
+
val url = data["url"] as? String ?: return@mapIndexedNotNull null
|
|
153
|
+
if (type.lowercase() == "video") {
|
|
154
|
+
val poster = data["poster"] as? String ?: url
|
|
155
|
+
NativeImagePreviewMediaItem(index.toString(), NativeImagePreviewMediaItem.MediaType.VIDEO, url, poster)
|
|
156
|
+
} else {
|
|
157
|
+
NativeImagePreviewMediaItem(index.toString(), NativeImagePreviewMediaItem.MediaType.IMAGE, url, url)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
val rawUrls = urls ?: return emptyList()
|
|
162
|
+
return rawUrls.mapIndexed { index, url ->
|
|
163
|
+
NativeImagePreviewMediaItem(index.toString(), NativeImagePreviewMediaItem.MediaType.IMAGE, url, url)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private fun findImageView(view: View): ImageView? {
|
|
168
|
+
var bestImageView: ImageView? = null
|
|
169
|
+
var bestScore = Int.MIN_VALUE
|
|
170
|
+
|
|
171
|
+
fun visit(candidate: View) {
|
|
172
|
+
if (candidate is ImageView) {
|
|
173
|
+
val score = sourceImageViewScore(candidate)
|
|
174
|
+
if (score > bestScore) {
|
|
175
|
+
bestImageView = candidate
|
|
176
|
+
bestScore = score
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (candidate is ViewGroup) {
|
|
180
|
+
for (i in 0 until candidate.childCount) {
|
|
181
|
+
visit(candidate.getChildAt(i))
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
visit(view)
|
|
187
|
+
return bestImageView
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private fun registerSourceImageViewWhenReady(imageView: ImageView) {
|
|
191
|
+
val transitionName = transitionNameForIndex(initialIndex)
|
|
192
|
+
if (isSourceImageViewUsable(imageView)) {
|
|
193
|
+
clearSourceImageObserver(imageView)
|
|
194
|
+
NativeImagePreviewSourceViewRegistry.register(transitionName, imageView)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
observeSourceImageView(imageView)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private fun observeSourceImageView(imageView: ImageView) {
|
|
202
|
+
if (observedSourceImageView === imageView && sourceImageLayoutListener != null) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
clearSourceImageObserver()
|
|
207
|
+
observedSourceImageView = imageView
|
|
208
|
+
val layoutListener = View.OnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
|
|
209
|
+
val observed = view as? ImageView ?: return@OnLayoutChangeListener
|
|
210
|
+
if (sourceImageView !== observed) {
|
|
211
|
+
clearSourceImageObserver(observed)
|
|
212
|
+
return@OnLayoutChangeListener
|
|
213
|
+
}
|
|
214
|
+
if (isSourceImageViewUsable(observed)) {
|
|
215
|
+
registerSourceImageViewWhenReady(observed)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
val attachListener = object : View.OnAttachStateChangeListener {
|
|
219
|
+
override fun onViewAttachedToWindow(view: View) {
|
|
220
|
+
val observed = view as? ImageView ?: return
|
|
221
|
+
if (sourceImageView !== observed) {
|
|
222
|
+
clearSourceImageObserver(observed)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
registerSourceImageViewWhenReady(observed)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
override fun onViewDetachedFromWindow(view: View) = Unit
|
|
229
|
+
}
|
|
230
|
+
sourceImageLayoutListener = layoutListener
|
|
231
|
+
sourceImageAttachListener = attachListener
|
|
232
|
+
imageView.addOnLayoutChangeListener(layoutListener)
|
|
233
|
+
imageView.addOnAttachStateChangeListener(attachListener)
|
|
234
|
+
imageView.post {
|
|
235
|
+
if (sourceImageView !== imageView) {
|
|
236
|
+
return@post
|
|
237
|
+
}
|
|
238
|
+
if (isSourceImageViewUsable(imageView)) {
|
|
239
|
+
registerSourceImageViewWhenReady(imageView)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private fun isSourceImageViewUsable(imageView: ImageView): Boolean {
|
|
245
|
+
return imageView.isAttachedToWindow && imageView.width > 0 && imageView.height > 0
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private fun sourceImageViewScore(imageView: ImageView): Int {
|
|
249
|
+
var score = 0
|
|
250
|
+
if (imageView.isAttachedToWindow) {
|
|
251
|
+
score += 16
|
|
252
|
+
}
|
|
253
|
+
if (imageView.width > 0 && imageView.height > 0) {
|
|
254
|
+
score += 16
|
|
255
|
+
}
|
|
256
|
+
if (imageView.visibility == View.VISIBLE) {
|
|
257
|
+
score += 16
|
|
258
|
+
}
|
|
259
|
+
if (imageView.isShown) {
|
|
260
|
+
score += 16
|
|
261
|
+
}
|
|
262
|
+
if (imageView.drawable != null) {
|
|
263
|
+
score += 8
|
|
264
|
+
}
|
|
265
|
+
if (imageView === sourceImageView) {
|
|
266
|
+
score += 1
|
|
267
|
+
}
|
|
268
|
+
return score
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private fun clearSourceImageObserver(imageView: ImageView? = null) {
|
|
272
|
+
val observed = observedSourceImageView ?: return
|
|
273
|
+
if (imageView != null && observed !== imageView) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
sourceImageLayoutListener?.let { listener ->
|
|
277
|
+
observed.removeOnLayoutChangeListener(listener)
|
|
278
|
+
}
|
|
279
|
+
sourceImageAttachListener?.let { listener ->
|
|
280
|
+
observed.removeOnAttachStateChangeListener(listener)
|
|
281
|
+
}
|
|
282
|
+
sourceImageLayoutListener = null
|
|
283
|
+
sourceImageAttachListener = null
|
|
284
|
+
observedSourceImageView = null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private fun getActivity(context: Context): Activity? {
|
|
288
|
+
var current = context
|
|
289
|
+
while (current is ContextWrapper) {
|
|
290
|
+
if (current is Activity) {
|
|
291
|
+
return current
|
|
292
|
+
}
|
|
293
|
+
current = current.baseContext
|
|
294
|
+
}
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private fun unregisterSourceImageView() {
|
|
299
|
+
clearSourceImageObserver()
|
|
300
|
+
sourceImageView?.let { imageView ->
|
|
301
|
+
NativeImagePreviewSourceViewRegistry.unregisterView(imageView)
|
|
302
|
+
}
|
|
303
|
+
sourceImageView = null
|
|
304
|
+
}
|
|
305
|
+
}
|