@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.
- package/README.md +3 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +356 -22
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageCache.kt +34 -4
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageMemoryVariants.kt +119 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageReusableView.kt +2 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageMemoryVariantsTest.kt +149 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageRequestSignatureTest.kt +14 -1
- package/ios/OneKeyImage.swift +273 -43
- package/ios/OneKeyImageCache.swift +40 -3
- package/ios/OneKeyImageMemoryVariants.swift +137 -0
- package/ios/OneKeyImageRequestContext.swift +51 -9
- package/ios/tests/OneKeyImageInfrastructureTests.swift +255 -18
- package/lib/module/index.js +26 -3
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +18 -0
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +4 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +10 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +12 -0
- package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +14 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +12 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +2 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +48 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +6 -0
- package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +2 -0
- package/lib/typescript/src/OneKeyImage.nitro.d.ts +8 -0
- package/lib/typescript/src/index.d.ts +1 -1
- package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +18 -0
- package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +10 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +12 -0
- package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +12 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +48 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +6 -0
- package/nitrogen/generated/shared/json/OneKeyImageConfig.json +2 -0
- package/package.json +2 -2
- package/src/OneKeyImage.nitro.ts +8 -0
- package/src/index.tsx +25 -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' }}
|
|
@@ -7,7 +7,9 @@ import android.content.ContextWrapper
|
|
|
7
7
|
import android.content.res.Configuration
|
|
8
8
|
import android.graphics.Canvas
|
|
9
9
|
import android.graphics.Color
|
|
10
|
+
import android.graphics.Outline
|
|
10
11
|
import android.graphics.Paint
|
|
12
|
+
import android.graphics.Path
|
|
11
13
|
import android.graphics.drawable.Animatable
|
|
12
14
|
import android.graphics.drawable.Drawable
|
|
13
15
|
import android.os.Build
|
|
@@ -15,6 +17,7 @@ import android.os.Looper
|
|
|
15
17
|
import android.os.SystemClock
|
|
16
18
|
import android.provider.Settings
|
|
17
19
|
import android.view.View
|
|
20
|
+
import android.view.ViewOutlineProvider
|
|
18
21
|
import android.view.animation.DecelerateInterpolator
|
|
19
22
|
import android.widget.ImageView
|
|
20
23
|
import com.bumptech.glide.Glide
|
|
@@ -23,6 +26,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
|
23
26
|
import com.bumptech.glide.load.engine.GlideException
|
|
24
27
|
import com.bumptech.glide.request.RequestListener
|
|
25
28
|
import com.bumptech.glide.request.RequestOptions
|
|
29
|
+
import com.bumptech.glide.request.target.CustomTarget
|
|
26
30
|
import com.bumptech.glide.request.target.CustomViewTarget
|
|
27
31
|
import com.bumptech.glide.request.target.Target
|
|
28
32
|
import com.bumptech.glide.request.transition.Transition
|
|
@@ -43,7 +47,45 @@ private fun Context.findActivity(): Activity? {
|
|
|
43
47
|
return currentContext as? Activity
|
|
44
48
|
}
|
|
45
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
|
+
|
|
46
78
|
private class OneKeyImageHostView(context: ThemedReactContext) : ImageView(context) {
|
|
79
|
+
private val roundClipPath = Path()
|
|
80
|
+
private val roundOutlineProvider = object : ViewOutlineProvider() {
|
|
81
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
82
|
+
if (view.width > 0 && view.height > 0) {
|
|
83
|
+
outline.setOval(0, 0, view.width, view.height)
|
|
84
|
+
} else {
|
|
85
|
+
outline.setEmpty()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
47
89
|
private val fallbackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
48
90
|
color = Color.rgb(110, 110, 120)
|
|
49
91
|
textAlign = Paint.Align.CENTER
|
|
@@ -52,6 +94,16 @@ private class OneKeyImageHostView(context: ThemedReactContext) : ImageView(conte
|
|
|
52
94
|
var drawStateSymbol = false
|
|
53
95
|
var stateSymbol = "◇"
|
|
54
96
|
var autoplayEnabled = false
|
|
97
|
+
var round = false
|
|
98
|
+
set(value) {
|
|
99
|
+
if (field == value) return
|
|
100
|
+
field = value
|
|
101
|
+
outlineProvider = if (value) roundOutlineProvider else ViewOutlineProvider.BACKGROUND
|
|
102
|
+
clipToOutline = value
|
|
103
|
+
updateRoundClipPath()
|
|
104
|
+
invalidateOutline()
|
|
105
|
+
invalidate()
|
|
106
|
+
}
|
|
55
107
|
private var aggregatedVisible = true
|
|
56
108
|
var skeletonRequested = false
|
|
57
109
|
set(value) {
|
|
@@ -65,9 +117,24 @@ private class OneKeyImageHostView(context: ThemedReactContext) : ImageView(conte
|
|
|
65
117
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
66
118
|
skeleton.updateBounds(w, h)
|
|
67
119
|
fallbackPaint.textSize = minOf(w, h) * 0.35f
|
|
120
|
+
if (round) {
|
|
121
|
+
updateRoundClipPath()
|
|
122
|
+
invalidateOutline()
|
|
123
|
+
}
|
|
68
124
|
if (w > 0 && h > 0) onReadyForRequest?.invoke()
|
|
69
125
|
}
|
|
70
126
|
|
|
127
|
+
override fun draw(canvas: Canvas) {
|
|
128
|
+
if (!round || roundClipPath.isEmpty) {
|
|
129
|
+
super.draw(canvas)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
val saveCount = canvas.save()
|
|
133
|
+
canvas.clipPath(roundClipPath)
|
|
134
|
+
super.draw(canvas)
|
|
135
|
+
canvas.restoreToCount(saveCount)
|
|
136
|
+
}
|
|
137
|
+
|
|
71
138
|
override fun onDraw(canvas: Canvas) {
|
|
72
139
|
super.onDraw(canvas)
|
|
73
140
|
if (drawStateSymbol) {
|
|
@@ -108,6 +175,13 @@ private class OneKeyImageHostView(context: ThemedReactContext) : ImageView(conte
|
|
|
108
175
|
skeleton.updateBounds(width, height)
|
|
109
176
|
}
|
|
110
177
|
|
|
178
|
+
private fun updateRoundClipPath() {
|
|
179
|
+
roundClipPath.reset()
|
|
180
|
+
if (round && width > 0 && height > 0) {
|
|
181
|
+
roundClipPath.addOval(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
111
185
|
fun syncPlayback() {
|
|
112
186
|
val canRun = isAttachedToWindow && aggregatedVisible
|
|
113
187
|
if (skeletonRequested && canRun) skeleton.start() else skeleton.stop()
|
|
@@ -130,10 +204,12 @@ internal fun oneKeyImageRequestSignature(
|
|
|
130
204
|
contentFit: OneKeyImageContentFit?,
|
|
131
205
|
optimizeTos: Boolean?,
|
|
132
206
|
overscan: Double?,
|
|
207
|
+
round: Boolean,
|
|
133
208
|
width: Int,
|
|
134
209
|
height: Int,
|
|
135
210
|
density: Float,
|
|
136
211
|
resizeWidth: Double? = null,
|
|
212
|
+
resizeHeight: Double? = null,
|
|
137
213
|
): String = listOf(
|
|
138
214
|
rawUrl,
|
|
139
215
|
sourceHeadersJson.orEmpty(),
|
|
@@ -142,7 +218,9 @@ internal fun oneKeyImageRequestSignature(
|
|
|
142
218
|
(contentFit ?: OneKeyImageContentFit.COVER).name,
|
|
143
219
|
optimizeTos.toString(),
|
|
144
220
|
overscan.toString(),
|
|
221
|
+
round.toString(),
|
|
145
222
|
resizeWidth.toString(),
|
|
223
|
+
resizeHeight.toString(),
|
|
146
224
|
width.toString(),
|
|
147
225
|
height.toString(),
|
|
148
226
|
density.toString(),
|
|
@@ -166,12 +244,16 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
166
244
|
}
|
|
167
245
|
private var loadRunnable: Runnable? = null
|
|
168
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
|
|
169
250
|
private var generation = 0L
|
|
170
251
|
private var lastSignature: String? = null
|
|
171
252
|
private var disposed = false
|
|
172
253
|
private var suppressPropEffects = false
|
|
173
254
|
private var requestActive = false
|
|
174
255
|
private var displayState = DisplayState.LOADING
|
|
256
|
+
private var isPreservingDisplayedImage = false
|
|
175
257
|
private var displayRunnable: Runnable? = null
|
|
176
258
|
private var pendingDisplayGeneration: Long? = null
|
|
177
259
|
private var fallbackRunnable: Runnable? = null
|
|
@@ -241,6 +323,10 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
241
323
|
field = value
|
|
242
324
|
requestIdentityChanged()
|
|
243
325
|
}
|
|
326
|
+
override var round: Boolean? = false
|
|
327
|
+
set(value) {
|
|
328
|
+
field = value
|
|
329
|
+
}
|
|
244
330
|
override var overscan: Double? = 1.1
|
|
245
331
|
set(value) {
|
|
246
332
|
if (field == value) return
|
|
@@ -253,6 +339,14 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
253
339
|
field = value
|
|
254
340
|
requestIdentityChanged()
|
|
255
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
|
+
}
|
|
256
350
|
override var loadingStrategy: OneKeyImageLoadingStrategy? = OneKeyImageLoadingStrategy.STATIC
|
|
257
351
|
set(value) {
|
|
258
352
|
field = value
|
|
@@ -287,6 +381,10 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
287
381
|
|
|
288
382
|
override fun afterUpdate() {
|
|
289
383
|
super.afterUpdate()
|
|
384
|
+
// Fabric can reset `round` before clearing `sourceUri` while removing a view.
|
|
385
|
+
// Apply the shape once per committed prop batch so the outgoing frame keeps
|
|
386
|
+
// its clipping, while mounted and recycled views still receive the new value.
|
|
387
|
+
if (!sourceUri.isNullOrBlank()) hostView.round = round == true
|
|
290
388
|
loadForCurrentLayout()
|
|
291
389
|
}
|
|
292
390
|
|
|
@@ -330,7 +428,11 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
330
428
|
if (suppressPropEffects || disposed) return
|
|
331
429
|
lastSignature = null
|
|
332
430
|
cancelCurrent(invalidateGeneration = true)
|
|
333
|
-
if (sourceUri.isNullOrBlank())
|
|
431
|
+
if (sourceUri.isNullOrBlank()) {
|
|
432
|
+
showFallback()
|
|
433
|
+
} else if (!showMemoryCachedPreviewBeforeLayout()) {
|
|
434
|
+
showLoading(requestIsActive = false)
|
|
435
|
+
}
|
|
334
436
|
scheduleLoad()
|
|
335
437
|
}
|
|
336
438
|
|
|
@@ -346,7 +448,9 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
346
448
|
autoplay = false
|
|
347
449
|
recyclingKey = null
|
|
348
450
|
optimizeTos = true
|
|
451
|
+
round = false
|
|
349
452
|
resizeWidth = null
|
|
453
|
+
resizeHeight = null
|
|
350
454
|
overscan = 1.1
|
|
351
455
|
loadingStrategy = OneKeyImageLoadingStrategy.STATIC
|
|
352
456
|
placeholderColor = null
|
|
@@ -358,6 +462,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
358
462
|
suppressPropEffects = false
|
|
359
463
|
requestActive = false
|
|
360
464
|
displayState = DisplayState.LOADING
|
|
465
|
+
isPreservingDisplayedImage = false
|
|
361
466
|
hostView.setImageDrawable(null)
|
|
362
467
|
hostView.drawStateSymbol = false
|
|
363
468
|
hostView.skeletonRequested = false
|
|
@@ -393,12 +498,147 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
393
498
|
}
|
|
394
499
|
}
|
|
395
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
|
+
|
|
396
634
|
private fun startLoad(force: Boolean) {
|
|
397
635
|
if (disposed) return
|
|
398
636
|
if (hostView.width <= 0 || hostView.height <= 0) {
|
|
399
|
-
cancelCurrent(invalidateGeneration = true)
|
|
637
|
+
cancelCurrent(invalidateGeneration = true, clearMemoryPreview = false)
|
|
400
638
|
lastSignature = null
|
|
401
|
-
|
|
639
|
+
if (memoryPreviewTarget == null || hostView.drawable == null) {
|
|
640
|
+
showLoading(requestIsActive = false)
|
|
641
|
+
}
|
|
402
642
|
return
|
|
403
643
|
}
|
|
404
644
|
val rawUrl = sourceUri?.takeIf { it.isNotBlank() }
|
|
@@ -416,15 +656,17 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
416
656
|
contentFit = contentFit,
|
|
417
657
|
optimizeTos = optimizeTos,
|
|
418
658
|
overscan = overscan,
|
|
659
|
+
round = round == true,
|
|
419
660
|
width = hostView.width,
|
|
420
661
|
height = hostView.height,
|
|
421
662
|
density = hostView.resources.displayMetrics.density,
|
|
422
663
|
resizeWidth = resizeWidth,
|
|
664
|
+
resizeHeight = resizeHeight,
|
|
423
665
|
)
|
|
424
666
|
if (!force && signature == lastSignature) return
|
|
425
667
|
lastSignature = signature
|
|
426
668
|
|
|
427
|
-
cancelCurrent(invalidateGeneration = true)
|
|
669
|
+
cancelCurrent(invalidateGeneration = true, clearMemoryPreview = false)
|
|
428
670
|
val requestGeneration = generation
|
|
429
671
|
requestStartedAtMs = SystemClock.uptimeMillis()
|
|
430
672
|
onLoadStart?.invoke()
|
|
@@ -432,8 +674,14 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
432
674
|
val customIdentity = OneKeyImageModel.headers(sourceHeadersJson).isNotEmpty()
|
|
433
675
|
val optimizedUrl = if (optimizeTos != false) {
|
|
434
676
|
val density = hostView.resources.displayMetrics.density.coerceAtLeast(1f)
|
|
435
|
-
val
|
|
436
|
-
|
|
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
|
+
}
|
|
437
685
|
OneKeyTosUrl.optimized(
|
|
438
686
|
rawUrl,
|
|
439
687
|
ceil(displaySize).toInt(),
|
|
@@ -443,6 +691,28 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
443
691
|
)
|
|
444
692
|
} else rawUrl
|
|
445
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
|
+
}
|
|
446
716
|
performRequest(
|
|
447
717
|
requestUrl = optimizedUrl,
|
|
448
718
|
rawUrl = rawUrl,
|
|
@@ -451,6 +721,8 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
451
721
|
headersJson = sourceHeadersJson,
|
|
452
722
|
policy = cachePolicy ?: OneKeyImageCachePolicy.MEMORY_DISK,
|
|
453
723
|
decodeDimensions = decodeDimensions,
|
|
724
|
+
memoryFamily = memoryFamily,
|
|
725
|
+
memoryVariant = exactVariant,
|
|
454
726
|
)
|
|
455
727
|
}
|
|
456
728
|
|
|
@@ -462,6 +734,8 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
462
734
|
headersJson: String?,
|
|
463
735
|
policy: OneKeyImageCachePolicy,
|
|
464
736
|
decodeDimensions: OneKeyImageDecodeDimensions,
|
|
737
|
+
memoryFamily: OneKeyImageMemoryFamilyKey,
|
|
738
|
+
memoryVariant: OneKeyImageMemoryVariant,
|
|
465
739
|
) {
|
|
466
740
|
if (requestGeneration != generation || disposed) return
|
|
467
741
|
clearCurrentTarget()
|
|
@@ -471,18 +745,34 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
471
745
|
val target = object : CustomViewTarget<OneKeyImageHostView, Drawable>(hostView) {
|
|
472
746
|
override fun onResourceLoading(placeholder: Drawable?) {
|
|
473
747
|
if (requestGeneration != generation || disposed) return
|
|
474
|
-
showLoading(
|
|
748
|
+
showLoading(
|
|
749
|
+
requestIsActive = true,
|
|
750
|
+
preserveDisplayedImage = memoryPreviewTarget != null && hostView.drawable != null,
|
|
751
|
+
)
|
|
475
752
|
}
|
|
476
753
|
|
|
477
754
|
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
|
478
755
|
if (requestGeneration != generation || disposed) return
|
|
756
|
+
val replacesMemoryPreview = memoryPreviewTarget != null && hostView.drawable != null
|
|
479
757
|
requestActive = false
|
|
480
758
|
displayState = DisplayState.IMAGE
|
|
481
759
|
hostView.skeletonRequested = false
|
|
482
760
|
hostView.drawStateSymbol = false
|
|
483
761
|
hostView.setBackgroundColor(Color.TRANSPARENT)
|
|
484
762
|
hostView.setImageDrawable(resource)
|
|
485
|
-
|
|
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
|
+
)
|
|
486
776
|
applyAutoplay()
|
|
487
777
|
val loadCallback = onLoad
|
|
488
778
|
val loadEndCallback = onLoadEnd
|
|
@@ -534,6 +824,8 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
534
824
|
headersJson = headersJson,
|
|
535
825
|
policy = policy,
|
|
536
826
|
decodeDimensions = decodeDimensions,
|
|
827
|
+
memoryFamily = memoryFamily,
|
|
828
|
+
memoryVariant = memoryVariant.copy(requestUrl = rawUrl),
|
|
537
829
|
)
|
|
538
830
|
}
|
|
539
831
|
}
|
|
@@ -549,7 +841,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
549
841
|
requestManager()
|
|
550
842
|
.asDrawable()
|
|
551
843
|
.load(OneKeyImageModel.build(requestUrl, headersJson))
|
|
552
|
-
.apply(requestOptions(policy, requestUrl))
|
|
844
|
+
.apply(requestOptions(policy, requestUrl, memoryVariant.round))
|
|
553
845
|
.override(decodeDimensions.width, decodeDimensions.height)
|
|
554
846
|
.listener(object : RequestListener<Drawable> {
|
|
555
847
|
override fun onLoadFailed(
|
|
@@ -577,11 +869,15 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
577
869
|
.into(target)
|
|
578
870
|
}
|
|
579
871
|
|
|
580
|
-
private fun requestOptions(
|
|
872
|
+
private fun requestOptions(
|
|
873
|
+
policy: OneKeyImageCachePolicy,
|
|
874
|
+
uri: String,
|
|
875
|
+
round: Boolean,
|
|
876
|
+
): RequestOptions {
|
|
581
877
|
val options = RequestOptions()
|
|
582
878
|
.withOneKeyAvatarCache(uri)
|
|
583
|
-
.dontTransform()
|
|
584
879
|
.downsample(OneKeyImageSafeDownsampleStrategy)
|
|
880
|
+
if (round) options.circleCrop() else options.dontTransform()
|
|
585
881
|
return when (policy) {
|
|
586
882
|
OneKeyImageCachePolicy.MEMORY_DISK -> options.diskCacheStrategy(oneKeyImageMemoryDiskStrategy(uri))
|
|
587
883
|
OneKeyImageCachePolicy.MEMORY -> options.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
@@ -605,7 +901,10 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
605
901
|
)
|
|
606
902
|
}
|
|
607
903
|
|
|
608
|
-
private fun cancelCurrent(
|
|
904
|
+
private fun cancelCurrent(
|
|
905
|
+
invalidateGeneration: Boolean,
|
|
906
|
+
clearMemoryPreview: Boolean = true,
|
|
907
|
+
) {
|
|
609
908
|
loadRunnable?.let(hostView::removeCallbacks)
|
|
610
909
|
loadRunnable = null
|
|
611
910
|
displayRunnable?.let(hostView::removeCallbacks)
|
|
@@ -614,7 +913,9 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
614
913
|
fallbackRunnable?.let(hostView::removeCallbacks)
|
|
615
914
|
fallbackRunnable = null
|
|
616
915
|
clearCurrentTarget()
|
|
916
|
+
if (clearMemoryPreview) clearMemoryPreviewTarget()
|
|
617
917
|
requestActive = false
|
|
918
|
+
isPreservingDisplayedImage = false
|
|
618
919
|
hostView.skeletonRequested = false
|
|
619
920
|
requestStartedAtMs = null
|
|
620
921
|
resetImageTransition()
|
|
@@ -629,6 +930,15 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
629
930
|
requestManager().clear(target)
|
|
630
931
|
}
|
|
631
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
|
+
|
|
632
942
|
private fun scheduleOnDisplay(requestGeneration: Long) {
|
|
633
943
|
pendingDisplayGeneration = requestGeneration
|
|
634
944
|
schedulePendingDisplayIfNeeded()
|
|
@@ -657,10 +967,22 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
657
967
|
hostView.postOnAnimation(runnable)
|
|
658
968
|
}
|
|
659
969
|
|
|
660
|
-
private fun showLoading(
|
|
970
|
+
private fun showLoading(
|
|
971
|
+
requestIsActive: Boolean,
|
|
972
|
+
preserveDisplayedImage: Boolean = false,
|
|
973
|
+
) {
|
|
661
974
|
displayState = DisplayState.LOADING
|
|
662
975
|
requestActive = requestIsActive
|
|
663
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
|
|
664
986
|
hostView.setImageDrawable(null)
|
|
665
987
|
hostView.drawStateSymbol = false
|
|
666
988
|
applyLoadingAppearance()
|
|
@@ -677,7 +999,9 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
677
999
|
private fun showTerminalState(state: DisplayState) {
|
|
678
1000
|
displayState = state
|
|
679
1001
|
requestActive = false
|
|
1002
|
+
isPreservingDisplayedImage = false
|
|
680
1003
|
resetImageTransition()
|
|
1004
|
+
clearMemoryPreviewTarget()
|
|
681
1005
|
hostView.setImageDrawable(null)
|
|
682
1006
|
hostView.skeletonRequested = false
|
|
683
1007
|
val hidesTerminalState = loadingStrategy == OneKeyImageLoadingStrategy.NONE
|
|
@@ -746,7 +1070,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
746
1070
|
private fun applyVariant() {
|
|
747
1071
|
when (displayState) {
|
|
748
1072
|
DisplayState.IMAGE -> Unit
|
|
749
|
-
DisplayState.LOADING -> applyLoadingAppearance()
|
|
1073
|
+
DisplayState.LOADING -> if (!isPreservingDisplayedImage) applyLoadingAppearance()
|
|
750
1074
|
DisplayState.ERROR, DisplayState.FALLBACK -> {
|
|
751
1075
|
val hidesTerminalState = loadingStrategy == OneKeyImageLoadingStrategy.NONE
|
|
752
1076
|
hostView.setBackgroundColor(
|
|
@@ -778,16 +1102,19 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
778
1102
|
OneKeyImageVariant.AVATAR -> "●"
|
|
779
1103
|
}
|
|
780
1104
|
|
|
781
|
-
private fun applyLoadedImageTransition(
|
|
1105
|
+
private fun applyLoadedImageTransition(
|
|
1106
|
+
cacheType: OneKeyImageCacheType,
|
|
1107
|
+
replacingMemoryPreview: Boolean,
|
|
1108
|
+
) {
|
|
782
1109
|
resetImageTransition()
|
|
783
1110
|
val startedAt = requestStartedAtMs ?: return
|
|
784
|
-
|
|
785
|
-
cacheType
|
|
786
|
-
SystemClock.uptimeMillis() - startedAt
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
791
1118
|
hostView.alpha = 0f
|
|
792
1119
|
hostView.animate()
|
|
793
1120
|
.alpha(1f)
|
|
@@ -841,6 +1168,13 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
|
|
|
841
1168
|
}
|
|
842
1169
|
|
|
843
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
|
+
}
|
|
844
1178
|
hostView.scaleType = when (contentFit ?: OneKeyImageContentFit.COVER) {
|
|
845
1179
|
OneKeyImageContentFit.COVER -> ImageView.ScaleType.CENTER_CROP
|
|
846
1180
|
OneKeyImageContentFit.CONTAIN -> ImageView.ScaleType.FIT_CENTER
|