@onekeyfe/react-native-image 3.0.156 → 3.0.157-alpha.249

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 (42) hide show
  1. package/README.md +3 -0
  2. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +293 -20
  3. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageCache.kt +34 -4
  4. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageMemoryVariants.kt +119 -0
  5. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageMemoryVariantsTest.kt +149 -0
  6. package/ios/OneKeyImage.swift +268 -38
  7. package/ios/OneKeyImageCache.swift +40 -3
  8. package/ios/OneKeyImageMemoryVariants.swift +137 -0
  9. package/ios/OneKeyImageRequestContext.swift +55 -0
  10. package/ios/tests/OneKeyImageInfrastructureTests.swift +218 -0
  11. package/lib/module/index.js +24 -3
  12. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
  13. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
  14. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
  15. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
  16. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
  17. package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
  18. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
  19. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
  20. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
  21. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
  22. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
  23. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
  24. package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
  25. package/lib/typescript/src/OneKeyImage.nitro.d.ts +6 -0
  26. package/lib/typescript/src/index.d.ts +1 -1
  27. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
  28. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
  29. package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
  31. package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
  32. package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
  33. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
  34. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
  35. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
  36. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
  37. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
  38. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
  39. package/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
  40. package/package.json +2 -2
  41. package/src/OneKeyImage.nitro.ts +6 -0
  42. package/src/index.tsx +23 -1
package/README.md CHANGED
@@ -5,6 +5,9 @@ native loading placeholders, opt-in shared-renderer skeleton, fallback states,
5
5
  cache controls,
6
6
  and conservative OneKey TOS URL resizing without depending on Expo Image.
7
7
 
