@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
|
@@ -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,6 +610,61 @@ enum OneKeyImageDecodeSizing {
|
|
|
610
610
|
}
|
|
611
611
|
}
|
|
612
612
|
|
|
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] {
|
|
650
|
+
if bounds.width.isFinite, bounds.height.isFinite, bounds.width > 0, bounds.height > 0 {
|
|
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
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return sizes
|
|
666
|
+
}
|
|
667
|
+
|
|
613
668
|
static func thumbnailPixelSize(
|
|
614
669
|
viewSize: CGSize,
|
|
615
670
|
scale: CGFloat,
|
|
@@ -310,6 +310,175 @@ final class OneKeyImageInfrastructureTests: XCTestCase {
|
|
|
310
310
|
)
|
|
311
311
|
}
|
|
312
312
|
|
|
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
|
+
)
|
|
360
|
+
XCTAssertEqual(
|
|
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
|
|
372
|
+
),
|
|
373
|
+
[try preloadKey(width: 48, height: 64)]
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
func testPreLayoutProbeWithoutHintHasNothingToTry() {
|
|
378
|
+
XCTAssertEqual(
|
|
379
|
+
OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
|
|
380
|
+
bounds: .zero, resizeWidth: nil, resizeHeight: nil, scale: 3, contentFit: .cover
|
|
381
|
+
),
|
|
382
|
+
[]
|
|
383
|
+
)
|
|
384
|
+
XCTAssertEqual(
|
|
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
|
|
465
|
+
)
|
|
466
|
+
)
|
|
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))
|
|
480
|
+
}
|
|
481
|
+
|
|
313
482
|
func testNoSizePreloadUsesCappedDecodeTarget() {
|
|
314
483
|
XCTAssertEqual(HybridOneKeyImageCache.maximumConcurrentPreloads, 4)
|
|
315
484
|
XCTAssertEqual(
|
|
@@ -544,6 +713,55 @@ final class OneKeyImageInfrastructureTests: XCTestCase {
|
|
|
544
713
|
)
|
|
545
714
|
}
|
|
546
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
|
+
|
|
547
765
|
func testSupersededRequestNeverPaintsRecycledView() throws {
|
|
548
766
|
let image = HybridOneKeyImage()
|
|
549
767
|
let imageView = try XCTUnwrap(image.view as? SDAnimatedImageView)
|
package/lib/module/index.js
CHANGED
|
@@ -46,6 +46,7 @@ export function OneKeyImage({
|
|
|
46
46
|
optimizeTos = true,
|
|
47
47
|
round = false,
|
|
48
48
|
resizeWidth,
|
|
49
|
+
resizeHeight,
|
|
49
50
|
overscan = 1.1,
|
|
50
51
|
loadingStrategy = OneKeyImageLoadingStrategy.STATIC,
|
|
51
52
|
placeholderColor,
|
|
@@ -60,7 +61,7 @@ export function OneKeyImage({
|
|
|
60
61
|
}) {
|
|
61
62
|
const normalized = useMemo(() => normalizeSource(source), [source]);
|
|
62
63
|
const resolvedCachePolicy = cachePolicy ?? normalized?.cachePolicy ?? OneKeyImageCachePolicy.MEMORY_DISK;
|
|
63
|
-
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}`;
|
|
64
65
|
const hasSource = normalized != null;
|
|
65
66
|
const hasOverlay = placeholder != null || fallback != null;
|
|
66
67
|
const [loadState, setLoadState] = useState({
|
|
@@ -199,7 +200,8 @@ export function OneKeyImage({
|
|
|
199
200
|
borderStartEndRadius: flattenedStyle.borderStartEndRadius,
|
|
200
201
|
borderEndStartRadius: flattenedStyle.borderEndStartRadius,
|
|
201
202
|
borderEndEndRadius: flattenedStyle.borderEndEndRadius,
|
|
202
|
-
zIndex: 1
|
|
203
|
+
zIndex: 1,
|
|
204
|
+
...(round ? ROUND_CLIP : null)
|
|
203
205
|
} : undefined;
|
|
204
206
|
const native = /*#__PURE__*/createElement(NativeOneKeyImage, {
|
|
205
207
|
...viewProps,
|
|
@@ -215,13 +217,14 @@ export function OneKeyImage({
|
|
|
215
217
|
optimizeTos,
|
|
216
218
|
round,
|
|
217
219
|
resizeWidth,
|
|
220
|
+
resizeHeight,
|
|
218
221
|
overscan,
|
|
219
222
|
loadingStrategy: placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE,
|
|
220
223
|
placeholderColor
|
|
221
224
|
});
|
|
222
225
|
if (!shouldWrapNative) return native;
|
|
223
226
|
return /*#__PURE__*/_jsxs(View, {
|
|
224
|
-
style: [style, styles.overlayContainer, hasRoundedCorners ? styles.borderlessContainer : undefined],
|
|
227
|
+
style: [style, styles.overlayContainer, hasRoundedCorners ? styles.borderlessContainer : undefined, round ? ROUND_CLIP : undefined],
|
|
225
228
|
collapsable: false,
|
|
226
229
|
children: [native, effectiveState === 'loading' && placeholder != null ? /*#__PURE__*/_jsx(View, {
|
|
227
230
|
pointerEvents: "none",
|
|
@@ -257,6 +260,24 @@ export const OneKeyImageCache = {
|
|
|
257
260
|
clearDisk: () => nativeCache.clearDisk(),
|
|
258
261
|
clearAll: () => nativeCache.clearAll()
|
|
259
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
|
+
};
|
|
260
281
|
const styles = StyleSheet.create({
|
|
261
282
|
overlayContainer: {
|
|
262
283
|
overflow: 'hidden'
|
|
@@ -156,6 +156,15 @@ namespace margelo::nitro::onekeyimage {
|
|
|
156
156
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JDouble> /* resizeWidth */)>("setResizeWidth");
|
|
157
157
|
method(_javaPart, resizeWidth.has_value() ? jni::JDouble::valueOf(resizeWidth.value()) : nullptr);
|
|
158
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
|
+
}
|
|
159
168
|
std::optional<double> JHybridOneKeyImageSpec::getOverscan() {
|
|
160
169
|
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JDouble>()>("getOverscan");
|
|
161
170
|
auto __result = method(_javaPart);
|
|
@@ -70,6 +70,8 @@ namespace margelo::nitro::onekeyimage {
|
|
|
70
70
|
void setRound(std::optional<bool> round) override;
|
|
71
71
|
std::optional<double> getResizeWidth() override;
|
|
72
72
|
void setResizeWidth(std::optional<double> resizeWidth) override;
|
|
73
|
+
std::optional<double> getResizeHeight() override;
|
|
74
|
+
void setResizeHeight(std::optional<double> resizeHeight) override;
|
|
73
75
|
std::optional<double> getOverscan() override;
|
|
74
76
|
void setOverscan(std::optional<double> overscan) override;
|
|
75
77
|
std::optional<OneKeyImageLoadingStrategy> getLoadingStrategy() override;
|
|
@@ -103,6 +103,11 @@ void JHybridOneKeyImageStateUpdater::updateViewProps(jni::alias_ref<jni::JClass>
|
|
|
103
103
|
: !newProps->resizeWidth.hasSameValue(oldProps->resizeWidth)) {
|
|
104
104
|
hybridView->setResizeWidth(newProps->resizeWidth.get());
|
|
105
105
|
}
|
|
106
|
+
if (oldProps == nullptr
|
|
107
|
+
? newProps->resizeHeight.isProvided()
|
|
108
|
+
: !newProps->resizeHeight.hasSameValue(oldProps->resizeHeight)) {
|
|
109
|
+
hybridView->setResizeHeight(newProps->resizeHeight.get());
|
|
110
|
+
}
|
|
106
111
|
if (oldProps == nullptr
|
|
107
112
|
? newProps->overscan.isProvided()
|
|
108
113
|
: !newProps->overscan.hasSameValue(oldProps->overscan)) {
|
package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt
CHANGED
|
@@ -87,6 +87,12 @@ abstract class HybridOneKeyImageSpec: HybridView() {
|
|
|
87
87
|
@set:Keep
|
|
88
88
|
abstract var resizeWidth: Double?
|
|
89
89
|
|
|
90
|
+
@get:DoNotStrip
|
|
91
|
+
@get:Keep
|
|
92
|
+
@set:DoNotStrip
|
|
93
|
+
@set:Keep
|
|
94
|
+
abstract var resizeHeight: Double?
|
|
95
|
+
|
|
90
96
|
@get:DoNotStrip
|
|
91
97
|
@get:Keep
|
|
92
98
|
@set:DoNotStrip
|
|
@@ -148,6 +148,13 @@ namespace margelo::nitro::onekeyimage {
|
|
|
148
148
|
inline void setResizeWidth(std::optional<double> resizeWidth) noexcept override {
|
|
149
149
|
_swiftPart.setResizeWidth(resizeWidth);
|
|
150
150
|
}
|
|
151
|
+
inline std::optional<double> getResizeHeight() noexcept override {
|
|
152
|
+
auto __result = _swiftPart.getResizeHeight();
|
|
153
|
+
return __result;
|
|
154
|
+
}
|
|
155
|
+
inline void setResizeHeight(std::optional<double> resizeHeight) noexcept override {
|
|
156
|
+
_swiftPart.setResizeHeight(resizeHeight);
|
|
157
|
+
}
|
|
151
158
|
inline std::optional<double> getOverscan() noexcept override {
|
|
152
159
|
auto __result = _swiftPart.getOverscan();
|
|
153
160
|
return __result;
|
|
@@ -158,6 +158,12 @@ using namespace margelo::nitro::onekeyimage::views;
|
|
|
158
158
|
: !newViewProps.resizeWidth.hasSameValue(oldViewProps->resizeWidth)) {
|
|
159
159
|
swiftPart.setResizeWidth(newViewProps.resizeWidth.get());
|
|
160
160
|
}
|
|
161
|
+
// resizeHeight: optional
|
|
162
|
+
if (oldViewProps == nullptr
|
|
163
|
+
? newViewProps.resizeHeight.isProvided()
|
|
164
|
+
: !newViewProps.resizeHeight.hasSameValue(oldViewProps->resizeHeight)) {
|
|
165
|
+
swiftPart.setResizeHeight(newViewProps.resizeHeight.get());
|
|
166
|
+
}
|
|
161
167
|
// overscan: optional
|
|
162
168
|
if (oldViewProps == nullptr
|
|
163
169
|
? newViewProps.overscan.isProvided()
|
|
@@ -20,6 +20,7 @@ public protocol HybridOneKeyImageSpec_protocol: HybridObject, HybridView {
|
|
|
20
20
|
var optimizeTos: Bool? { get set }
|
|
21
21
|
var round: Bool? { get set }
|
|
22
22
|
var resizeWidth: Double? { get set }
|
|
23
|
+
var resizeHeight: Double? { get set }
|
|
23
24
|
var overscan: Double? { get set }
|
|
24
25
|
var loadingStrategy: OneKeyImageLoadingStrategy? { get set }
|
|
25
26
|
var placeholderColor: String? { get set }
|
|
@@ -340,6 +340,30 @@ open class HybridOneKeyImageSpec_cxx {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
public final var resizeHeight: bridge.std__optional_double_ {
|
|
344
|
+
@inline(__always)
|
|
345
|
+
get {
|
|
346
|
+
return { () -> bridge.std__optional_double_ in
|
|
347
|
+
if let __unwrappedValue = self.__implementation.resizeHeight {
|
|
348
|
+
return bridge.create_std__optional_double_(__unwrappedValue)
|
|
349
|
+
} else {
|
|
350
|
+
return .init()
|
|
351
|
+
}
|
|
352
|
+
}()
|
|
353
|
+
}
|
|
354
|
+
@inline(__always)
|
|
355
|
+
set {
|
|
356
|
+
self.__implementation.resizeHeight = { () -> Double? in
|
|
357
|
+
if bridge.has_value_std__optional_double_(newValue) {
|
|
358
|
+
let __unwrapped = bridge.get_std__optional_double_(newValue)
|
|
359
|
+
return __unwrapped
|
|
360
|
+
} else {
|
|
361
|
+
return nil
|
|
362
|
+
}
|
|
363
|
+
}()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
343
367
|
public final var overscan: bridge.std__optional_double_ {
|
|
344
368
|
@inline(__always)
|
|
345
369
|
get {
|
|
@@ -34,6 +34,8 @@ namespace margelo::nitro::onekeyimage {
|
|
|
34
34
|
prototype.registerHybridSetter("round", &HybridOneKeyImageSpec::setRound);
|
|
35
35
|
prototype.registerHybridGetter("resizeWidth", &HybridOneKeyImageSpec::getResizeWidth);
|
|
36
36
|
prototype.registerHybridSetter("resizeWidth", &HybridOneKeyImageSpec::setResizeWidth);
|
|
37
|
+
prototype.registerHybridGetter("resizeHeight", &HybridOneKeyImageSpec::getResizeHeight);
|
|
38
|
+
prototype.registerHybridSetter("resizeHeight", &HybridOneKeyImageSpec::setResizeHeight);
|
|
37
39
|
prototype.registerHybridGetter("overscan", &HybridOneKeyImageSpec::getOverscan);
|
|
38
40
|
prototype.registerHybridSetter("overscan", &HybridOneKeyImageSpec::setOverscan);
|
|
39
41
|
prototype.registerHybridGetter("loadingStrategy", &HybridOneKeyImageSpec::getLoadingStrategy);
|
|
@@ -80,6 +80,8 @@ namespace margelo::nitro::onekeyimage {
|
|
|
80
80
|
virtual void setRound(std::optional<bool> round) = 0;
|
|
81
81
|
virtual std::optional<double> getResizeWidth() = 0;
|
|
82
82
|
virtual void setResizeWidth(std::optional<double> resizeWidth) = 0;
|
|
83
|
+
virtual std::optional<double> getResizeHeight() = 0;
|
|
84
|
+
virtual void setResizeHeight(std::optional<double> resizeHeight) = 0;
|
|
83
85
|
virtual std::optional<double> getOverscan() = 0;
|
|
84
86
|
virtual void setOverscan(std::optional<double> overscan) = 0;
|
|
85
87
|
virtual std::optional<OneKeyImageLoadingStrategy> getLoadingStrategy() = 0;
|
|
@@ -30,6 +30,7 @@ namespace margelo::nitro::onekeyimage::views {
|
|
|
30
30
|
optimizeTos(nitro::ReactProp<std::optional<bool>>::fromRawValue("OneKeyImage", "optimizeTos", rawProps, sourceProps.optimizeTos)),
|
|
31
31
|
round(nitro::ReactProp<std::optional<bool>>::fromRawValue("OneKeyImage", "round", rawProps, sourceProps.round)),
|
|
32
32
|
resizeWidth(nitro::ReactProp<std::optional<double>>::fromRawValue("OneKeyImage", "resizeWidth", rawProps, sourceProps.resizeWidth)),
|
|
33
|
+
resizeHeight(nitro::ReactProp<std::optional<double>>::fromRawValue("OneKeyImage", "resizeHeight", rawProps, sourceProps.resizeHeight)),
|
|
33
34
|
overscan(nitro::ReactProp<std::optional<double>>::fromRawValue("OneKeyImage", "overscan", rawProps, sourceProps.overscan)),
|
|
34
35
|
loadingStrategy(nitro::ReactProp<std::optional<OneKeyImageLoadingStrategy>>::fromRawValue("OneKeyImage", "loadingStrategy", rawProps, sourceProps.loadingStrategy)),
|
|
35
36
|
placeholderColor(nitro::ReactProp<std::optional<std::string>>::fromRawValue("OneKeyImage", "placeholderColor", rawProps, sourceProps.placeholderColor)),
|
|
@@ -52,6 +53,7 @@ namespace margelo::nitro::onekeyimage::views {
|
|
|
52
53
|
case hashString("optimizeTos"): return true;
|
|
53
54
|
case hashString("round"): return true;
|
|
54
55
|
case hashString("resizeWidth"): return true;
|
|
56
|
+
case hashString("resizeHeight"): return true;
|
|
55
57
|
case hashString("overscan"): return true;
|
|
56
58
|
case hashString("loadingStrategy"): return true;
|
|
57
59
|
case hashString("placeholderColor"): return true;
|