@onekeyfe/react-native-image 3.0.99 → 3.0.101

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 CHANGED
@@ -56,6 +56,12 @@ large lists conservative, and `autoplay={true}` on other native platforms. Pass
56
56
  `autoplay` explicitly whenever a screen needs the same behavior on both
57
57
  platforms.
58
58
 
59
+ Animated-image canvas limits are intentionally platform-specific. Android caps
60
+ the unavoidable decoded ARGB canvas at 4,194,304 pixels (16 MiB), while iOS
61
+ allows metadata dimensions up to 16,000,000 pixels and separately caps its
62
+ decoded animation frame buffer at 16 MiB. These limits reflect the native
63
+ decoders' different allocation behavior.
64
+
59
65
  `recyclingKey` is available for recycled list cells. Changing it clears stale
60
66
  content before the next source is displayed.
61
67
 
@@ -38,6 +38,7 @@ private class OneKeyImageHostView(context: ThemedReactContext) : ImageView(conte
38
38
  syncPlayback()
39
39
  }
40
40
  var onReadyForRequest: (() -> Unit)? = null
41
+ var onAttachmentChanged: ((Boolean) -> Unit)? = null
41
42
 
42
43
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
43
44
  super.onSizeChanged(w, h, oldw, oldh)
@@ -58,11 +59,13 @@ private class OneKeyImageHostView(context: ThemedReactContext) : ImageView(conte
58
59
  override fun onAttachedToWindow() {
59
60
  super.onAttachedToWindow()
60
61
  syncPlayback()
62
+ onAttachmentChanged?.invoke(true)
61
63
  }
62
64
 
63
65
  override fun onDetachedFromWindow() {
64
66
  super.onDetachedFromWindow()
65
67
  syncPlayback()
68
+ onAttachmentChanged?.invoke(false)
66
69
  }
67
70
 
68
71
  override fun onVisibilityAggregated(isVisible: Boolean) {
@@ -129,6 +132,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
129
132
  private var requestActive = false
130
133
  private var displayState = DisplayState.LOADING
131
134
  private var displayRunnable: Runnable? = null
135
+ private var pendingDisplayGeneration: Long? = null
132
136
  private var fallbackRunnable: Runnable? = null
133
137
 
134
138
  override val view: View = hostView
@@ -202,6 +206,14 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
202
206
  OneKeyImageGlideRegistry.ensureRegistered(context)
203
207
  hostView.updateSkeletonStyle()
204
208
  hostView.onReadyForRequest = { scheduleLoad() }
209
+ hostView.onAttachmentChanged = { attached ->
210
+ if (attached) {
211
+ schedulePendingDisplayIfNeeded()
212
+ } else {
213
+ displayRunnable?.let(hostView::removeCallbacks)
214
+ displayRunnable = null
215
+ }
216
+ }
205
217
  applyContentFit()
206
218
  applyVariant()
207
219
  }
@@ -229,6 +241,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
229
241
  resetForReuse()
230
242
  disposed = true
231
243
  hostView.onReadyForRequest = null
244
+ hostView.onAttachmentChanged = null
232
245
  super.onDropView()
233
246
  }
234
247
 
@@ -236,6 +249,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
236
249
  disposed = true
237
250
  cancelCurrent(invalidateGeneration = true)
238
251
  hostView.onReadyForRequest = null
252
+ hostView.onAttachmentChanged = null
239
253
  super.dispose()
240
254
  }
241
255
 
