@onekeyfe/react-native-skeleton 3.0.96 → 3.0.98

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 (19) hide show
  1. package/android/src/main/java/com/margelo/nitro/skeleton/OneKeySkeletonRenderer.kt +173 -0
  2. package/android/src/main/java/com/margelo/nitro/skeleton/Skeleton.kt +63 -194
  3. package/ios/Skeleton.swift +58 -272
  4. package/ios/SkeletonRenderer.swift +146 -0
  5. package/lib/nitrogen/generated/android/c++/views/JHybridSkeletonStateUpdater.cpp +42 -23
  6. package/lib/nitrogen/generated/android/c++/views/JHybridSkeletonStateUpdater.hpp +6 -1
  7. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/skeleton/views/HybridSkeletonManager.kt +26 -10
  8. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/skeleton/views/HybridSkeletonStateUpdater.kt +2 -2
  9. package/lib/nitrogen/generated/ios/c++/views/HybridSkeletonComponent.mm +50 -27
  10. package/lib/nitrogen/generated/shared/c++/views/HybridSkeletonComponent.cpp +7 -65
  11. package/lib/nitrogen/generated/shared/c++/views/HybridSkeletonComponent.hpp +26 -50
  12. package/nitrogen/generated/android/c++/views/JHybridSkeletonStateUpdater.cpp +42 -23
  13. package/nitrogen/generated/android/c++/views/JHybridSkeletonStateUpdater.hpp +6 -1
  14. package/nitrogen/generated/android/kotlin/com/margelo/nitro/skeleton/views/HybridSkeletonManager.kt +26 -10
  15. package/nitrogen/generated/android/kotlin/com/margelo/nitro/skeleton/views/HybridSkeletonStateUpdater.kt +2 -2
  16. package/nitrogen/generated/ios/c++/views/HybridSkeletonComponent.mm +50 -27
  17. package/nitrogen/generated/shared/c++/views/HybridSkeletonComponent.cpp +7 -65
  18. package/nitrogen/generated/shared/c++/views/HybridSkeletonComponent.hpp +26 -50
  19. package/package.json +4 -4
@@ -1,305 +1,91 @@
1
1
  import Foundation
2
2
  import UIKit
3
3
 