8
+ The authoritative lifecycle, cache identity, cross-size memory-preview, safety,
9
+ and platform contract is defined in [docs/SPEC.md](docs/SPEC.md).
10
+
8
11
  ```tsx
9
12
  <OneKeyImage
10
13
  source={{ uri: 'https://example.com/avatar.png' }}
@@ -26,6 +26,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
26
26
  import com.bumptech.glide.load.engine.GlideException
27
27
  import com.bumptech.glide.request.RequestListener
28
28
  import com.bumptech.glide.request.RequestOptions
29
+ import com.bumptech.glide.request.target.CustomTarget
29
30
  import com.bumptech.glide.request.target.CustomViewTarget
30
31
  import com.bumptech.glide.request.target.Target
31
32
  import com.bumptech.glide.request.transition.Transition
@@ -46,6 +47,34 @@ private fun Context.findActivity(): Activity? {
46
47
  return currentContext as? Activity
47
48
  }
48
49
 
50
+ internal fun shouldScaleCenterMemoryPreview(
51
+ contentFit: OneKeyImageContentFit,
52
+ candidate: OneKeyImageMemoryVariant,
53
+ target: OneKeyImageMemoryVariant,
54
+ ): Boolean =
55
+ contentFit == OneKeyImageContentFit.CENTER &&
56
+ (candidate.width != target.width || candidate.height != target.height)
57
+
58
+ internal fun shouldProbeMemoryPreview(
59
+ hasTarget: Boolean,
60
+ sameFamily: Boolean,
61
+ hasDrawable: Boolean,
62
+ ): Boolean = !hasTarget || !sameFamily || !hasDrawable
63
+
64
+ internal fun shouldAnimateLoadedImageTransition(
65
+ cacheType: OneKeyImageCacheType,
66
+ fadeDelayElapsed: Boolean,
67
+ animationsEnabled: Boolean,
68
+ replacingMemoryPreview: Boolean,
69
+ ): Boolean =
70
+ !replacingMemoryPreview && cacheType != OneKeyImageCacheType.MEMORY &&
71
+ fadeDelayElapsed && animationsEnabled
72
+
73
+ internal fun memoryProbeRequestOptions(base: RequestOptions): RequestOptions =
74
+ base
75
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
76
+ .onlyRetrieveFromCache(true)
77
+
49
78
  private class OneKeyImageHostView(context: ThemedReactContext) : ImageView(context) {
50
79
  private val roundClipPath = Path()
51
80
  private val roundOutlineProvider = object : ViewOutlineProvider() {
@@ -180,6 +209,7 @@ internal fun oneKeyImageRequestSignature(
180
209
  height: Int,
181
210
  density: Float,
182
211
  resizeWidth: Double? = null,
212
+ resizeHeight: Double? = null,
183
213
  ): String = listOf(
184
214
  rawUrl,
185
215
  sourceHeadersJson.orEmpty(),
@@ -190,6 +220,7 @@ internal fun oneKeyImageRequestSignature(
190
220
  overscan.toString(),
191
221
  round.toString(),
192
222
  resizeWidth.toString(),
223
+ resizeHeight.toString(),
193
224
  width.toString(),
194
225
  height.toString(),
195
226
  density.toString(),
@@ -213,12 +244,16 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
213
244
  }
214
245
  private var loadRunnable: Runnable? = null
215
246
  private var currentTarget: CustomViewTarget<OneKeyImageHostView, Drawable>? = null
247
+ private var memoryPreviewTarget: CustomTarget<Drawable>? = null
248
+ private var memoryPreviewFamily: OneKeyImageMemoryFamilyKey? = null
249
+ private var scalesCenterMemoryPreview = false
216
250
  private var generation = 0L
217
251
  private var lastSignature: String? = null
218
252
  private var disposed = false
219
253
  private var suppressPropEffects = false
220
254
  private var requestActive = false
221
255
  private var displayState = DisplayState.LOADING
256
+ private var isPreservingDisplayedImage = false
222
257
  private var displayRunnable: Runnable? = null
223
258
  private var pendingDisplayGeneration: Long? = null
224
259
  private var fallbackRunnable: Runnable? = null
@@ -304,6 +339,14 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
304
339
  field = value
305
340
  requestIdentityChanged()
306
341
  }
342
+ // Layout hint pair with resizeWidth; the pre-layout memory probe uses both
343
+ // values to reproduce a preload's physical decode size.
344
+ override var resizeHeight: Double? = null
345
+ set(value) {
346
+ if (field == value) return
347
+ field = value
348
+ requestIdentityChanged()
349
+ }
307
350
  override var loadingStrategy: OneKeyImageLoadingStrategy? = OneKeyImageLoadingStrategy.STATIC
308
351
  set(value) {
309
352
  field = value
@@ -385,7 +428,11 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
385
428
  if (suppressPropEffects || disposed) return
386
429
  lastSignature = null
387
430
  cancelCurrent(invalidateGeneration = true)
388
- if (sourceUri.isNullOrBlank()) showFallback() else showLoading(requestIsActive = false)
431
+ if (sourceUri.isNullOrBlank()) {
432
+ showFallback()
433
+ } else if (!showMemoryCachedPreviewBeforeLayout()) {
434
+ showLoading(requestIsActive = false)
435
+ }
389
436
  scheduleLoad()
390
437
  }
391
438
 
@@ -403,6 +450,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
403
450
  optimizeTos = true
404
451
  round = false
405
452
  resizeWidth = null
453
+ resizeHeight = null
406
454
  overscan = 1.1
407
455
  loadingStrategy = OneKeyImageLoadingStrategy.STATIC
408
456
  placeholderColor = null
@@ -414,6 +462,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
414
462
  suppressPropEffects = false
415
463
  requestActive = false
416
464
  displayState = DisplayState.LOADING
465
+ isPreservingDisplayedImage = false
417
466
  hostView.setImageDrawable(null)
418
467
  hostView.drawStateSymbol = false
419
468
  hostView.skeletonRequested = false
@@ -449,12 +498,147 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
449
498
  }
450
499
  }
451
500
 
501
+ private fun showMemoryCachedPreviewBeforeLayout(): Boolean {
502
+ if (Looper.myLooper() != Looper.getMainLooper()) return false
503
+ val rawUrl = sourceUri?.takeIf { it.isNotBlank() } ?: return false
504
+ val policy = cachePolicy ?: OneKeyImageCachePolicy.MEMORY_DISK
505
+ if (
506
+ policy != OneKeyImageCachePolicy.MEMORY &&
507
+ policy != OneKeyImageCachePolicy.MEMORY_DISK
508
+ ) {
509
+ return false
510
+ }
511
+ val displayWidth = resizeWidth?.takeIf { it.isFinite() && it > 0.0 }
512
+ val displayHeight = resizeHeight?.takeIf { it.isFinite() && it > 0.0 }
513
+ if (displayWidth == null && displayHeight == null) return false
514
+ val density = hostView.resources.displayMetrics.density.coerceAtLeast(1f)
515
+ val displaySize = when {
516
+ displayWidth != null && displayHeight != null -> maxOf(displayWidth, displayHeight)
517
+ displayWidth != null -> displayWidth
518
+ else -> requireNotNull(displayHeight)
519
+ }
520
+ val customIdentity = OneKeyImageModel.headers(sourceHeadersJson).isNotEmpty()
521
+ val requestUrl = if (optimizeTos != false) {
522
+ OneKeyTosUrl.optimized(
523
+ rawUrl,
524
+ ceil(displaySize).toInt(),
525
+ density,
526
+ overscan ?: 1.1,
527
+ customIdentity,
528
+ )
529
+ } else rawUrl
530
+ val dimensions = OneKeyImageDecodeDimensions.forPreload(
531
+ displayWidth,
532
+ displayHeight,
533
+ density.toDouble(),
534
+ )
535
+ return showMemoryCachedPreview(
536
+ family = OneKeyImageMemoryFamilyKey.from(rawUrl, sourceHeadersJson),
537
+ exactVariant = OneKeyImageMemoryVariant(
538
+ requestUrl = requestUrl,
539
+ width = dimensions.width,
540
+ height = dimensions.height,
541
+ round = round == true,
542
+ ),
543
+ includeExact = true,
544
+ policy = policy,
545
+ requestGeneration = generation,
546
+ )
547
+ }
548
+
549
+ private fun showMemoryCachedPreview(
550
+ family: OneKeyImageMemoryFamilyKey,
551
+ exactVariant: OneKeyImageMemoryVariant,
552
+ includeExact: Boolean,
553
+ policy: OneKeyImageCachePolicy,
554
+ requestGeneration: Long,
555
+ ): Boolean {
556
+ if (
557
+ Looper.myLooper() != Looper.getMainLooper() ||
558
+ disposed ||
559
+ requestGeneration != generation ||
560
+ (policy != OneKeyImageCachePolicy.MEMORY &&
561
+ policy != OneKeyImageCachePolicy.MEMORY_DISK)
562
+ ) {
563
+ return false
564
+ }
565
+ val candidates = buildList {
566
+ if (includeExact) {
567
+ add(exactVariant)
568
+ if (exactVariant.requestUrl != family.rawUrl) {
569
+ add(exactVariant.copy(requestUrl = family.rawUrl))
570
+ }
571
+ }
572
+ addAll(OneKeyImageMemoryVariantRegistry.candidates(family, exactVariant))
573
+ }.distinct()
574
+
575
+ for (candidate in candidates) {
576
+ var synchronousResource: Drawable? = null
577
+ val target = object : CustomTarget<Drawable>(candidate.width, candidate.height) {
578
+ override fun onResourceReady(
579
+ resource: Drawable,
580
+ transition: Transition<in Drawable>?,
581
+ ) {
582
+ if (
583
+ requestGeneration == generation &&
584
+ !disposed &&
585
+ resource !is Animatable
586
+ ) {
587
+ synchronousResource = resource
588
+ }
589
+ }
590
+
591
+ override fun onLoadCleared(placeholder: Drawable?) = Unit
592
+ }
593
+ requestManager()
594
+ .asDrawable()
595
+ .load(OneKeyImageModel.build(candidate.requestUrl, sourceHeadersJson))
596
+ .apply(
597
+ memoryProbeRequestOptions(
598
+ requestOptions(policy, candidate.requestUrl, candidate.round),
599
+ ),
600
+ )
601
+ .override(candidate.width, candidate.height)
602
+ .into(target)
603
+
604
+ val resource = synchronousResource
605
+ if (resource == null) {
606
+ requestManager().clear(target)
607
+ OneKeyImageMemoryVariantRegistry.remove(family, candidate)
608
+ continue
609
+ }
610
+
611
+ clearMemoryPreviewTarget()
612
+ memoryPreviewTarget = target
613
+ memoryPreviewFamily = family
614
+ scalesCenterMemoryPreview = shouldScaleCenterMemoryPreview(
615
+ contentFit = contentFit ?: OneKeyImageContentFit.COVER,
616
+ candidate = candidate,
617
+ target = exactVariant,
618
+ )
619
+ applyContentFit()
620
+ displayState = DisplayState.IMAGE
621
+ requestActive = false
622
+ isPreservingDisplayedImage = false
623
+ hostView.skeletonRequested = false
624
+ hostView.drawStateSymbol = false
625
+ hostView.setBackgroundColor(Color.TRANSPARENT)
626
+ hostView.setImageDrawable(resource)
627
+ applyAutoplay()
628
+ OneKeyImageMemoryVariantRegistry.record(family, candidate)
629
+ return true
630
+ }
631
+ return false
632
+ }
633
+
452
634
  private fun startLoad(force: Boolean) {
453
635
  if (disposed) return
454
636
  if (hostView.width <= 0 || hostView.height <= 0) {
455
- cancelCurrent(invalidateGeneration = true)
637
+ cancelCurrent(invalidateGeneration = true, clearMemoryPreview = false)
456
638
  lastSignature = null
457
- showLoading(requestIsActive = false)
639
+ if (memoryPreviewTarget == null || hostView.drawable == null) {
640
+ showLoading(requestIsActive = false)
641
+ }
458
642
  return
459
643
  }
460
644
  val rawUrl = sourceUri?.takeIf { it.isNotBlank() }
@@ -477,11 +661,12 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
477
661
  height = hostView.height,
478
662
  density = hostView.resources.displayMetrics.density,
479
663
  resizeWidth = resizeWidth,
664
+ resizeHeight = resizeHeight,
480
665
  )
481
666
  if (!force && signature == lastSignature) return
482
667
  lastSignature = signature
483
668
 
484
- cancelCurrent(invalidateGeneration = true)
669
+ cancelCurrent(invalidateGeneration = true, clearMemoryPreview = false)
485
670
  val requestGeneration = generation
486
671
  requestStartedAtMs = SystemClock.uptimeMillis()
487
672
  onLoadStart?.invoke()
@@ -489,8 +674,14 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
489
674
  val customIdentity = OneKeyImageModel.headers(sourceHeadersJson).isNotEmpty()
490
675
  val optimizedUrl = if (optimizeTos != false) {
491
676
  val density = hostView.resources.displayMetrics.density.coerceAtLeast(1f)
492
- val displaySize = resizeWidth?.takeIf { it.isFinite() && it > 0.0 }
493
- ?: (maxOf(hostView.width, hostView.height) / density.toDouble())
677
+ val displayWidth = resizeWidth?.takeIf { it.isFinite() && it > 0.0 }
678
+ val displayHeight = resizeHeight?.takeIf { it.isFinite() && it > 0.0 }
679
+ val displaySize = when {
680
+ displayWidth != null && displayHeight != null -> maxOf(displayWidth, displayHeight)
681
+ displayWidth != null -> displayWidth
682
+ displayHeight != null -> displayHeight
683
+ else -> maxOf(hostView.width, hostView.height) / density.toDouble()
684
+ }
494
685
  OneKeyTosUrl.optimized(
495
686
  rawUrl,
496
687
  ceil(displaySize).toInt(),
@@ -500,6 +691,28 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
500
691
  )
501
692
  } else rawUrl
502
693
  val decodeDimensions = OneKeyImageDecodeDimensions.forRender(hostView.width, hostView.height)
694
+ val memoryFamily = OneKeyImageMemoryFamilyKey.from(rawUrl, sourceHeadersJson)
695
+ val exactVariant = OneKeyImageMemoryVariant(
696
+ requestUrl = optimizedUrl,
697
+ width = decodeDimensions.width,
698
+ height = decodeDimensions.height,
699
+ round = round == true,
700
+ )
701
+ if (
702
+ shouldProbeMemoryPreview(
703
+ hasTarget = memoryPreviewTarget != null,
704
+ sameFamily = memoryPreviewFamily == memoryFamily,
705
+ hasDrawable = hostView.drawable != null,
706
+ )
707
+ ) {
708
+ showMemoryCachedPreview(
709
+ family = memoryFamily,
710
+ exactVariant = exactVariant,
711
+ includeExact = false,
712
+ policy = cachePolicy ?: OneKeyImageCachePolicy.MEMORY_DISK,
713
+ requestGeneration = requestGeneration,
714
+ )
715
+ }
503
716
  performRequest(
504
717
  requestUrl = optimizedUrl,
505
718
  rawUrl = rawUrl,
@@ -508,6 +721,8 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
508
721
  headersJson = sourceHeadersJson,
509
722
  policy = cachePolicy ?: OneKeyImageCachePolicy.MEMORY_DISK,
510
723
  decodeDimensions = decodeDimensions,
724
+ memoryFamily = memoryFamily,
725
+ memoryVariant = exactVariant,
511
726
  )
512
727
  }
513
728
 
@@ -519,6 +734,8 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
519
734
  headersJson: String?,
520
735
  policy: OneKeyImageCachePolicy,
521
736
  decodeDimensions: OneKeyImageDecodeDimensions,
737
+ memoryFamily: OneKeyImageMemoryFamilyKey,
738
+ memoryVariant: OneKeyImageMemoryVariant,
522
739
  ) {
523
740
  if (requestGeneration != generation || disposed) return
524
741
  clearCurrentTarget()
@@ -528,18 +745,34 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
528
745
  val target = object : CustomViewTarget<OneKeyImageHostView, Drawable>(hostView) {
529
746
  override fun onResourceLoading(placeholder: Drawable?) {
530
747
  if (requestGeneration != generation || disposed) return
531
- showLoading(requestIsActive = true)
748
+ showLoading(
749
+ requestIsActive = true,
750
+ preserveDisplayedImage = memoryPreviewTarget != null && hostView.drawable != null,
751
+ )
532
752
  }
533
753
 
534
754
  override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
535
755
  if (requestGeneration != generation || disposed) return
756
+ val replacesMemoryPreview = memoryPreviewTarget != null && hostView.drawable != null
536
757
  requestActive = false
537
758
  displayState = DisplayState.IMAGE
538
759
  hostView.skeletonRequested = false
539
760
  hostView.drawStateSymbol = false
540
761
  hostView.setBackgroundColor(Color.TRANSPARENT)
541
762
  hostView.setImageDrawable(resource)
542
- applyLoadedImageTransition(resolvedCacheType)
763
+ isPreservingDisplayedImage = false
764
+ if (
765
+ resource !is Animatable &&
766
+ (policy == OneKeyImageCachePolicy.MEMORY ||
767
+ policy == OneKeyImageCachePolicy.MEMORY_DISK)
768
+ ) {
769
+ OneKeyImageMemoryVariantRegistry.record(memoryFamily, memoryVariant)
770
+ }
771
+ clearMemoryPreviewTarget()
772
+ applyLoadedImageTransition(
773
+ cacheType = resolvedCacheType,
774
+ replacingMemoryPreview = replacesMemoryPreview,
775
+ )
543
776
  applyAutoplay()
544
777
  val loadCallback = onLoad
545
778
  val loadEndCallback = onLoadEnd
@@ -591,6 +824,8 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
591
824
  headersJson = headersJson,
592
825
  policy = policy,
593
826
  decodeDimensions = decodeDimensions,
827
+ memoryFamily = memoryFamily,
828
+ memoryVariant = memoryVariant.copy(requestUrl = rawUrl),
594
829
  )
595
830
  }
596
831
  }
@@ -606,7 +841,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
606
841
  requestManager()
607
842
  .asDrawable()
608
843
  .load(OneKeyImageModel.build(requestUrl, headersJson))
609
- .apply(requestOptions(policy, requestUrl, round == true))
844
+ .apply(requestOptions(policy, requestUrl, memoryVariant.round))
610
845
  .override(decodeDimensions.width, decodeDimensions.height)
611
846
  .listener(object : RequestListener<Drawable> {
612
847
  override fun onLoadFailed(
@@ -666,7 +901,10 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
666
901
  )
667
902
  }
668
903
 
669
- private fun cancelCurrent(invalidateGeneration: Boolean) {
904
+ private fun cancelCurrent(
905
+ invalidateGeneration: Boolean,
906
+ clearMemoryPreview: Boolean = true,
907
+ ) {
670
908
  loadRunnable?.let(hostView::removeCallbacks)
671
909
  loadRunnable = null
672
910
  displayRunnable?.let(hostView::removeCallbacks)
@@ -675,7 +913,9 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
675
913
  fallbackRunnable?.let(hostView::removeCallbacks)
676
914
  fallbackRunnable = null
677
915
  clearCurrentTarget()
916
+ if (clearMemoryPreview) clearMemoryPreviewTarget()
678
917
  requestActive = false
918
+ isPreservingDisplayedImage = false
679
919
  hostView.skeletonRequested = false
680
920
  requestStartedAtMs = null
681
921
  resetImageTransition()
@@ -690,6 +930,15 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
690
930
  requestManager().clear(target)
691
931
  }
692
932
 
933
+ private fun clearMemoryPreviewTarget() {
934
+ val target = memoryPreviewTarget
935
+ memoryPreviewTarget = null
936
+ memoryPreviewFamily = null
937
+ scalesCenterMemoryPreview = false
938
+ applyContentFit()
939
+ if (target != null) requestManager().clear(target)
940
+ }
941
+
693
942
  private fun scheduleOnDisplay(requestGeneration: Long) {
694
943
  pendingDisplayGeneration = requestGeneration
695
944
  schedulePendingDisplayIfNeeded()
@@ -718,10 +967,22 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
718
967
  hostView.postOnAnimation(runnable)
719
968
  }
720
969
 
721
- private fun showLoading(requestIsActive: Boolean) {
970
+ private fun showLoading(
971
+ requestIsActive: Boolean,
972
+ preserveDisplayedImage: Boolean = false,
973
+ ) {
722
974
  displayState = DisplayState.LOADING
723
975
  requestActive = requestIsActive
724
976
  resetImageTransition()
977
+ if (preserveDisplayedImage && hostView.drawable != null) {
978
+ isPreservingDisplayedImage = true
979
+ hostView.skeletonRequested = false
980
+ hostView.drawStateSymbol = false
981
+ hostView.setBackgroundColor(Color.TRANSPARENT)
982
+ hostView.invalidate()
983
+ return
984
+ }
985
+ isPreservingDisplayedImage = false
725
986
  hostView.setImageDrawable(null)
726
987
  hostView.drawStateSymbol = false
727
988
  applyLoadingAppearance()
@@ -738,7 +999,9 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
738
999
  private fun showTerminalState(state: DisplayState) {
739
1000
  displayState = state
740
1001
  requestActive = false
1002
+ isPreservingDisplayedImage = false
741
1003
  resetImageTransition()
1004
+ clearMemoryPreviewTarget()
742
1005
  hostView.setImageDrawable(null)
743
1006
  hostView.skeletonRequested = false
744
1007
  val hidesTerminalState = loadingStrategy == OneKeyImageLoadingStrategy.NONE
@@ -807,7 +1070,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
807
1070
  private fun applyVariant() {
808
1071
  when (displayState) {
809
1072
  DisplayState.IMAGE -> Unit
810
- DisplayState.LOADING -> applyLoadingAppearance()
1073
+ DisplayState.LOADING -> if (!isPreservingDisplayedImage) applyLoadingAppearance()
811
1074
  DisplayState.ERROR, DisplayState.FALLBACK -> {
812
1075
  val hidesTerminalState = loadingStrategy == OneKeyImageLoadingStrategy.NONE
813
1076
  hostView.setBackgroundColor(
@@ -839,16 +1102,19 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
839
1102
  OneKeyImageVariant.AVATAR -> "●"
840
1103
  }
841
1104
 
842
- private fun applyLoadedImageTransition(cacheType: OneKeyImageCacheType) {
1105
+ private fun applyLoadedImageTransition(
1106
+ cacheType: OneKeyImageCacheType,
1107
+ replacingMemoryPreview: Boolean,
1108
+ ) {
843
1109
  resetImageTransition()
844
1110
  val startedAt = requestStartedAtMs ?: return
845
- if (
846
- cacheType == OneKeyImageCacheType.MEMORY ||
847
- SystemClock.uptimeMillis() - startedAt < FADE_DELAY_THRESHOLD_MS ||
848
- !animationsEnabled()
849
- ) {
850
- return
851
- }
1111
+ val shouldAnimate = shouldAnimateLoadedImageTransition(
1112
+ cacheType = cacheType,
1113
+ fadeDelayElapsed = SystemClock.uptimeMillis() - startedAt >= FADE_DELAY_THRESHOLD_MS,
1114
+ animationsEnabled = animationsEnabled(),
1115
+ replacingMemoryPreview = replacingMemoryPreview,
1116
+ )
1117
+ if (!shouldAnimate) return
852
1118
  hostView.alpha = 0f
853
1119
  hostView.animate()
854
1120
  .alpha(1f)
@@ -902,6 +1168,13 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
902
1168
  }
903
1169
 
904
1170
  private fun applyContentFit() {
1171
+ if (
1172
+ scalesCenterMemoryPreview &&
1173
+ (contentFit ?: OneKeyImageContentFit.COVER) == OneKeyImageContentFit.CENTER
1174
+ ) {
1175
+ hostView.scaleType = ImageView.ScaleType.FIT_CENTER
1176
+ return
1177
+ }
905
1178
  hostView.scaleType = when (contentFit ?: OneKeyImageContentFit.COVER) {
906
1179
  OneKeyImageContentFit.COVER -> ImageView.ScaleType.CENTER_CROP
907
1180
  OneKeyImageContentFit.CONTAIN -> ImageView.ScaleType.FIT_CENTER
@@ -1,5 +1,7 @@
1
1
  package com.margelo.nitro.onekeyimage
2
2
 
3
+ import android.graphics.drawable.Animatable
4
+ import android.graphics.drawable.Drawable
3
5
  import com.bumptech.glide.Glide
4
6
  import com.bumptech.glide.load.engine.DiskCacheStrategy
5
7
  import com.bumptech.glide.request.RequestOptions
@@ -65,7 +67,8 @@ class HybridOneKeyImageCache : HybridOneKeyImageCacheSpec() {
65
67
  } else source.uri
66
68
 
67
69
  val succeeded = try {
68
- load(context, requestUrl, source, options, decodeDimensions)
70
+ val resource = load(context, requestUrl, source, options, decodeDimensions)
71
+ recordMemoryVariant(source, requestUrl, decodeDimensions, resource)
69
72
  true
70
73
  } catch (optimizedError: Exception) {
71
74
  if (
@@ -75,7 +78,8 @@ class HybridOneKeyImageCache : HybridOneKeyImageCacheSpec() {
75
78
  false
76
79
  } else {
77
80
  try {
78
- load(context, source.uri, source, options, decodeDimensions)
81
+ val resource = load(context, source.uri, source, options, decodeDimensions)
82
+ recordMemoryVariant(source, source.uri, decodeDimensions, resource)
79
83
  true
80
84
  } catch (_: Exception) {
81
85
  false
@@ -92,6 +96,7 @@ class HybridOneKeyImageCache : HybridOneKeyImageCacheSpec() {
92
96
  ?: throw IllegalStateException("React application context is unavailable")
93
97
  withContext(Dispatchers.Main) {
94
98
  Glide.get(context).clearMemory()
99
+ OneKeyImageMemoryVariantRegistry.clear()
95
100
  }
96
101
  }
97
102
 
@@ -106,6 +111,7 @@ class HybridOneKeyImageCache : HybridOneKeyImageCacheSpec() {
106
111
  ?: throw IllegalStateException("React application context is unavailable")
107
112
  withContext(Dispatchers.Main) {
108
113
  Glide.get(context).clearMemory()
114
+ OneKeyImageMemoryVariantRegistry.clear()
109
115
  }
110
116
  Glide.get(context).clearDiskCache()
111
117
  }
@@ -116,7 +122,7 @@ class HybridOneKeyImageCache : HybridOneKeyImageCacheSpec() {
116
122
  source: OneKeyImagePreloadSource,
117
123
  options: RequestOptions,
118
124
  decodeDimensions: OneKeyImageDecodeDimensions,
119
- ) {
125
+ ): Drawable {
120
126
  val request = Glide.with(context)
121
127
  .asDrawable()
122
128
  .load(OneKeyImageModel.build(url, source.headersJson))
@@ -124,9 +130,33 @@ class HybridOneKeyImageCache : HybridOneKeyImageCacheSpec() {
124
130
  .override(decodeDimensions.width, decodeDimensions.height)
125
131
  val future = request.submit()
126
132
  try {
127
- future.get()
133
+ return future.get()
128
134
  } finally {
129
135
  Glide.with(context).clear(future)
130
136
  }
131
137
  }
138
+
139
+ private fun recordMemoryVariant(
140
+ source: OneKeyImagePreloadSource,
141
+ requestUrl: String,
142
+ decodeDimensions: OneKeyImageDecodeDimensions,
143
+ resource: Drawable,
144
+ ) {
145
+ val policy = source.cachePolicy ?: OneKeyImageCachePolicy.MEMORY_DISK
146
+ if (
147
+ resource is Animatable ||
148
+ (policy != OneKeyImageCachePolicy.MEMORY && policy != OneKeyImageCachePolicy.MEMORY_DISK)
149
+ ) {
150
+ return
151
+ }
152
+ OneKeyImageMemoryVariantRegistry.record(
153
+ family = OneKeyImageMemoryFamilyKey.from(source.uri, source.headersJson),
154
+ variant = OneKeyImageMemoryVariant(
155
+ requestUrl = requestUrl,
156
+ width = decodeDimensions.width,
157
+ height = decodeDimensions.height,
158
+ round = false,
159
+ ),
160
+ )
161
+ }
132
162
  }