@onekeyfe/react-native-image 3.0.155 → 3.0.157-alpha.248

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 (44) hide show
  1. package/README.md +3 -0
  2. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +356 -22
  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/main/java/com/margelo/nitro/onekeyimage/OneKeyImageReusableView.kt +2 -0
  6. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageMemoryVariantsTest.kt +149 -0
  7. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageRequestSignatureTest.kt +14 -1
  8. package/ios/OneKeyImage.swift +273 -43
  9. package/ios/OneKeyImageCache.swift +40 -3
  10. package/ios/OneKeyImageMemoryVariants.swift +137 -0
  11. package/ios/OneKeyImageRequestContext.swift +51 -9
  12. package/ios/tests/OneKeyImageInfrastructureTests.swift +255 -18
  13. package/lib/module/index.js +26 -3
  14. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +18 -0
  15. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +4 -0
  16. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +10 -0
  17. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +12 -0
  18. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +14 -0
  19. package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +12 -0
  20. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +2 -0
  21. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +48 -0
  22. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +4 -0
  23. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +4 -0
  24. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +4 -0
  25. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +6 -0
  26. package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +2 -0
  27. package/lib/typescript/src/OneKeyImage.nitro.d.ts +8 -0
  28. package/lib/typescript/src/index.d.ts +1 -1
  29. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +18 -0
  30. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +4 -0
  31. package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +10 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +12 -0
  33. package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +14 -0
  34. package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +12 -0
  35. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +2 -0
  36. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +48 -0
  37. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +4 -0
  38. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +4 -0
  39. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +4 -0
  40. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +6 -0
  41. package/nitrogen/generated/shared/json/OneKeyImageConfig.json +2 -0
  42. package/package.json +2 -2
  43. package/src/OneKeyImage.nitro.ts +8 -0
  44. package/src/index.tsx +25 -1
@@ -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
  }