4
- // Animation constants
5
- let DEFAULT_GRADIENT_COLORS: [UIColor] = [UIColor(red: 210.0/255.0, green: 210.0/255.0, blue: 210.0/255.0, alpha: 1.0), UIColor(red: 235.0/255.0, green: 235.0/255.0, blue: 235.0/255.0, alpha: 1.0)]
4
+ private final class SkeletonHostView: UIView {
5
+ var onLayout: ((CGRect) -> Void)?
6
+ var onWindowChanged: ((Bool) -> Void)?
6
7
 
7
- class HybridSkeleton : HybridSkeletonSpec {
8
+ override func layoutSubviews() {
9
+ super.layoutSubviews()
10
+ onLayout?(bounds)
11
+ }
8
12
 
9
- // Shimmer layers
10
- private var shimmerLayer: CAGradientLayer?
11
- private var skeletonLayer: CALayer?
12
- private var customGradientColors: [UIColor]?
13
- private var isActive: Bool = true
14
- private var retryCount: Int = 0
15
- private let maxRetryCount: Int = 10
16
- private var viewObserver: NSKeyValueObservation?
13
+ override func didMoveToWindow() {
14
+ super.didMoveToWindow()
15
+ onWindowChanged?(window != nil)
16
+ }
17
+ }
17
18
 
18
- // Guard against high-frequency afterUpdate() animation churn (REACT-NATIVE-40K ANR).
19
- // Fabric calls afterUpdate() on every prop commit; list/order-book screens update very
20
- // frequently. Unconditionally restarting the shimmer tears down/rebuilds the
21
- // CAGradientLayer + CABasicAnimation each time on the main thread. We cache a signature
22
- // of the inputs that actually drive the animation (speed + gradient colors + bounds) and
23
- // only restart when that signature changes. Otherwise the running animation is left as-is.
24
- private var lastShimmerSignature: String?
25
- private var isShimmerRunning: Bool = false
19
+ /// Nitro adapter around OneKeySkeletonRenderer. Rendering stays view-independent
20
+ /// so OneKeyImage can reuse it without nesting another HybridView or UIView.
21
+ final class HybridSkeleton: HybridSkeletonSpec {
22
+ private let hostView = SkeletonHostView()
23
+ private lazy var renderer = OneKeySkeletonRenderer(hostLayer: hostView.layer)
24
+ private var attached = false
25
+ private var colors = oneKeySkeletonDefaultGradientColors
26
+ private var duration: Double = 3
26
27
 
27
- // Build a signature from every input that decides the shimmer animation:
28
- // - shimmerSpeed -> CABasicAnimation.duration
29
- // - gradient colors -> CAGradientLayer.colors
30
- // - view bounds -> animation travel range (-width..width) and layer frames
31
- private func currentShimmerSignature() -> String {
32
- let colors: [UIColor] = customGradientColors ?? DEFAULT_GRADIENT_COLORS
33
- let colorsKey = colors.map { color -> String in
34
- var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
35
- color.getRed(&r, green: &g, blue: &b, alpha: &a)
36
- return "\(r)-\(g)-\(b)-\(a)"
37
- }.joined(separator: ",")
38
- let speed = shimmerSpeed ?? 3
39
- let bounds = view.bounds
40
- return "\(speed)|\(colorsKey)|\(bounds.width)x\(bounds.height)"
41
- }
28
+ var view: UIView { hostView }
42
29
 
43
30
  var shimmerGradientColors: [String]? {
44
31
  didSet {
45
- guard isActive else { return }
46
- DispatchQueue.main.async { [weak self] in
47
- guard let self = self, self.isActive else { return }
48
- self.customGradientColors = shimmerGradientColors?.map { self.hexStringToUIColor(hexColor: $0) }
49
- self.restartShimmer()
32
+ if let values = shimmerGradientColors, values.count >= 2 {
33
+ colors = Array(values.prefix(2)).map(Self.color(from:))
34
+ } else {
35
+ colors = oneKeySkeletonDefaultGradientColors
50
36
  }
37
+ updateRenderer()
51
38
  }
52
39
  }
53
40
 
54
- // UIView
55
- var view: UIView = UIView()
56
-
57
41
  var shimmerSpeed: Double? {
58
42
  didSet {
59
- guard isActive else { return }
60
- DispatchQueue.main.async { [weak self] in
61
- guard let self = self, self.isActive else { return }
62
- self.restartShimmer()
63
- }
43
+ duration = max(shimmerSpeed ?? 3, 0.1)
44
+ updateRenderer()
64
45
  }
65
46
  }
66
47
 
67
48
  override init() {
68
49
  super.init()
69
- setupView()
70
- }
71
-
72
- deinit {
73
- // Mark as inactive to prevent any new operations
74
- isActive = false
75
-
76
- // Clean up observer
77
- viewObserver?.invalidate()
78
- viewObserver = nil
79
-
80
- // Stop observing app lifecycle notifications
81
- NotificationCenter.default.removeObserver(self)
82
-
83
- // Cleanup must happen on main thread, but avoid deadlock
84
- // Use async if not on main thread to prevent blocking
85
- if Thread.isMainThread {
86
- stopShimmer()
87
- // Clear view reference and ensure no dangling animations
88
- view.layer.removeAllAnimations()
89
- view.layer.sublayers?.removeAll()
90
- view.layer.mask = nil
91
- } else {
92
- // Use async to avoid potential deadlock with sync
93
- // All CALayer operations must happen on main thread
94
- let shimmer = self.shimmerLayer
95
- let skeleton = self.skeletonLayer
96
- DispatchQueue.main.async { [weak view = self.view] in
97
- guard let view = view else { return }
98
- // Stop shimmer animations
99
- shimmer?.removeAllAnimations()
100
- shimmer?.removeFromSuperlayer()
101
-
102
- // Clear mask reference
103
- if view.layer.mask === skeleton {
104
- view.layer.mask = nil
105
- }
106
-
107
- // Clean up remaining animations and sublayers
108
- view.layer.removeAllAnimations()
109
- view.layer.sublayers?.removeAll()
110
- }
111
- }
112
- }
113
-
114
- private func setupView() {
115
- view.clipsToBounds = true
116
-
117
- // Observe view hierarchy changes for cleanup
118
- viewObserver = view.observe(\.superview, options: [.new]) { [weak self] view, change in
119
- guard let self = self else { return }
120
- if change.newValue == nil {
121
- // View was removed from hierarchy, clean up
122
- DispatchQueue.main.async { [weak self] in
123
- self?.stopShimmer()
124
- }
125
- }
126
- }
127
-
128
- // Recover the shimmer when returning from background. CoreAnimation strips the running
129
- // CAAnimation on backgrounding and does not re-add it on foreground, which would freeze
130
- // the gradient for the view's lifetime. Re-add the animation when the app reactivates.
131
- NotificationCenter.default.addObserver(
132
- self,
133
- selector: #selector(appDidBecomeActive),
134
- name: UIApplication.didBecomeActiveNotification,
135
- object: nil
136
- )
137
-
138
- // Start animation when view is ready
139
- DispatchQueue.main.async { [weak self] in
140
- guard let self = self, self.isActive else { return }
141
- self.startShimmer()
50
+ hostView.clipsToBounds = true
51
+ hostView.onLayout = { [weak self] bounds in
52
+ self?.renderer.update(width: bounds.width, height: bounds.height)
142
53
  }
143
- }
144
-
145
- @objc private func appDidBecomeActive() {
146
- guard isActive else { return }
147
- DispatchQueue.main.async { [weak self] in
148
- guard let self = self, self.isActive else { return }
149
- // Only restart if the shimmer should be running but its animation was stripped.
150
- if self.isShimmerRunning, self.shimmerLayer?.animation(forKey: "shimmerAnimation") == nil {
151
- self.restartShimmer()
152
- }
54
+ hostView.onWindowChanged = { [weak self] attached in
55
+ guard let self else { return }
56
+ self.attached = attached
57
+ attached ? self.renderer.start() : self.renderer.stop()
153
58
  }
154
59
  }
155
60
 
156
- func startShimmer() {
157
- retryCount = 0 // Reset retry count when starting new shimmer
158
- startShimmerInternal()
159
- }
160
-
161
- private func startShimmerInternal() {
162
- guard isActive else { return }
163
-
164
- stopShimmer()
165
-
166
- guard !view.bounds.isEmpty else {
167
- // Check if we have exceeded max retry count
168
- guard retryCount < maxRetryCount else {
169
- print("⚠️ Skeleton shimmer: Max retry count reached. View bounds are still empty.")
170
- return
171
- }
172
-
173
- retryCount += 1
174
- // Retry after a short delay if bounds are not ready
175
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
176
- guard let self = self, self.isActive else { return }
177
- self.startShimmerInternal()
178
- }
179
- return
180
- }
181
-
182
- let colors: [UIColor] = customGradientColors ?? DEFAULT_GRADIENT_COLORS
183
- let backgroundColor = colors[0].cgColor
184
- let highlightColor = colors[1].cgColor
185
-
186
- // Create skeleton layer for masking
187
- let newSkeletonLayer = CALayer()
188
- newSkeletonLayer.backgroundColor = backgroundColor
189
- newSkeletonLayer.name = "SkeletonLayer"
190
- newSkeletonLayer.anchorPoint = .zero
191
- newSkeletonLayer.frame = view.bounds
192
-
193
- // Create gradient layer for animation
194
- let gradientLayer = CAGradientLayer()
195
- gradientLayer.colors = [backgroundColor, highlightColor, backgroundColor]
196
- gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
197
- gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
198
- gradientLayer.frame = view.bounds
199
- gradientLayer.name = "ShimmerLayer"
200
-
201
- // Set mask and add gradient layer (don't add skeleton layer as sublayer)
202
- view.layer.mask = newSkeletonLayer
203
- view.layer.addSublayer(gradientLayer)
204
- view.clipsToBounds = true
205
- view.backgroundColor = UIColor(cgColor: backgroundColor)
206
-
207
- // Store references
208
- skeletonLayer = newSkeletonLayer
209
- shimmerLayer = gradientLayer
210
-
211
- // Create animation
212
- let width = view.bounds.width
213
- let animation = CABasicAnimation(keyPath: "transform.translation.x")
214
- animation.duration = shimmerSpeed ?? 3
215
- animation.fromValue = -width
216
- animation.toValue = width
217
- animation.repeatCount = .infinity
218
- animation.autoreverses = false
219
- animation.fillMode = CAMediaTimingFillMode.forwards
220
- animation.isRemovedOnCompletion = false // Keep animation when completed
221
-
222
- // Use weak self pattern for potential future animation delegates/completion handlers
223
- gradientLayer.add(animation, forKey: "shimmerAnimation")
224
-
225
- // Record the inputs this running animation was built from, so afterUpdate() can skip
226
- // restarting while none of them change.
227
- lastShimmerSignature = currentShimmerSignature()
228
- isShimmerRunning = true
229
- }
230
-
231
- func stopShimmer() {
232
- // Remove animation from shimmer layer
233
- shimmerLayer?.removeAllAnimations() // Remove all animations, more comprehensive
234
- shimmerLayer?.removeFromSuperlayer()
235
-
236
- // Clear mask reference before setting to nil
237
- if view.layer.mask === skeletonLayer {
238
- view.layer.mask = nil
239
- }
240
-
241
- // Clean up all sublayers that might be shimmer layers
242
- view.layer.sublayers?.forEach { layer in
243
- if layer.name == "ShimmerLayer" {
244
- layer.removeAllAnimations()
245
- layer.removeFromSuperlayer()
246
- }
247
- }
248
-
249
- // Nil out references
250
- shimmerLayer = nil
251
- skeletonLayer = nil
252
- isShimmerRunning = false
253
- }
254
-
255
61
  func afterUpdate() {
256
- guard isActive else { return }
257
- DispatchQueue.main.async { [weak self] in
258
- guard let self = self, self.isActive else { return }
259
-
260
- // Defensive guard against animation churn (REACT-NATIVE-40K ANR):
261
- // afterUpdate() runs on every Fabric prop commit. Only restart the shimmer when an
262
- // input that actually drives the animation changed (speed / colors / bounds). If the
263
- // shimmer is already running and nothing relevant changed, leave the existing
264
- // CABasicAnimation alone to avoid main-thread layer teardown/rebuild churn.
265
- //
266
- // Verify the CAAnimation is actually still attached rather than trusting the cached
267
- // signature alone: on backgrounding, CoreAnimation strips the running animation (and
268
- // does not re-add it on foreground) while the layer, isShimmerRunning, and signature
269
- // stay unchanged. Checking the live animation lets afterUpdate() recover by restarting.
270
- let signature = self.currentShimmerSignature()
271
- if self.isShimmerRunning,
272
- self.shimmerLayer?.animation(forKey: "shimmerAnimation") != nil,
273
- signature == self.lastShimmerSignature {
274
- return
275
- }
276
-
277
- self.restartShimmer()
278
- }
62
+ updateRenderer()
279
63
  }
280
64
 
281
- func restartShimmer() {
282
- guard isActive else { return }
283
- stopShimmer()
284
- retryCount = 0 // Reset retry count on restart
285
- startShimmerInternal()
65
+ private func updateRenderer() {
66
+ hostView.backgroundColor = colors[0]
67
+ renderer.update(
68
+ width: hostView.bounds.width,
69
+ height: hostView.bounds.height,
70
+ colors: colors,
71
+ duration: duration
72
+ )
73
+ if attached {
74
+ renderer.start()
75
+ }
286
76
  }
287
77
 
288
- func hexStringToUIColor(hexColor: String) -> UIColor {
289
- var hexSanitized = hexColor.trimmingCharacters(in: .whitespacesAndNewlines)
290
-
291
- if hexSanitized.hasPrefix("#") {
292
- hexSanitized.remove(at: hexSanitized.startIndex)
78
+ private static func color(from value: String) -> UIColor {
79
+ var hex = value.trimmingCharacters(in: .whitespacesAndNewlines)
80
+ if hex.hasPrefix("#") { hex.removeFirst() }
81
+ guard hex.count == 6, let raw = UInt64(hex, radix: 16) else {
82
+ return oneKeySkeletonDefaultGradientColors[0]
293
83
  }
294
-
295
- var color: UInt32 = 0
296
- let stringScanner = Scanner(string: hexSanitized)
297
- stringScanner.scanHexInt32(&color)
298
-
299
- let r = CGFloat(Int(color >> 16) & 0x000000FF)
300
- let g = CGFloat(Int(color >> 8) & 0x000000FF)
301
- let b = CGFloat(Int(color) & 0x000000FF)
302
-
303
- return UIColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: 1)
84
+ return UIColor(
85
+ red: CGFloat((raw >> 16) & 0xff) / 255,
86
+ green: CGFloat((raw >> 8) & 0xff) / 255,
87
+ blue: CGFloat(raw & 0xff) / 255,
88
+ alpha: 1
89
+ )
304
90
  }
305
91
  }
@@ -0,0 +1,146 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ public let oneKeySkeletonDefaultGradientColors: [UIColor] = [
5
+ UIColor(red: 210.0 / 255.0, green: 210.0 / 255.0, blue: 210.0 / 255.0, alpha: 1.0),
6
+ UIColor(red: 235.0 / 255.0, green: 235.0 / 255.0, blue: 235.0 / 255.0, alpha: 1.0),
7
+ ]
8
+
9
+ /// Lightweight shimmer renderer shared by the standalone Skeleton HybridView and
10
+ /// native consumers such as OneKeyImage. It owns layers, not a UIView, so native
11
+ /// consumers can render in their existing view hierarchy.
12
+ public final class OneKeySkeletonRenderer {
13
+ private weak var hostLayer: CALayer?
14
+ private let gradientLayer = CAGradientLayer()
15
+ private var colors = oneKeySkeletonDefaultGradientColors
16
+ private var duration: CFTimeInterval = 3
17
+ private var requestedRunning = false
18
+ private var lastAnimationSignature: String?
19
+
20
+ @nonobjc public init(hostLayer: CALayer) {
21
+ self.hostLayer = hostLayer
22
+ gradientLayer.name = "OneKeySkeletonShimmerLayer"
23
+ gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
24
+ gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
25
+ NotificationCenter.default.addObserver(
26
+ self,
27
+ selector: #selector(appDidBecomeActive),
28
+ name: UIApplication.didBecomeActiveNotification,
29
+ object: nil
30
+ )
31
+ }
32
+
33
+ deinit {
34
+ NotificationCenter.default.removeObserver(self)
35
+ let cleanup = { [gradientLayer] in
36
+ gradientLayer.removeAllAnimations()
37
+ gradientLayer.removeFromSuperlayer()
38
+ }
39
+ if Thread.isMainThread {
40
+ cleanup()
41
+ } else {
42
+ DispatchQueue.main.async(execute: cleanup)
43
+ }
44
+ }
45
+
46
+ @nonobjc public func update(
47
+ width: Double,
48
+ height: Double,
49
+ colors: [UIColor]? = nil,
50
+ duration: CFTimeInterval? = nil
51
+ ) {
52
+ dispatchOnMain { [weak self] in
53
+ guard let self else { return }
54
+ if let colors, colors.count >= 2 {
55
+ self.colors = Array(colors.prefix(2))
56
+ }
57
+ if let duration, duration > 0 {
58
+ self.duration = duration
59
+ }
60
+
61
+ CATransaction.begin()
62
+ CATransaction.setDisableActions(true)
63
+ self.gradientLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
64
+ self.gradientLayer.cornerRadius = self.hostLayer?.cornerRadius ?? 0
65
+ CATransaction.commit()
66
+
67
+ if self.requestedRunning {
68
+ self.installAnimationIfNeeded()
69
+ }
70
+ }
71
+ }
72
+
73
+ @nonobjc public func start() {
74
+ dispatchOnMain { [weak self] in
75
+ guard let self else { return }
76
+ self.requestedRunning = true
77
+ self.installAnimationIfNeeded()
78
+ }
79
+ }
80
+
81
+ @nonobjc public func stop() {
82
+ dispatchOnMain { [weak self] in
83
+ guard let self else { return }
84
+ self.requestedRunning = false
85
+ self.gradientLayer.removeAllAnimations()
86
+ self.gradientLayer.removeFromSuperlayer()
87
+ self.lastAnimationSignature = nil
88
+ }
89
+ }
90
+
91
+ private func installAnimationIfNeeded() {
92
+ guard requestedRunning,
93
+ let hostLayer,
94
+ !gradientLayer.bounds.isEmpty
95
+ else { return }
96
+
97
+ let colorKey = colors.map { color in
98
+ (color.cgColor.components ?? []).map(String.init).joined(separator: "-")
99
+ }.joined(separator: ",")
100
+ let signature =
101
+ "\(gradientLayer.bounds.width)x\(gradientLayer.bounds.height)|\(duration)|\(colorKey)"
102
+ if gradientLayer.superlayer === hostLayer,
103
+ gradientLayer.animation(forKey: "oneKeySkeletonShimmer") != nil,
104
+ lastAnimationSignature == signature
105
+ {
106
+ return
107
+ }
108
+
109
+ gradientLayer.removeAllAnimations()
110
+ if gradientLayer.superlayer !== hostLayer {
111
+ gradientLayer.removeFromSuperlayer()
112
+ hostLayer.addSublayer(gradientLayer)
113
+ }
114
+
115
+ let background = colors[0].cgColor
116
+ let highlight = colors[1].cgColor
117
+ gradientLayer.colors = [background, highlight, background]
118
+ gradientLayer.locations = [0, 0.5, 1]
119
+
120
+ let width = gradientLayer.bounds.width
121
+ let animation = CABasicAnimation(keyPath: "transform.translation.x")
122
+ animation.duration = duration
123
+ animation.fromValue = -width
124
+ animation.toValue = width
125
+ animation.repeatCount = .infinity
126
+ animation.timingFunction = CAMediaTimingFunction(name: .linear)
127
+ animation.beginTime = 0
128
+ gradientLayer.add(animation, forKey: "oneKeySkeletonShimmer")
129
+ lastAnimationSignature = signature
130
+ }
131
+
132
+ @objc private func appDidBecomeActive() {
133
+ dispatchOnMain { [weak self] in
134
+ guard let self, self.requestedRunning else { return }
135
+ self.installAnimationIfNeeded()
136
+ }
137
+ }
138
+
139
+ private func dispatchOnMain(_ work: @escaping () -> Void) {
140
+ if Thread.isMainThread {
141
+ work()
142
+ } else {
143
+ DispatchQueue.main.async(execute: work)
144
+ }
145
+ }
146
+ }
@@ -15,45 +15,64 @@ namespace margelo::nitro::skeleton::views {
15
15
  using namespace facebook;
16
16
  using ConcreteStateData = react::ConcreteState<HybridSkeletonState>;
17
17
 
18
- void JHybridSkeletonStateUpdater::updateViewProps(jni::alias_ref<jni::JClass> /* class */,
19
- jni::alias_ref<JHybridSkeletonSpec::JavaPart> javaView,
20
- jni::alias_ref<JStateWrapper::javaobject> stateWrapperInterface) {
21
- std::shared_ptr<JHybridSkeletonSpec> hybridView = javaView->getJHybridSkeletonSpec();
22
-
18
+ std::shared_ptr<const HybridSkeletonProps> JHybridSkeletonStateUpdater::getPropsFromStateWrapper(
19
+ jni::alias_ref<JStateWrapper::javaobject> stateWrapper) {
20
+ if (stateWrapper.get() == nullptr) {
21
+ return nullptr;
22
+ }
23
23
  // Get concrete StateWrapperImpl from passed StateWrapper interface object
24
- jobject rawStateWrapper = stateWrapperInterface.get();
25
- if (!stateWrapperInterface->isInstanceOf(react::StateWrapperImpl::javaClassStatic())) [[unlikely]] {
26
- throw std::runtime_error("StateWrapper is not a StateWrapperImpl");
24
+ jobject rawStateWrapper = stateWrapper.get();
25
+ if (!stateWrapper->isInstanceOf(react::StateWrapperImpl::javaClassStatic())) [[unlikely]] {
26
+ throw std::runtime_error("StateWrapper is not a StateWrapperImpl");
27
+ }
28
+ auto stateWrapperImpl = jni::alias_ref<react::StateWrapperImpl::javaobject>{
29
+ static_cast<react::StateWrapperImpl::javaobject>(rawStateWrapper)
30
+ };
31
+ std::shared_ptr<const react::State> state = stateWrapperImpl->cthis()->getState();
32
+ if (state == nullptr) {
33
+ return nullptr;
27
34
  }
28
- auto stateWrapper = jni::alias_ref<react::StateWrapperImpl::javaobject>{
29
- static_cast<react::StateWrapperImpl::javaobject>(rawStateWrapper)};
30
- std::shared_ptr<const react::State> state = stateWrapper->cthis()->getState();
31
35
  auto concreteState = std::static_pointer_cast<const ConcreteStateData>(state);
32
36
  const HybridSkeletonState& data = concreteState->getData();
33
- const std::shared_ptr<HybridSkeletonProps>& props = data.getProps();
37
+ const std::shared_ptr<const HybridSkeletonProps>& props = data.getProps();
34
38
  if (props == nullptr) [[unlikely]] {
35
- // Props aren't set yet!
36
39
  throw std::runtime_error("HybridSkeletonState's data doesn't contain any props!");
37
40
  }
41
+ return props;
42
+ }
43
+
44
+ void JHybridSkeletonStateUpdater::updateViewProps(jni::alias_ref<jni::JClass> /* class */,
45
+ jni::alias_ref<JHybridSkeletonSpec::JavaPart> javaView,
46
+ jni::alias_ref<JStateWrapper::javaobject> newState,
47
+ jni::alias_ref<JStateWrapper::javaobject> oldState) {
48
+ std::shared_ptr<JHybridSkeletonSpec> hybridView = javaView->getJHybridSkeletonSpec();
49
+ std::shared_ptr<const HybridSkeletonProps> newProps = getPropsFromStateWrapper(newState);
50
+ std::shared_ptr<const HybridSkeletonProps> oldProps = getPropsFromStateWrapper(oldState);
51
+ if (newProps == nullptr) [[unlikely]] {
52
+ throw std::runtime_error("Current StateWrapper doesn't contain any props!");
53
+ }
38
54
 
39
- // Update all props if they are dirty
40
- if (props->shimmerSpeed.isDirty) {
41
- hybridView->setShimmerSpeed(props->shimmerSpeed.value);
42
- props->shimmerSpeed.isDirty = false;
55
+ // Update only props that differ from the previous State snapshot.
56
+ if (oldProps == nullptr
57
+ ? newProps->shimmerSpeed.isProvided()
58
+ : !newProps->shimmerSpeed.hasSameValue(oldProps->shimmerSpeed)) {
59
+ hybridView->setShimmerSpeed(newProps->shimmerSpeed.get());
43
60
  }
44
- if (props->shimmerGradientColors.isDirty) {
45
- hybridView->setShimmerGradientColors(props->shimmerGradientColors.value);
46
- props->shimmerGradientColors.isDirty = false;
61
+ if (oldProps == nullptr
62
+ ? newProps->shimmerGradientColors.isProvided()
63
+ : !newProps->shimmerGradientColors.hasSameValue(oldProps->shimmerGradientColors)) {
64
+ hybridView->setShimmerGradientColors(newProps->shimmerGradientColors.get());
47
65
  }
48
66
 
49
67
  // Update hybridRef if it changed
50
- if (props->hybridRef.isDirty) {
68
+ if (oldProps == nullptr
69
+ ? newProps->hybridRef.isProvided()
70
+ : !newProps->hybridRef.hasSameValue(oldProps->hybridRef)) {
51
71
  // hybridRef changed - call it with new this
52
- const auto& maybeFunc = props->hybridRef.value;
72
+ const auto& maybeFunc = newProps->hybridRef.get();
53
73
  if (maybeFunc.has_value()) {
54
74
  maybeFunc.value()(hybridView);
55
75
  }
56
- props->hybridRef.isDirty = false;
57
76
  }
58
77
  }
59
78
 
@@ -31,7 +31,12 @@ public:
31
31
  public:
32
32
  static void updateViewProps(jni::alias_ref<jni::JClass> /* class */,
33
33
  jni::alias_ref<JHybridSkeletonSpec::JavaPart> view,
34
- jni::alias_ref<JStateWrapper::javaobject> stateWrapperInterface);
34
+ jni::alias_ref<JStateWrapper::javaobject> newState,
35
+ jni::alias_ref<JStateWrapper::javaobject> oldState);
36
+
37
+ private:
38
+ static std::shared_ptr<const HybridSkeletonProps> getPropsFromStateWrapper(
39
+ jni::alias_ref<JStateWrapper::javaobject> stateWrapper);
35
40
 
36
41
  public:
37
42
  static void registerNatives() {