@onekeyfe/react-native-image 3.0.100 → 3.0.102

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) {
@@ -120,6 +123,11 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
120
123
  private enum class DisplayState { LOADING, IMAGE, ERROR, FALLBACK }
121
124
 
122
125
  private val hostView = OneKeyImageHostView(context)
126
+ // Keep requests independent of ScreenStack Fragment teardown while React
127
+ // retains the view.
128
+ private val requestManager by lazy(LazyThreadSafetyMode.NONE) {
129
+ Glide.with(context.applicationContext)
130
+ }
123
131
  private var loadRunnable: Runnable? = null
124
132
  private var currentTarget: CustomViewTarget<OneKeyImageHostView, Drawable>? = null
125
133
  private var generation = 0L
@@ -129,6 +137,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
129
137
  private var requestActive = false
130
138
  private var displayState = DisplayState.LOADING
131
139
  private var displayRunnable: Runnable? = null
140
+ private var pendingDisplayGeneration: Long? = null
132
141
  private var fallbackRunnable: Runnable? = null
133
142
 
134
143
  override val view: View = hostView
@@ -202,6 +211,14 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
202
211
  OneKeyImageGlideRegistry.ensureRegistered(context)
203
212
  hostView.updateSkeletonStyle()
204
213
  hostView.onReadyForRequest = { scheduleLoad() }
214
+ hostView.onAttachmentChanged = { attached ->
215
+ if (attached) {
216
+ schedulePendingDisplayIfNeeded()
217
+ } else {
218
+ displayRunnable?.let(hostView::removeCallbacks)
219
+ displayRunnable = null
220
+ }
221
+ }
205
222
  applyContentFit()
206
223
  applyVariant()
207
224
  }
@@ -229,6 +246,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
229
246
  resetForReuse()
230
247
  disposed = true
231
248
  hostView.onReadyForRequest = null
249
+ hostView.onAttachmentChanged = null
232
250
  super.onDropView()
233
251
  }
234
252
 
@@ -236,6 +254,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
236
254
  disposed = true
237
255
  cancelCurrent(invalidateGeneration = true)
238
256
  hostView.onReadyForRequest = null
257
+ hostView.onAttachmentChanged = null
239
258
  super.dispose()
240
259
  }
241
260
 
@@ -432,7 +451,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
432
451
  }
433
452
  }
434
453
  currentTarget = target
435
- Glide.with(hostView)
454
+ requestManager
436
455
  .asDrawable()
437
456
  .load(OneKeyImageModel.build(requestUrl, headersJson))
438
457
  .apply(requestOptions(policy))
@@ -495,6 +514,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
495
514
  loadRunnable = null
496
515
  displayRunnable?.let(hostView::removeCallbacks)
497
516
  displayRunnable = null
517
+ pendingDisplayGeneration = null
498
518
  fallbackRunnable?.let(hostView::removeCallbacks)
499
519
  fallbackRunnable = null
500
520
  clearCurrentTarget()
@@ -508,18 +528,30 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
508
528
  // Clear the field first so onResourceCleared from our own cancellation
509
529
  // cannot erase state belonging to the next generation/request.
510
530
  currentTarget = null
511
- Glide.with(hostView).clear(target)
531
+ requestManager.clear(target)
512
532
  }
513
533
 
514
534
  private fun scheduleOnDisplay(requestGeneration: Long) {
535
+ pendingDisplayGeneration = requestGeneration
536
+ schedulePendingDisplayIfNeeded()
537
+ }
538
+
539
+ private fun schedulePendingDisplayIfNeeded() {
540
+ val requestGeneration = pendingDisplayGeneration ?: return
515
541
  displayRunnable?.let(hostView::removeCallbacks)
542
+ displayRunnable = null
543
+ if (!hostView.isAttachedToWindow) return
516
544
  val runnable = Runnable {
517
545
  displayRunnable = null
518
546
  if (
547
+ pendingDisplayGeneration == requestGeneration &&
519
548
  requestGeneration == generation &&
520
549
  !disposed &&
521
- displayState == DisplayState.IMAGE
550
+ displayState == DisplayState.IMAGE &&
551
+ hostView.drawable != null &&
552
+ hostView.isAttachedToWindow
522
553
  ) {
554
+ pendingDisplayGeneration = null
523
555
  onDisplay?.invoke()
524
556
  }
525
557
  }
@@ -21,7 +21,7 @@ function unwrap(value) {
21
21
  function normalizeSource(source) {
22
22
  if (source == null) return undefined;
23
23
  const resolved = Image.resolveAssetSource(source);
24
- if (resolved?.uri) {
24
+ if (resolved?.uri?.trim()) {
25
25
  const original = typeof source === 'object' && !Array.isArray(source) ? source : undefined;
26
26
  return {
27
27
  uri: resolved.uri,
@@ -203,7 +203,10 @@ export function OneKeyImage({
203
203
  }
204
204
  export const OneKeyImageCache = {
205
205
  preload(sources) {
206
- 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 => ({
207
210
  uri: source.uri,
208
211
  headersJson: source.headers ? JSON.stringify(source.headers) : undefined,
209
212
  cachePolicy: source.cachePolicy ?? OneKeyImageCachePolicy.MEMORY_DISK,
@@ -212,7 +215,7 @@ export const OneKeyImageCache = {
212
215
  pixelRatio: source.pixelRatio,
213
216
  overscan: source.overscan,
214
217
  optimizeTos: source.optimizeTos
215
- })));
218
+ }))).then(success => success && !hasInvalidSource);
216
219
  },
217
220
  clearMemory: () => nativeCache.clearMemory(),
218
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.100",
3
+ "version": "3.0.102",
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.100",
84
+ "@onekeyfe/react-native-skeleton": "3.0.102",
85
85
  "react": "*",
86
86
  "react-native": "*",
87
87
  "react-native-nitro-modules": "0.37.0"
package/src/index.tsx CHANGED
@@ -126,7 +126,7 @@ function normalizeSource(
126
126
  ): OneKeyImageSource | undefined {
127
127
  if (source == null) return undefined;
128
128
  const resolved = Image.resolveAssetSource(source as ImageSourcePropType);
129
- if (resolved?.uri) {
129
+ if (resolved?.uri?.trim()) {
130
130
  const original =
131
131
  typeof source === 'object' && !Array.isArray(source)
132
132
  ? (source as OneKeyImageSource)
@@ -364,11 +364,17 @@ export function OneKeyImage({
364
364
 
365
365
  export const OneKeyImageCache = {
366
366
  preload(sources: OneKeyImagePreloadInput[]) {
367
- return nativeCache.preload(
368
- sources
369
- .filter((source) => Boolean(source.uri))
370
- .map((source) => ({
371
- 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,
372
378
  headersJson: source.headers
373
379
  ? JSON.stringify(source.headers)
374
380
  : undefined,
@@ -379,7 +385,8 @@ export const OneKeyImageCache = {
379
385
  overscan: source.overscan,
380
386
  optimizeTos: source.optimizeTos,
381
387
  }))
382
- );
388
+ )
389
+ .then((success) => success && !hasInvalidSource);
383
390
  },
384
391
  clearMemory: () => nativeCache.clearMemory(),
385
392
  clearDisk: () => nativeCache.clearDisk(),