@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/ios/OneKeyImage.swift
CHANGED
|
@@ -7,9 +7,13 @@ import UIKit
|
|
|
7
7
|
private final class OneKeyImageHostView: SDAnimatedImageView {
|
|
8
8
|
var onLayout: (() -> Void)?
|
|
9
9
|
var onWindowChanged: ((Bool) -> Void)?
|
|
10
|
+
var round = false {
|
|
11
|
+
didSet { updateRoundMask() }
|
|
12
|
+
}
|
|
10
13
|
|
|
11
14
|
override func layoutSubviews() {
|
|
12
15
|
super.layoutSubviews()
|
|
16
|
+
updateRoundMask()
|
|
13
17
|
onLayout?()
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -17,6 +21,10 @@ private final class OneKeyImageHostView: SDAnimatedImageView {
|
|
|
17
21
|
super.didMoveToWindow()
|
|
18
22
|
onWindowChanged?(window != nil)
|
|
19
23
|
}
|
|
24
|
+
|
|
25
|
+
private func updateRoundMask() {
|
|
26
|
+
layer.cornerRadius = round ? min(bounds.width, bounds.height) / 2 : 0
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
private final class OneKeyImageSkeletonView: UIView {
|
|
@@ -90,6 +98,7 @@ private final class OneKeyImageSkeletonIndicator: NSObject, SDWebImageIndicator
|
|
|
90
98
|
|
|
91
99
|
final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
92
100
|
private enum DisplayState { case loading, image, error, fallback }
|
|
101
|
+
private enum MemoryProbeResult { case miss, preview, exact }
|
|
93
102
|
private static let fadeDelayThreshold: CFTimeInterval = 0.1
|
|
94
103
|
private static let fadeDuration: TimeInterval = 0.14
|
|
95
104
|
|
|
@@ -107,12 +116,19 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
107
116
|
private var requestActive = false
|
|
108
117
|
private var isResetting = false
|
|
109
118
|
private var displayState = DisplayState.loading
|
|
119
|
+
/// `.loading` with the previous image kept on screen as the placeholder
|
|
120
|
+
/// (see `showLoading(preservedImage:)`).
|
|
121
|
+
private var isPreservingDisplayedImage = false
|
|
122
|
+
private var isShowingMemoryPreview = false
|
|
123
|
+
private var scalesCenterMemoryPreview = false
|
|
110
124
|
private var requestStartedAt: CFTimeInterval?
|
|
111
125
|
|
|
112
126
|
var view: UIView { hostView }
|
|
113
127
|
|
|
114
128
|
var sourceUri: String? {
|
|
115
|
-
didSet {
|
|
129
|
+
didSet {
|
|
130
|
+
if sourceUri != oldValue { identityDidChange() }
|
|
131
|
+
}
|
|
116
132
|
}
|
|
117
133
|
var sourceHeadersJson: String? {
|
|
118
134
|
didSet { if sourceHeadersJson != oldValue { identityDidChange() } }
|
|
@@ -134,11 +150,17 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
134
150
|
var optimizeTos: Bool? = true {
|
|
135
151
|
didSet { if optimizeTos != oldValue { identityDidChange() } }
|
|
136
152
|
}
|
|
153
|
+
var round: Bool? = false
|
|
137
154
|
var resizeWidth: Double? {
|
|
138
155
|
didSet {
|
|
139
156
|
if !Self.equalOptionalDouble(resizeWidth, oldValue) { identityDidChange() }
|
|
140
157
|
}
|
|
141
158
|
}
|
|
159
|
+
var resizeHeight: Double? {
|
|
160
|
+
didSet {
|
|
161
|
+
if !Self.equalOptionalDouble(resizeHeight, oldValue) { identityDidChange() }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
142
164
|
var overscan: Double? = 1.1 {
|
|
143
165
|
didSet {
|
|
144
166
|
if !Self.equalOptionalDouble(overscan, oldValue) { identityDidChange() }
|
|
@@ -185,6 +207,12 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
185
207
|
}
|
|
186
208
|
|
|
187
209
|
func afterUpdate() {
|
|
210
|
+
// Fabric can reset `round` before clearing `sourceUri` while removing a view.
|
|
211
|
+
// Apply the shape once per committed prop batch so the outgoing frame keeps
|
|
212
|
+
// its clipping, while mounted and recycled views still receive the new value.
|
|
213
|
+
if let sourceUri, !sourceUri.isEmpty {
|
|
214
|
+
hostView.round = round == true
|
|
215
|
+
}
|
|
188
216
|
scheduleLoad()
|
|
189
217
|
}
|
|
190
218
|
|
|
@@ -229,7 +257,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
229
257
|
self.cancelCurrentRequest(invalidateGeneration: true)
|
|
230
258
|
self.lastRequestSignature = nil
|
|
231
259
|
if let source = self.sourceUri, !source.isEmpty {
|
|
232
|
-
if
|
|
260
|
+
if self.showMemoryCachedImageIfAvailable(requestIsActive: false) == .miss {
|
|
233
261
|
self.showLoading(requestIsActive: false, letLibraryStartIndicator: false)
|
|
234
262
|
}
|
|
235
263
|
} else {
|
|
@@ -245,7 +273,10 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
245
273
|
lastRequestSignature = nil
|
|
246
274
|
// A memory-cached image shown from `identityDidChange` before layout
|
|
247
275
|
// stays up; layout re-runs this load with real bounds and confirms it.
|
|
248
|
-
if
|
|
276
|
+
if !Self.shouldPreserveDisplayedImage(
|
|
277
|
+
isShowingImage: displayState == .image,
|
|
278
|
+
hasImage: hostView.image != nil
|
|
279
|
+
) {
|
|
249
280
|
showLoading(requestIsActive: false, letLibraryStartIndicator: false)
|
|
250
281
|
}
|
|
251
282
|
return
|
|
@@ -267,6 +298,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
267
298
|
contentFit?.stringValue ?? "cover",
|
|
268
299
|
String(optimizeTos ?? true),
|
|
269
300
|
resizeWidth.map(String.init(describing:)) ?? "",
|
|
301
|
+
resizeHeight.map(String.init(describing:)) ?? "",
|
|
270
302
|
String(overscan ?? 1.1),
|
|
271
303
|
String(Int(hostView.bounds.width.rounded())),
|
|
272
304
|
String(Int(hostView.bounds.height.rounded())),
|
|
@@ -281,8 +313,21 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
281
313
|
requestStartedAt = CACurrentMediaTime()
|
|
282
314
|
onLoadStart?()
|
|
283
315
|
guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else { return }
|
|
284
|
-
if showMemoryCachedImageIfAvailable(requestIsActive: true) { return }
|
|
285
|
-
|
|
316
|
+
if showMemoryCachedImageIfAvailable(requestIsActive: true) == .exact { return }
|
|
317
|
+
// The pre-layout probe may have shown this identity from another thumbnail
|
|
318
|
+
// key (a preload's, or an earlier layout size). Keep it on screen as the
|
|
319
|
+
// placeholder while the exact rendition loads: blanking to a skeleton and
|
|
320
|
+
// redrawing is worse than the skeleton it replaced.
|
|
321
|
+
let preservedImage =
|
|
322
|
+
Self.shouldPreserveDisplayedImage(
|
|
323
|
+
isShowingImage: displayState == .image,
|
|
324
|
+
hasImage: hostView.image != nil
|
|
325
|
+
) ? hostView.image : nil
|
|
326
|
+
showLoading(
|
|
327
|
+
requestIsActive: true,
|
|
328
|
+
letLibraryStartIndicator: true,
|
|
329
|
+
preservedImage: preservedImage
|
|
330
|
+
)
|
|
286
331
|
|
|
287
332
|
if let violation = OneKeyImageSafetyPolicy.preflight(source: rawString) {
|
|
288
333
|
finishWithError(violation, generation: generation)
|
|
@@ -298,8 +343,11 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
298
343
|
}
|
|
299
344
|
|
|
300
345
|
let hasCustomIdentity = OneKeyImageRequestContext.headers(from: sourceHeadersJson) != nil
|
|
301
|
-
let displaySize =
|
|
302
|
-
|
|
346
|
+
let displaySize = OneKeyImageDecodeSizing.tosDisplaySize(
|
|
347
|
+
bounds: hostView.bounds.size,
|
|
348
|
+
resizeWidth: resizeWidth,
|
|
349
|
+
resizeHeight: resizeHeight
|
|
350
|
+
) ?? 0
|
|
303
351
|
let requestURL =
|
|
304
352
|
(optimizeTos ?? true)
|
|
305
353
|
? OneKeyTosURL.optimized(
|
|
@@ -314,7 +362,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
314
362
|
url: requestURL,
|
|
315
363
|
rawURL: rawURL,
|
|
316
364
|
generation: generation,
|
|
317
|
-
mayFallbackToRaw: requestURL != rawURL
|
|
365
|
+
mayFallbackToRaw: requestURL != rawURL,
|
|
366
|
+
placeholderImage: preservedImage
|
|
318
367
|
)
|
|
319
368
|
}
|
|
320
369
|
|
|
@@ -322,7 +371,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
322
371
|
url: URL,
|
|
323
372
|
rawURL: URL,
|
|
324
373
|
generation: UInt64,
|
|
325
|
-
mayFallbackToRaw: Bool
|
|
374
|
+
mayFallbackToRaw: Bool,
|
|
375
|
+
placeholderImage: UIImage? = nil
|
|
326
376
|
) {
|
|
327
377
|
guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else { return }
|
|
328
378
|
let rawScreenScale: CGFloat = hostView.window?.screen.scale ?? UIScreen.main.scale
|
|
@@ -350,7 +400,9 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
350
400
|
)
|
|
351
401
|
hostView.sd_internalSetImage(
|
|
352
402
|
with: url,
|
|
353
|
-
|
|
403
|
+
// SDWebImage resets the image view when it starts a load; hand it the
|
|
404
|
+
// preserved image so the view keeps showing it until the load lands.
|
|
405
|
+
placeholderImage: placeholderImage,
|
|
354
406
|
options: OneKeyImageRequestContext.renderOptions,
|
|
355
407
|
context: context,
|
|
356
408
|
setImageBlock: { [weak self] image, _, _, _ in
|
|
@@ -386,21 +438,43 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
386
438
|
url: rawURL,
|
|
387
439
|
rawURL: rawURL,
|
|
388
440
|
generation: generation,
|
|
389
|
-
mayFallbackToRaw: false
|
|
441
|
+
mayFallbackToRaw: false,
|
|
442
|
+
placeholderImage: self.hostView.image
|
|
390
443
|
)
|
|
391
444
|
return
|
|
392
445
|
}
|
|
393
446
|
self.finishWithError(safetyViolation ?? error, generation: generation)
|
|
394
447
|
return
|
|
395
448
|
}
|
|
449
|
+
if let image,
|
|
450
|
+
Self.canUseAsMemoryPreview(image),
|
|
451
|
+
let thumbnailPixelSize,
|
|
452
|
+
self.cachePolicy == .memory || self.cachePolicy == .memoryDisk || self.cachePolicy == nil
|
|
453
|
+
{
|
|
454
|
+
OneKeyImageMemoryVariantRegistry.shared.record(
|
|
455
|
+
OneKeyImageMemoryVariant(
|
|
456
|
+
requestURL: url,
|
|
457
|
+
thumbnailPixelSize: thumbnailPixelSize
|
|
458
|
+
),
|
|
459
|
+
for: OneKeyImageMemoryFamilyKey(
|
|
460
|
+
rawURL: rawURL,
|
|
461
|
+
headersJson: self.sourceHeadersJson
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
}
|
|
396
465
|
guard self.claimTerminal(generation) else { return }
|
|
466
|
+
let replacesMemoryPreview = self.clearMemoryPreviewPresentation()
|
|
397
467
|
self.requestActive = false
|
|
398
468
|
self.displayState = .image
|
|
469
|
+
self.isPreservingDisplayedImage = false
|
|
399
470
|
self.skeletonIndicator.stopAnimatingIndicator()
|
|
400
471
|
self.fallbackLayer.isHidden = true
|
|
401
472
|
self.hostView.backgroundColor = .clear
|
|
402
473
|
let resolvedCacheType = Self.cacheType(cacheType)
|
|
403
|
-
self.applyLoadedImageTransition(
|
|
474
|
+
self.applyLoadedImageTransition(
|
|
475
|
+
cacheType: resolvedCacheType,
|
|
476
|
+
replacingMemoryPreview: replacesMemoryPreview
|
|
477
|
+
)
|
|
404
478
|
self.applyAutoplay()
|
|
405
479
|
let onLoad = self.onLoad
|
|
406
480
|
let onLoadEnd = self.onLoadEnd
|
|
@@ -502,7 +576,9 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
502
576
|
autoplay = true
|
|
503
577
|
recyclingKey = nil
|
|
504
578
|
optimizeTos = true
|
|
579
|
+
round = false
|
|
505
580
|
resizeWidth = nil
|
|
581
|
+
resizeHeight = nil
|
|
506
582
|
overscan = 1.1
|
|
507
583
|
loadingStrategy = .static
|
|
508
584
|
placeholderColor = nil
|
|
@@ -533,22 +609,50 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
533
609
|
guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else {
|
|
534
610
|
return
|
|
535
611
|
}
|
|
612
|
+
// Also the placeholder write path (SDWebImage hands the preserved image
|
|
613
|
+
// back through setImageBlock when the load starts), so the preservation
|
|
614
|
+
// flag is NOT touched here; it ends with the terminal callbacks.
|
|
536
615
|
hostView.image = image
|
|
537
616
|
}
|
|
538
617
|
|
|
539
618
|
private func showLoading(
|
|
540
619
|
requestIsActive: Bool,
|
|
541
|
-
letLibraryStartIndicator: Bool
|
|
620
|
+
letLibraryStartIndicator: Bool,
|
|
621
|
+
preservedImage: UIImage? = nil
|
|
542
622
|
) {
|
|
543
623
|
displayState = .loading
|
|
544
624
|
requestActive = requestIsActive
|
|
545
625
|
resetImageTransition()
|
|
546
|
-
hostView.image = nil
|
|
547
626
|
fallbackLayer.isHidden = true
|
|
627
|
+
if let preservedImage {
|
|
628
|
+
// Same identity, different thumbnail key: the image already on screen
|
|
629
|
+
// stands in for the placeholder, so no blank frame and no indicator.
|
|
630
|
+
// The indicator is unmounted, not just stopped: SDWebImage restarts a
|
|
631
|
+
// mounted `sd_imageIndicator` when the load begins, and `applyVariant`
|
|
632
|
+
// must not reinstall it while this state lasts.
|
|
633
|
+
isPreservingDisplayedImage = true
|
|
634
|
+
hostView.image = preservedImage
|
|
635
|
+
skeletonIndicator.stopAnimatingIndicator()
|
|
636
|
+
if hostView.sd_imageIndicator != nil {
|
|
637
|
+
hostView.sd_imageIndicator = nil
|
|
638
|
+
}
|
|
639
|
+
hostView.backgroundColor = .clear
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
clearMemoryPreviewPresentation()
|
|
643
|
+
isPreservingDisplayedImage = false
|
|
644
|
+
hostView.image = nil
|
|
548
645
|
applyLoadingAppearance(letLibraryStartIndicator: letLibraryStartIndicator)
|
|
549
646
|
}
|
|
550
647
|
|
|
551
|
-
|
|
648
|
+
/// Whether the image currently on screen should stay up while a load for the
|
|
649
|
+
/// SAME identity runs (bounds not laid out yet, or the laid-out request key
|
|
650
|
+
/// differs from the one the pre-layout probe hit). Pure for testability.
|
|
651
|
+
static func shouldPreserveDisplayedImage(isShowingImage: Bool, hasImage: Bool) -> Bool {
|
|
652
|
+
isShowingImage && hasImage
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private func showMemoryCachedImageIfAvailable(requestIsActive: Bool) -> MemoryProbeResult {
|
|
552
656
|
// JS leaves `cachePolicy` unset for the common case and Fabric then hands
|
|
553
657
|
// Nitro a nil, which the request path already treats as memory-disk; the
|
|
554
658
|
// probe must agree, otherwise a nil policy skips the memory lookup entirely
|
|
@@ -558,13 +662,16 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
558
662
|
guard effectivePolicy == .memory || effectivePolicy == .memoryDisk,
|
|
559
663
|
let sourceUri,
|
|
560
664
|
let rawURL = URL(string: sourceUri) else {
|
|
561
|
-
return
|
|
665
|
+
return .miss
|
|
562
666
|
}
|
|
563
667
|
let rawScreenScale: CGFloat = hostView.window?.screen.scale ?? UIScreen.main.scale
|
|
564
668
|
let screenScale = min(max(rawScreenScale, 1), 3)
|
|
565
669
|
let hasCustomIdentity = OneKeyImageRequestContext.headers(from: sourceHeadersJson) != nil
|
|
566
|
-
let displaySize =
|
|
567
|
-
|
|
670
|
+
let displaySize = OneKeyImageDecodeSizing.tosDisplaySize(
|
|
671
|
+
bounds: hostView.bounds.size,
|
|
672
|
+
resizeWidth: resizeWidth,
|
|
673
|
+
resizeHeight: resizeHeight
|
|
674
|
+
) ?? 0
|
|
568
675
|
let requestURL =
|
|
569
676
|
(optimizeTos ?? true)
|
|
570
677
|
? OneKeyTosURL.optimized(
|
|
@@ -575,20 +682,21 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
575
682
|
hasCustomIdentity: hasCustomIdentity
|
|
576
683
|
)
|
|
577
684
|
: rawURL
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
685
|
+
// The thumbnail size is part of the cache key. Laid out, it is this view's
|
|
686
|
+
// own request key; before layout it comes from the resize hints, under
|
|
687
|
+
// this view's contentFit and under `.cover` (a preload's key). With no
|
|
688
|
+
// bounds and no hint the un-thumbnailed key is the only thing to try.
|
|
689
|
+
let thumbnailCandidates: [CGSize?] = {
|
|
690
|
+
let sizes = OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
|
|
691
|
+
bounds: hostView.bounds.size,
|
|
692
|
+
resizeWidth: resizeWidth,
|
|
693
|
+
resizeHeight: resizeHeight,
|
|
587
694
|
scale: screenScale,
|
|
588
695
|
contentFit: contentFit ?? .cover
|
|
589
696
|
)
|
|
590
|
-
|
|
591
|
-
|
|
697
|
+
return sizes.isEmpty ? [nil] : sizes
|
|
698
|
+
}()
|
|
699
|
+
func memoryCachedImage(for url: URL, thumbnailPixelSize: CGSize?) -> UIImage? {
|
|
592
700
|
let context = OneKeyImageRequestContext.make(
|
|
593
701
|
headersJson: sourceHeadersJson,
|
|
594
702
|
cachePolicy: cachePolicy ?? .memoryDisk,
|
|
@@ -603,24 +711,100 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
603
711
|
}
|
|
604
712
|
return cache.imageFromMemoryCache(forKey: key)
|
|
605
713
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
714
|
+
|
|
715
|
+
var exactProbes: [(variant: OneKeyImageMemoryVariant?, url: URL, size: CGSize?)] = []
|
|
716
|
+
for url in requestURL == rawURL ? [requestURL] : [requestURL, rawURL] {
|
|
717
|
+
for size in thumbnailCandidates {
|
|
718
|
+
exactProbes.append(
|
|
719
|
+
(
|
|
720
|
+
variant: size.map {
|
|
721
|
+
OneKeyImageMemoryVariant(requestURL: url, thumbnailPixelSize: $0)
|
|
722
|
+
},
|
|
723
|
+
url: url,
|
|
724
|
+
size: size
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
}
|
|
609
728
|
}
|
|
729
|
+
let family = OneKeyImageMemoryFamilyKey(rawURL: rawURL, headersJson: sourceHeadersJson)
|
|
730
|
+
let exactVariants = Set(exactProbes.compactMap(\.variant))
|
|
731
|
+
for probe in exactProbes {
|
|
732
|
+
guard let image = memoryCachedImage(for: probe.url, thumbnailPixelSize: probe.size) else {
|
|
733
|
+
if let variant = probe.variant {
|
|
734
|
+
OneKeyImageMemoryVariantRegistry.shared.remove(variant, for: family)
|
|
735
|
+
}
|
|
736
|
+
continue
|
|
737
|
+
}
|
|
738
|
+
if let variant = probe.variant, Self.canUseAsMemoryPreview(image) {
|
|
739
|
+
OneKeyImageMemoryVariantRegistry.shared.record(variant, for: family)
|
|
740
|
+
}
|
|
741
|
+
displayMemoryCachedImage(
|
|
742
|
+
image,
|
|
743
|
+
requestIsActive: requestIsActive,
|
|
744
|
+
terminal: true,
|
|
745
|
+
scalesCenterPreview: false
|
|
746
|
+
)
|
|
747
|
+
return .exact
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if let target = exactProbes.compactMap(\.variant).first {
|
|
751
|
+
let candidates = OneKeyImageMemoryVariantRegistry.shared.candidates(
|
|
752
|
+
for: family,
|
|
753
|
+
target: target,
|
|
754
|
+
excluding: exactVariants
|
|
755
|
+
)
|
|
756
|
+
for candidate in candidates {
|
|
757
|
+
guard
|
|
758
|
+
let image = memoryCachedImage(
|
|
759
|
+
for: candidate.requestURL,
|
|
760
|
+
thumbnailPixelSize: candidate.thumbnailPixelSize
|
|
761
|
+
),
|
|
762
|
+
Self.canUseAsMemoryPreview(image)
|
|
763
|
+
else {
|
|
764
|
+
OneKeyImageMemoryVariantRegistry.shared.remove(candidate, for: family)
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
displayMemoryCachedImage(
|
|
768
|
+
image,
|
|
769
|
+
requestIsActive: false,
|
|
770
|
+
terminal: false,
|
|
771
|
+
scalesCenterPreview: Self.shouldScaleCenterMemoryPreview(
|
|
772
|
+
contentFit: contentFit ?? .cover,
|
|
773
|
+
candidate: candidate,
|
|
774
|
+
target: target
|
|
775
|
+
)
|
|
776
|
+
)
|
|
777
|
+
OneKeyImageMemoryVariantRegistry.shared.record(candidate, for: family)
|
|
778
|
+
return .preview
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return .miss
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private func displayMemoryCachedImage(
|
|
785
|
+
_ image: UIImage,
|
|
786
|
+
requestIsActive: Bool,
|
|
787
|
+
terminal: Bool,
|
|
788
|
+
scalesCenterPreview: Bool
|
|
789
|
+
) {
|
|
610
790
|
let generation = requestGeneration
|
|
611
|
-
if requestIsActive, !claimTerminal(generation) { return
|
|
791
|
+
if terminal, requestIsActive, !claimTerminal(generation) { return }
|
|
792
|
+
isShowingMemoryPreview = !terminal
|
|
793
|
+
self.scalesCenterMemoryPreview = !terminal && scalesCenterPreview
|
|
794
|
+
applyContentFit()
|
|
612
795
|
displayState = .image
|
|
796
|
+
isPreservingDisplayedImage = false
|
|
613
797
|
requestActive = false
|
|
614
798
|
skeletonIndicator.stopAnimatingIndicator()
|
|
615
799
|
fallbackLayer.isHidden = true
|
|
616
800
|
resetImageTransition()
|
|
617
801
|
applyDisplayedImage(image, generation: generation)
|
|
618
802
|
hostView.backgroundColor = .clear
|
|
619
|
-
if requestIsActive {
|
|
620
|
-
applyLoadedImageTransition(cacheType: .memory)
|
|
803
|
+
if terminal, requestIsActive {
|
|
804
|
+
applyLoadedImageTransition(cacheType: .memory, replacingMemoryPreview: false)
|
|
621
805
|
}
|
|
622
806
|
applyAutoplay()
|
|
623
|
-
if requestIsActive {
|
|
807
|
+
if terminal, requestIsActive {
|
|
624
808
|
let onLoad = onLoad
|
|
625
809
|
let onLoadEnd = onLoadEnd
|
|
626
810
|
Self.deliverTerminalCallbacks(
|
|
@@ -629,11 +813,32 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
629
813
|
},
|
|
630
814
|
onLoadEnd: { onLoadEnd?() }
|
|
631
815
|
)
|
|
632
|
-
guard requestGeneration == generation else { return
|
|
816
|
+
guard requestGeneration == generation else { return }
|
|
633
817
|
pendingDisplayGeneration = generation
|
|
634
818
|
schedulePendingDisplayIfNeeded()
|
|
635
819
|
}
|
|
636
|
-
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private static func canUseAsMemoryPreview(_ image: UIImage) -> Bool {
|
|
823
|
+
!(image is SDAnimatedImage) && image.images == nil
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
static func shouldScaleCenterMemoryPreview(
|
|
827
|
+
contentFit: OneKeyImageContentFit,
|
|
828
|
+
candidate: OneKeyImageMemoryVariant,
|
|
829
|
+
target: OneKeyImageMemoryVariant
|
|
830
|
+
) -> Bool {
|
|
831
|
+
guard case .center = contentFit else { return false }
|
|
832
|
+
return candidate.pixelWidth != target.pixelWidth || candidate.pixelHeight != target.pixelHeight
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
@discardableResult
|
|
836
|
+
private func clearMemoryPreviewPresentation() -> Bool {
|
|
837
|
+
let wasShowingMemoryPreview = isShowingMemoryPreview
|
|
838
|
+
isShowingMemoryPreview = false
|
|
839
|
+
scalesCenterMemoryPreview = false
|
|
840
|
+
applyContentFit()
|
|
841
|
+
return wasShowingMemoryPreview
|
|
637
842
|
}
|
|
638
843
|
|
|
639
844
|
private func showError() {
|
|
@@ -647,6 +852,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
647
852
|
private func showTerminalState(_ state: DisplayState) {
|
|
648
853
|
displayState = state
|
|
649
854
|
requestActive = false
|
|
855
|
+
isPreservingDisplayedImage = false
|
|
856
|
+
clearMemoryPreviewPresentation()
|
|
650
857
|
skeletonIndicator.stopAnimatingIndicator()
|
|
651
858
|
resetImageTransition()
|
|
652
859
|
hostView.image = nil
|
|
@@ -664,6 +871,9 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
664
871
|
case .image:
|
|
665
872
|
break
|
|
666
873
|
case .loading:
|
|
874
|
+
// A preserved image is the placeholder; re-applying the loading look
|
|
875
|
+
// would cover it with the skeleton / placeholder color again.
|
|
876
|
+
if isPreservingDisplayedImage { break }
|
|
667
877
|
applyLoadingAppearance(letLibraryStartIndicator: false)
|
|
668
878
|
case .error, .fallback:
|
|
669
879
|
let hidesTerminalState = loadingStrategy == .none
|
|
@@ -739,12 +949,28 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
739
949
|
return [base, highlight]
|
|
740
950
|
}
|
|
741
951
|
|
|
742
|
-
|
|
952
|
+
static func shouldAnimateLoadedImageTransition(
|
|
953
|
+
cacheType: OneKeyImageCacheType,
|
|
954
|
+
requestDuration: CFTimeInterval?,
|
|
955
|
+
reduceMotionEnabled: Bool,
|
|
956
|
+
replacingMemoryPreview: Bool
|
|
957
|
+
) -> Bool {
|
|
958
|
+
!replacingMemoryPreview && cacheType != .memory
|
|
959
|
+
&& (requestDuration ?? 0) >= fadeDelayThreshold && !reduceMotionEnabled
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
private func applyLoadedImageTransition(
|
|
963
|
+
cacheType: OneKeyImageCacheType,
|
|
964
|
+
replacingMemoryPreview: Bool
|
|
965
|
+
) {
|
|
743
966
|
resetImageTransition()
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
967
|
+
let requestDuration = requestStartedAt.map { CACurrentMediaTime() - $0 }
|
|
968
|
+
guard Self.shouldAnimateLoadedImageTransition(
|
|
969
|
+
cacheType: cacheType,
|
|
970
|
+
requestDuration: requestDuration,
|
|
971
|
+
reduceMotionEnabled: UIAccessibility.isReduceMotionEnabled,
|
|
972
|
+
replacingMemoryPreview: replacingMemoryPreview
|
|
973
|
+
) else {
|
|
748
974
|
return
|
|
749
975
|
}
|
|
750
976
|
hostView.alpha = 0
|
|
@@ -789,6 +1015,10 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
789
1015
|
}
|
|
790
1016
|
|
|
791
1017
|
private func applyContentFit() {
|
|
1018
|
+
if scalesCenterMemoryPreview, case .center = contentFit ?? .cover {
|
|
1019
|
+
hostView.contentMode = .scaleAspectFit
|
|
1020
|
+
return
|
|
1021
|
+
}
|
|
792
1022
|
switch contentFit ?? .cover {
|
|
793
1023
|
case .cover: hostView.contentMode = .scaleAspectFill
|
|
794
1024
|
case .contain: hostView.contentMode = .scaleAspectFit
|
|
@@ -76,6 +76,7 @@ final class HybridOneKeyImageCache: HybridOneKeyImageCacheSpec {
|
|
|
76
76
|
SDImageCache.shared.clearMemory()
|
|
77
77
|
// OneKey patch: Clear the dedicated local-avatar cache with public cache operations.
|
|
78
78
|
OneKeyAvatarImageLoader.cache.clearMemory()
|
|
79
|
+
OneKeyImageMemoryVariantRegistry.shared.clear()
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -94,6 +95,7 @@ final class HybridOneKeyImageCache: HybridOneKeyImageCacheSpec {
|
|
|
94
95
|
SDImageCache.shared.clearMemory()
|
|
95
96
|
// OneKey patch: The avatar cache is shared by render and preload requests.
|
|
96
97
|
OneKeyAvatarImageLoader.cache.clearMemory()
|
|
98
|
+
OneKeyImageMemoryVariantRegistry.shared.clear()
|
|
97
99
|
return try clearDisk()
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -136,14 +138,49 @@ final class HybridOneKeyImageCache: HybridOneKeyImageCacheSpec {
|
|
|
136
138
|
source: source,
|
|
137
139
|
thumbnailPixelSize: thumbnailPixelSize
|
|
138
140
|
)
|
|
141
|
+
if result == .success {
|
|
142
|
+
recordMemoryVariant(
|
|
143
|
+
rawURL: rawURL,
|
|
144
|
+
requestURL: requestURL,
|
|
145
|
+
source: source,
|
|
146
|
+
thumbnailPixelSize: thumbnailPixelSize
|
|
147
|
+
)
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
139
150
|
if case .failure = result, requestURL != rawURL {
|
|
140
|
-
|
|
151
|
+
let fallbackResult = await loadAttempt(
|
|
141
152
|
url: rawURL,
|
|
142
153
|
source: source,
|
|
143
154
|
thumbnailPixelSize: thumbnailPixelSize
|
|
144
|
-
)
|
|
155
|
+
)
|
|
156
|
+
if fallbackResult == .success {
|
|
157
|
+
recordMemoryVariant(
|
|
158
|
+
rawURL: rawURL,
|
|
159
|
+
requestURL: rawURL,
|
|
160
|
+
source: source,
|
|
161
|
+
thumbnailPixelSize: thumbnailPixelSize
|
|
162
|
+
)
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
145
165
|
}
|
|
146
|
-
return
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private static func recordMemoryVariant(
|
|
170
|
+
rawURL: URL,
|
|
171
|
+
requestURL: URL,
|
|
172
|
+
source: OneKeyImagePreloadSource,
|
|
173
|
+
thumbnailPixelSize: CGSize
|
|
174
|
+
) {
|
|
175
|
+
let policy = source.cachePolicy ?? .memoryDisk
|
|
176
|
+
guard policy == .memory || policy == .memoryDisk else { return }
|
|
177
|
+
OneKeyImageMemoryVariantRegistry.shared.record(
|
|
178
|
+
OneKeyImageMemoryVariant(
|
|
179
|
+
requestURL: requestURL,
|
|
180
|
+
thumbnailPixelSize: thumbnailPixelSize
|
|
181
|
+
),
|
|
182
|
+
for: OneKeyImageMemoryFamilyKey(rawURL: rawURL, headersJson: source.headersJson)
|
|
183
|
+
)
|
|
147
184
|
}
|
|
148
185
|
|
|
149
186
|
private static func loadAttempt(
|