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