@onekeyfe/react-native-skeleton 3.0.48 → 3.0.49

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.
@@ -36,6 +36,27 @@ class HybridSkeleton(val context: ThemedReactContext) : HybridSkeletonSpec() {
36
36
  private val maxRetryCount: Int = 10
37
37
  private var pendingStartRunnable: Runnable? = null
38
38
 
39
+ // Guard against high-frequency afterUpdate() ValueAnimator churn (REACT-NATIVE-40K ANR).
40
+ // Fabric calls afterUpdate() on every prop commit, and list/order-book screens update
41
+ // very frequently. Unconditionally restarting the shimmer cancels/rebuilds the
42
+ // ValueAnimator each time (ValueAnimator.cancel -> AnimationHandler.removeCallback ->
43
+ // ArrayList.indexOf), blocking the main thread. We cache a signature of the inputs that
44
+ // actually drive the animation (speed + gradient colors + view size) and only restart
45
+ // when that signature changes. If nothing relevant changed and the shimmer is already
46
+ // running, we leave the existing animator untouched (no cancel, no rebuild).
47
+ private var lastShimmerSignature: String? = null
48
+ private var isShimmerRunning: Boolean = false
49
+
50
+ // Build a signature from every input that decides the shimmer animation:
51
+ // - animationSpeed -> ValueAnimator.duration
52
+ // - gradient colors -> shimmer gradient / draw output
53
+ // - view width/height -> animation travel range (-width..width*2) and draw bounds
54
+ private fun currentShimmerSignature(): String {
55
+ val colors = customGradientColors ?: DEFAULT_GRADIENT_COLORS
56
+ val colorsKey = colors.joinToString(",")
57
+ return "$animationSpeed|$colorsKey|${view.width}x${view.height}"
58
+ }
59
+
39
60
  // View with shimmer effect
40
61
  override val view: View = object : View(context) {
41
62
  override fun onDraw(canvas: Canvas) {
@@ -137,6 +158,11 @@ class HybridSkeleton(val context: ThemedReactContext) : HybridSkeletonSpec() {
137
158
  }
138
159
  start()
139
160
  }
161
+
162
+ // Record the inputs this running animator was built from, so afterUpdate() can skip
163
+ // restarting while none of them change.
164
+ lastShimmerSignature = currentShimmerSignature()
165
+ isShimmerRunning = true
140
166
  }
141
167
 
142
168
  private fun stopShimmer() {
@@ -152,13 +178,24 @@ class HybridSkeleton(val context: ThemedReactContext) : HybridSkeletonSpec() {
152
178
  cancel()
153
179
  }
154
180
  shimmerAnimator = null
181
+ isShimmerRunning = false
155
182
  }
156
183
 
157
184
  override fun afterUpdate() {
158
185
  super.afterUpdate()
159
- if (!isDisposed) {
160
- restartShimmer()
186
+ if (isDisposed) return
187
+
188
+ // Defensive guard against ValueAnimator churn (REACT-NATIVE-40K ANR):
189
+ // afterUpdate() runs on every Fabric prop commit. Only restart the shimmer when an
190
+ // input that actually drives the animation changed (speed / colors / view size).
191
+ // If the shimmer is already running and nothing relevant changed, leave the existing
192
+ // ValueAnimator alone to avoid main-thread cancel/rebuild churn.
193
+ val signature = currentShimmerSignature()
194
+ if (isShimmerRunning && shimmerAnimator != null && signature == lastShimmerSignature) {
195
+ return
161
196
  }
197
+
198
+ restartShimmer()
162
199
  }
163
200
 
164
201
  private fun restartShimmer() {
@@ -15,6 +15,31 @@ class HybridSkeleton : HybridSkeletonSpec {
15
15
  private let maxRetryCount: Int = 10
16
16
  private var viewObserver: NSKeyValueObservation?
17
17
 
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
26
+
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
+ }
42
+
18
43
  var shimmerGradientColors: [String]? {
19
44
  didSet {
20
45
  guard isActive else { return }
@@ -172,6 +197,11 @@ class HybridSkeleton : HybridSkeletonSpec {
172
197
 
173
198
  // Use weak self pattern for potential future animation delegates/completion handlers
174
199
  gradientLayer.add(animation, forKey: "shimmerAnimation")
200
+
201
+ // Record the inputs this running animation was built from, so afterUpdate() can skip
202
+ // restarting while none of them change.
203
+ lastShimmerSignature = currentShimmerSignature()
204
+ isShimmerRunning = true
175
205
  }
176
206
 
177
207
  func stopShimmer() {
@@ -195,12 +225,24 @@ class HybridSkeleton : HybridSkeletonSpec {
195
225
  // Nil out references
196
226
  shimmerLayer = nil
197
227
  skeletonLayer = nil
228
+ isShimmerRunning = false
198
229
  }
199
230
 
200
231
  func afterUpdate() {
201
232
  guard isActive else { return }
202
233
  DispatchQueue.main.async { [weak self] in
203
234
  guard let self = self, self.isActive else { return }
235
+
236
+ // Defensive guard against animation churn (REACT-NATIVE-40K ANR):
237
+ // afterUpdate() runs on every Fabric prop commit. Only restart the shimmer when an
238
+ // input that actually drives the animation changed (speed / colors / bounds). If the
239
+ // shimmer is already running and nothing relevant changed, leave the existing
240
+ // CABasicAnimation alone to avoid main-thread layer teardown/rebuild churn.
241
+ let signature = self.currentShimmerSignature()
242
+ if self.isShimmerRunning, self.shimmerLayer != nil, signature == self.lastShimmerSignature {
243
+ return
244
+ }
245
+
204
246
  self.restartShimmer()
205
247
  }
206
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-skeleton",
3
- "version": "3.0.48",
3
+ "version": "3.0.49",
4
4
  "description": "react-native-skeleton",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",