@onekeyfe/react-native-image 3.0.156 → 3.0.157-alpha.249

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.
Files changed (42) hide show
  1. package/README.md +3 -0
  2. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +293 -20
  3. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageCache.kt +34 -4
  4. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageMemoryVariants.kt +119 -0
  5. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageMemoryVariantsTest.kt +149 -0
  6. package/ios/OneKeyImage.swift +268 -38
  7. package/ios/OneKeyImageCache.swift +40 -3
  8. package/ios/OneKeyImageMemoryVariants.swift +137 -0
  9. package/ios/OneKeyImageRequestContext.swift +55 -0
  10. package/ios/tests/OneKeyImageInfrastructureTests.swift +218 -0
  11. package/lib/module/index.js +24 -3
  12. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
  13. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
  14. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
  15. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
  16. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
  17. package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
  18. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
  19. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
  20. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
  21. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
  22. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
  23. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
  24. package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
  25. package/lib/typescript/src/OneKeyImage.nitro.d.ts +6 -0
  26. package/lib/typescript/src/index.d.ts +1 -1
  27. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
  28. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
  29. package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
  31. package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
  32. package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
  33. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
  34. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
  35. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
  36. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
  37. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
  38. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
  39. package/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
  40. package/package.json +2 -2
  41. package/src/OneKeyImage.nitro.ts +6 -0
  42. 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)
@@ -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)) {
@@ -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;