@@ -0,0 +1,119 @@
1
+ package com.margelo.nitro.onekeyimage
2
+
3
+ import kotlin.math.max
4
+ import kotlin.math.min
5
+
6
+ internal data class OneKeyImageMemoryFamilyKey(
7
+ val rawUrl: String,
8
+ val headersDigest: String?,
9
+ ) {
10
+ companion object {
11
+ fun from(rawUrl: String, headersJson: String?): OneKeyImageMemoryFamilyKey =
12
+ OneKeyImageMemoryFamilyKey(
13
+ rawUrl = rawUrl,
14
+ headersDigest = OneKeyImageModel.remoteHeadersDigest(
15
+ OneKeyImageModel.headers(headersJson),
16
+ ),
17
+ )
18
+ }
19
+ }
20
+
21
+ internal data class OneKeyImageMemoryVariant(
22
+ val requestUrl: String,
23
+ val width: Int,
24
+ val height: Int,
25
+ val round: Boolean,
26
+ ) {
27
+ val area: Long
28
+ get() = width.toLong() * height.toLong()
29
+ }
30
+
31
+ /**
32
+ * A bounded index of size-specific Glide resources that may still be in memory.
33
+ *
34
+ * Glide remains the source of truth. Entries are only hints used to reproduce a
35
+ * candidate EngineKey; a synchronous cache miss removes the stale hint.
36
+ */
37
+ internal object OneKeyImageMemoryVariantRegistry {
38
+ private const val MAX_FAMILIES = 1024
39
+ private const val MAX_VARIANTS_PER_FAMILY = 8
40
+ private const val MAX_ASPECT_RATIO_DRIFT = 1.1
41
+
42
+ private val variantsByFamily =
43
+ LinkedHashMap<OneKeyImageMemoryFamilyKey, MutableList<OneKeyImageMemoryVariant>>(
44
+ 16,
45
+ 0.75f,
46
+ true,
47
+ )
48
+
49
+ @Synchronized
50
+ fun record(family: OneKeyImageMemoryFamilyKey, variant: OneKeyImageMemoryVariant) {
51
+ if (variant.width <= 0 || variant.height <= 0) return
52
+ val variants = variantsByFamily.getOrPut(family) { mutableListOf() }
53
+ variants.remove(variant)
54
+ variants.add(variant)
55
+ while (variants.size > MAX_VARIANTS_PER_FAMILY) variants.removeAt(0)
56
+ while (variantsByFamily.size > MAX_FAMILIES) {
57
+ val eldest = variantsByFamily.entries.firstOrNull()?.key ?: break
58
+ variantsByFamily.remove(eldest)
59
+ }
60
+ }
61
+
62
+ @Synchronized
63
+ fun remove(family: OneKeyImageMemoryFamilyKey, variant: OneKeyImageMemoryVariant) {
64
+ val variants = variantsByFamily[family] ?: return
65
+ variants.remove(variant)
66
+ if (variants.isEmpty()) variantsByFamily.remove(family)
67
+ }
68
+
69
+ @Synchronized
70
+ fun candidates(
71
+ family: OneKeyImageMemoryFamilyKey,
72
+ target: OneKeyImageMemoryVariant,
73
+ ): List<OneKeyImageMemoryVariant> = orderedCandidates(
74
+ variants = variantsByFamily[family].orEmpty(),
75
+ target = target,
76
+ )
77
+
78
+ @Synchronized
79
+ fun clear() {
80
+ variantsByFamily.clear()
81
+ }
82
+
83
+ internal fun orderedCandidates(
84
+ variants: List<OneKeyImageMemoryVariant>,
85
+ target: OneKeyImageMemoryVariant,
86
+ ): List<OneKeyImageMemoryVariant> {
87
+ val compatible = variants
88
+ .asSequence()
89
+ .filter { it != target }
90
+ .filter { candidate -> target.round || !candidate.round }
91
+ .filter { candidate -> aspectRatioIsCompatible(candidate, target) }
92
+ .distinct()
93
+ .toList()
94
+
95
+ val larger = compatible
96
+ .filter { it.width >= target.width && it.height >= target.height }
97
+ .minWithOrNull(
98
+ compareBy<OneKeyImageMemoryVariant> { it.area }
99
+ .thenBy { if (it.round == target.round) 0 else 1 },
100
+ )
101
+ val smaller = compatible
102
+ .filter { it.width <= target.width && it.height <= target.height }
103
+ .maxWithOrNull(
104
+ compareBy<OneKeyImageMemoryVariant> { it.area }
105
+ .thenByDescending { if (it.round == target.round) 0 else 1 },
106
+ )
107
+ return listOfNotNull(larger, smaller).distinct()
108
+ }
109
+
110
+ private fun aspectRatioIsCompatible(
111
+ candidate: OneKeyImageMemoryVariant,
112
+ target: OneKeyImageMemoryVariant,
113
+ ): Boolean {
114
+ val candidateRatio = candidate.width.toDouble() / candidate.height.toDouble()
115
+ val targetRatio = target.width.toDouble() / target.height.toDouble()
116
+ return max(candidateRatio, targetRatio) / min(candidateRatio, targetRatio) <=
117
+ MAX_ASPECT_RATIO_DRIFT
118
+ }
119
+ }
@@ -29,6 +29,7 @@ class OneKeyImageReusableView(context: ThemedReactContext) : FrameLayout(context
29
29
  overscan: Double,
30
30
  loadingStrategy: String,
31
31
  placeholderColor: String,
32
+ round: Boolean = false,
32
33
  onLoad: (() -> Unit)? = null,
33
34
  onError: (() -> Unit)? = null,
34
35
  ) {
@@ -57,6 +58,7 @@ class OneKeyImageReusableView(context: ThemedReactContext) : FrameLayout(context
57
58
  image.autoplay = autoplay
58
59
  image.recyclingKey = recyclingKey
59
60
  image.optimizeTos = optimizeTos
61
+ image.round = round
60
62
  image.overscan = overscan
61
63
  image.loadingStrategy = when (loadingStrategy) {
62
64
  "skeleton" -> OneKeyImageLoadingStrategy.SKELETON
@@ -0,0 +1,149 @@
1
+ package com.margelo.nitro.onekeyimage
2
+
3
+ import com.bumptech.glide.load.engine.DiskCacheStrategy
4
+ import com.bumptech.glide.request.RequestOptions
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertFalse
7
+ import org.junit.Assert.assertSame
8
+ import org.junit.Assert.assertTrue
9
+ import org.junit.Test
10
+
11
+ class OneKeyImageMemoryVariantsTest {
12
+ @Test
13
+ fun choosesNearestLargerThenNearestSmallerVariant() {
14
+ val target = variant(96)
15
+ val candidates = OneKeyImageMemoryVariantRegistry.orderedCandidates(
16
+ variants = listOf(variant(48), variant(72), variant(120), variant(144)),
17
+ target = target,
18
+ )
19
+
20
+ assertEquals(listOf(variant(120), variant(72)), candidates)
21
+ }
22
+
23
+ @Test
24
+ fun ignoresExactAndIncompatibleAspectRatios() {
25
+ val target = variant(96)
26
+ val candidates = OneKeyImageMemoryVariantRegistry.orderedCandidates(
27
+ variants = listOf(
28
+ target,
29
+ OneKeyImageMemoryVariant("wide", 120, 60, round = false),
30
+ variant(120),
31
+ ),
32
+ target = target,
33
+ )
34
+
35
+ assertEquals(listOf(variant(120)), candidates)
36
+ }
37
+
38
+ @Test
39
+ fun roundTargetCanPreviewUntransformedResourceButSquareTargetCannotUseRoundResource() {
40
+ val untransformed = variant(120, round = false)
41
+ val transformed = variant(120, round = true)
42
+
43
+ assertEquals(
44
+ listOf(untransformed),
45
+ OneKeyImageMemoryVariantRegistry.orderedCandidates(
46
+ variants = listOf(untransformed),
47
+ target = variant(96, round = true),
48
+ ),
49
+ )
50
+ assertEquals(
51
+ emptyList<OneKeyImageMemoryVariant>(),
52
+ OneKeyImageMemoryVariantRegistry.orderedCandidates(
53
+ variants = listOf(transformed),
54
+ target = variant(96, round = false),
55
+ ),
56
+ )
57
+ }
58
+
59
+ @Test
60
+ fun crossSizeCenterPreviewUsesTemporaryScaling() {
61
+ assertTrue(
62
+ shouldScaleCenterMemoryPreview(
63
+ contentFit = OneKeyImageContentFit.CENTER,
64
+ candidate = variant(48),
65
+ target = variant(96),
66
+ ),
67
+ )
68
+ assertTrue(
69
+ shouldScaleCenterMemoryPreview(
70
+ contentFit = OneKeyImageContentFit.CENTER,
71
+ candidate = variant(144),
72
+ target = variant(96),
73
+ ),
74
+ )
75
+ assertFalse(
76
+ shouldScaleCenterMemoryPreview(
77
+ contentFit = OneKeyImageContentFit.CENTER,
78
+ candidate = variant(96),
79
+ target = variant(96),
80
+ ),
81
+ )
82
+ assertFalse(
83
+ shouldScaleCenterMemoryPreview(
84
+ contentFit = OneKeyImageContentFit.COVER,
85
+ candidate = variant(48),
86
+ target = variant(96),
87
+ ),
88
+ )
89
+ }
90
+
91
+ @Test
92
+ fun missingPreviewDrawableAllowsSameFamilyProbeRetry() {
93
+ assertTrue(
94
+ shouldProbeMemoryPreview(
95
+ hasTarget = true,
96
+ sameFamily = true,
97
+ hasDrawable = false,
98
+ ),
99
+ )
100
+ assertFalse(
101
+ shouldProbeMemoryPreview(
102
+ hasTarget = true,
103
+ sameFamily = true,
104
+ hasDrawable = true,
105
+ ),
106
+ )
107
+ }
108
+
109
+ @Test
110
+ fun replacingMemoryPreviewSkipsWholeViewFade() {
111
+ assertFalse(
112
+ shouldAnimateLoadedImageTransition(
113
+ cacheType = OneKeyImageCacheType.DISK,
114
+ fadeDelayElapsed = true,
115
+ animationsEnabled = true,
116
+ replacingMemoryPreview = true,
117
+ ),
118
+ )
119
+ assertTrue(
120
+ shouldAnimateLoadedImageTransition(
121
+ cacheType = OneKeyImageCacheType.DISK,
122
+ fadeDelayElapsed = true,
123
+ animationsEnabled = true,
124
+ replacingMemoryPreview = false,
125
+ ),
126
+ )
127
+ }
128
+
129
+ @Test
130
+ fun memoryProbeKeepsMemoryEnabledAndDisablesDiskReads() {
131
+ val options = memoryProbeRequestOptions(
132
+ RequestOptions().diskCacheStrategy(DiskCacheStrategy.AUTOMATIC),
133
+ )
134
+
135
+ assertSame(DiskCacheStrategy.NONE, options.diskCacheStrategy)
136
+ assertTrue(options.onlyRetrieveFromCache)
137
+ assertTrue(options.isMemoryCacheable)
138
+ }
139
+
140
+ private fun variant(
141
+ size: Int,
142
+ round: Boolean = false,
143
+ ) = OneKeyImageMemoryVariant(
144
+ requestUrl = "https://example.com/image-$size.png",
145
+ width = size,
146
+ height = size,
147
+ round = round,
148
+ )
149
+ }
@@ -26,7 +26,19 @@ class OneKeyImageRequestSignatureTest {
26
26
  )
27
27
  }
28
28
 
29
- private fun signature(contentFit: OneKeyImageContentFit?, resizeWidth: Double? = null): String = oneKeyImageRequestSignature(
29
+ @Test
30
+ fun roundParticipatesInRequestIdentity() {
31
+ assertNotEquals(
32
+ signature(OneKeyImageContentFit.COVER, round = false),
33
+ signature(OneKeyImageContentFit.COVER, round = true),
34
+ )
35
+ }
36
+
37
+ private fun signature(
38
+ contentFit: OneKeyImageContentFit?,
39
+ resizeWidth: Double? = null,
40
+ round: Boolean = false,
41
+ ): String = oneKeyImageRequestSignature(
30
42
  rawUrl = "https://example.com/image.png",
31
43
  sourceHeadersJson = null,
32
44
  recyclingKey = null,
@@ -34,6 +46,7 @@ class OneKeyImageRequestSignatureTest {
34
46
  contentFit = contentFit,
35
47
  optimizeTos = true,
36
48
  overscan = 1.1,
49
+ round = round,
37
50
  width = 200,
38
51
  height = 100,
39
52
  density = 2f,