@onekeyfe/react-native-image 3.0.116 → 3.0.118

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 (37) hide show
  1. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +107 -13
  2. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageReusableView.kt +2 -0
  3. package/ios/OneKeyAvatarImageLoader.swift +3 -1
  4. package/ios/OneKeyImage.swift +186 -17
  5. package/ios/OneKeyImageReusableView.swift +2 -0
  6. package/lib/module/index.js +4 -2
  7. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
  8. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
  9. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
  10. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
  11. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
  12. package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
  13. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
  14. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
  15. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
  16. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
  17. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
  18. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
  19. package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
  20. package/lib/typescript/src/OneKeyImage.nitro.d.ts +2 -0
  21. package/lib/typescript/src/index.d.ts +1 -1
  22. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +9 -0
  23. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +2 -0
  24. package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +5 -0
  25. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +6 -0
  26. package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +7 -0
  27. package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +6 -0
  28. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +1 -0
  29. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +24 -0
  30. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +2 -0
  31. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +2 -0
  32. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +2 -0
  33. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +3 -0
  34. package/nitrogen/generated/shared/json/OneKeyImageConfig.json +1 -0
  35. package/package.json +2 -2
  36. package/src/OneKeyImage.nitro.ts +2 -0
  37. package/src/index.tsx +3 -1
@@ -1,14 +1,20 @@
1
1
  package com.margelo.nitro.onekeyimage
2
2
 
3
+ import android.animation.ValueAnimator
3
4
  import android.app.Activity
4
5
  import android.content.Context
5
6
  import android.content.ContextWrapper
7
+ import android.content.res.Configuration
6
8
  import android.graphics.Canvas
7
9
  import android.graphics.Color
8
10
  import android.graphics.Paint
9
11
  import android.graphics.drawable.Animatable
10
12
  import android.graphics.drawable.Drawable
13
+ import android.os.Build
14
+ import android.os.SystemClock
15
+ import android.provider.Settings
11
16
  import android.view.View
17
+ import android.view.animation.DecelerateInterpolator
12
18
  import android.widget.ImageView
13
19
  import com.bumptech.glide.Glide
14
20
  import com.bumptech.glide.load.DataSource
@@ -88,8 +94,16 @@ private class OneKeyImageHostView(context: ThemedReactContext) : ImageView(conte
88
94
  syncPlayback()
89
95
  }
90
96
 