@@ -466,7 +480,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
466
480
  private fun requestOptions(policy: OneKeyImageCachePolicy): RequestOptions {
467
481
  val options = RequestOptions()
468
482
  .dontTransform()
469
- .downsample(OneKeyImageSafeDownsampleStrategy())
483
+ .downsample(OneKeyImageSafeDownsampleStrategy)
470
484
  return when (policy) {
471
485
  OneKeyImageCachePolicy.MEMORY_DISK -> options.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
472
486
  OneKeyImageCachePolicy.MEMORY -> options.diskCacheStrategy(DiskCacheStrategy.NONE)
@@ -495,6 +509,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
495
509
  loadRunnable = null
496
510
  displayRunnable?.let(hostView::removeCallbacks)
497
511
  displayRunnable = null
512
+ pendingDisplayGeneration = null
498
513
  fallbackRunnable?.let(hostView::removeCallbacks)
499
514
  fallbackRunnable = null
500
515
  clearCurrentTarget()
@@ -512,14 +527,26 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
512
527
  }
513
528
 
514
529
  private fun scheduleOnDisplay(requestGeneration: Long) {
530
+ pendingDisplayGeneration = requestGeneration
531
+ schedulePendingDisplayIfNeeded()
532
+ }
533
+
534
+ private fun schedulePendingDisplayIfNeeded() {
535
+ val requestGeneration = pendingDisplayGeneration ?: return
515
536
  displayRunnable?.let(hostView::removeCallbacks)
537
+ displayRunnable = null
538
+ if (!hostView.isAttachedToWindow) return
516
539
  val runnable = Runnable {
517
540
  displayRunnable = null
518
541
  if (
542
+ pendingDisplayGeneration == requestGeneration &&
519
543
  requestGeneration == generation &&
520
544
  !disposed &&
521
- displayState == DisplayState.IMAGE
545
+ displayState == DisplayState.IMAGE &&
546
+ hostView.drawable != null &&
547
+ hostView.isAttachedToWindow
522
548
  ) {
549
+ pendingDisplayGeneration = null
523
550
  onDisplay?.invoke()
524
551
  }
525
552
  }
@@ -23,7 +23,7 @@ class HybridOneKeyImageCache : HybridOneKeyImageCacheSpec() {
23
23
  }
24
24
  val baseOptions = RequestOptions()
25
25
  .dontTransform()
26
- .downsample(OneKeyImageSafeDownsampleStrategy())
26
+ .downsample(OneKeyImageSafeDownsampleStrategy)
27
27
  val options = when (source.cachePolicy ?: OneKeyImageCachePolicy.MEMORY_DISK) {
28
28
  OneKeyImageCachePolicy.MEMORY_DISK -> baseOptions
29
29
  .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
@@ -147,7 +147,7 @@ internal data class OneKeyImageDecodeDimensions(
147
147
  * 16 MiB area cap. `override()` alone cannot enforce an area cap for very wide
148
148
  * or tall sources because Glide's default strategy follows target edges.
149
149
  */
150
- internal class OneKeyImageSafeDownsampleStrategy : DownsampleStrategy() {
150
+ internal object OneKeyImageSafeDownsampleStrategy : DownsampleStrategy() {
151
151
  override fun getScaleFactor(
152
152
  sourceWidth: Int,
153
153
  sourceHeight: Int,
@@ -3,6 +3,7 @@ package com.margelo.nitro.onekeyimage
3
3
  import org.junit.Assert.assertEquals
4
4
  import org.junit.Assert.assertTrue
5
5
  import org.junit.Test
6
+ import java.lang.reflect.Modifier
6
7
 
7
8
  class OneKeyImageDecodeDimensionsTest {
8
9
  @Test
@@ -67,4 +68,19 @@ class OneKeyImageDecodeDimensionsTest {
67
68
 
68
69
  assertTrue(decodedBytes <= OneKeyImageDecodeDimensions.MAX_DECODE_BYTES)
69
70
  }
71
+
72
+ @Test
73
+ fun safeDownsampleStrategyIsSingletonForStableMemoryCacheIdentity() {
74
+ val strategyClass = Class.forName(
75
+ "com.margelo.nitro.onekeyimage.OneKeyImageSafeDownsampleStrategy",
76
+ false,
77
+ javaClass.classLoader,
78
+ )
79
+
80
+ assertTrue(
81
+ strategyClass.declaredFields.any { field ->
82
+ field.name == "INSTANCE" && Modifier.isStatic(field.modifiers)
83
+ },
84
+ )
85
+ }
70
86
  }
@@ -168,10 +168,13 @@ export function OneKeyImage({
168
168
  }) : undefined,
169
169
  onLoadEnd: needsLoadEnd ? callback(() => refs.current.onLoadEnd?.()) : undefined
170
170
  }), [hasOverlay, identity, needsDisplay, needsError, needsHybridRef, needsLoad, needsLoadEnd, needsLoadStart, normalized?.uri, placeholder]);
171
+ const flattenedStyle = StyleSheet.flatten(style);
172
+ const hasRoundedCorners = Boolean(flattenedStyle && [flattenedStyle.borderRadius, flattenedStyle.borderTopLeftRadius, flattenedStyle.borderTopRightRadius, flattenedStyle.borderBottomLeftRadius, flattenedStyle.borderBottomRightRadius, flattenedStyle.borderStartStartRadius, flattenedStyle.borderStartEndRadius, flattenedStyle.borderEndStartRadius, flattenedStyle.borderEndEndRadius].some(radius => typeof radius === 'number' && radius > 0));
173
+ const shouldWrapNative = hasOverlay || hasRoundedCorners;
171
174
  const native = /*#__PURE__*/createElement(NativeOneKeyImage, {
172
175
  ...viewProps,
173
176
  ...nativeCallbacks,
174
- style: hasOverlay ? StyleSheet.absoluteFill : style,
177
+ style: shouldWrapNative ? StyleSheet.absoluteFill : style,
175
178
  sourceUri: normalized?.uri,
176
179
  sourceHeadersJson: normalized?.headers ? JSON.stringify(normalized.headers) : undefined,
177
180
  variant,
@@ -183,7 +186,7 @@ export function OneKeyImage({
183
186
  overscan,
184
187
  loadingStrategy: placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE
185
188
  });
186
- if (!hasOverlay) return native;
189
+ if (!shouldWrapNative) return native;
187
190
  return /*#__PURE__*/_jsxs(View, {
188
191
  style: [style, styles.overlayContainer],
189
192
  collapsable: false,
@@ -200,7 +203,10 @@ export function OneKeyImage({
200
203
  }
201
204
  export const OneKeyImageCache = {
202
205
  preload(sources) {
203
- return nativeCache.preload(sources.filter(source => Boolean(source.uri)).map(source => ({
206
+ const hasInvalidSource = sources.some(source => !source.uri?.trim());
207
+ const validSources = sources.filter(source => Boolean(source.uri?.trim()));
208
+ if (validSources.length === 0) return Promise.resolve(false);
209
+ return nativeCache.preload(validSources.map(source => ({
204
210
  uri: source.uri,
205
211
  headersJson: source.headers ? JSON.stringify(source.headers) : undefined,
206
212
  cachePolicy: source.cachePolicy ?? OneKeyImageCachePolicy.MEMORY_DISK,
@@ -209,7 +215,7 @@ export const OneKeyImageCache = {
209
215
  pixelRatio: source.pixelRatio,
210
216
  overscan: source.overscan,
211
217
  optimizeTos: source.optimizeTos
212
- })));
218
+ }))).then(success => success && !hasInvalidSource);
213
219
  },
214
220
  clearMemory: () => nativeCache.clearMemory(),
215
221
  clearDisk: () => nativeCache.clearDisk(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-image",
3
- "version": "3.0.99",
3
+ "version": "3.0.101",
4
4
  "description": "High-performance native image view for OneKey",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -81,7 +81,7 @@
81
81
  "typescript": "^5.9.2"
82
82
  },
83
83
  "peerDependencies": {
84
- "@onekeyfe/react-native-skeleton": "3.0.99",
84
+ "@onekeyfe/react-native-skeleton": "3.0.101",
85
85
  "react": "*",
86
86
  "react-native": "*",
87
87
  "react-native-nitro-modules": "0.37.0"
package/src/index.tsx CHANGED
@@ -307,10 +307,27 @@ export function OneKeyImage({
307
307
  ]
308
308
  );
309
309
 
310
+ const flattenedStyle = StyleSheet.flatten(style);
311
+ const hasRoundedCorners = Boolean(
312
+ flattenedStyle &&
313
+ [
314
+ flattenedStyle.borderRadius,
315
+ flattenedStyle.borderTopLeftRadius,
316
+ flattenedStyle.borderTopRightRadius,
317
+ flattenedStyle.borderBottomLeftRadius,
318
+ flattenedStyle.borderBottomRightRadius,
319
+ flattenedStyle.borderStartStartRadius,
320
+ flattenedStyle.borderStartEndRadius,
321
+ flattenedStyle.borderEndStartRadius,
322
+ flattenedStyle.borderEndEndRadius,
323
+ ].some((radius) => typeof radius === 'number' && radius > 0)
324
+ );
325
+ const shouldWrapNative = hasOverlay || hasRoundedCorners;
326
+
310
327
  const native = createElement(NativeOneKeyImage, {
311
328
  ...viewProps,
312
329
  ...nativeCallbacks,
313
- style: hasOverlay ? StyleSheet.absoluteFill : style,
330
+ style: shouldWrapNative ? StyleSheet.absoluteFill : style,
314
331
  sourceUri: normalized?.uri,
315
332
  sourceHeadersJson: normalized?.headers
316
333
  ? JSON.stringify(normalized.headers)
@@ -326,7 +343,7 @@ export function OneKeyImage({
326
343
  placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE,
327
344
  });
328
345
 
329
- if (!hasOverlay) return native;
346
+ if (!shouldWrapNative) return native;
330
347
 
331
348
  return (
332
349
  <View style={[style, styles.overlayContainer]} collapsable={false}>
@@ -347,11 +364,17 @@ export function OneKeyImage({
347
364
 
348
365
  export const OneKeyImageCache = {
349
366
  preload(sources: OneKeyImagePreloadInput[]) {
350
- return nativeCache.preload(
351
- sources
352
- .filter((source) => Boolean(source.uri))
353
- .map((source) => ({
354
- uri: source.uri!,
367
+ const hasInvalidSource = sources.some((source) => !source.uri?.trim());
368
+ const validSources = sources.filter(
369
+ (source): source is OneKeyImagePreloadInput & { uri: string } =>
370
+ Boolean(source.uri?.trim())
371
+ );
372
+ if (validSources.length === 0) return Promise.resolve(false);
373
+
374
+ return nativeCache
375
+ .preload(
376
+ validSources.map((source) => ({
377
+ uri: source.uri,
355
378
  headersJson: source.headers
356
379
  ? JSON.stringify(source.headers)
357
380
  : undefined,
@@ -362,7 +385,8 @@ export const OneKeyImageCache = {
362
385
  overscan: source.overscan,
363
386
  optimizeTos: source.optimizeTos,
364
387
  }))
365
- );
388
+ )
389
+ .then((success) => success && !hasInvalidSource);
366
390
  },
367
391
  clearMemory: () => nativeCache.clearMemory(),
368
392
  clearDisk: () => nativeCache.clearDisk(),