@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 +6 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +29 -2
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageCache.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageDecodeDimensions.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageDecodeDimensionsTest.kt +16 -0
- package/lib/module/index.js +10 -4
- package/package.json +2 -2
- package/src/index.tsx +32 -8
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
|
|
150
|
+
internal object OneKeyImageSafeDownsampleStrategy : DownsampleStrategy() {
|
|
151
151
|
override fun getScaleFactor(
|
|
152
152
|
sourceWidth: Int,
|
|
153
153
|
sourceHeight: Int,
|
package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageDecodeDimensionsTest.kt
CHANGED
|
@@ -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
|
}
|
package/lib/module/index.js
CHANGED
|
@@ -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:
|
|
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 (!
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
.
|
|
354
|
-
|
|
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(),
|