91
- fun updateSkeletonStyle() {
92
- skeleton.updateStyle(null, 3.0)
97
+ fun updateSkeletonStyle(baseColor: Int) {
98
+ val average = (Color.red(baseColor) + Color.green(baseColor) + Color.blue(baseColor)) / 3
99
+ val delta = if (average < 128) 18 else -12
100
+ val highlightColor = Color.argb(
101
+ Color.alpha(baseColor),
102
+ (Color.red(baseColor) + delta).coerceIn(0, 255),
103
+ (Color.green(baseColor) + delta).coerceIn(0, 255),
104
+ (Color.blue(baseColor) + delta).coerceIn(0, 255),
105
+ )
106
+ skeleton.updateStyle(intArrayOf(baseColor, highlightColor), 3.0)
93
107
  skeleton.updateBounds(width, height)
94
108
  }
95
109
 
@@ -137,6 +151,10 @@ internal fun oneKeyImageRequestSignature(
137
151
  class HybridOneKeyImage(private val context: ThemedReactContext) :
138
152
  HybridOneKeyImageSpec(), RecyclableView {
139
153
  private enum class DisplayState { LOADING, IMAGE, ERROR, FALLBACK }
154
+ private companion object {
155
+ const val FADE_DELAY_THRESHOLD_MS = 100L
156
+ const val FADE_DURATION_MS = 140L
157
+ }
140
158
 
141
159
  private val hostView = OneKeyImageHostView(context)
142
160
  // The Activity outlives ScreenStack Fragments but still provides bounded
@@ -156,6 +174,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
156
174
  private var displayRunnable: Runnable? = null
157
175
  private var pendingDisplayGeneration: Long? = null
158
176
  private var fallbackRunnable: Runnable? = null
177
+ private var requestStartedAtMs: Long? = null
159
178
 
160
179
  /**
161
180
  * Native reusable containers own their request cleanup explicitly, so their
@@ -236,6 +255,11 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
236
255
  field = value
237
256
  if (!suppressPropEffects) applyVariant()
238
257
  }
258
+ override var placeholderColor: String? = null
259
+ set(value) {
260
+ field = value
261
+ if (!suppressPropEffects) applyVariant()
262
+ }
239
263
  override var onLoadStart: (() -> Unit)? = null
240
264
  override var onLoad: ((width: Double, height: Double, cacheType: OneKeyImageCacheType) -> Unit)? = null
241
265
  override var onDisplay: (() -> Unit)? = null
@@ -244,7 +268,6 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
244
268
 
245
269
  init {
246
270
  OneKeyImageGlideRegistry.ensureRegistered(context)
247
- hostView.updateSkeletonStyle()
248
271
  hostView.onReadyForRequest = { scheduleLoad() }
249
272
  hostView.onAttachmentChanged = { attached ->
250
273
  if (attached) {
@@ -317,6 +340,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
317
340
  resizeWidth = null
318
341
  overscan = 1.1
319
342
  loadingStrategy = OneKeyImageLoadingStrategy.STATIC
343
+ placeholderColor = null
320
344
  onLoadStart = null
321
345
  onLoad = null
322
346
  onDisplay = null
@@ -375,6 +399,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
375
399
 
376
400
  cancelCurrent(invalidateGeneration = true)
377
401
  val requestGeneration = generation
402
+ requestStartedAtMs = SystemClock.uptimeMillis()
378
403
  onLoadStart?.invoke()
379
404
 
380
405
  val customIdentity = OneKeyImageModel.headers(sourceHeadersJson).isNotEmpty()
@@ -430,6 +455,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
430
455
  hostView.drawStateSymbol = false
431
456
  hostView.setBackgroundColor(Color.TRANSPARENT)
432
457
  hostView.setImageDrawable(resource)
458
+ applyLoadedImageTransition(resolvedCacheType)
433
459
  applyAutoplay()
434
460
  val loadCallback = onLoad
435
461
  val loadEndCallback = onLoadEnd
@@ -563,6 +589,8 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
563
589
  clearCurrentTarget()
564
590
  requestActive = false
565
591
  hostView.skeletonRequested = false
592
+ requestStartedAtMs = null
593
+ resetImageTransition()
566
594
  if (invalidateGeneration) generation++
567
595
  }
568
596
 
@@ -605,6 +633,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
605
633
  private fun showLoading(requestIsActive: Boolean) {
606
634
  displayState = DisplayState.LOADING
607
635
  requestActive = requestIsActive
636
+ resetImageTransition()
608
637
  hostView.setImageDrawable(null)
609
638
  hostView.drawStateSymbol = false
610
639
  applyLoadingAppearance()
@@ -621,11 +650,15 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
621
650
  private fun showTerminalState(state: DisplayState) {
622
651
  displayState = state
623
652
  requestActive = false
653
+ resetImageTransition()
624
654
  hostView.setImageDrawable(null)
625
655
  hostView.skeletonRequested = false
626
- hostView.drawStateSymbol = true
656
+ val hidesTerminalState = loadingStrategy == OneKeyImageLoadingStrategy.NONE
657
+ hostView.drawStateSymbol = !hidesTerminalState
627
658
  hostView.stateSymbol = stateSymbol()
628
- hostView.setBackgroundColor(placeholderColor())
659
+ hostView.setBackgroundColor(
660
+ if (hidesTerminalState) Color.TRANSPARENT else resolvedPlaceholderColor(),
661
+ )
629
662
  hostView.invalidate()
630
663
  }
631
664
 
@@ -634,7 +667,11 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
634
667
  DisplayState.IMAGE -> Unit
635
668
  DisplayState.LOADING -> applyLoadingAppearance()
636
669
  DisplayState.ERROR, DisplayState.FALLBACK -> {
637
- hostView.setBackgroundColor(placeholderColor())
670
+ val hidesTerminalState = loadingStrategy == OneKeyImageLoadingStrategy.NONE
671
+ hostView.setBackgroundColor(
672
+ if (hidesTerminalState) Color.TRANSPARENT else resolvedPlaceholderColor(),
673
+ )
674
+ hostView.drawStateSymbol = !hidesTerminalState
638
675
  hostView.stateSymbol = stateSymbol()
639
676
  hostView.invalidate()
640
677
  }
@@ -643,11 +680,13 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
643
680
 
644
681
  private fun applyLoadingAppearance() {
645
682
  val strategy = loadingStrategy ?: OneKeyImageLoadingStrategy.STATIC
683
+ val placeholder = resolvedPlaceholderColor()
646
684
  hostView.setBackgroundColor(
647
- if (strategy == OneKeyImageLoadingStrategy.NONE) Color.TRANSPARENT else placeholderColor(),
685
+ if (strategy == OneKeyImageLoadingStrategy.NONE) Color.TRANSPARENT else placeholder,
648
686
  )
687
+ hostView.updateSkeletonStyle(placeholder)
649
688
  hostView.skeletonRequested =
650
- requestActive && strategy == OneKeyImageLoadingStrategy.SKELETON
689
+ requestActive && strategy == OneKeyImageLoadingStrategy.SKELETON && animationsEnabled()
651
690
  hostView.invalidate()
652
691
  }
653
692
 
@@ -658,11 +697,66 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
658
697
  OneKeyImageVariant.AVATAR -> "●"
659
698
  }
660
699
 
661
- private fun placeholderColor(): Int = when (variant ?: OneKeyImageVariant.GENERIC) {
662
- OneKeyImageVariant.GENERIC -> Color.rgb(232, 232, 232)
663
- OneKeyImageVariant.TOKEN -> Color.rgb(232, 237, 249)
664
- OneKeyImageVariant.NETWORK -> Color.rgb(229, 240, 245)
665
- OneKeyImageVariant.AVATAR -> Color.rgb(234, 234, 240)
700
+ private fun applyLoadedImageTransition(cacheType: OneKeyImageCacheType) {
701
+ resetImageTransition()
702
+ val startedAt = requestStartedAtMs ?: return
703
+ if (
704
+ cacheType == OneKeyImageCacheType.MEMORY ||
705
+ SystemClock.uptimeMillis() - startedAt < FADE_DELAY_THRESHOLD_MS ||
706
+ !animationsEnabled()
707
+ ) {
708
+ return
709
+ }
710
+ hostView.alpha = 0f
711
+ hostView.animate()
712
+ .alpha(1f)
713
+ .setDuration(FADE_DURATION_MS)
714
+ .setInterpolator(DecelerateInterpolator())
715
+ .start()
716
+ }
717
+
718
+ private fun resetImageTransition() {
719
+ hostView.animate().cancel()
720
+ hostView.alpha = 1f
721
+ }
722
+
723
+ private fun animationsEnabled(): Boolean =
724
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
725
+ ValueAnimator.areAnimatorsEnabled()
726
+ } else {
727
+ Settings.Global.getFloat(
728
+ context.contentResolver,
729
+ Settings.Global.ANIMATOR_DURATION_SCALE,
730
+ 1f,
731
+ ) != 0f
732
+ }
733
+
734
+ private fun resolvedPlaceholderColor(): Int {
735
+ parseRgbaHex(placeholderColor)?.let { return it }
736
+ // OneKey patch: fall back to a dark-aware low-contrast fill instead of light-only RGB values.
737
+ // return when (variant ?: OneKeyImageVariant.GENERIC) {
738
+ // OneKeyImageVariant.GENERIC -> Color.rgb(232, 232, 232)
739
+ // OneKeyImageVariant.TOKEN -> Color.rgb(232, 237, 249)
740
+ // OneKeyImageVariant.NETWORK -> Color.rgb(229, 240, 245)
741
+ // OneKeyImageVariant.AVATAR -> Color.rgb(234, 234, 240)
742
+ // }
743
+ val nightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
744
+ return if (nightMode == Configuration.UI_MODE_NIGHT_YES) {
745
+ Color.rgb(38, 38, 38)
746
+ } else {
747
+ Color.rgb(238, 238, 238)
748
+ }
749
+ }
750
+
751
+ private fun parseRgbaHex(value: String?): Int? {
752
+ val hex = value?.trim()?.removePrefix("#") ?: return null
753
+ if (hex.length != 6 && hex.length != 8) return null
754
+ val parsed = hex.toLongOrNull(16) ?: return null
755
+ val red = ((parsed shr (if (hex.length == 8) 24 else 16)) and 0xFF).toInt()
756
+ val green = ((parsed shr (if (hex.length == 8) 16 else 8)) and 0xFF).toInt()
757
+ val blue = ((parsed shr (if (hex.length == 8) 8 else 0)) and 0xFF).toInt()
758
+ val alpha = if (hex.length == 8) (parsed and 0xFF).toInt() else 0xFF
759
+ return Color.argb(alpha, red, green, blue)
666
760
  }
667
761
 
668
762
  private fun applyContentFit() {
@@ -28,6 +28,7 @@ class OneKeyImageReusableView(context: ThemedReactContext) : FrameLayout(context
28
28
  optimizeTos: Boolean,
29
29
  overscan: Double,
30
30
  loadingStrategy: String,
31
+ placeholderColor: String,
31
32
  onLoad: (() -> Unit)? = null,
32
33
  onError: (() -> Unit)? = null,
33
34
  ) {
@@ -62,6 +63,7 @@ class OneKeyImageReusableView(context: ThemedReactContext) : FrameLayout(context
62
63
  "none" -> OneKeyImageLoadingStrategy.NONE
63
64
  else -> OneKeyImageLoadingStrategy.STATIC
64
65
  }
66
+ image.placeholderColor = placeholderColor
65
67
  image.sourceUri = sourceUri
66
68
  image.afterUpdate()
67
69
  }
@@ -11,7 +11,9 @@ final class OneKeyAvatarImageLoader: NSObject, SDImageLoader {
11
11
  config.maxDiskAge = 30 * 24 * 60 * 60
12
12
  config.maxDiskSize = 32 * 1024 * 1024
13
13
  config.maxMemoryCost = 8 * 1024 * 1024
14
- config.maxMemoryCount = 128
14
+ // OneKey patch: Keep several large-account wallets resident while the byte
15
+ // budget remains the hard memory bound.
16
+ config.maxMemoryCount = 512
15
17
  return SDImageCache(namespace: "onekey-avatar-blockie-v1", diskCacheDirectory: nil, config: config)
16
18
  }()
17
19
 
@@ -22,6 +22,7 @@ private final class OneKeyImageHostView: SDAnimatedImageView {
22
22
  private final class OneKeyImageSkeletonView: UIView {
23
23
  private lazy var renderer = OneKeySkeletonRenderer(hostLayer: layer)
24
24
  private var requestedRunning = false
25
+ private var colors: [UIColor]?
25
26
 
26
27
  override init(frame: CGRect) {
27
28
  super.init(frame: frame)
@@ -36,7 +37,7 @@ private final class OneKeyImageSkeletonView: UIView {
36
37
 
37
38
  override func layoutSubviews() {
38
39
  super.layoutSubviews()
39
- renderer.update(width: bounds.width, height: bounds.height)
40
+ renderer.update(width: bounds.width, height: bounds.height, colors: colors)
40
41
  }
41
42
 
42
43
  override func didMoveToWindow() {
@@ -50,9 +51,14 @@ private final class OneKeyImageSkeletonView: UIView {
50
51
  syncRenderer()
51
52
  }
52
53
 
54
+ func updateColors(_ colors: [UIColor]) {
55
+ self.colors = colors
56
+ renderer.update(width: bounds.width, height: bounds.height, colors: colors)
57
+ }
58
+
53
59
  private func syncRenderer() {
54
60
  if requestedRunning, window != nil, !bounds.isEmpty {
55
- renderer.update(width: bounds.width, height: bounds.height)
61
+ renderer.update(width: bounds.width, height: bounds.height, colors: colors)
56
62
  renderer.start()
57
63
  } else {
58
64
  renderer.stop()
@@ -66,7 +72,7 @@ private final class OneKeyImageSkeletonIndicator: NSObject, SDWebImageIndicator
66
72
  var indicatorView: UIView { skeletonView }
67
73
 
68
74
  func startAnimatingIndicator() {
69
- skeletonView.setRunning(true)
75
+ skeletonView.setRunning(!UIAccessibility.isReduceMotionEnabled)
70
76
  }
71
77
 
72
78
  func stopAnimatingIndicator() {
@@ -76,10 +82,16 @@ private final class OneKeyImageSkeletonIndicator: NSObject, SDWebImageIndicator
76
82
  func updateFrame(_ frame: CGRect) {
77
83
  skeletonView.frame = frame
78
84
  }
85
+
86
+ func updateColors(_ colors: [UIColor]) {
87
+ skeletonView.updateColors(colors)
88
+ }
79
89
  }
80
90
 
81
91
  final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
82
92
  private enum DisplayState { case loading, image, error, fallback }
93
+ private static let fadeDelayThreshold: CFTimeInterval = 0.1
94
+ private static let fadeDuration: TimeInterval = 0.14
83
95
 
84
96
  private let hostView = OneKeyImageHostView()
85
97
  private let skeletonIndicator = OneKeyImageSkeletonIndicator()
@@ -95,6 +107,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
95
107
  private var requestActive = false
96
108
  private var isResetting = false
97
109
  private var displayState = DisplayState.loading
110
+ private var requestStartedAt: CFTimeInterval?
98
111
 
99
112
  var view: UIView { hostView }
100
113
 
@@ -132,6 +145,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
132
145
  }
133
146
  }
134
147
  var loadingStrategy: OneKeyImageLoadingStrategy? = .static { didSet { applyVariant() } }
148
+ var placeholderColor: String? { didSet { applyVariant() } }
135
149
  var onLoadStart: (() -> Void)?
136
150
  var onLoad: ((_ width: Double, _ height: Double, _ cacheType: OneKeyImageCacheType) -> Void)?
137
151
  var onDisplay: (() -> Void)?
@@ -215,7 +229,9 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
215
229
  self.cancelCurrentRequest(invalidateGeneration: true)
216
230
  self.lastRequestSignature = nil
217
231
  if let source = self.sourceUri, !source.isEmpty {
218
- self.showLoading(requestIsActive: false, letLibraryStartIndicator: false)
232
+ if !self.showMemoryCachedImageIfAvailable(requestIsActive: false) {
233
+ self.showLoading(requestIsActive: false, letLibraryStartIndicator: false)
234
+ }
219
235
  } else {
220
236
  self.showFallback()
221
237
  }
@@ -258,9 +274,11 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
258
274
 
259
275
  cancelCurrentRequest(invalidateGeneration: true)
260
276
  let generation = requestGeneration
261
- showLoading(requestIsActive: true, letLibraryStartIndicator: true)
277
+ requestStartedAt = CACurrentMediaTime()
262
278
  onLoadStart?()
263
279
  guard Self.isCurrentRequestGeneration(generation, current: requestGeneration) else { return }
280
+ if showMemoryCachedImageIfAvailable(requestIsActive: true) { return }
281
+ showLoading(requestIsActive: true, letLibraryStartIndicator: true)
264
282
 
265
283
  if let violation = OneKeyImageSafetyPolicy.preflight(source: rawString) {
266
284
  finishWithError(violation, generation: generation)
@@ -372,6 +390,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
372
390
  self.skeletonIndicator.stopAnimatingIndicator()
373
391
  self.fallbackLayer.isHidden = true
374
392
  self.hostView.backgroundColor = .clear
393
+ let resolvedCacheType = Self.cacheType(cacheType)
394
+ self.applyLoadedImageTransition(cacheType: resolvedCacheType)
375
395
  self.applyAutoplay()
376
396
  let onLoad = self.onLoad
377
397
  let onLoadEnd = self.onLoadEnd
@@ -380,7 +400,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
380
400
  onLoad?(
381
401
  Double(image?.size.width ?? 0),
382
402
  Double(image?.size.height ?? 0),
383
- Self.cacheType(cacheType)
403
+ resolvedCacheType
384
404
  )
385
405
  },
386
406
  onLoadEnd: { onLoadEnd?() }
@@ -428,6 +448,8 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
428
448
  activeSafetyHandle = nil
429
449
  requestActive = false
430
450
  skeletonIndicator.stopAnimatingIndicator()
451
+ requestStartedAt = nil
452
+ resetImageTransition()
431
453
  if invalidateGeneration { requestGeneration &+= 1 }
432
454
  }
433
455
 
@@ -474,6 +496,7 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
474
496
  resizeWidth = nil
475
497
  overscan = 1.1
476
498
  loadingStrategy = .static
499
+ placeholderColor = nil
477
500
  onLoadStart = nil
478
501
  onLoad = nil
479
502
  onDisplay = nil
@@ -498,11 +521,86 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
498
521
  ) {
499
522
  displayState = .loading
500
523
  requestActive = requestIsActive
524
+ resetImageTransition()
501
525
  hostView.image = nil
502
526
  fallbackLayer.isHidden = true
503
527
  applyLoadingAppearance(letLibraryStartIndicator: letLibraryStartIndicator)
504
528
  }
505
529
 
530
+ private func showMemoryCachedImageIfAvailable(requestIsActive: Bool) -> Bool {
531
+ guard cachePolicy == .memory || cachePolicy == .memoryDisk,
532
+ let sourceUri,
533
+ let rawURL = URL(string: sourceUri) else {
534
+ return false
535
+ }
536
+ let rawScreenScale: CGFloat = hostView.window?.screen.scale ?? UIScreen.main.scale
537
+ let screenScale = min(max(rawScreenScale, 1), 3)
538
+ let hasCustomIdentity = OneKeyImageRequestContext.headers(from: sourceHeadersJson) != nil
539
+ let displaySize = resizeWidth.flatMap { $0.isFinite && $0 > 0 ? CGFloat($0) : nil }
540
+ ?? max(hostView.bounds.width, hostView.bounds.height)
541
+ let requestURL =
542
+ (optimizeTos ?? true)
543
+ ? OneKeyTosURL.optimized(
544
+ rawURL: rawURL,
545
+ displaySize: displaySize,
546
+ scale: screenScale,
547
+ overscan: overscan ?? 1.1,
548
+ hasCustomIdentity: hasCustomIdentity
549
+ )
550
+ : rawURL
551
+ let thumbnailPixelSize = OneKeyImageDecodeSizing.thumbnailPixelSize(
552
+ viewSize: hostView.bounds.size,
553
+ scale: screenScale,
554
+ contentFit: contentFit ?? .cover
555
+ )
556
+ func memoryCachedImage(for url: URL) -> UIImage? {
557
+ let context = OneKeyImageRequestContext.make(
558
+ headersJson: sourceHeadersJson,
559
+ cachePolicy: cachePolicy ?? .memoryDisk,
560
+ thumbnailPixelSize: thumbnailPixelSize,
561
+ safetyTracker: nil,
562
+ manager: OneKeyImagePipeline.manager,
563
+ url: url
564
+ )
565
+ let cache = context[.imageCache] as? SDImageCache ?? SDImageCache.shared
566
+ guard let key = OneKeyImagePipeline.manager.cacheKey(for: url, context: context) else {
567
+ return nil
568
+ }
569
+ return cache.imageFromMemoryCache(forKey: key)
570
+ }
571
+ guard let image = memoryCachedImage(for: requestURL)
572
+ ?? (requestURL == rawURL ? nil : memoryCachedImage(for: rawURL)) else {
573
+ return false
574
+ }
575
+ let generation = requestGeneration
576
+ if requestIsActive, !claimTerminal(generation) { return true }
577
+ displayState = .image
578
+ requestActive = false
579
+ skeletonIndicator.stopAnimatingIndicator()
580
+ fallbackLayer.isHidden = true
581
+ resetImageTransition()
582
+ hostView.image = image
583
+ hostView.backgroundColor = .clear
584
+ if requestIsActive {
585
+ applyLoadedImageTransition(cacheType: .memory)
586
+ }
587
+ applyAutoplay()
588
+ if requestIsActive {
589
+ let onLoad = onLoad
590
+ let onLoadEnd = onLoadEnd
591
+ Self.deliverTerminalCallbacks(
592
+ primary: {
593
+ onLoad?(Double(image.size.width), Double(image.size.height), .memory)
594
+ },
595
+ onLoadEnd: { onLoadEnd?() }
596
+ )
597
+ guard requestGeneration == generation else { return true }
598
+ pendingDisplayGeneration = generation
599
+ schedulePendingDisplayIfNeeded()
600
+ }
601
+ return true
602
+ }
603
+
506
604
  private func showError() {
507
605
  showTerminalState(.error)
508
606
  }
@@ -515,13 +613,15 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
515
613
  displayState = state
516
614
  requestActive = false
517
615
  skeletonIndicator.stopAnimatingIndicator()
616
+ resetImageTransition()
518
617
  hostView.image = nil
519
- hostView.backgroundColor = placeholderColor
618
+ let hidesTerminalState = loadingStrategy == .none
619
+ hostView.backgroundColor = hidesTerminalState ? .clear : resolvedPlaceholderColor
520
620
  fallbackLayer.string = stateSymbol
521
621
  fallbackLayer.fontSize = min(hostView.bounds.width, hostView.bounds.height) * 0.35
522
622
  layoutFallbackLayer()
523
623
  fallbackLayer.foregroundColor = UIColor.secondaryLabel.cgColor
524
- fallbackLayer.isHidden = false
624
+ fallbackLayer.isHidden = hidesTerminalState
525
625
  }
526
626
 
527
627
  private func applyVariant() {
@@ -531,22 +631,27 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
531
631
  case .loading:
532
632
  applyLoadingAppearance(letLibraryStartIndicator: false)
533
633
  case .error, .fallback:
534
- hostView.backgroundColor = placeholderColor
634
+ let hidesTerminalState = loadingStrategy == .none
635
+ hostView.backgroundColor = hidesTerminalState ? .clear : resolvedPlaceholderColor
535
636
  fallbackLayer.string = stateSymbol
637
+ fallbackLayer.isHidden = hidesTerminalState
536
638
  }
537
639
  }
538
640
 
539
641
  private func applyLoadingAppearance(letLibraryStartIndicator: Bool) {
540
642
  let strategy = loadingStrategy ?? .static
541
- hostView.backgroundColor = strategy == .none ? .clear : placeholderColor
643
+ hostView.backgroundColor = strategy == .none ? .clear : resolvedPlaceholderColor
542
644
 
543
645
  if strategy == .skeleton {
646
+ skeletonIndicator.updateColors(skeletonGradientColors)
544
647
  if hostView.sd_imageIndicator !== skeletonIndicator {
545
648
  hostView.sd_imageIndicator = skeletonIndicator
546
649
  }
547
650
  skeletonIndicator.updateFrame(hostView.bounds)
548
- if requestActive, !letLibraryStartIndicator {
651
+ if requestActive, !letLibraryStartIndicator, !UIAccessibility.isReduceMotionEnabled {
549
652
  skeletonIndicator.startAnimatingIndicator()
653
+ } else if UIAccessibility.isReduceMotionEnabled {
654
+ skeletonIndicator.stopAnimatingIndicator()
550
655
  }
551
656
  } else {
552
657
  skeletonIndicator.stopAnimatingIndicator()
@@ -566,13 +671,77 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
566
671
  )
567
672
  }
568
673
 
569
- private var placeholderColor: UIColor {
570
- switch variant ?? .generic {
571
- case .generic: return UIColor(white: 0.91, alpha: 1)
572
- case .token: return UIColor(red: 0.91, green: 0.93, blue: 0.98, alpha: 1)
573
- case .network: return UIColor(red: 0.90, green: 0.94, blue: 0.96, alpha: 1)
574
- case .avatar: return UIColor(red: 0.92, green: 0.92, blue: 0.94, alpha: 1)
674
+ private var resolvedPlaceholderColor: UIColor {
675
+ if let color = Self.color(from: placeholderColor) { return color }
676
+ // OneKey patch: fall back to a semantic system fill instead of a light-only color.
677
+ // switch variant ?? .generic {
678
+ // case .generic: return UIColor(white: 0.91, alpha: 1)
679
+ // case .token: return UIColor(red: 0.91, green: 0.93, blue: 0.98, alpha: 1)
680
+ // case .network: return UIColor(red: 0.90, green: 0.94, blue: 0.96, alpha: 1)
681
+ // case .avatar: return UIColor(red: 0.92, green: 0.92, blue: 0.94, alpha: 1)
682
+ // }
683
+ return .secondarySystemFill
684
+ }
685
+
686
+ private var skeletonGradientColors: [UIColor] {
687
+ let base = resolvedPlaceholderColor
688
+ let resolvedBase = base.resolvedColor(with: hostView.traitCollection)
689
+ var red: CGFloat = 0
690
+ var green: CGFloat = 0
691
+ var blue: CGFloat = 0
692
+ var alpha: CGFloat = 0
693
+ guard resolvedBase.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
694
+ return [base, base]
695
+ }
696
+ let average = (red + green + blue) / 3
697
+ let delta: CGFloat = average < 0.5 ? 18.0 / 255.0 : -12.0 / 255.0
698
+ let highlight = UIColor(
699
+ red: min(max(red + delta, 0), 1),
700
+ green: min(max(green + delta, 0), 1),
701
+ blue: min(max(blue + delta, 0), 1),
702
+ alpha: alpha
703
+ )
704
+ return [base, highlight]
705
+ }
706
+
707
+ private func applyLoadedImageTransition(cacheType: OneKeyImageCacheType) {
708
+ resetImageTransition()
709
+ guard cacheType != .memory,
710
+ let requestStartedAt,
711
+ CACurrentMediaTime() - requestStartedAt >= Self.fadeDelayThreshold,
712
+ !UIAccessibility.isReduceMotionEnabled else {
713
+ return
714
+ }
715
+ hostView.alpha = 0
716
+ UIView.animate(
717
+ withDuration: Self.fadeDuration,
718
+ delay: 0,
719
+ options: [.allowUserInteraction, .beginFromCurrentState, .curveEaseOut]
720
+ ) {
721
+ self.hostView.alpha = 1
722
+ }
723
+ }
724
+
725
+ private func resetImageTransition() {
726
+ hostView.layer.removeAllAnimations()
727
+ hostView.alpha = 1
728
+ }
729
+
730
+ private static func color(from value: String?) -> UIColor? {
731
+ guard var hex = value?.trimmingCharacters(in: .whitespacesAndNewlines),
732
+ hex.hasPrefix("#") else {
733
+ return nil
734
+ }
735
+ hex.removeFirst()
736
+ guard hex.count == 6 || hex.count == 8,
737
+ let parsed = UInt64(hex, radix: 16) else {
738
+ return nil
575
739
  }
740
+ let red = CGFloat((parsed >> (hex.count == 8 ? 24 : 16)) & 0xFF) / 255
741
+ let green = CGFloat((parsed >> (hex.count == 8 ? 16 : 8)) & 0xFF) / 255
742
+ let blue = CGFloat((parsed >> (hex.count == 8 ? 8 : 0)) & 0xFF) / 255
743
+ let alpha = hex.count == 8 ? CGFloat(parsed & 0xFF) / 255 : 1
744
+ return UIColor(red: red, green: green, blue: blue, alpha: alpha)
576
745
  }
577
746
 
578
747
  private var stateSymbol: String {
@@ -28,6 +28,7 @@ public final class OneKeyImageReusableView: UIView {
28
28
  optimizeTos: Bool,
29
29
  overscan: Double,
30
30
  loadingStrategy: String,
31
+ placeholderColor: String,
31
32
  onLoad: (() -> Void)? = nil,
32
33
  onError: (() -> Void)? = nil
33
34
  ) {
@@ -43,6 +44,7 @@ public final class OneKeyImageReusableView: UIView {
43
44
  image.optimizeTos = optimizeTos
44
45
  image.overscan = overscan
45
46
  image.loadingStrategy = OneKeyImageLoadingStrategy(fromString: loadingStrategy) ?? .static
47
+ image.placeholderColor = placeholderColor
46
48
  image.sourceUri = sourceUri
47
49
  image.afterUpdate()
48
50
  }
@@ -47,6 +47,7 @@ export function OneKeyImage({
47
47
  resizeWidth,
48
48
  overscan = 1.1,
49
49
  loadingStrategy = OneKeyImageLoadingStrategy.STATIC,
50
+ placeholderColor,
50
51
  hybridRef,
51
52
  onLoadStart,
52
53
  onLoad,
@@ -176,7 +177,7 @@ export function OneKeyImage({
176
177
  ...viewProps,
177
178
  ...nativeCallbacks,
178
179
  style: shouldWrapNative ? StyleSheet.absoluteFill : style,
179
- sourceUri: normalized?.uri,
180
+ sourceUri: normalized?.uri ?? '',
180
181
  sourceHeadersJson: normalized?.headers ? JSON.stringify(normalized.headers) : undefined,
181
182
  variant,
182
183
  contentFit,
@@ -186,7 +187,8 @@ export function OneKeyImage({
186
187
  optimizeTos,
187
188
  resizeWidth,
188
189
  overscan,
189
- loadingStrategy: placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE
190
+ loadingStrategy: placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE,
191
+ placeholderColor
190
192
  });
191
193
  if (!shouldWrapNative) return native;
192
194
  return /*#__PURE__*/_jsxs(View, {
@@ -165,6 +165,15 @@ namespace margelo::nitro::onekeyimage {
165
165
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JOneKeyImageLoadingStrategy> /* loadingStrategy */)>("setLoadingStrategy");
166
166
  method(_javaPart, loadingStrategy.has_value() ? JOneKeyImageLoadingStrategy::fromCpp(loadingStrategy.value()) : nullptr);
167
167
  }
168
+ std::optional<std::string> JHybridOneKeyImageSpec::getPlaceholderColor() {
169
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JString>()>("getPlaceholderColor");
170
+ auto __result = method(_javaPart);
171
+ return __result != nullptr ? std::make_optional(__result->toStdString()) : std::nullopt;
172
+ }
173
+ void JHybridOneKeyImageSpec::setPlaceholderColor(const std::optional<std::string>& placeholderColor) {
174
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* placeholderColor */)>("setPlaceholderColor");
175
+ method(_javaPart, placeholderColor.has_value() ? jni::make_jstring(placeholderColor.value()) : nullptr);
176
+ }
168
177
  std::optional<std::function<void()>> JHybridOneKeyImageSpec::getOnLoadStart() {
169
178
  static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JFunc_void::javaobject>()>("getOnLoadStart_cxx");
170
179
  auto __result = method(_javaPart);
@@ -72,6 +72,8 @@ namespace margelo::nitro::onekeyimage {
72
72
  void setOverscan(std::optional<double> overscan) override;
73
73
  std::optional<OneKeyImageLoadingStrategy> getLoadingStrategy() override;
74
74
  void setLoadingStrategy(std::optional<OneKeyImageLoadingStrategy> loadingStrategy) override;
75
+ std::optional<std::string> getPlaceholderColor() override;
76
+ void setPlaceholderColor(const std::optional<std::string>& placeholderColor) override;
75
77
  std::optional<std::function<void()>> getOnLoadStart() override;
76
78
  void setOnLoadStart(const std::optional<std::function<void()>>& onLoadStart) override;
77
79
  std::optional<std::function<void(double /* width */, double /* height */, OneKeyImageCacheType /* cacheType */)>> getOnLoad() override;
@@ -108,6 +108,11 @@ void JHybridOneKeyImageStateUpdater::updateViewProps(jni::alias_ref<jni::JClass>
108
108
  : !newProps->loadingStrategy.hasSameValue(oldProps->loadingStrategy)) {
109
109
  hybridView->setLoadingStrategy(newProps->loadingStrategy.get());
110
110
  }
111
+ if (oldProps == nullptr
112
+ ? newProps->placeholderColor.isProvided()
113
+ : !newProps->placeholderColor.hasSameValue(oldProps->placeholderColor)) {
114
+ hybridView->setPlaceholderColor(newProps->placeholderColor.get());
115
+ }
111
116
  if (oldProps == nullptr
112
117
  ? newProps->onLoadStart.isProvided()
113
118
  : !newProps->onLoadStart.hasSameValue(oldProps->onLoadStart)) {
@@ -93,6 +93,12 @@ abstract class HybridOneKeyImageSpec: HybridView() {
93
93
  @set:Keep
94
94
  abstract var loadingStrategy: OneKeyImageLoadingStrategy?
95
95
 
96
+ @get:DoNotStrip
97
+ @get:Keep
98
+ @set:DoNotStrip
99
+ @set:Keep
100
+ abstract var placeholderColor: String?
101
+
96
102
  abstract var onLoadStart: (() -> Unit)?
97
103
 
98
104
  private var onLoadStart_cxx: Func_void?
@@ -155,6 +155,13 @@ namespace margelo::nitro::onekeyimage {
155
155
  inline void setLoadingStrategy(std::optional<OneKeyImageLoadingStrategy> loadingStrategy) noexcept override {
156
156
  _swiftPart.setLoadingStrategy(loadingStrategy);
157
157
  }
158
+ inline std::optional<std::string> getPlaceholderColor() noexcept override {
159
+ auto __result = _swiftPart.getPlaceholderColor();
160
+ return __result;
161
+ }
162
+ inline void setPlaceholderColor(const std::optional<std::string>& placeholderColor) noexcept override {
163
+ _swiftPart.setPlaceholderColor(placeholderColor);
164
+ }
158
165
  inline std::optional<std::function<void()>> getOnLoadStart() noexcept override {
159
166
  auto __result = _swiftPart.getOnLoadStart();
160
167
  return __result;
@@ -164,6 +164,12 @@ using namespace margelo::nitro::onekeyimage::views;
164
164
  : !newViewProps.loadingStrategy.hasSameValue(oldViewProps->loadingStrategy)) {
165
165
  swiftPart.setLoadingStrategy(newViewProps.loadingStrategy.get());
166
166
  }
167
+ // placeholderColor: optional
168
+ if (oldViewProps == nullptr
169
+ ? newViewProps.placeholderColor.isProvided()
170
+ : !newViewProps.placeholderColor.hasSameValue(oldViewProps->placeholderColor)) {
171
+ swiftPart.setPlaceholderColor(newViewProps.placeholderColor.get());
172
+ }
167
173
  // onLoadStart: optional
168
174
  if (oldViewProps == nullptr
169
175
  ? newViewProps.onLoadStart.isProvided()
@@ -21,6 +21,7 @@ public protocol HybridOneKeyImageSpec_protocol: HybridObject, HybridView {
21
21
  var resizeWidth: Double? { get set }
22
22
  var overscan: Double? { get set }
23
23
  var loadingStrategy: OneKeyImageLoadingStrategy? { get set }
24
+ var placeholderColor: String? { get set }
24
25
  var onLoadStart: (() -> Void)? { get set }
25
26
  var onLoad: ((_ width: Double, _ height: Double, _ cacheType: OneKeyImageCacheType) -> Void)? { get set }
26
27
  var onDisplay: (() -> Void)? { get set }
@@ -357,6 +357,30 @@ open class HybridOneKeyImageSpec_cxx {
357
357
  }
358
358
  }
359
359
 
360
+ public final var placeholderColor: bridge.std__optional_std__string_ {
361
+ @inline(__always)
362
+ get {
363
+ return { () -> bridge.std__optional_std__string_ in
364
+ if let __unwrappedValue = self.__implementation.placeholderColor {
365
+ return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
366
+ } else {
367
+ return .init()
368
+ }
369
+ }()
370
+ }
371
+ @inline(__always)
372
+ set {
373
+ self.__implementation.placeholderColor = { () -> String? in
374
+ if bridge.has_value_std__optional_std__string_(newValue) {
375
+ let __unwrapped = bridge.get_std__optional_std__string_(newValue)
376
+ return String(__unwrapped)
377
+ } else {
378
+ return nil
379
+ }
380
+ }()
381
+ }
382
+ }
383
+
360
384
  public final var onLoadStart: bridge.std__optional_std__function_void____ {
361
385
  @inline(__always)
362
386
  get {
@@ -36,6 +36,8 @@ namespace margelo::nitro::onekeyimage {
36
36
  prototype.registerHybridSetter("overscan", &HybridOneKeyImageSpec::setOverscan);
37
37
  prototype.registerHybridGetter("loadingStrategy", &HybridOneKeyImageSpec::getLoadingStrategy);
38
38
  prototype.registerHybridSetter("loadingStrategy", &HybridOneKeyImageSpec::setLoadingStrategy);
39
+ prototype.registerHybridGetter("placeholderColor", &HybridOneKeyImageSpec::getPlaceholderColor);
40
+ prototype.registerHybridSetter("placeholderColor", &HybridOneKeyImageSpec::setPlaceholderColor);
39
41
  prototype.registerHybridGetter("onLoadStart", &HybridOneKeyImageSpec::getOnLoadStart);
40
42
  prototype.registerHybridSetter("onLoadStart", &HybridOneKeyImageSpec::setOnLoadStart);
41
43
  prototype.registerHybridGetter("onLoad", &HybridOneKeyImageSpec::getOnLoad);
@@ -82,6 +82,8 @@ namespace margelo::nitro::onekeyimage {
82
82
  virtual void setOverscan(std::optional<double> overscan) = 0;
83
83
  virtual std::optional<OneKeyImageLoadingStrategy> getLoadingStrategy() = 0;
84
84
  virtual void setLoadingStrategy(std::optional<OneKeyImageLoadingStrategy> loadingStrategy) = 0;
85
+ virtual std::optional<std::string> getPlaceholderColor() = 0;
86
+ virtual void setPlaceholderColor(const std::optional<std::string>& placeholderColor) = 0;
85
87
  virtual std::optional<std::function<void()>> getOnLoadStart() = 0;
86
88
  virtual void setOnLoadStart(const std::optional<std::function<void()>>& onLoadStart) = 0;
87
89
  virtual std::optional<std::function<void(double /* width */, double /* height */, OneKeyImageCacheType /* cacheType */)>> getOnLoad() = 0;
@@ -31,6 +31,7 @@ namespace margelo::nitro::onekeyimage::views {
31
31
  resizeWidth(nitro::ReactProp<std::optional<double>>::fromRawValue("OneKeyImage", "resizeWidth", rawProps, sourceProps.resizeWidth)),
32
32
  overscan(nitro::ReactProp<std::optional<double>>::fromRawValue("OneKeyImage", "overscan", rawProps, sourceProps.overscan)),
33
33
  loadingStrategy(nitro::ReactProp<std::optional<OneKeyImageLoadingStrategy>>::fromRawValue("OneKeyImage", "loadingStrategy", rawProps, sourceProps.loadingStrategy)),
34
+ placeholderColor(nitro::ReactProp<std::optional<std::string>>::fromRawValue("OneKeyImage", "placeholderColor", rawProps, sourceProps.placeholderColor)),
34
35
  onLoadStart(nitro::ReactProp<std::optional<std::function<void()>>>::fromRawValue("OneKeyImage", "onLoadStart", rawProps, sourceProps.onLoadStart)),
35
36
  onLoad(nitro::ReactProp<std::optional<std::function<void(double /* width */, double /* height */, OneKeyImageCacheType /* cacheType */)>>>::fromRawValue("OneKeyImage", "onLoad", rawProps, sourceProps.onLoad)),
36
37
  onDisplay(nitro::ReactProp<std::optional<std::function<void()>>>::fromRawValue("OneKeyImage", "onDisplay", rawProps, sourceProps.onDisplay)),
@@ -51,6 +52,7 @@ namespace margelo::nitro::onekeyimage::views {
51
52
  case hashString("resizeWidth"): return true;
52
53
  case hashString("overscan"): return true;
53
54
  case hashString("loadingStrategy"): return true;
55
+ case hashString("placeholderColor"): return true;
54
56
  case hashString("onLoadStart"): return true;
55
57
  case hashString("onLoad"): return true;
56
58
  case hashString("onDisplay"): return true;
@@ -59,6 +59,7 @@ namespace margelo::nitro::onekeyimage::views {
59
59
  nitro::ReactProp<std::optional<double>> resizeWidth;
60
60
  nitro::ReactProp<std::optional<double>> overscan;
61
61
  nitro::ReactProp<std::optional<OneKeyImageLoadingStrategy>> loadingStrategy;
62
+ nitro::ReactProp<std::optional<std::string>> placeholderColor;
62
63
  nitro::ReactProp<std::optional<std::function<void()>>> onLoadStart;
63
64
  nitro::ReactProp<std::optional<std::function<void(double /* width */, double /* height */, OneKeyImageCacheType /* cacheType */)>>> onLoad;
64
65
  nitro::ReactProp<std::optional<std::function<void()>>> onDisplay;
@@ -79,6 +80,7 @@ namespace margelo::nitro::onekeyimage::views {
79
80
  resizeWidth.hasSameValue(other.resizeWidth) &&
80
81
  overscan.hasSameValue(other.overscan) &&
81
82
  loadingStrategy.hasSameValue(other.loadingStrategy) &&
83
+ placeholderColor.hasSameValue(other.placeholderColor) &&
82
84
  onLoadStart.hasSameValue(other.onLoadStart) &&
83
85
  onLoad.hasSameValue(other.onLoad) &&
84
86
  onDisplay.hasSameValue(other.onDisplay) &&
@@ -100,6 +102,7 @@ namespace margelo::nitro::onekeyimage::views {
100
102
  resizeWidth.isProvided() ||
101
103
  overscan.isProvided() ||
102
104
  loadingStrategy.isProvided() ||
105
+ placeholderColor.isProvided() ||
103
106
  onLoadStart.isProvided() ||
104
107
  onLoad.isProvided() ||
105
108
  onDisplay.isProvided() ||
@@ -15,6 +15,7 @@
15
15
  "resizeWidth": true,
16
16
  "overscan": true,
17
17
  "loadingStrategy": true,
18
+ "placeholderColor": true,
18
19
  "onLoadStart": true,
19
20
  "onLoad": true,
20
21
  "onDisplay": true,
@@ -36,6 +36,8 @@ export interface OneKeyImageNativeProps extends HybridViewProps {
36
36
  resizeWidth?: number;
37
37
  overscan?: number;
38
38
  loadingStrategy?: OneKeyImageLoadingStrategy;
39
+ /** Theme-aware color used by static loading, error, and fallback states. */
40
+ placeholderColor?: string;
39
41
  onLoadStart?: () => void;
40
42
  onLoad?: (width: number, height: number, cacheType: OneKeyImageCacheType) => void;
41
43
  onDisplay?: () => void;
@@ -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, resizeWidth, overscan, loadingStrategy, 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, resizeWidth, 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>;
@@ -165,6 +165,15 @@ namespace margelo::nitro::onekeyimage {
165
165
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JOneKeyImageLoadingStrategy> /* loadingStrategy */)>("setLoadingStrategy");
166
166
  method(_javaPart, loadingStrategy.has_value() ? JOneKeyImageLoadingStrategy::fromCpp(loadingStrategy.value()) : nullptr);
167
167
  }
168
+ std::optional<std::string> JHybridOneKeyImageSpec::getPlaceholderColor() {
169
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JString>()>("getPlaceholderColor");
170
+ auto __result = method(_javaPart);
171
+ return __result != nullptr ? std::make_optional(__result->toStdString()) : std::nullopt;
172
+ }
173
+ void JHybridOneKeyImageSpec::setPlaceholderColor(const std::optional<std::string>& placeholderColor) {
174
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* placeholderColor */)>("setPlaceholderColor");
175
+ method(_javaPart, placeholderColor.has_value() ? jni::make_jstring(placeholderColor.value()) : nullptr);
176
+ }
168
177
  std::optional<std::function<void()>> JHybridOneKeyImageSpec::getOnLoadStart() {
169
178
  static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JFunc_void::javaobject>()>("getOnLoadStart_cxx");
170
179
  auto __result = method(_javaPart);
@@ -72,6 +72,8 @@ namespace margelo::nitro::onekeyimage {
72
72
  void setOverscan(std::optional<double> overscan) override;
73
73
  std::optional<OneKeyImageLoadingStrategy> getLoadingStrategy() override;
74
74
  void setLoadingStrategy(std::optional<OneKeyImageLoadingStrategy> loadingStrategy) override;
75
+ std::optional<std::string> getPlaceholderColor() override;
76
+ void setPlaceholderColor(const std::optional<std::string>& placeholderColor) override;
75
77
  std::optional<std::function<void()>> getOnLoadStart() override;
76
78
  void setOnLoadStart(const std::optional<std::function<void()>>& onLoadStart) override;
77
79
  std::optional<std::function<void(double /* width */, double /* height */, OneKeyImageCacheType /* cacheType */)>> getOnLoad() override;
@@ -108,6 +108,11 @@ void JHybridOneKeyImageStateUpdater::updateViewProps(jni::alias_ref<jni::JClass>
108
108
  : !newProps->loadingStrategy.hasSameValue(oldProps->loadingStrategy)) {
109
109
  hybridView->setLoadingStrategy(newProps->loadingStrategy.get());
110
110
  }
111
+ if (oldProps == nullptr
112
+ ? newProps->placeholderColor.isProvided()
113
+ : !newProps->placeholderColor.hasSameValue(oldProps->placeholderColor)) {
114
+ hybridView->setPlaceholderColor(newProps->placeholderColor.get());
115
+ }
111
116
  if (oldProps == nullptr
112
117
  ? newProps->onLoadStart.isProvided()
113
118
  : !newProps->onLoadStart.hasSameValue(oldProps->onLoadStart)) {
@@ -93,6 +93,12 @@ abstract class HybridOneKeyImageSpec: HybridView() {
93
93
  @set:Keep
94
94
  abstract var loadingStrategy: OneKeyImageLoadingStrategy?
95
95
 
96
+ @get:DoNotStrip
97
+ @get:Keep
98
+ @set:DoNotStrip
99
+ @set:Keep
100
+ abstract var placeholderColor: String?
101
+
96
102
  abstract var onLoadStart: (() -> Unit)?
97
103
 
98
104
  private var onLoadStart_cxx: Func_void?
@@ -155,6 +155,13 @@ namespace margelo::nitro::onekeyimage {
155
155
  inline void setLoadingStrategy(std::optional<OneKeyImageLoadingStrategy> loadingStrategy) noexcept override {
156
156
  _swiftPart.setLoadingStrategy(loadingStrategy);
157
157
  }
158
+ inline std::optional<std::string> getPlaceholderColor() noexcept override {
159
+ auto __result = _swiftPart.getPlaceholderColor();
160
+ return __result;
161
+ }
162
+ inline void setPlaceholderColor(const std::optional<std::string>& placeholderColor) noexcept override {
163
+ _swiftPart.setPlaceholderColor(placeholderColor);
164
+ }
158
165
  inline std::optional<std::function<void()>> getOnLoadStart() noexcept override {
159
166
  auto __result = _swiftPart.getOnLoadStart();
160
167
  return __result;
@@ -164,6 +164,12 @@ using namespace margelo::nitro::onekeyimage::views;
164
164
  : !newViewProps.loadingStrategy.hasSameValue(oldViewProps->loadingStrategy)) {
165
165
  swiftPart.setLoadingStrategy(newViewProps.loadingStrategy.get());
166
166
  }
167
+ // placeholderColor: optional
168
+ if (oldViewProps == nullptr
169
+ ? newViewProps.placeholderColor.isProvided()
170
+ : !newViewProps.placeholderColor.hasSameValue(oldViewProps->placeholderColor)) {
171
+ swiftPart.setPlaceholderColor(newViewProps.placeholderColor.get());
172
+ }
167
173
  // onLoadStart: optional
168
174
  if (oldViewProps == nullptr
169
175
  ? newViewProps.onLoadStart.isProvided()
@@ -21,6 +21,7 @@ public protocol HybridOneKeyImageSpec_protocol: HybridObject, HybridView {
21
21
  var resizeWidth: Double? { get set }
22
22
  var overscan: Double? { get set }
23
23
  var loadingStrategy: OneKeyImageLoadingStrategy? { get set }
24
+ var placeholderColor: String? { get set }
24
25
  var onLoadStart: (() -> Void)? { get set }
25
26
  var onLoad: ((_ width: Double, _ height: Double, _ cacheType: OneKeyImageCacheType) -> Void)? { get set }
26
27
  var onDisplay: (() -> Void)? { get set }
@@ -357,6 +357,30 @@ open class HybridOneKeyImageSpec_cxx {
357
357
  }
358
358
  }
359
359
 
360
+ public final var placeholderColor: bridge.std__optional_std__string_ {
361
+ @inline(__always)
362
+ get {
363
+ return { () -> bridge.std__optional_std__string_ in
364
+ if let __unwrappedValue = self.__implementation.placeholderColor {
365
+ return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
366
+ } else {
367
+ return .init()
368
+ }
369
+ }()
370
+ }
371
+ @inline(__always)
372
+ set {
373
+ self.__implementation.placeholderColor = { () -> String? in
374
+ if bridge.has_value_std__optional_std__string_(newValue) {
375
+ let __unwrapped = bridge.get_std__optional_std__string_(newValue)
376
+ return String(__unwrapped)
377
+ } else {
378
+ return nil
379
+ }
380
+ }()
381
+ }
382
+ }
383
+
360
384
  public final var onLoadStart: bridge.std__optional_std__function_void____ {
361
385
  @inline(__always)
362
386
  get {
@@ -36,6 +36,8 @@ namespace margelo::nitro::onekeyimage {
36
36
  prototype.registerHybridSetter("overscan", &HybridOneKeyImageSpec::setOverscan);
37
37
  prototype.registerHybridGetter("loadingStrategy", &HybridOneKeyImageSpec::getLoadingStrategy);
38
38
  prototype.registerHybridSetter("loadingStrategy", &HybridOneKeyImageSpec::setLoadingStrategy);
39
+ prototype.registerHybridGetter("placeholderColor", &HybridOneKeyImageSpec::getPlaceholderColor);
40
+ prototype.registerHybridSetter("placeholderColor", &HybridOneKeyImageSpec::setPlaceholderColor);
39
41
  prototype.registerHybridGetter("onLoadStart", &HybridOneKeyImageSpec::getOnLoadStart);
40
42
  prototype.registerHybridSetter("onLoadStart", &HybridOneKeyImageSpec::setOnLoadStart);
41
43
  prototype.registerHybridGetter("onLoad", &HybridOneKeyImageSpec::getOnLoad);
@@ -82,6 +82,8 @@ namespace margelo::nitro::onekeyimage {
82
82
  virtual void setOverscan(std::optional<double> overscan) = 0;
83
83
  virtual std::optional<OneKeyImageLoadingStrategy> getLoadingStrategy() = 0;
84
84
  virtual void setLoadingStrategy(std::optional<OneKeyImageLoadingStrategy> loadingStrategy) = 0;
85
+ virtual std::optional<std::string> getPlaceholderColor() = 0;
86
+ virtual void setPlaceholderColor(const std::optional<std::string>& placeholderColor) = 0;
85
87
  virtual std::optional<std::function<void()>> getOnLoadStart() = 0;
86
88
  virtual void setOnLoadStart(const std::optional<std::function<void()>>& onLoadStart) = 0;
87
89
  virtual std::optional<std::function<void(double /* width */, double /* height */, OneKeyImageCacheType /* cacheType */)>> getOnLoad() = 0;
@@ -31,6 +31,7 @@ namespace margelo::nitro::onekeyimage::views {
31
31
  resizeWidth(nitro::ReactProp<std::optional<double>>::fromRawValue("OneKeyImage", "resizeWidth", rawProps, sourceProps.resizeWidth)),
32
32
  overscan(nitro::ReactProp<std::optional<double>>::fromRawValue("OneKeyImage", "overscan", rawProps, sourceProps.overscan)),
33
33
  loadingStrategy(nitro::ReactProp<std::optional<OneKeyImageLoadingStrategy>>::fromRawValue("OneKeyImage", "loadingStrategy", rawProps, sourceProps.loadingStrategy)),
34
+ placeholderColor(nitro::ReactProp<std::optional<std::string>>::fromRawValue("OneKeyImage", "placeholderColor", rawProps, sourceProps.placeholderColor)),
34
35
  onLoadStart(nitro::ReactProp<std::optional<std::function<void()>>>::fromRawValue("OneKeyImage", "onLoadStart", rawProps, sourceProps.onLoadStart)),
35
36
  onLoad(nitro::ReactProp<std::optional<std::function<void(double /* width */, double /* height */, OneKeyImageCacheType /* cacheType */)>>>::fromRawValue("OneKeyImage", "onLoad", rawProps, sourceProps.onLoad)),
36
37
  onDisplay(nitro::ReactProp<std::optional<std::function<void()>>>::fromRawValue("OneKeyImage", "onDisplay", rawProps, sourceProps.onDisplay)),
@@ -51,6 +52,7 @@ namespace margelo::nitro::onekeyimage::views {
51
52
  case hashString("resizeWidth"): return true;
52
53
  case hashString("overscan"): return true;
53
54
  case hashString("loadingStrategy"): return true;
55
+ case hashString("placeholderColor"): return true;
54
56
  case hashString("onLoadStart"): return true;
55
57
  case hashString("onLoad"): return true;
56
58
  case hashString("onDisplay"): return true;
@@ -59,6 +59,7 @@ namespace margelo::nitro::onekeyimage::views {
59
59
  nitro::ReactProp<std::optional<double>> resizeWidth;
60
60
  nitro::ReactProp<std::optional<double>> overscan;
61
61
  nitro::ReactProp<std::optional<OneKeyImageLoadingStrategy>> loadingStrategy;
62
+ nitro::ReactProp<std::optional<std::string>> placeholderColor;
62
63
  nitro::ReactProp<std::optional<std::function<void()>>> onLoadStart;
63
64
  nitro::ReactProp<std::optional<std::function<void(double /* width */, double /* height */, OneKeyImageCacheType /* cacheType */)>>> onLoad;
64
65
  nitro::ReactProp<std::optional<std::function<void()>>> onDisplay;
@@ -79,6 +80,7 @@ namespace margelo::nitro::onekeyimage::views {
79
80
  resizeWidth.hasSameValue(other.resizeWidth) &&
80
81
  overscan.hasSameValue(other.overscan) &&
81
82
  loadingStrategy.hasSameValue(other.loadingStrategy) &&
83
+ placeholderColor.hasSameValue(other.placeholderColor) &&
82
84
  onLoadStart.hasSameValue(other.onLoadStart) &&
83
85
  onLoad.hasSameValue(other.onLoad) &&
84
86
  onDisplay.hasSameValue(other.onDisplay) &&
@@ -100,6 +102,7 @@ namespace margelo::nitro::onekeyimage::views {
100
102
  resizeWidth.isProvided() ||
101
103
  overscan.isProvided() ||
102
104
  loadingStrategy.isProvided() ||
105
+ placeholderColor.isProvided() ||
103
106
  onLoadStart.isProvided() ||
104
107
  onLoad.isProvided() ||
105
108
  onDisplay.isProvided() ||
@@ -15,6 +15,7 @@
15
15
  "resizeWidth": true,
16
16
  "overscan": true,
17
17
  "loadingStrategy": true,
18
+ "placeholderColor": true,
18
19
  "onLoadStart": true,
19
20
  "onLoad": true,
20
21
  "onDisplay": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-image",
3
- "version": "3.0.116",
3
+ "version": "3.0.118",
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.116",
84
+ "@onekeyfe/react-native-skeleton": "3.0.118",
85
85
  "react": "*",
86
86
  "react-native": "*",
87
87
  "react-native-nitro-modules": "0.37.0"
@@ -47,6 +47,8 @@ export interface OneKeyImageNativeProps extends HybridViewProps {
47
47
  resizeWidth?: number;
48
48
  overscan?: number;
49
49
  loadingStrategy?: OneKeyImageLoadingStrategy;
50
+ /** Theme-aware color used by static loading, error, and fallback states. */
51
+ placeholderColor?: string;
50
52
  onLoadStart?: () => void;
51
53
  onLoad?: (
52
54
  width: number,
package/src/index.tsx CHANGED
@@ -156,6 +156,7 @@ export function OneKeyImage({
156
156
  resizeWidth,
157
157
  overscan = 1.1,
158
158
  loadingStrategy = OneKeyImageLoadingStrategy.STATIC,
159
+ placeholderColor,
159
160
  hybridRef,
160
161
  onLoadStart,
161
162
  onLoad,
@@ -329,7 +330,7 @@ export function OneKeyImage({
329
330
  ...viewProps,
330
331
  ...nativeCallbacks,
331
332
  style: shouldWrapNative ? StyleSheet.absoluteFill : style,
332
- sourceUri: normalized?.uri,
333
+ sourceUri: normalized?.uri ?? '',
333
334
  sourceHeadersJson: normalized?.headers
334
335
  ? JSON.stringify(normalized.headers)
335
336
  : undefined,
@@ -343,6 +344,7 @@ export function OneKeyImage({
343
344
  overscan,
344
345
  loadingStrategy:
345
346
  placeholder == null ? loadingStrategy : OneKeyImageLoadingStrategy.NONE,
347
+ placeholderColor,
346
348
  });
347
349
 
348
350
  if (!shouldWrapNative) return native;