@onekeyfe/react-native-image 3.0.156 → 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 +293 -20
- 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/test/java/com/margelo/nitro/onekeyimage/OneKeyImageMemoryVariantsTest.kt +149 -0
- package/ios/OneKeyImage.swift +268 -38
- package/ios/OneKeyImageCache.swift +40 -3
- package/ios/OneKeyImageMemoryVariants.swift +137 -0
- package/ios/OneKeyImageRequestContext.swift +55 -0
- package/ios/tests/OneKeyImageInfrastructureTests.swift +218 -0
- package/lib/module/index.js +24 -3
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
- package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
- package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
- package/lib/typescript/src/OneKeyImage.nitro.d.ts +6 -0
- package/lib/typescript/src/index.d.ts +1 -1
- package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
- package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
- package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
- package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
- package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
- package/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
- package/package.json +2 -2
- package/src/OneKeyImage.nitro.ts +6 -0
- package/src/index.tsx +23 -1
package/ios/OneKeyImage.swift
CHANGED
|
@@ -98,6 +98,7 @@ private final class OneKeyImageSkeletonIndicator: NSObject, SDWebImageIndicator
|
|
|
98
98
|
|
|
99
99
|
final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
100
100
|
private enum DisplayState { case loading, image, error, fallback }
|
|
101
|
+
private enum MemoryProbeResult { case miss, preview, exact }
|
|
101
102
|
private static let fadeDelayThreshold: CFTimeInterval = 0.1
|
|
102
103
|
private static let fadeDuration: TimeInterval = 0.14
|
|
103
104
|
|
|
@@ -115,6 +116,11 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
115
116
|
private var requestActive = false
|
|
116
117
|
private var isResetting = false
|
|
117
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
|
|
118
124
|
private var requestStartedAt: CFTimeInterval?
|
|
119
125
|
|
|
120
126
|
var view: UIView { hostView }
|
|
@@ -150,6 +156,11 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
150
156
|
if !Self.equalOptionalDouble(resizeWidth, oldValue) { identityDidChange() }
|
|
151
157
|
}
|
|
152
158
|
}
|
|
159
|
+
var resizeHeight: Double? {
|
|
160
|
+
didSet {
|
|
161
|
+
if !Self.equalOptionalDouble(resizeHeight, oldValue) { identityDidChange() }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
153
164
|
var overscan: Double? = 1.1 {
|
|
154
165
|
didSet {
|
|
155
166
|
if !Self.equalOptionalDouble(overscan, oldValue) { identityDidChange() }
|
|
@@ -246,7 +257,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
246
257
|
self.cancelCurrentRequest(invalidateGeneration: true)
|
|
247
258
|
self.lastRequestSignature = nil
|
|
248
259
|
if let source = self.sourceUri, !source.isEmpty {
|
|
249
|
-
if
|
|
260
|
+
if self.showMemoryCachedImageIfAvailable(requestIsActive: false) == .miss {
|
|
250
261
|
self.showLoading(requestIsActive: false, letLibraryStartIndicator: false)
|
|
251
262
|
}
|
|
252
263
|
} else {
|
|
@@ -260,7 +271,14 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
260
271
|
guard !hostView.bounds.isEmpty else {
|
|
261
272
|
cancelCurrentRequest(invalidateGeneration: true)
|
|
262
273
|
lastRequestSignature = nil
|
|
263
|
-
|
|
274
|
+
// A memory-cached image shown from `identityDidChange` before layout
|
|
275
|
+
// stays up; layout re-runs this load with real bounds and confirms it.
|
|
276
|
+
if !Self.shouldPreserveDisplayedImage(
|
|
277
|
+
isShowingImage: displayState == .image,
|
|
278
|
+
hasImage: hostView.image != nil
|
|
279
|
+
) {
|
|
280
|
+
showLoading(requestIsActive: false, letLibraryStartIndicator: false)
|
|
281
|
+
}
|
|
264
282
|
return
|
|
265
283
|
}
|
|
266
284
|
guard let rawString = sourceUri, !rawString.isEmpty else {
|
|
@@ -280,6 +298,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
280
298
|
contentFit?.stringValue ?? "cover",
|
|
281
299
|
String(optimizeTos ?? true),
|
|
282
300
|
resizeWidth.map(String.init(describing:)) ?? "",
|
|
301
|
+
resizeHeight.map(String.init(describing:)) ?? "",
|
|
283
302
|
String(overscan ?? 1.1),
|
|
284
303
|
String(Int(hostView.bounds.width.rounded())),
|
|
285
304
|
String(Int(hostView.bounds.height.rounded())),
|
|
@@ -294,8 +313,21 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
294
313
|
requestStartedAt = CACurrentMediaTime()
|
|
295
314
|
onLoadStart?()
|
|
296
315
|
guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else { return }
|
|
297
|
-
if showMemoryCachedImageIfAvailable(requestIsActive: true) { return }
|
|
298
|
-
|
|
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
|
+
)
|
|
299
331
|
|
|
300
332
|
if let violation = OneKeyImageSafetyPolicy.preflight(source: rawString) {
|
|
301
333
|
finishWithError(violation, generation: generation)
|
|
@@ -311,8 +343,11 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
311
343
|
}
|
|
312
344
|
|
|
313
345
|
let hasCustomIdentity = OneKeyImageRequestContext.headers(from: sourceHeadersJson) != nil
|
|
314
|
-
let displaySize =
|
|
315
|
-
|
|
346
|
+
let displaySize = OneKeyImageDecodeSizing.tosDisplaySize(
|
|
347
|
+
bounds: hostView.bounds.size,
|
|
348
|
+
resizeWidth: resizeWidth,
|
|
349
|
+
resizeHeight: resizeHeight
|
|
350
|
+
) ?? 0
|
|
316
351
|
let requestURL =
|
|
317
352
|
(optimizeTos ?? true)
|
|
318
353
|
? OneKeyTosURL.optimized(
|
|
@@ -327,7 +362,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
327
362
|
url: requestURL,
|
|
328
363
|
rawURL: rawURL,
|
|
329
364
|
generation: generation,
|
|
330
|
-
mayFallbackToRaw: requestURL != rawURL
|
|
365
|
+
mayFallbackToRaw: requestURL != rawURL,
|
|
366
|
+
placeholderImage: preservedImage
|
|
331
367
|
)
|
|
332
368
|
}
|
|
333
369
|
|
|
@@ -335,7 +371,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
335
371
|
url: URL,
|
|
336
372
|
rawURL: URL,
|
|
337
373
|
generation: UInt64,
|
|
338
|
-
mayFallbackToRaw: Bool
|
|
374
|
+
mayFallbackToRaw: Bool,
|
|
375
|
+
placeholderImage: UIImage? = nil
|
|
339
376
|
) {
|
|
340
377
|
guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else { return }
|
|
341
378
|
let rawScreenScale: CGFloat = hostView.window?.screen.scale ?? UIScreen.main.scale
|
|
@@ -363,7 +400,9 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
363
400
|
)
|
|
364
401
|
hostView.sd_internalSetImage(
|
|
365
402
|
with: url,
|
|
366
|
-
|
|
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,
|
|
367
406
|
options: OneKeyImageRequestContext.renderOptions,
|
|
368
407
|
context: context,
|
|
369
408
|
setImageBlock: { [weak self] image, _, _, _ in
|
|
@@ -399,21 +438,43 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
399
438
|
url: rawURL,
|
|
400
439
|
rawURL: rawURL,
|
|
401
440
|
generation: generation,
|
|
402
|
-
mayFallbackToRaw: false
|
|
441
|
+
mayFallbackToRaw: false,
|
|
442
|
+
placeholderImage: self.hostView.image
|
|
403
443
|
)
|
|
404
444
|
return
|
|
405
445
|
}
|
|
406
446
|
self.finishWithError(safetyViolation ?? error, generation: generation)
|
|
407
447
|
return
|
|
408
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
|
+
}
|
|
409
465
|
guard self.claimTerminal(generation) else { return }
|
|
466
|
+
let replacesMemoryPreview = self.clearMemoryPreviewPresentation()
|
|
410
467
|
self.requestActive = false
|
|
411
468
|
self.displayState = .image
|
|
469
|
+
self.isPreservingDisplayedImage = false
|
|
412
470
|
self.skeletonIndicator.stopAnimatingIndicator()
|
|
413
471
|
self.fallbackLayer.isHidden = true
|
|
414
472
|
self.hostView.backgroundColor = .clear
|
|
415
473
|
let resolvedCacheType = Self.cacheType(cacheType)
|
|
416
|
-
self.applyLoadedImageTransition(
|
|
474
|
+
self.applyLoadedImageTransition(
|
|
475
|
+
cacheType: resolvedCacheType,
|
|
476
|
+
replacingMemoryPreview: replacesMemoryPreview
|
|
477
|
+
)
|
|
417
478
|
self.applyAutoplay()
|
|
418
479
|
let onLoad = self.onLoad
|
|
419
480
|
let onLoadEnd = self.onLoadEnd
|
|
@@ -517,6 +578,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
517
578
|
optimizeTos = true
|
|
518
579
|
round = false
|
|
519
580
|
resizeWidth = nil
|
|
581
|
+
resizeHeight = nil
|
|
520
582
|
overscan = 1.1
|
|
521
583
|
loadingStrategy = .static
|
|
522
584
|
placeholderColor = nil
|
|
@@ -547,32 +609,69 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
547
609
|
guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else {
|
|
548
610
|
return
|
|
549
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.
|
|
550
615
|
hostView.image = image
|
|
551
616
|
}
|
|
552
617
|
|
|
553
618
|
private func showLoading(
|
|
554
619
|
requestIsActive: Bool,
|
|
555
|
-
letLibraryStartIndicator: Bool
|
|
620
|
+
letLibraryStartIndicator: Bool,
|
|
621
|
+
preservedImage: UIImage? = nil
|
|
556
622
|
) {
|
|
557
623
|
displayState = .loading
|
|
558
624
|
requestActive = requestIsActive
|
|
559
625
|
resetImageTransition()
|
|
560
|
-
hostView.image = nil
|
|
561
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
|
|
562
645
|
applyLoadingAppearance(letLibraryStartIndicator: letLibraryStartIndicator)
|
|
563
646
|
}
|
|
564
647
|
|
|
565
|
-
|
|
566
|
-
|
|
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 {
|
|
656
|
+
// JS leaves `cachePolicy` unset for the common case and Fabric then hands
|
|
657
|
+
// Nitro a nil, which the request path already treats as memory-disk; the
|
|
658
|
+
// probe must agree, otherwise a nil policy skips the memory lookup entirely
|
|
659
|
+
// and every mount pays the async pipeline round trip (an icon skeleton
|
|
660
|
+
// frame) for an image that is already decoded in memory.
|
|
661
|
+
let effectivePolicy = cachePolicy ?? .memoryDisk
|
|
662
|
+
guard effectivePolicy == .memory || effectivePolicy == .memoryDisk,
|
|
567
663
|
let sourceUri,
|
|
568
664
|
let rawURL = URL(string: sourceUri) else {
|
|
569
|
-
return
|
|
665
|
+
return .miss
|
|
570
666
|
}
|
|
571
667
|
let rawScreenScale: CGFloat = hostView.window?.screen.scale ?? UIScreen.main.scale
|
|
572
668
|
let screenScale = min(max(rawScreenScale, 1), 3)
|
|
573
669
|
let hasCustomIdentity = OneKeyImageRequestContext.headers(from: sourceHeadersJson) != nil
|
|
574
|
-
let displaySize =
|
|
575
|
-
|
|
670
|
+
let displaySize = OneKeyImageDecodeSizing.tosDisplaySize(
|
|
671
|
+
bounds: hostView.bounds.size,
|
|
672
|
+
resizeWidth: resizeWidth,
|
|
673
|
+
resizeHeight: resizeHeight
|
|
674
|
+
) ?? 0
|
|
576
675
|
let requestURL =
|
|
577
676
|
(optimizeTos ?? true)
|
|
578
677
|
? OneKeyTosURL.optimized(
|
|
@@ -583,12 +682,21 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
583
682
|
hasCustomIdentity: hasCustomIdentity
|
|
584
683
|
)
|
|
585
684
|
: rawURL
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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,
|
|
694
|
+
scale: screenScale,
|
|
695
|
+
contentFit: contentFit ?? .cover
|
|
696
|
+
)
|
|
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
|
+
}
|
|
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
|
+
}
|
|
609
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(
|