@onekeyfe/react-native-image 3.0.156 → 3.0.158

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 (36) hide show
  1. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +12 -0
  2. package/ios/OneKeyImage.swift +114 -18
  3. package/ios/OneKeyImageRequestContext.swift +55 -0
  4. package/ios/tests/OneKeyImageInfrastructureTests.swift +134 -0
  5. package/lib/module/index.js +3 -1
  6. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
  7. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
  8. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
  9. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
  10. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
  11. package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
  12. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
  13. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
  14. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
  15. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
  16. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
  17. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
  18. package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
  19. package/lib/typescript/src/OneKeyImage.nitro.d.ts +6 -0
  20. package/lib/typescript/src/index.d.ts +1 -1
  21. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
  22. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
  23. package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
  25. package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
  26. package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
  27. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
  28. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
  29. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
  30. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
  31. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
  32. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
  33. package/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
  34. package/package.json +2 -2
  35. package/src/OneKeyImage.nitro.ts +6 -0
  36. package/src/index.tsx +3 -1
@@ -180,6 +180,7 @@ internal fun oneKeyImageRequestSignature(
180
180
  height: Int,
181
181
  density: Float,
182
182
  resizeWidth: Double? = null,
183
+ resizeHeight: Double? = null,
183
184
  ): String = listOf(
184
185
  rawUrl,
185
186
  sourceHeadersJson.orEmpty(),
@@ -190,6 +191,7 @@ internal fun oneKeyImageRequestSignature(
190
191
  overscan.toString(),
191
192
  round.toString(),
192
193
  resizeWidth.toString(),
194
+ resizeHeight.toString(),
193
195
  width.toString(),
194
196
  height.toString(),
195
197
  density.toString(),
@@ -304,6 +306,14 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
304
306
  field = value
305
307
  requestIdentityChanged()
306
308
  }
309
+ // Layout hint pair with resizeWidth; only the iOS pre-layout memory probe
310
+ // consumes it today, Android keys the request on the measured view size.
311
+ override var resizeHeight: Double? = null
312
+ set(value) {
313
+ if (field == value) return
314
+ field = value
315
+ requestIdentityChanged()
316
+ }
307
317
  override var loadingStrategy: OneKeyImageLoadingStrategy? = OneKeyImageLoadingStrategy.STATIC
308
318
  set(value) {
309
319
  field = value
@@ -403,6 +413,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
403
413
  optimizeTos = true
404
414
  round = false
405
415
  resizeWidth = null
416
+ resizeHeight = null
406
417
  overscan = 1.1
407
418
  loadingStrategy = OneKeyImageLoadingStrategy.STATIC
408
419
  placeholderColor = null
@@ -477,6 +488,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
477
488
  height = hostView.height,
478
489
  density = hostView.resources.displayMetrics.density,
479
490
  resizeWidth = resizeWidth,
491
+ resizeHeight = resizeHeight,
480
492
  )
481
493
  if (!force && signature == lastSignature) return
482
494
  lastSignature = signature
@@ -115,6 +115,9 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
115
115
  private var requestActive = false
116
116
  private var isResetting = false
117
117
  private var displayState = DisplayState.loading
118
+ /// `.loading` with the previous image kept on screen as the placeholder
119
+ /// (see `showLoading(preservedImage:)`).
120
+ private var isPreservingDisplayedImage = false
118
121
  private var requestStartedAt: CFTimeInterval?
119
122
 
120
123
  var view: UIView { hostView }
@@ -150,6 +153,11 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
150
153
  if !Self.equalOptionalDouble(resizeWidth, oldValue) { identityDidChange() }
151
154
  }
152
155
  }
