@onekeyfe/react-native-skeleton 3.0.95 → 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.
- package/android/src/main/java/com/margelo/nitro/skeleton/OneKeySkeletonRenderer.kt +173 -0
- package/android/src/main/java/com/margelo/nitro/skeleton/Skeleton.kt +63 -194
- package/ios/Skeleton.swift +58 -272
- package/ios/SkeletonRenderer.swift +146 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridSkeletonStateUpdater.cpp +42 -23
- package/lib/nitrogen/generated/android/c++/views/JHybridSkeletonStateUpdater.hpp +6 -1
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/skeleton/views/HybridSkeletonManager.kt +26 -10
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/skeleton/views/HybridSkeletonStateUpdater.kt +2 -2
- package/lib/nitrogen/generated/ios/c++/views/HybridSkeletonComponent.mm +50 -27
- package/lib/nitrogen/generated/shared/c++/views/HybridSkeletonComponent.cpp +7 -65
- package/lib/nitrogen/generated/shared/c++/views/HybridSkeletonComponent.hpp +26 -50
- package/nitrogen/generated/android/c++/views/JHybridSkeletonStateUpdater.cpp +42 -23
- package/nitrogen/generated/android/c++/views/JHybridSkeletonStateUpdater.hpp +6 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/skeleton/views/HybridSkeletonManager.kt +26 -10
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/skeleton/views/HybridSkeletonStateUpdater.kt +2 -2
- package/nitrogen/generated/ios/c++/views/HybridSkeletonComponent.mm +50 -27
- package/nitrogen/generated/shared/c++/views/HybridSkeletonComponent.cpp +7 -65
- package/nitrogen/generated/shared/c++/views/HybridSkeletonComponent.hpp +26 -50
- package/package.json +4 -4
package/ios/Skeleton.swift
CHANGED
|
@@ -1,305 +1,91 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import UIKit
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
private final class SkeletonHostView: UIView {
|
|
5
|
+
var onLayout: ((CGRect) -> Void)?
|
|
6
|
+
var onWindowChanged: ((Bool) -> Void)?
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
override func layoutSubviews() {
|
|
9
|
+
super.layoutSubviews()
|
|
10
|
+
onLayout?(bounds)
|
|
11
|
+
}
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
private var
|
|
25
|
-
private var
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
289
|
-
var
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 =
|
|
25
|
-
if (!
|
|
26
|
-
|
|
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
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
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 (
|
|
45
|
-
|
|
46
|
-
|
|
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 (
|
|
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 =
|
|
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>
|
|
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() {
|