@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct OneKeyImageMemoryFamilyKey: Hashable, Sendable {
|
|
4
|
+
let rawURL: URL
|
|
5
|
+
let headersDigest: String?
|
|
6
|
+
|
|
7
|
+
init(rawURL: URL, headersJson: String?) {
|
|
8
|
+
self.rawURL = rawURL
|
|
9
|
+
headersDigest =
|
|
10
|
+
OneKeyImageHeadersIdentity(
|
|
11
|
+
headers: OneKeyImageRequestContext.headers(from: headersJson)
|
|
12
|
+
).cacheKeyComponent
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct OneKeyImageMemoryVariant: Hashable, Sendable {
|
|
17
|
+
let requestURL: URL
|
|
18
|
+
let pixelWidth: Int
|
|
19
|
+
let pixelHeight: Int
|
|
20
|
+
|
|
21
|
+
init(requestURL: URL, thumbnailPixelSize: CGSize) {
|
|
22
|
+
self.requestURL = requestURL
|
|
23
|
+
pixelWidth = max(Int(thumbnailPixelSize.width.rounded()), 1)
|
|
24
|
+
pixelHeight = max(Int(thumbnailPixelSize.height.rounded()), 1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var thumbnailPixelSize: CGSize {
|
|
28
|
+
CGSize(width: pixelWidth, height: pixelHeight)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var area: Int64 { Int64(pixelWidth) * Int64(pixelHeight) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Bounded hints for reproducing size-specific SDWebImage memory-cache keys.
|
|
35
|
+
/// SDImageCache remains authoritative; stale hints are removed on probe miss.
|
|
36
|
+
final class OneKeyImageMemoryVariantRegistry: @unchecked Sendable {
|
|
37
|
+
static let shared = OneKeyImageMemoryVariantRegistry()
|
|
38
|
+
|
|
39
|
+
private static let maximumFamilies = 1024
|
|
40
|
+
private static let maximumVariantsPerFamily = 8
|
|
41
|
+
private static let maximumAspectRatioDrift = 1.1
|
|
42
|
+
|
|
43
|
+
private let lock = NSLock()
|
|
44
|
+
private var variantsByFamily: [OneKeyImageMemoryFamilyKey: [OneKeyImageMemoryVariant]] = [:]
|
|
45
|
+
private var familyAccessOrder: [OneKeyImageMemoryFamilyKey] = []
|
|
46
|
+
|
|
47
|
+
func record(_ variant: OneKeyImageMemoryVariant, for family: OneKeyImageMemoryFamilyKey) {
|
|
48
|
+
lock.lock()
|
|
49
|
+
defer { lock.unlock() }
|
|
50
|
+
var variants = variantsByFamily[family] ?? []
|
|
51
|
+
variants.removeAll { $0 == variant }
|
|
52
|
+
variants.append(variant)
|
|
53
|
+
if variants.count > Self.maximumVariantsPerFamily {
|
|
54
|
+
variants.removeFirst(variants.count - Self.maximumVariantsPerFamily)
|
|
55
|
+
}
|
|
56
|
+
variantsByFamily[family] = variants
|
|
57
|
+
touch(family)
|
|
58
|
+
while familyAccessOrder.count > Self.maximumFamilies {
|
|
59
|
+
let oldest = familyAccessOrder.removeFirst()
|
|
60
|
+
variantsByFamily.removeValue(forKey: oldest)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func remove(_ variant: OneKeyImageMemoryVariant, for family: OneKeyImageMemoryFamilyKey) {
|
|
65
|
+
lock.lock()
|
|
66
|
+
defer { lock.unlock() }
|
|
67
|
+
guard var variants = variantsByFamily[family] else { return }
|
|
68
|
+
variants.removeAll { $0 == variant }
|
|
69
|
+
if variants.isEmpty {
|
|
70
|
+
variantsByFamily.removeValue(forKey: family)
|
|
71
|
+
familyAccessOrder.removeAll { $0 == family }
|
|
72
|
+
} else {
|
|
73
|
+
variantsByFamily[family] = variants
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func candidates(
|
|
78
|
+
for family: OneKeyImageMemoryFamilyKey,
|
|
79
|
+
target: OneKeyImageMemoryVariant,
|
|
80
|
+
excluding exactVariants: Set<OneKeyImageMemoryVariant>
|
|
81
|
+
) -> [OneKeyImageMemoryVariant] {
|
|
82
|
+
lock.lock()
|
|
83
|
+
let variants = variantsByFamily[family] ?? []
|
|
84
|
+
if !variants.isEmpty { touch(family) }
|
|
85
|
+
lock.unlock()
|
|
86
|
+
return Self.orderedCandidates(
|
|
87
|
+
variants: variants,
|
|
88
|
+
target: target,
|
|
89
|
+
excluding: exactVariants
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func clear() {
|
|
94
|
+
lock.lock()
|
|
95
|
+
variantsByFamily.removeAll()
|
|
96
|
+
familyAccessOrder.removeAll()
|
|
97
|
+
lock.unlock()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static func orderedCandidates(
|
|
101
|
+
variants: [OneKeyImageMemoryVariant],
|
|
102
|
+
target: OneKeyImageMemoryVariant,
|
|
103
|
+
excluding exactVariants: Set<OneKeyImageMemoryVariant> = []
|
|
104
|
+
) -> [OneKeyImageMemoryVariant] {
|
|
105
|
+
var seen: Set<OneKeyImageMemoryVariant> = []
|
|
106
|
+
let compatible = variants.reversed().filter {
|
|
107
|
+
seen.insert($0).inserted && !exactVariants.contains($0) && aspectRatioIsCompatible($0, target)
|
|
108
|
+
}
|
|
109
|
+
let larger =
|
|
110
|
+
compatible
|
|
111
|
+
.filter { $0.pixelWidth >= target.pixelWidth && $0.pixelHeight >= target.pixelHeight }
|
|
112
|
+
.min { lhs, rhs in lhs.area < rhs.area }
|
|
113
|
+
let smaller =
|
|
114
|
+
compatible
|
|
115
|
+
.filter { $0.pixelWidth <= target.pixelWidth && $0.pixelHeight <= target.pixelHeight }
|
|
116
|
+
.max { lhs, rhs in lhs.area < rhs.area }
|
|
117
|
+
var result: [OneKeyImageMemoryVariant] = []
|
|
118
|
+
if let larger { result.append(larger) }
|
|
119
|
+
if let smaller, smaller != larger { result.append(smaller) }
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private static func aspectRatioIsCompatible(
|
|
124
|
+
_ candidate: OneKeyImageMemoryVariant,
|
|
125
|
+
_ target: OneKeyImageMemoryVariant
|
|
126
|
+
) -> Bool {
|
|
127
|
+
let candidateRatio = Double(candidate.pixelWidth) / Double(candidate.pixelHeight)
|
|
128
|
+
let targetRatio = Double(target.pixelWidth) / Double(target.pixelHeight)
|
|
129
|
+
return max(candidateRatio, targetRatio) / min(candidateRatio, targetRatio)
|
|
130
|
+
<= maximumAspectRatioDrift
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func touch(_ family: OneKeyImageMemoryFamilyKey) {
|
|
134
|
+
familyAccessOrder.removeAll { $0 == family }
|
|
135
|
+
familyAccessOrder.append(family)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -610,17 +610,59 @@ enum OneKeyImageDecodeSizing {
|
|
|
610
610
|
}
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
-
/// The logical size
|
|
614
|
-
///
|
|
615
|
-
///
|
|
616
|
-
///
|
|
617
|
-
///
|
|
618
|
-
|
|
619
|
-
|
|
613
|
+
/// The logical display size the TOS rendition is picked for. A preload picks
|
|
614
|
+
/// it from the long edge of its `resizeWidth`/`resizeHeight` hint, so a view
|
|
615
|
+
/// carrying the same hint must do the same (before AND after layout) or its
|
|
616
|
+
/// request URL, and with it the cache key, diverges from what the preload
|
|
617
|
+
/// stored. Without a hint the laid-out bounds decide; nil before layout.
|
|
618
|
+
static func tosDisplaySize(
|
|
619
|
+
bounds: CGSize,
|
|
620
|
+
resizeWidth: Double?,
|
|
621
|
+
resizeHeight: Double?
|
|
622
|
+
) -> CGFloat? {
|
|
623
|
+
if let hint = logicalViewSize(width: resizeWidth, height: resizeHeight) {
|
|
624
|
+
return max(hint.width, hint.height)
|
|
625
|
+
}
|
|
626
|
+
let edge = max(bounds.width, bounds.height)
|
|
627
|
+
return edge.isFinite && edge > 0 ? edge : nil
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/// Decode-thumbnail sizes worth probing the memory cache with, most specific
|
|
631
|
+
/// first. The thumbnail size is part of the SDWebImage cache key, so a probe
|
|
632
|
+
/// only hits when it reproduces the key some earlier load stored under.
|
|
633
|
+
///
|
|
634
|
+
/// - Laid out: exactly the size this view's own request uses (`bounds` +
|
|
635
|
+
/// `contentFit`), nothing else.
|
|
636
|
+
/// - Before layout the bounds are empty, but the JS side already knows the
|
|
637
|
+
/// display size through the `resizeWidth` / `resizeHeight` hints (the same
|
|
638
|
+
/// fields a preload takes). Two keys can hold the image then: the one the
|
|
639
|
+
/// laid-out request will ask for (hint + this view's `contentFit`) and the
|
|
640
|
+
/// one a preload stored (hint + `.cover`, see `HybridOneKeyImageCache`).
|
|
641
|
+
/// They coincide for a square hint or `.cover`; otherwise both are probed.
|
|
642
|
+
/// - No bounds and no usable hint: nothing to probe with.
|
|
643
|
+
static func probeThumbnailPixelSizes(
|
|
644
|
+
bounds: CGSize,
|
|
645
|
+
resizeWidth: Double?,
|
|
646
|
+
resizeHeight: Double?,
|
|
647
|
+
scale: CGFloat,
|
|
648
|
+
contentFit: OneKeyImageContentFit
|
|
649
|
+
) -> [CGSize] {
|
|
620
650
|
if bounds.width.isFinite, bounds.height.isFinite, bounds.width > 0, bounds.height > 0 {
|
|
621
|
-
return bounds
|
|
651
|
+
return thumbnailPixelSize(viewSize: bounds, scale: scale, contentFit: contentFit)
|
|
652
|
+
.map { [$0] } ?? []
|
|
653
|
+
}
|
|
654
|
+
guard let hint = logicalViewSize(width: resizeWidth, height: resizeHeight) else {
|
|
655
|
+
return []
|
|
656
|
+
}
|
|
657
|
+
var sizes: [CGSize] = []
|
|
658
|
+
for fit in [contentFit, .cover] {
|
|
659
|
+
if let size = thumbnailPixelSize(viewSize: hint, scale: scale, contentFit: fit),
|
|
660
|
+
!sizes.contains(size)
|
|
661
|
+
{
|
|
662
|
+
sizes.append(size)
|
|
663
|
+
}
|
|
622
664
|
}
|
|
623
|
-
return
|
|
665
|
+
return sizes
|
|
624
666
|
}
|
|
625
667
|
|
|
626
668
|
static func thumbnailPixelSize(
|
|
@@ -310,31 +310,173 @@ final class OneKeyImageInfrastructureTests: XCTestCase {
|
|
|
310
310
|
)
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
// The pre-layout memory probe is only useful if it reproduces a key some
|
|
314
|
+
// earlier load stored under. These pin the probe against the two real key
|
|
315
|
+
// producers: the laid-out render request (bounds + contentFit) and the
|
|
316
|
+
// preload (logicalViewSize(resizeWidth, resizeHeight) + .cover).
|
|
317
|
+
private func renderKey(bounds: CGSize, fit: OneKeyImageContentFit) throws -> CGSize {
|
|
318
|
+
try XCTUnwrap(
|
|
319
|
+
OneKeyImageDecodeSizing.thumbnailPixelSize(viewSize: bounds, scale: 3, contentFit: fit)
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private func preloadKey(width: Double?, height: Double?) throws -> CGSize {
|
|
324
|
+
try renderKey(
|
|
325
|
+
bounds: try XCTUnwrap(OneKeyImageDecodeSizing.logicalViewSize(width: width, height: height)),
|
|
326
|
+
fit: .cover
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func testLaidOutProbeUsesExactlyTheRenderRequestKey() throws {
|
|
331
|
+
let bounds = CGSize(width: 40, height: 64)
|
|
332
|
+
for fit in [OneKeyImageContentFit.cover, .contain, .fill, .center] {
|
|
333
|
+
XCTAssertEqual(
|
|
334
|
+
OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
|
|
335
|
+
bounds: bounds, resizeWidth: 32, resizeHeight: 48, scale: 3, contentFit: fit
|
|
336
|
+
),
|
|
337
|
+
[try renderKey(bounds: bounds, fit: fit)],
|
|
338
|
+
"\(fit)"
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
func testPreLayoutSquareHintMatchesPreloadAndRenderKeys() throws {
|
|
344
|
+
// A token icon: resizeWidth only, laid out square, cover. One key, shared
|
|
345
|
+
// by the preload, the laid-out request and the probe.
|
|
346
|
+
let sizes = OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
|
|
347
|
+
bounds: .zero, resizeWidth: 40, resizeHeight: nil, scale: 3, contentFit: .cover
|
|
348
|
+
)
|
|
349
|
+
XCTAssertEqual(sizes, [CGSize(width: 120, height: 120)])
|
|
350
|
+
XCTAssertEqual(sizes, [try preloadKey(width: 40, height: nil)])
|
|
351
|
+
XCTAssertEqual(sizes, [try renderKey(bounds: CGSize(width: 40, height: 40), fit: .cover)])
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
func testPreLayoutRectangularHintProbesRenderAndPreloadKeys() throws {
|
|
355
|
+
// The README preload shape (48x64 @3) under a non-cover view: the laid-out
|
|
356
|
+
// request key comes first, the preload's cover key second, no duplicates.
|
|
357
|
+
let sizes = OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
|
|
358
|
+
bounds: .zero, resizeWidth: 48, resizeHeight: 64, scale: 3, contentFit: .contain
|
|
359
|
+
)
|
|
315
360
|
XCTAssertEqual(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
361
|
+
sizes,
|
|
362
|
+
[
|
|
363
|
+
try renderKey(bounds: CGSize(width: 48, height: 64), fit: .contain),
|
|
364
|
+
try preloadKey(width: 48, height: 64),
|
|
365
|
+
]
|
|
366
|
+
)
|
|
367
|
+
XCTAssertEqual(sizes, [CGSize(width: 144, height: 192), CGSize(width: 192, height: 192)])
|
|
368
|
+
// Under cover both collapse to the preload key.
|
|
369
|
+
XCTAssertEqual(
|
|
370
|
+
OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
|
|
371
|
+
bounds: .zero, resizeWidth: 48, resizeHeight: 64, scale: 3, contentFit: .cover
|
|
319
372
|
),
|
|
320
|
-
|
|
373
|
+
[try preloadKey(width: 48, height: 64)]
|
|
321
374
|
)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
func testPreLayoutProbeWithoutHintHasNothingToTry() {
|
|
378
|
+
XCTAssertEqual(
|
|
379
|
+
OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
|
|
380
|
+
bounds: .zero, resizeWidth: nil, resizeHeight: nil, scale: 3, contentFit: .cover
|
|
381
|
+
),
|
|
382
|
+
[]
|
|
325
383
|
)
|
|
326
|
-
XCTAssertEqual(probe, CGSize(width: 40, height: 40))
|
|
327
384
|
XCTAssertEqual(
|
|
328
|
-
OneKeyImageDecodeSizing.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
385
|
+
OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
|
|
386
|
+
bounds: .zero, resizeWidth: 0, resizeHeight: -1, scale: 3, contentFit: .cover
|
|
387
|
+
),
|
|
388
|
+
[]
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func testMemoryVariantsChooseNearestLargerThenNearestSmaller() {
|
|
393
|
+
let target = memoryVariant(96)
|
|
394
|
+
XCTAssertEqual(
|
|
395
|
+
OneKeyImageMemoryVariantRegistry.orderedCandidates(
|
|
396
|
+
variants: [memoryVariant(48), memoryVariant(72), memoryVariant(120), memoryVariant(144)],
|
|
397
|
+
target: target
|
|
398
|
+
),
|
|
399
|
+
[memoryVariant(120), memoryVariant(72)]
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
func testMemoryVariantsIgnoreExactAndIncompatibleAspectRatios() {
|
|
404
|
+
let target = memoryVariant(96)
|
|
405
|
+
let wide = OneKeyImageMemoryVariant(
|
|
406
|
+
requestURL: URL(string: "https://example.com/wide.png")!,
|
|
407
|
+
thumbnailPixelSize: CGSize(width: 120, height: 60)
|
|
408
|
+
)
|
|
409
|
+
XCTAssertEqual(
|
|
410
|
+
OneKeyImageMemoryVariantRegistry.orderedCandidates(
|
|
411
|
+
variants: [target, wide, memoryVariant(120)],
|
|
412
|
+
target: target,
|
|
413
|
+
excluding: [target]
|
|
414
|
+
),
|
|
415
|
+
[memoryVariant(120)]
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
func testCrossSizeCenterPreviewUsesTemporaryScaling() {
|
|
420
|
+
XCTAssertTrue(
|
|
421
|
+
HybridOneKeyImage.shouldScaleCenterMemoryPreview(
|
|
422
|
+
contentFit: .center,
|
|
423
|
+
candidate: memoryVariant(48),
|
|
424
|
+
target: memoryVariant(96)
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
XCTAssertTrue(
|
|
428
|
+
HybridOneKeyImage.shouldScaleCenterMemoryPreview(
|
|
429
|
+
contentFit: .center,
|
|
430
|
+
candidate: memoryVariant(144),
|
|
431
|
+
target: memoryVariant(96)
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
XCTAssertFalse(
|
|
435
|
+
HybridOneKeyImage.shouldScaleCenterMemoryPreview(
|
|
436
|
+
contentFit: .center,
|
|
437
|
+
candidate: memoryVariant(96),
|
|
438
|
+
target: memoryVariant(96)
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
XCTAssertFalse(
|
|
442
|
+
HybridOneKeyImage.shouldScaleCenterMemoryPreview(
|
|
443
|
+
contentFit: .cover,
|
|
444
|
+
candidate: memoryVariant(48),
|
|
445
|
+
target: memoryVariant(96)
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func testReplacingMemoryPreviewSkipsWholeViewFade() {
|
|
451
|
+
XCTAssertFalse(
|
|
452
|
+
HybridOneKeyImage.shouldAnimateLoadedImageTransition(
|
|
453
|
+
cacheType: .disk,
|
|
454
|
+
requestDuration: 1,
|
|
455
|
+
reduceMotionEnabled: false,
|
|
456
|
+
replacingMemoryPreview: true
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
XCTAssertTrue(
|
|
460
|
+
HybridOneKeyImage.shouldAnimateLoadedImageTransition(
|
|
461
|
+
cacheType: .disk,
|
|
462
|
+
requestDuration: 1,
|
|
463
|
+
reduceMotionEnabled: false,
|
|
464
|
+
replacingMemoryPreview: false
|
|
333
465
|
)
|
|
334
466
|
)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private func memoryVariant(_ size: Int) -> OneKeyImageMemoryVariant {
|
|
470
|
+
OneKeyImageMemoryVariant(
|
|
471
|
+
requestURL: URL(string: "https://example.com/image-\(size).png")!,
|
|
472
|
+
thumbnailPixelSize: CGSize(width: size, height: size)
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func testDisplayedImageIsPreservedOnlyWhileShowingOne() {
|
|
477
|
+
XCTAssertTrue(HybridOneKeyImage.shouldPreserveDisplayedImage(isShowingImage: true, hasImage: true))
|
|
478
|
+
XCTAssertFalse(HybridOneKeyImage.shouldPreserveDisplayedImage(isShowingImage: true, hasImage: false))
|
|
479
|
+
XCTAssertFalse(HybridOneKeyImage.shouldPreserveDisplayedImage(isShowingImage: false, hasImage: true))
|
|
338
480
|
}
|
|
339
481
|
|
|
340
482
|
func testNoSizePreloadUsesCappedDecodeTarget() {
|
|
@@ -571,6 +713,55 @@ final class OneKeyImageInfrastructureTests: XCTestCase {
|
|
|
571
713
|
)
|
|
572
714
|
}
|
|
573
715
|
|
|
716
|
+
func testRecycledViewForgetsBothResizeHints() throws {
|
|
717
|
+
let image = HybridOneKeyImage()
|
|
718
|
+
image.sourceUri = "https://example.com/hinted.png"
|
|
719
|
+
image.resizeWidth = 48
|
|
720
|
+
image.resizeHeight = 64
|
|
721
|
+
image.prepareForRecycle()
|
|
722
|
+
XCTAssertNil(image.resizeWidth)
|
|
723
|
+
XCTAssertNil(image.resizeHeight)
|
|
724
|
+
XCTAssertNil(image.sourceUri)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
func testViewAndPreloadPickTheSameTosRenditionForOneHint() throws {
|
|
728
|
+
// Preload: long edge of logicalViewSize(resizeWidth, resizeHeight).
|
|
729
|
+
// View: the same, before and after layout, so the request URL (and with it
|
|
730
|
+
// the cache key) is identical on both sides.
|
|
731
|
+
let raw = try XCTUnwrap(URL(string: "https://common.onekey-asset.com/token.png"))
|
|
732
|
+
let hint = try XCTUnwrap(OneKeyImageDecodeSizing.logicalViewSize(width: 48, height: 64))
|
|
733
|
+
let preloadDisplaySize = max(hint.width, hint.height)
|
|
734
|
+
for bounds in [CGSize.zero, CGSize(width: 48, height: 64), CGSize(width: 30, height: 30)] {
|
|
735
|
+
let viewDisplaySize = try XCTUnwrap(
|
|
736
|
+
OneKeyImageDecodeSizing.tosDisplaySize(bounds: bounds, resizeWidth: 48, resizeHeight: 64)
|
|
737
|
+
)
|
|
738
|
+
XCTAssertEqual(viewDisplaySize, preloadDisplaySize)
|
|
739
|
+
XCTAssertEqual(
|
|
740
|
+
OneKeyTosURL.optimized(
|
|
741
|
+
rawURL: raw, displaySize: viewDisplaySize, scale: 3, overscan: 1.1, hasCustomIdentity: false
|
|
742
|
+
),
|
|
743
|
+
OneKeyTosURL.optimized(
|
|
744
|
+
rawURL: raw, displaySize: preloadDisplaySize, scale: 3, overscan: 1.1, hasCustomIdentity: false
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
// Width-only hint keeps the historical behaviour (the width itself).
|
|
749
|
+
XCTAssertEqual(
|
|
750
|
+
OneKeyImageDecodeSizing.tosDisplaySize(bounds: .zero, resizeWidth: 40, resizeHeight: nil),
|
|
751
|
+
40
|
|
752
|
+
)
|
|
753
|
+
// No hint: the laid-out bounds decide; nothing before layout.
|
|
754
|
+
XCTAssertEqual(
|
|
755
|
+
OneKeyImageDecodeSizing.tosDisplaySize(
|
|
756
|
+
bounds: CGSize(width: 40, height: 64), resizeWidth: nil, resizeHeight: nil
|
|
757
|
+
),
|
|
758
|
+
64
|
|
759
|
+
)
|
|
760
|
+
XCTAssertNil(
|
|
761
|
+
OneKeyImageDecodeSizing.tosDisplaySize(bounds: .zero, resizeWidth: nil, resizeHeight: nil)
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
|
|
574
765
|
func testSupersededRequestNeverPaintsRecycledView() throws {
|
|
575
766
|
let image = HybridOneKeyImage()
|
|
576
767
|
let imageView = try XCTUnwrap(image.view as? SDAnimatedImageView)
|
|
@@ -592,4 +783,50 @@ final class OneKeyImageInfrastructureTests: XCTestCase {
|
|
|
592
783
|
)
|
|
593
784
|
XCTAssertTrue(imageView.clearBufferWhenStopped)
|
|
594
785
|
}
|
|
786
|
+
|
|
787
|
+
func testRoundPropUpdatesNativeCornerRadius() throws {
|
|
788
|
+
let image = HybridOneKeyImage()
|
|
789
|
+
let imageView = try XCTUnwrap(image.view as? SDAnimatedImageView)
|
|
790
|
+
imageView.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
|
|
791
|
+
|
|
792
|
+
image.sourceUri = "https://example.com/round.png"
|
|
793
|
+
image.round = true
|
|
794
|
+
image.afterUpdate()
|
|
795
|
+
imageView.layoutIfNeeded()
|
|
796
|
+
XCTAssertEqual(imageView.layer.cornerRadius, 20)
|
|
797
|
+
|
|
798
|
+
image.round = nil
|
|
799
|
+
image.afterUpdate()
|
|
800
|
+
XCTAssertEqual(imageView.layer.cornerRadius, 20)
|
|
801
|
+
|
|
802
|
+
image.round = false
|
|
803
|
+
image.afterUpdate()
|
|
804
|
+
XCTAssertEqual(imageView.layer.cornerRadius, 0)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
func testRecycleKeepsRoundMaskUntilVisibleViewDetaches() throws {
|
|
808
|
+
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
|
809
|
+
let image = HybridOneKeyImage()
|
|
810
|
+
let imageView = try XCTUnwrap(image.view as? SDAnimatedImageView)
|
|
811
|
+
imageView.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
|
|
812
|
+
window.addSubview(imageView)
|
|
813
|
+
image.sourceUri = "https://example.com/round.png"
|
|
814
|
+
image.round = true
|
|
815
|
+
image.afterUpdate()
|
|
816
|
+
imageView.layoutIfNeeded()
|
|
817
|
+
|
|
818
|
+
// Fabric resets props in setter order before the view leaves the hierarchy.
|
|
819
|
+
image.round = false
|
|
820
|
+
image.sourceUri = nil
|
|
821
|
+
image.afterUpdate()
|
|
822
|
+
XCTAssertEqual(imageView.layer.cornerRadius, 20)
|
|
823
|
+
|
|
824
|
+
image.prepareForRecycle()
|
|
825
|
+
|
|
826
|
+
XCTAssertEqual(imageView.layer.cornerRadius, 20)
|
|
827
|
+
|
|
828
|
+
image.sourceUri = "https://example.com/reused.png"
|
|
829
|
+
image.afterUpdate()
|
|
830
|
+
XCTAssertEqual(imageView.layer.cornerRadius, 0)
|
|
831
|
+
}
|
|
595
832
|
}
|
package/lib/module/index.js
CHANGED
|
@@ -44,7 +44,9 @@ export function OneKeyImage({
|
|
|
44
44
|
autoplay,
|
|
45
45
|
recyclingKey,
|
|
46
46
|
optimizeTos = true,
|
|
47
|
+
round = false,
|
|
47
48
|
resizeWidth,
|
|
49
|
+
resizeHeight,
|
|
48
50
|
overscan = 1.1,
|
|
49
51
|
loadingStrategy = OneKeyImageLoadingStrategy.STATIC,
|
|
50
52
|
placeholderColor,
|
|
@@ -59,7 +61,7 @@ export function OneKeyImage({
|
|
|
59
61
|
}) {
|
|
60
62
|
const normalized = useMemo(() => normalizeSource(source), [source]);
|
|
61
63
|
const resolvedCachePolicy = cachePolicy ?? normalized?.cachePolicy ?? OneKeyImageCachePolicy.MEMORY_DISK;
|
|
62
|
-
const identity = `${normalized?.uri ?? ''}|${JSON.stringify(normalized?.headers ?? {})}|${resolvedCachePolicy}|${recyclingKey ?? ''}|${contentFit}|${optimizeTos ? '1' : '0'}|${resizeWidth ?? ''}|${overscan}`;
|
|
64
|
+
const identity = `${normalized?.uri ?? ''}|${JSON.stringify(normalized?.headers ?? {})}|${resolvedCachePolicy}|${recyclingKey ?? ''}|${contentFit}|${optimizeTos ? '1' : '0'}|${resizeWidth ?? ''}|${resizeHeight ?? ''}|${overscan}`;
|
|
63
65
|
const hasSource = normalized != null;
|
|
64
66
|
const hasOverlay = placeholder != null || fallback != null;
|
|
65
67
|
const [loadState, setLoadState] = useState({
|
|
@@ -198,7 +200,8 @@ export function OneKeyImage({
|
|
|
198
200
|
borderStartEndRadius: flattenedStyle.borderStartEndRadius,
|
|
199
201
|
borderEndStartRadius: flattenedStyle.borderEndStartRadius,
|
|
200
202
|
borderEndEndRadius: flattenedStyle.borderEndEndRadius,
|
|
201
|
-
zIndex: 1
|
|
203
|
+
zIndex: 1,
|
|
204
|
+
...(round ? ROUND_CLIP : null)
|
|
202
205
|
} : undefined;
|
|
203
206
|
const native = /*#__PURE__*/createElement(NativeOneKeyImage, {
|
|
204
207
|
...viewProps,
|
|
@@ -212,14 +215,16 @@ export function OneKeyImage({
|
|
|
212
215
|
autoplay: autoplay ?? Platform.OS !== 'android',
|
|
213
216
|
recyclingKey,
|
|
214
217
|
optimizeTos,
|
|
218
|
+
round,
|
|
215
219
|
resizeWidth,
|
|
220
|
+
resizeHeight,
|
|
216
221
|
overscan,
|
|
217
222
|
loadingStrategy: placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE,
|
|
218
223
|
placeholderColor
|
|
219
224
|
});
|
|
220
225
|
if (!shouldWrapNative) return native;
|
|
221
226
|
return /*#__PURE__*/_jsxs(View, {
|
|
222
|
-
style: [style, styles.overlayContainer, hasRoundedCorners ? styles.borderlessContainer : undefined],
|
|
227
|
+
style: [style, styles.overlayContainer, hasRoundedCorners ? styles.borderlessContainer : undefined, round ? ROUND_CLIP : undefined],
|
|
223
228
|
collapsable: false,
|
|
224
229
|
children: [native, effectiveState === 'loading' && placeholder != null ? /*#__PURE__*/_jsx(View, {
|
|
225
230
|
pointerEvents: "none",
|
|
@@ -255,6 +260,24 @@ export const OneKeyImageCache = {
|
|
|
255
260
|
clearDisk: () => nativeCache.clearDisk(),
|
|
256
261
|
clearAll: () => nativeCache.clearAll()
|
|
257
262
|
};
|
|
263
|
+
|
|
264
|
+
// `round` clips the native view to a circle: an oval path on Android
|
|
265
|
+
// (`OneKeyImage.kt` `addOval`), a `min(width, height) / 2` corner radius on iOS
|
|
266
|
+
// (`OneKeyImage.swift` `updateRoundMask`). The overlay container has to clip the
|
|
267
|
+
// same way, or a custom placeholder or fallback is painted square over a round
|
|
268
|
+
// image. A percentage radius says that without measuring the view, and covers
|
|
269
|
+
// every per-corner radius the caller's own style may have set.
|
|
270
|
+
const ROUND_CLIP = {
|
|
271
|
+
borderRadius: '50%',
|
|
272
|
+
borderTopLeftRadius: '50%',
|
|
273
|
+
borderTopRightRadius: '50%',
|
|
274
|
+
borderBottomLeftRadius: '50%',
|
|
275
|
+
borderBottomRightRadius: '50%',
|
|
276
|
+
borderStartStartRadius: '50%',
|
|
277
|
+
borderStartEndRadius: '50%',
|
|
278
|
+
borderEndStartRadius: '50%',
|
|
279
|
+
borderEndEndRadius: '50%'
|
|
280
|
+
};
|
|
258
281
|
const styles = StyleSheet.create({
|
|
259
282
|
overlayContainer: {
|
|
260
283
|
overflow: 'hidden'
|
|
@@ -138,6 +138,15 @@ namespace margelo::nitro::onekeyimage {
|
|
|
138
138
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* optimizeTos */)>("setOptimizeTos");
|
|
139
139
|
method(_javaPart, optimizeTos.has_value() ? jni::JBoolean::valueOf(optimizeTos.value()) : nullptr);
|
|
140
140
|
}
|
|
141
|
+
std::optional<bool> JHybridOneKeyImageSpec::getRound() {
|
|
142
|
+
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JBoolean>()>("getRound");
|
|
143
|
+
auto __result = method(_javaPart);
|
|
144
|
+
return __result != nullptr ? std::make_optional(static_cast<bool>(__result->value())) : std::nullopt;
|
|
145
|
+
}
|
|
146
|
+
void JHybridOneKeyImageSpec::setRound(std::optional<bool> round) {
|
|
147
|
+
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* round */)>("setRound");
|
|
148
|
+
method(_javaPart, round.has_value() ? jni::JBoolean::valueOf(round.value()) : nullptr);
|
|
149
|
+
}
|
|
141
150
|
std::optional<double> JHybridOneKeyImageSpec::getResizeWidth() {
|
|
142
151
|
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JDouble>()>("getResizeWidth");
|
|
143
152
|
auto __result = method(_javaPart);
|
|
@@ -147,6 +156,15 @@ namespace margelo::nitro::onekeyimage {
|
|
|
147
156
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JDouble> /* resizeWidth */)>("setResizeWidth");
|
|
148
157
|
method(_javaPart, resizeWidth.has_value() ? jni::JDouble::valueOf(resizeWidth.value()) : nullptr);
|
|
149
158
|
}
|
|
159
|
+
std::optional<double> JHybridOneKeyImageSpec::getResizeHeight() {
|
|
160
|
+
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JDouble>()>("getResizeHeight");
|
|
161
|
+
auto __result = method(_javaPart);
|
|
162
|
+
return __result != nullptr ? std::make_optional(__result->value()) : std::nullopt;
|
|
163
|
+
}
|
|
164
|
+
void JHybridOneKeyImageSpec::setResizeHeight(std::optional<double> resizeHeight) {
|
|
165
|
+
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JDouble> /* resizeHeight */)>("setResizeHeight");
|
|
166
|
+
method(_javaPart, resizeHeight.has_value() ? jni::JDouble::valueOf(resizeHeight.value()) : nullptr);
|
|
167
|
+
}
|
|
150
168
|
std::optional<double> JHybridOneKeyImageSpec::getOverscan() {
|
|
151
169
|
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JDouble>()>("getOverscan");
|
|
152
170
|
auto __result = method(_javaPart);
|
|
@@ -66,8 +66,12 @@ namespace margelo::nitro::onekeyimage {
|
|
|
66
66
|
void setRecyclingKey(const std::optional<std::string>& recyclingKey) override;
|
|
67
67
|
std::optional<bool> getOptimizeTos() override;
|
|
68
68
|
void setOptimizeTos(std::optional<bool> optimizeTos) override;
|
|
69
|
+
std::optional<bool> getRound() override;
|
|
70
|
+
void setRound(std::optional<bool> round) override;
|
|
69
71
|
std::optional<double> getResizeWidth() override;
|
|
70
72
|
void setResizeWidth(std::optional<double> resizeWidth) override;
|
|
73
|
+
std::optional<double> getResizeHeight() override;
|
|
74
|
+
void setResizeHeight(std::optional<double> resizeHeight) override;
|
|
71
75
|
std::optional<double> getOverscan() override;
|
|
72
76
|
void setOverscan(std::optional<double> overscan) override;
|
|
73
77
|
std::optional<OneKeyImageLoadingStrategy> getLoadingStrategy() override;
|
|
@@ -93,11 +93,21 @@ void JHybridOneKeyImageStateUpdater::updateViewProps(jni::alias_ref<jni::JClass>
|
|
|
93
93
|
: !newProps->optimizeTos.hasSameValue(oldProps->optimizeTos)) {
|
|
94
94
|
hybridView->setOptimizeTos(newProps->optimizeTos.get());
|
|
95
95
|
}
|
|
96
|
+
if (oldProps == nullptr
|
|
97
|
+
? newProps->round.isProvided()
|
|
98
|
+
: !newProps->round.hasSameValue(oldProps->round)) {
|
|
99
|
+
hybridView->setRound(newProps->round.get());
|
|
100
|
+
}
|
|
96
101
|
if (oldProps == nullptr
|
|
97
102
|
? newProps->resizeWidth.isProvided()
|
|
98
103
|
: !newProps->resizeWidth.hasSameValue(oldProps->resizeWidth)) {
|
|
99
104
|
hybridView->setResizeWidth(newProps->resizeWidth.get());
|
|
100
105
|
}
|
|
106
|
+
if (oldProps == nullptr
|
|
107
|
+
? newProps->resizeHeight.isProvided()
|
|
108
|
+
: !newProps->resizeHeight.hasSameValue(oldProps->resizeHeight)) {
|
|
109
|
+
hybridView->setResizeHeight(newProps->resizeHeight.get());
|
|
110
|
+
}
|
|
101
111
|
if (oldProps == nullptr
|
|
102
112
|
? newProps->overscan.isProvided()
|
|
103
113
|
: !newProps->overscan.hasSameValue(oldProps->overscan)) {
|
package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt
CHANGED
|
@@ -75,12 +75,24 @@ abstract class HybridOneKeyImageSpec: HybridView() {
|
|
|
75
75
|
@set:Keep
|
|
76
76
|
abstract var optimizeTos: Boolean?
|
|
77
77
|
|
|
78
|
+
@get:DoNotStrip
|
|
79
|
+
@get:Keep
|
|
80
|
+
@set:DoNotStrip
|
|
81
|
+
@set:Keep
|
|
82
|
+
abstract var round: Boolean?
|
|
83
|
+
|
|
78
84
|
@get:DoNotStrip
|
|
79
85
|
@get:Keep
|
|
80
86
|
@set:DoNotStrip
|
|
81
87
|
@set:Keep
|
|
82
88
|
abstract var resizeWidth: Double?
|
|
83
89
|
|
|
90
|
+
@get:DoNotStrip
|
|
91
|
+
@get:Keep
|
|
92
|
+
@set:DoNotStrip
|
|
93
|
+
@set:Keep
|
|
94
|
+
abstract var resizeHeight: Double?
|
|
95
|
+
|
|
84
96
|
@get:DoNotStrip
|
|
85
97
|
@get:Keep
|
|
86
98
|
@set:DoNotStrip
|