156
+ var resizeHeight: Double? {
157
+ didSet {
158
+ if !Self.equalOptionalDouble(resizeHeight, oldValue) { identityDidChange() }
159
+ }
160
+ }
153
161
  var overscan: Double? = 1.1 {
154
162
  didSet {
155
163
  if !Self.equalOptionalDouble(overscan, oldValue) { identityDidChange() }
@@ -260,7 +268,14 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
260
268
  guard !hostView.bounds.isEmpty else {
261
269
  cancelCurrentRequest(invalidateGeneration: true)
262
270
  lastRequestSignature = nil
263
- showLoading(requestIsActive: false, letLibraryStartIndicator: false)
271
+ // A memory-cached image shown from `identityDidChange` before layout
272
+ // stays up; layout re-runs this load with real bounds and confirms it.
273
+ if !Self.shouldPreserveDisplayedImage(
274
+ isShowingImage: displayState == .image,
275
+ hasImage: hostView.image != nil
276
+ ) {
277
+ showLoading(requestIsActive: false, letLibraryStartIndicator: false)
278
+ }
264
279
  return
265
280
  }
266
281
  guard let rawString = sourceUri, !rawString.isEmpty else {
@@ -280,6 +295,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
280
295
  contentFit?.stringValue ?? "cover",
281
296
  String(optimizeTos ?? true),
282
297
  resizeWidth.map(String.init(describing:)) ?? "",
298
+ resizeHeight.map(String.init(describing:)) ?? "",
283
299
  String(overscan ?? 1.1),
284
300
  String(Int(hostView.bounds.width.rounded())),
285
301
  String(Int(hostView.bounds.height.rounded())),
@@ -295,7 +311,20 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
295
311
  onLoadStart?()
296
312
  guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else { return }
297
313
  if showMemoryCachedImageIfAvailable(requestIsActive: true) { return }
298
- showLoading(requestIsActive: true, letLibraryStartIndicator: true)
314
+ // The pre-layout probe may have shown this identity from another thumbnail
315
+ // key (a preload's, or an earlier layout size). Keep it on screen as the
316
+ // placeholder while the exact rendition loads: blanking to a skeleton and
317
+ // redrawing is worse than the skeleton it replaced.
318
+ let preservedImage =
319
+ Self.shouldPreserveDisplayedImage(
320
+ isShowingImage: displayState == .image,
321
+ hasImage: hostView.image != nil
322
+ ) ? hostView.image : nil
323
+ showLoading(
324
+ requestIsActive: true,
325
+ letLibraryStartIndicator: true,
326
+ preservedImage: preservedImage
327
+ )
299
328
 
300
329
  if let violation = OneKeyImageSafetyPolicy.preflight(source: rawString) {
301
330
  finishWithError(violation, generation: generation)
@@ -311,8 +340,11 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
311
340
  }
312
341
 
313
342
  let hasCustomIdentity = OneKeyImageRequestContext.headers(from: sourceHeadersJson) != nil
314
- let displaySize = resizeWidth.flatMap { $0.isFinite && $0 > 0 ? CGFloat($0) : nil }
315
- ?? max(hostView.bounds.width, hostView.bounds.height)
343
+ let displaySize = OneKeyImageDecodeSizing.tosDisplaySize(
344
+ bounds: hostView.bounds.size,
345
+ resizeWidth: resizeWidth,
346
+ resizeHeight: resizeHeight
347
+ ) ?? 0
316
348
  let requestURL =
317
349
  (optimizeTos ?? true)
318
350
  ? OneKeyTosURL.optimized(
@@ -327,7 +359,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
327
359
  url: requestURL,
328
360
  rawURL: rawURL,
329
361
  generation: generation,
330
- mayFallbackToRaw: requestURL != rawURL
362
+ mayFallbackToRaw: requestURL != rawURL,
363
+ placeholderImage: preservedImage
331
364
  )
332
365
  }
333
366
 
@@ -335,7 +368,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
335
368
  url: URL,
336
369
  rawURL: URL,
337
370
  generation: UInt64,
338
- mayFallbackToRaw: Bool
371
+ mayFallbackToRaw: Bool,
372
+ placeholderImage: UIImage? = nil
339
373
  ) {
340
374
  guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else { return }
341
375
  let rawScreenScale: CGFloat = hostView.window?.screen.scale ?? UIScreen.main.scale
@@ -363,7 +397,9 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
363
397
  )
364
398
  hostView.sd_internalSetImage(
365
399
  with: url,
366
- placeholderImage: nil,
400
+ // SDWebImage resets the image view when it starts a load; hand it the
401
+ // preserved image so the view keeps showing it until the load lands.
402
+ placeholderImage: placeholderImage,
367
403
  options: OneKeyImageRequestContext.renderOptions,
368
404
  context: context,
369
405
  setImageBlock: { [weak self] image, _, _, _ in
@@ -409,6 +445,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
409
445
  guard self.claimTerminal(generation) else { return }
410
446
  self.requestActive = false
411
447
  self.displayState = .image
448
+ self.isPreservingDisplayedImage = false
412
449
  self.skeletonIndicator.stopAnimatingIndicator()
413
450
  self.fallbackLayer.isHidden = true
414
451
  self.hostView.backgroundColor = .clear
@@ -517,6 +554,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
517
554
  optimizeTos = true
518
555
  round = false
519
556
  resizeWidth = nil
557
+ resizeHeight = nil
520
558
  overscan = 1.1
521
559
  loadingStrategy = .static
522
560
  placeholderColor = nil
@@ -547,23 +585,56 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
547
585
  guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else {
548
586
  return
549
587
  }
588
+ // Also the placeholder write path (SDWebImage hands the preserved image
589
+ // back through setImageBlock when the load starts), so the preservation
590
+ // flag is NOT touched here; it ends with the terminal callbacks.
550
591
  hostView.image = image
551
592
  }
552
593
 
553
594
  private func showLoading(
554
595
  requestIsActive: Bool,
555
- letLibraryStartIndicator: Bool
596
+ letLibraryStartIndicator: Bool,
597
+ preservedImage: UIImage? = nil
556
598
  ) {
557
599
  displayState = .loading
558
600
  requestActive = requestIsActive
559
601
  resetImageTransition()
560
- hostView.image = nil
561
602
  fallbackLayer.isHidden = true
603
+ if let preservedImage {
604
+ // Same identity, different thumbnail key: the image already on screen
605
+ // stands in for the placeholder, so no blank frame and no indicator.
606
+ // The indicator is unmounted, not just stopped: SDWebImage restarts a
607
+ // mounted `sd_imageIndicator` when the load begins, and `applyVariant`
608
+ // must not reinstall it while this state lasts.
609
+ isPreservingDisplayedImage = true
610
+ hostView.image = preservedImage
611
+ skeletonIndicator.stopAnimatingIndicator()
612
+ if hostView.sd_imageIndicator != nil {
613
+ hostView.sd_imageIndicator = nil
614
+ }
615
+ hostView.backgroundColor = .clear
616
+ return
617
+ }
618
+ isPreservingDisplayedImage = false
619
+ hostView.image = nil
562
620
  applyLoadingAppearance(letLibraryStartIndicator: letLibraryStartIndicator)
563
621
  }
564
622
 
623
+ /// Whether the image currently on screen should stay up while a load for the
624
+ /// SAME identity runs (bounds not laid out yet, or the laid-out request key
625
+ /// differs from the one the pre-layout probe hit). Pure for testability.
626
+ static func shouldPreserveDisplayedImage(isShowingImage: Bool, hasImage: Bool) -> Bool {
627
+ isShowingImage && hasImage
628
+ }
629
+
565
630
  private func showMemoryCachedImageIfAvailable(requestIsActive: Bool) -> Bool {
566
- guard cachePolicy == .memory || cachePolicy == .memoryDisk,
631
+ // JS leaves `cachePolicy` unset for the common case and Fabric then hands
632
+ // Nitro a nil, which the request path already treats as memory-disk; the
633
+ // probe must agree, otherwise a nil policy skips the memory lookup entirely
634
+ // and every mount pays the async pipeline round trip (an icon skeleton
635
+ // frame) for an image that is already decoded in memory.
636
+ let effectivePolicy = cachePolicy ?? .memoryDisk
637
+ guard effectivePolicy == .memory || effectivePolicy == .memoryDisk,
567
638
  let sourceUri,
568
639
  let rawURL = URL(string: sourceUri) else {
569
640
  return false
@@ -571,8 +642,11 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
571
642
  let rawScreenScale: CGFloat = hostView.window?.screen.scale ?? UIScreen.main.scale
572
643
  let screenScale = min(max(rawScreenScale, 1), 3)
573
644
  let hasCustomIdentity = OneKeyImageRequestContext.headers(from: sourceHeadersJson) != nil
574
- let displaySize = resizeWidth.flatMap { $0.isFinite && $0 > 0 ? CGFloat($0) : nil }
575
- ?? max(hostView.bounds.width, hostView.bounds.height)
645
+ let displaySize = OneKeyImageDecodeSizing.tosDisplaySize(
646
+ bounds: hostView.bounds.size,
647
+ resizeWidth: resizeWidth,
648
+ resizeHeight: resizeHeight
649
+ ) ?? 0
576
650
  let requestURL =
577
651
  (optimizeTos ?? true)
578
652
  ? OneKeyTosURL.optimized(
@@ -583,12 +657,21 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
583
657
  hasCustomIdentity: hasCustomIdentity
584
658
  )
585
659
  : rawURL
586
- let thumbnailPixelSize = OneKeyImageDecodeSizing.thumbnailPixelSize(
587
- viewSize: hostView.bounds.size,
588
- scale: screenScale,
589
- contentFit: contentFit ?? .cover
590
- )
591
- func memoryCachedImage(for url: URL) -> UIImage? {
660
+ // The thumbnail size is part of the cache key. Laid out, it is this view's
661
+ // own request key; before layout it comes from the resize hints, under
662
+ // this view's contentFit and under `.cover` (a preload's key). With no
663
+ // bounds and no hint the un-thumbnailed key is the only thing to try.
664
+ let thumbnailCandidates: [CGSize?] = {
665
+ let sizes = OneKeyImageDecodeSizing.probeThumbnailPixelSizes(
666
+ bounds: hostView.bounds.size,
667
+ resizeWidth: resizeWidth,
668
+ resizeHeight: resizeHeight,
669
+ scale: screenScale,
670
+ contentFit: contentFit ?? .cover
671
+ )
672
+ return sizes.isEmpty ? [nil] : sizes
673
+ }()
674
+ func memoryCachedImage(for url: URL, thumbnailPixelSize: CGSize?) -> UIImage? {
592
675
  let context = OneKeyImageRequestContext.make(
593
676
  headersJson: sourceHeadersJson,
594
677
  cachePolicy: cachePolicy ?? .memoryDisk,
@@ -603,6 +686,14 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
603
686
  }
604
687
  return cache.imageFromMemoryCache(forKey: key)
605
688
  }
689
+ func memoryCachedImage(for url: URL) -> UIImage? {
690
+ for thumbnailPixelSize in thumbnailCandidates {
691
+ if let image = memoryCachedImage(for: url, thumbnailPixelSize: thumbnailPixelSize) {
692
+ return image
693
+ }
694
+ }
695
+ return nil
696
+ }
606
697
  guard let image = memoryCachedImage(for: requestURL)
607
698
  ?? (requestURL == rawURL ? nil : memoryCachedImage(for: rawURL)) else {
608
699
  return false
@@ -610,6 +701,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
610
701
  let generation = requestGeneration
611
702
  if requestIsActive, !claimTerminal(generation) { return true }
612
703
  displayState = .image
704
+ isPreservingDisplayedImage = false
613
705
  requestActive = false
614
706
  skeletonIndicator.stopAnimatingIndicator()
615
707
  fallbackLayer.isHidden = true
@@ -647,6 +739,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
647
739
  private func showTerminalState(_ state: DisplayState) {
648
740
  displayState = state
649
741
  requestActive = false
742
+ isPreservingDisplayedImage = false
650
743
  skeletonIndicator.stopAnimatingIndicator()
651
744
  resetImageTransition()
652
745
  hostView.image = nil
@@ -664,6 +757,9 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
664
757
  case .image:
665
758
  break
666
759
  case .loading:
760
+ // A preserved image is the placeholder; re-applying the loading look
761
+ // would cover it with the skeleton / placeholder color again.
762
+ if isPreservingDisplayedImage { break }
667
763
  applyLoadingAppearance(letLibraryStartIndicator: false)
668
764
  case .error, .fallback:
669
765
  let hidesTerminalState = loadingStrategy == .none
@@ -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,91 @@ 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 testDisplayedImageIsPreservedOnlyWhileShowingOne() {
393
+ XCTAssertTrue(HybridOneKeyImage.shouldPreserveDisplayedImage(isShowingImage: true, hasImage: true))
394
+ XCTAssertFalse(HybridOneKeyImage.shouldPreserveDisplayedImage(isShowingImage: true, hasImage: false))
395
+ XCTAssertFalse(HybridOneKeyImage.shouldPreserveDisplayedImage(isShowingImage: false, hasImage: true))
396
+ }
397
+
313
398
  func testNoSizePreloadUsesCappedDecodeTarget() {
314
399
  XCTAssertEqual(HybridOneKeyImageCache.maximumConcurrentPreloads, 4)
315
400
  XCTAssertEqual(
@@ -544,6 +629,55 @@ final class OneKeyImageInfrastructureTests: XCTestCase {
544
629
  )
545
630
  }
546
631
 
632
+ func testRecycledViewForgetsBothResizeHints() throws {
633
+ let image = HybridOneKeyImage()
634
+ image.sourceUri = "https://example.com/hinted.png"
635
+ image.resizeWidth = 48
636
+ image.resizeHeight = 64
637
+ image.prepareForRecycle()
638
+ XCTAssertNil(image.resizeWidth)
639
+ XCTAssertNil(image.resizeHeight)
640
+ XCTAssertNil(image.sourceUri)
641
+ }
642
+
643
+ func testViewAndPreloadPickTheSameTosRenditionForOneHint() throws {
644
+ // Preload: long edge of logicalViewSize(resizeWidth, resizeHeight).
645
+ // View: the same, before and after layout, so the request URL (and with it
646
+ // the cache key) is identical on both sides.
647
+ let raw = try XCTUnwrap(URL(string: "https://common.onekey-asset.com/token.png"))
648
+ let hint = try XCTUnwrap(OneKeyImageDecodeSizing.logicalViewSize(width: 48, height: 64))
649
+ let preloadDisplaySize = max(hint.width, hint.height)
650
+ for bounds in [CGSize.zero, CGSize(width: 48, height: 64), CGSize(width: 30, height: 30)] {
651
+ let viewDisplaySize = try XCTUnwrap(
652
+ OneKeyImageDecodeSizing.tosDisplaySize(bounds: bounds, resizeWidth: 48, resizeHeight: 64)
653
+ )
654
+ XCTAssertEqual(viewDisplaySize, preloadDisplaySize)
655
+ XCTAssertEqual(
656
+ OneKeyTosURL.optimized(
657
+ rawURL: raw, displaySize: viewDisplaySize, scale: 3, overscan: 1.1, hasCustomIdentity: false
658
+ ),
659
+ OneKeyTosURL.optimized(
660
+ rawURL: raw, displaySize: preloadDisplaySize, scale: 3, overscan: 1.1, hasCustomIdentity: false
661
+ )
662
+ )
663
+ }
664
+ // Width-only hint keeps the historical behaviour (the width itself).
665
+ XCTAssertEqual(
666
+ OneKeyImageDecodeSizing.tosDisplaySize(bounds: .zero, resizeWidth: 40, resizeHeight: nil),
667
+ 40
668
+ )
669
+ // No hint: the laid-out bounds decide; nothing before layout.
670
+ XCTAssertEqual(
671
+ OneKeyImageDecodeSizing.tosDisplaySize(
672
+ bounds: CGSize(width: 40, height: 64), resizeWidth: nil, resizeHeight: nil
673
+ ),
674
+ 64
675
+ )
676
+ XCTAssertNil(
677
+ OneKeyImageDecodeSizing.tosDisplaySize(bounds: .zero, resizeWidth: nil, resizeHeight: nil)
678
+ )
679
+ }
680
+
547
681
  func testSupersededRequestNeverPaintsRecycledView() throws {
548
682
  let image = HybridOneKeyImage()
549
683
  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({
@@ -215,6 +216,7 @@ export function OneKeyImage({
215
216
  optimizeTos,
216
217
  round,
217
218
  resizeWidth,
219
+ resizeHeight,
218
220
  overscan,
219
221
  loadingStrategy: placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE,
220
222
  placeholderColor
@@ -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;
@@ -58,6 +58,7 @@ namespace margelo::nitro::onekeyimage::views {
58
58
  nitro::ReactProp<std::optional<bool>> optimizeTos;
59
59
  nitro::ReactProp<std::optional<bool>> round;
60
60
  nitro::ReactProp<std::optional<double>> resizeWidth;
61
+ nitro::ReactProp<std::optional<double>> resizeHeight;
61
62
  nitro::ReactProp<std::optional<double>> overscan;
62
63
  nitro::ReactProp<std::optional<OneKeyImageLoadingStrategy>> loadingStrategy;
63
64
  nitro::ReactProp<std::optional<std::string>> placeholderColor;
@@ -80,6 +81,7 @@ namespace margelo::nitro::onekeyimage::views {
80
81
  optimizeTos.hasSameValue(other.optimizeTos) &&
81
82
  round.hasSameValue(other.round) &&
82
83
  resizeWidth.hasSameValue(other.resizeWidth) &&
84
+ resizeHeight.hasSameValue(other.resizeHeight) &&
83
85
  overscan.hasSameValue(other.overscan) &&
84
86
  loadingStrategy.hasSameValue(other.loadingStrategy) &&
85
87
  placeholderColor.hasSameValue(other.placeholderColor) &&
@@ -103,6 +105,7 @@ namespace margelo::nitro::onekeyimage::views {
103
105
  optimizeTos.isProvided() ||
104
106
  round.isProvided() ||
105
107
  resizeWidth.isProvided() ||
108
+ resizeHeight.isProvided() ||
106
109
  overscan.isProvided() ||
107
110
  loadingStrategy.isProvided() ||
108
111
  placeholderColor.isProvided() ||
@@ -14,6 +14,7 @@
14
14
  "optimizeTos": true,
15
15
  "round": true,
16
16
  "resizeWidth": true,
17
+ "resizeHeight": true,
17
18
  "overscan": true,
18
19
  "loadingStrategy": true,
19
20
  "placeholderColor": true,
@@ -36,6 +36,12 @@ export interface OneKeyImageNativeProps extends HybridViewProps {
36
36
  round?: boolean;
37
37
  /** Display width hint in layout units. Native applies the screen density. */
38
38
  resizeWidth?: number;
39
+ /**
40
+ * Display height hint in layout units. With `resizeWidth` it lets the
41
+ * pre-layout memory-cache probe derive the same decode thumbnail a preload
42
+ * with the same hints stored; omitted, the hint is a square of `resizeWidth`.
43
+ */
44
+ resizeHeight?: number;
39
45
  overscan?: number;
40
46
  loadingStrategy?: OneKeyImageLoadingStrategy;
41
47
  /** Theme-aware color used by static loading, error, and fallback states. */
@@ -45,7 +45,7 @@ export interface OneKeyImageProps extends Omit<OneKeyImageNativeProps, 'sourceUr
45
45
  onError?: MaybeWrappedCallback<(event: OneKeyImageErrorEvent) => void>;
46
46
  onLoadEnd?: MaybeWrappedCallback<() => void>;
47
47
  }
48
- export declare function OneKeyImage({ source, placeholder, fallback, variant, contentFit, cachePolicy, autoplay, recyclingKey, optimizeTos, round, resizeWidth, overscan, loadingStrategy, placeholderColor, hybridRef, onLoadStart, onLoad, onDisplay, onError, onLoadEnd, style, ...viewProps }: OneKeyImageProps): import("react/jsx-runtime").JSX.Element;
48
+ export declare function OneKeyImage({ source, placeholder, fallback, variant, contentFit, cachePolicy, autoplay, recyclingKey, optimizeTos, round, resizeWidth, resizeHeight, overscan, loadingStrategy, placeholderColor, hybridRef, onLoadStart, onLoad, onDisplay, onError, onLoadEnd, style, ...viewProps }: OneKeyImageProps): import("react/jsx-runtime").JSX.Element;
49
49
  export declare const OneKeyImageCache: {
50
50
  preload(sources: OneKeyImagePreloadInput[]): Promise<boolean>;
51
51
  clearMemory: () => Promise<void>;
@@ -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;
@@ -58,6 +58,7 @@ namespace margelo::nitro::onekeyimage::views {
58
58
  nitro::ReactProp<std::optional<bool>> optimizeTos;
59
59
  nitro::ReactProp<std::optional<bool>> round;
60
60
  nitro::ReactProp<std::optional<double>> resizeWidth;
61
+ nitro::ReactProp<std::optional<double>> resizeHeight;
61
62
  nitro::ReactProp<std::optional<double>> overscan;
62
63
  nitro::ReactProp<std::optional<OneKeyImageLoadingStrategy>> loadingStrategy;
63
64
  nitro::ReactProp<std::optional<std::string>> placeholderColor;
@@ -80,6 +81,7 @@ namespace margelo::nitro::onekeyimage::views {
80
81
  optimizeTos.hasSameValue(other.optimizeTos) &&
81
82
  round.hasSameValue(other.round) &&
82
83
  resizeWidth.hasSameValue(other.resizeWidth) &&
84
+ resizeHeight.hasSameValue(other.resizeHeight) &&
83
85
  overscan.hasSameValue(other.overscan) &&
84
86
  loadingStrategy.hasSameValue(other.loadingStrategy) &&
85
87
  placeholderColor.hasSameValue(other.placeholderColor) &&
@@ -103,6 +105,7 @@ namespace margelo::nitro::onekeyimage::views {
103
105
  optimizeTos.isProvided() ||
104
106
  round.isProvided() ||
105
107
  resizeWidth.isProvided() ||
108
+ resizeHeight.isProvided() ||
106
109
  overscan.isProvided() ||
107
110
  loadingStrategy.isProvided() ||
108
111
  placeholderColor.isProvided() ||
@@ -14,6 +14,7 @@
14
14
  "optimizeTos": true,
15
15
  "round": true,
16
16
  "resizeWidth": true,
17
+ "resizeHeight": true,
17
18
  "overscan": true,
18
19
  "loadingStrategy": true,
19
20
  "placeholderColor": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-image",
3
- "version": "3.0.156",
3
+ "version": "3.0.158",
4
4
  "description": "High-performance native image view for OneKey",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -81,7 +81,7 @@
81
81
  "typescript": "^5.9.2"
82
82
  },
83
83
  "peerDependencies": {
84
- "@onekeyfe/react-native-skeleton": "3.0.156",
84
+ "@onekeyfe/react-native-skeleton": "3.0.158",
85
85
  "react": "*",
86
86
  "react-native": "*",
87
87
  "react-native-nitro-modules": "0.37.0"
@@ -47,6 +47,12 @@ export interface OneKeyImageNativeProps extends HybridViewProps {
47
47
  round?: boolean;
48
48
  /** Display width hint in layout units. Native applies the screen density. */
49
49
  resizeWidth?: number;
50
+ /**
51
+ * Display height hint in layout units. With `resizeWidth` it lets the
52
+ * pre-layout memory-cache probe derive the same decode thumbnail a preload
53
+ * with the same hints stored; omitted, the hint is a square of `resizeWidth`.
54
+ */
55
+ resizeHeight?: number;
50
56
  overscan?: number;
51
57
  loadingStrategy?: OneKeyImageLoadingStrategy;
52
58
  /** Theme-aware color used by static loading, error, and fallback states. */
package/src/index.tsx CHANGED
@@ -155,6 +155,7 @@ export function OneKeyImage({
155
155
  optimizeTos = true,
156
156
  round = false,
157
157
  resizeWidth,
158
+ resizeHeight,
158
159
  overscan = 1.1,
159
160
  loadingStrategy = OneKeyImageLoadingStrategy.STATIC,
160
161
  placeholderColor,
@@ -176,7 +177,7 @@ export function OneKeyImage({
176
177
  normalized?.headers ?? {}
177
178
  )}|${resolvedCachePolicy}|${recyclingKey ?? ''}|${contentFit}|${
178
179
  optimizeTos ? '1' : '0'
179
- }|${resizeWidth ?? ''}|${overscan}`;
180
+ }|${resizeWidth ?? ''}|${resizeHeight ?? ''}|${overscan}`;
180
181
  const hasSource = normalized != null;
181
182
  const hasOverlay = placeholder != null || fallback != null;
182
183
  type LoadState = {
@@ -373,6 +374,7 @@ export function OneKeyImage({
373
374
  optimizeTos,
374
375
  round,
375
376
  resizeWidth,
377
+ resizeHeight,
376
378
  overscan,
377
379
  loadingStrategy:
378
380
  placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE,