react-native-gleam 1.0.4 → 1.0.6
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/README.md +1 -1
- package/android/src/main/java/com/gleam/GleamView.kt +29 -10
- package/ios/GleamView.mm +107 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Native-powered shimmer loading effect for React Native. Built with pure native animations — no reanimated, no SVG, zero dependencies.
|
|
4
4
|
|
|
5
5
|
- **iOS**: `CAGradientLayer` + `CADisplayLink`
|
|
6
|
-
- **Android**: `Choreographer` + `LinearGradient`
|
|
6
|
+
- **Android**: `Choreographer` + `LinearGradient` + `ValueAnimator`
|
|
7
7
|
- **Fabric only** (New Architecture)
|
|
8
8
|
|
|
9
9
|
https://github.com/user-attachments/assets/70eb886c-f3e2-4611-8ecc-0b03227267d0
|
|
@@ -11,6 +11,7 @@ import android.graphics.RectF
|
|
|
11
11
|
import android.graphics.Shader
|
|
12
12
|
import android.os.SystemClock
|
|
13
13
|
import android.view.Choreographer
|
|
14
|
+
import android.view.View
|
|
14
15
|
import android.view.animation.DecelerateInterpolator
|
|
15
16
|
import androidx.annotation.UiThread
|
|
16
17
|
import com.facebook.react.bridge.Arguments
|
|
@@ -141,16 +142,14 @@ class GleamView(context: Context) : ReactViewGroup(context) {
|
|
|
141
142
|
if (loading) {
|
|
142
143
|
registerClock()
|
|
143
144
|
} else {
|
|
144
|
-
|
|
145
|
-
shimmerOpacity = 0f
|
|
145
|
+
forceLoadedState()
|
|
146
146
|
}
|
|
147
147
|
} else {
|
|
148
148
|
// Re-attachment: restore correct state
|
|
149
149
|
if (loading) {
|
|
150
150
|
registerClock()
|
|
151
151
|
} else if (!isTransitioning) {
|
|
152
|
-
|
|
153
|
-
shimmerOpacity = 0f
|
|
152
|
+
forceLoadedState()
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
}
|
|
@@ -171,6 +170,20 @@ class GleamView(context: Context) : ReactViewGroup(context) {
|
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
|
|
173
|
+
override fun onVisibilityChanged(changedView: View, visibility: Int) {
|
|
174
|
+
super.onVisibilityChanged(changedView, visibility)
|
|
175
|
+
if (!didAttach) return
|
|
176
|
+
|
|
177
|
+
if (visibility == VISIBLE) {
|
|
178
|
+
if (loading) {
|
|
179
|
+
registerClock()
|
|
180
|
+
invalidate()
|
|
181
|
+
} else if (!isTransitioning) {
|
|
182
|
+
forceLoadedState()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
174
187
|
/** Called by ViewManager when the view is dropped */
|
|
175
188
|
fun cleanup() {
|
|
176
189
|
unregisterClock()
|
|
@@ -369,22 +382,28 @@ class GleamView(context: Context) : ReactViewGroup(context) {
|
|
|
369
382
|
start()
|
|
370
383
|
}
|
|
371
384
|
} else {
|
|
372
|
-
|
|
373
|
-
contentOpacity = 1f
|
|
374
|
-
shimmerOpacity = 0f
|
|
375
|
-
invalidate()
|
|
385
|
+
forceLoadedState()
|
|
376
386
|
emitTransitionEnd(true)
|
|
377
387
|
}
|
|
378
388
|
}
|
|
379
389
|
}
|
|
380
390
|
|
|
381
|
-
private fun
|
|
382
|
-
if (!isTransitioning) return
|
|
391
|
+
private fun forceLoadedState() {
|
|
383
392
|
isTransitioning = false
|
|
393
|
+
transitionGeneration++
|
|
394
|
+
transitionAnimator?.removeAllListeners()
|
|
395
|
+
transitionAnimator?.cancel()
|
|
396
|
+
transitionAnimator = null
|
|
384
397
|
unregisterClock()
|
|
398
|
+
transitionProgress = 1f
|
|
385
399
|
contentOpacity = 1f
|
|
386
400
|
shimmerOpacity = 0f
|
|
387
401
|
invalidate()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private fun finishTransition() {
|
|
405
|
+
if (!isTransitioning) return
|
|
406
|
+
forceLoadedState()
|
|
388
407
|
emitTransitionEnd(true)
|
|
389
408
|
}
|
|
390
409
|
|
package/ios/GleamView.mm
CHANGED
|
@@ -26,6 +26,7 @@ static GleamView * __strong *_views = NULL;
|
|
|
26
26
|
static NSUInteger _viewCount = 0;
|
|
27
27
|
static NSUInteger _viewCapacity = 0;
|
|
28
28
|
static CADisplayLink *_displayLink;
|
|
29
|
+
static void *kHiddenKVOContext = &kHiddenKVOContext;
|
|
29
30
|
|
|
30
31
|
static void _startDisplayLinkIfNeeded(void) {
|
|
31
32
|
if (_displayLink) return;
|
|
@@ -103,6 +104,7 @@ static void _unregisterView(GleamView *view) {
|
|
|
103
104
|
CGFloat _contentAlpha;
|
|
104
105
|
CGFloat _lastSetChildrenAlpha;
|
|
105
106
|
BOOL _didInitialSetup;
|
|
107
|
+
BOOL _isObservingHidden;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
@@ -158,6 +160,9 @@ static void _unregisterView(GleamView *view) {
|
|
|
158
160
|
@"transform": [NSNull null],
|
|
159
161
|
};
|
|
160
162
|
_shimmerLayer.locations = @[@0.0, @0.5, @1.0];
|
|
163
|
+
|
|
164
|
+
[self addObserver:self forKeyPath:@"hidden" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:kHiddenKVOContext];
|
|
165
|
+
_isObservingHidden = YES;
|
|
161
166
|
}
|
|
162
167
|
return self;
|
|
163
168
|
}
|
|
@@ -167,6 +172,8 @@ static void _unregisterView(GleamView *view) {
|
|
|
167
172
|
[super mountChildComponentView:childComponentView index:index];
|
|
168
173
|
if (_loading || _isTransitioning) {
|
|
169
174
|
childComponentView.alpha = _contentAlpha;
|
|
175
|
+
} else {
|
|
176
|
+
childComponentView.alpha = 1.0;
|
|
170
177
|
}
|
|
171
178
|
}
|
|
172
179
|
|
|
@@ -183,10 +190,7 @@ static void _unregisterView(GleamView *view) {
|
|
|
183
190
|
}
|
|
184
191
|
[self _registerClock];
|
|
185
192
|
} else if (!_isTransitioning) {
|
|
186
|
-
[self
|
|
187
|
-
_shimmerLayer.opacity = 0.0;
|
|
188
|
-
[_shimmerLayer removeFromSuperlayer];
|
|
189
|
-
[self _unregisterClock];
|
|
193
|
+
[self _forceLoadedState];
|
|
190
194
|
}
|
|
191
195
|
}
|
|
192
196
|
|
|
@@ -205,10 +209,7 @@ static void _unregisterView(GleamView *view) {
|
|
|
205
209
|
} else if (!_isTransitioning) {
|
|
206
210
|
// _isTransitioning=YES means ticks are actively driving it to
|
|
207
211
|
// completion (we no longer bail on !self.window) — let it finish.
|
|
208
|
-
[self
|
|
209
|
-
_shimmerLayer.opacity = 0.0;
|
|
210
|
-
[_shimmerLayer removeFromSuperlayer];
|
|
211
|
-
[self _unregisterClock];
|
|
212
|
+
[self _forceLoadedState];
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
|
|
@@ -236,6 +237,10 @@ static void _unregisterView(GleamView *view) {
|
|
|
236
237
|
|
|
237
238
|
- (void)prepareForRecycle
|
|
238
239
|
{
|
|
240
|
+
if (_isObservingHidden) {
|
|
241
|
+
[self removeObserver:self forKeyPath:@"hidden" context:kHiddenKVOContext];
|
|
242
|
+
_isObservingHidden = NO;
|
|
243
|
+
}
|
|
239
244
|
[super prepareForRecycle];
|
|
240
245
|
[self _unregisterClock];
|
|
241
246
|
_isTransitioning = NO;
|
|
@@ -249,6 +254,9 @@ static void _unregisterView(GleamView *view) {
|
|
|
249
254
|
_loading = YES;
|
|
250
255
|
_wasLoading = YES;
|
|
251
256
|
_didInitialSetup = NO;
|
|
257
|
+
|
|
258
|
+
[self addObserver:self forKeyPath:@"hidden" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:kHiddenKVOContext];
|
|
259
|
+
_isObservingHidden = YES;
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
// Invariant: a registered view (_isRegistered=YES) is held by the static
|
|
@@ -259,6 +267,10 @@ static void _unregisterView(GleamView *view) {
|
|
|
259
267
|
// by the display link on the main thread. The view leaks in _views but no crash.
|
|
260
268
|
- (void)dealloc
|
|
261
269
|
{
|
|
270
|
+
if (_isObservingHidden) {
|
|
271
|
+
_isObservingHidden = NO;
|
|
272
|
+
[self removeObserver:self forKeyPath:@"hidden" context:kHiddenKVOContext];
|
|
273
|
+
}
|
|
262
274
|
if (_isRegistered) {
|
|
263
275
|
_isRegistered = NO;
|
|
264
276
|
if ([NSThread isMainThread]) {
|
|
@@ -287,63 +299,57 @@ static void _unregisterView(GleamView *view) {
|
|
|
287
299
|
|
|
288
300
|
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
289
301
|
{
|
|
290
|
-
const auto &oldViewProps = *std::static_pointer_cast<GleamViewProps const>(_props);
|
|
291
302
|
const auto &newViewProps = *std::static_pointer_cast<GleamViewProps const>(props);
|
|
292
303
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
} else if (dir == GleamViewDirection::Ttb) {
|
|
320
|
-
_direction = GleamDirectionTTB;
|
|
321
|
-
} else {
|
|
322
|
-
_direction = GleamDirectionLTR;
|
|
323
|
-
}
|
|
304
|
+
// Fabric can recycle a native component without changing the JS props for
|
|
305
|
+
// the next cell. prepareForRecycle resets internal state, so every update
|
|
306
|
+
// must resync from the new props instead of relying only on old/new prop
|
|
307
|
+
// diffs. This keeps FlatList-recycled cells from staying in shimmer mode
|
|
308
|
+
// when the recycled JS prop is still loading=false.
|
|
309
|
+
double s = newViewProps.speed / 1000.0;
|
|
310
|
+
_speed = (isnan(s) || isinf(s) || s <= 0) ? 1.0 : fmax(s, 0.001);
|
|
311
|
+
|
|
312
|
+
double d = newViewProps.delay / 1000.0;
|
|
313
|
+
_delay = (isnan(d) || isinf(d)) ? 0.0 : d;
|
|
314
|
+
|
|
315
|
+
double td = newViewProps.transitionDuration / 1000.0;
|
|
316
|
+
_transitionDuration = (isnan(td) || isinf(td) || td < 0) ? 0.3 : td;
|
|
317
|
+
|
|
318
|
+
auto tt = newViewProps.transitionType;
|
|
319
|
+
if (tt == GleamViewTransitionType::Shrink) _transitionTypeValue = 1;
|
|
320
|
+
else if (tt == GleamViewTransitionType::Collapse) _transitionTypeValue = 2;
|
|
321
|
+
else _transitionTypeValue = 0;
|
|
322
|
+
|
|
323
|
+
auto dir = newViewProps.direction;
|
|
324
|
+
if (dir == GleamViewDirection::Rtl) {
|
|
325
|
+
_direction = GleamDirectionRTL;
|
|
326
|
+
} else if (dir == GleamViewDirection::Ttb) {
|
|
327
|
+
_direction = GleamDirectionTTB;
|
|
328
|
+
} else {
|
|
329
|
+
_direction = GleamDirectionLTR;
|
|
324
330
|
}
|
|
325
331
|
|
|
326
332
|
BOOL colorsChanged = NO;
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
CGFloat nextIntensity = fmin(fmax(newViewProps.intensity, 0.0), 1.0);
|
|
334
|
+
if (_intensity != nextIntensity) {
|
|
329
335
|
colorsChanged = YES;
|
|
330
336
|
}
|
|
337
|
+
_intensity = nextIntensity;
|
|
331
338
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
_baseColor = color ?: [UIColor colorWithRed:0.878 green:0.878 blue:0.878 alpha:1.0];
|
|
339
|
+
UIColor *baseColor = RCTUIColorFromSharedColor(newViewProps.baseColor) ?: [UIColor colorWithRed:0.878 green:0.878 blue:0.878 alpha:1.0];
|
|
340
|
+
if (![_baseColor isEqual:baseColor]) {
|
|
335
341
|
colorsChanged = YES;
|
|
336
342
|
}
|
|
343
|
+
_baseColor = baseColor;
|
|
337
344
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
_highlightColor = color ?: [UIColor colorWithRed:0.961 green:0.961 blue:0.961 alpha:1.0];
|
|
345
|
+
UIColor *highlightColor = RCTUIColorFromSharedColor(newViewProps.highlightColor) ?: [UIColor colorWithRed:0.961 green:0.961 blue:0.961 alpha:1.0];
|
|
346
|
+
if (![_highlightColor isEqual:highlightColor]) {
|
|
341
347
|
colorsChanged = YES;
|
|
342
348
|
}
|
|
349
|
+
_highlightColor = highlightColor;
|
|
343
350
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
351
|
+
BOOL loadingChanged = _loading != newViewProps.loading;
|
|
352
|
+
_loading = newViewProps.loading;
|
|
347
353
|
|
|
348
354
|
if (!_didInitialSetup) {
|
|
349
355
|
_didInitialSetup = YES;
|
|
@@ -353,20 +359,16 @@ static void _unregisterView(GleamView *view) {
|
|
|
353
359
|
[self _applyLoadingState];
|
|
354
360
|
} else {
|
|
355
361
|
_wasLoading = NO;
|
|
356
|
-
|
|
357
|
-
_shimmerOpacity = 0.0;
|
|
358
|
-
_lastSetChildrenAlpha = -1.0;
|
|
359
|
-
for (UIView *subview in self.subviews) {
|
|
360
|
-
subview.alpha = 1.0;
|
|
361
|
-
}
|
|
362
|
-
_lastSetChildrenAlpha = 1.0;
|
|
362
|
+
[self _forceLoadedState];
|
|
363
363
|
}
|
|
364
364
|
} else {
|
|
365
365
|
if (colorsChanged) {
|
|
366
366
|
[self _updateShimmerColors];
|
|
367
367
|
}
|
|
368
|
-
if (
|
|
368
|
+
if (loadingChanged) {
|
|
369
369
|
[self _applyLoadingState];
|
|
370
|
+
} else if (!_loading && !_isTransitioning) {
|
|
371
|
+
[self _forceLoadedState];
|
|
370
372
|
}
|
|
371
373
|
}
|
|
372
374
|
|
|
@@ -375,6 +377,32 @@ static void _unregisterView(GleamView *view) {
|
|
|
375
377
|
|
|
376
378
|
#pragma mark - Private
|
|
377
379
|
|
|
380
|
+
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
381
|
+
ofObject:(id)object
|
|
382
|
+
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
|
|
383
|
+
context:(void *)context
|
|
384
|
+
{
|
|
385
|
+
if (context != kHiddenKVOContext) {
|
|
386
|
+
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
BOOL wasHidden = [change[NSKeyValueChangeOldKey] boolValue];
|
|
391
|
+
BOOL isHidden = [change[NSKeyValueChangeNewKey] boolValue];
|
|
392
|
+
|
|
393
|
+
if (wasHidden && !isHidden) {
|
|
394
|
+
// hidden YES→NO: ancestor removed display:'none' — resync visual state
|
|
395
|
+
if (_loading) {
|
|
396
|
+
if (_shimmerLayer.superlayer != self.layer) {
|
|
397
|
+
[self.layer addSublayer:_shimmerLayer];
|
|
398
|
+
}
|
|
399
|
+
[self _registerClock];
|
|
400
|
+
} else if (!_isTransitioning) {
|
|
401
|
+
[self _forceLoadedState];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
378
406
|
- (CGFloat)_computeProgressWithTime:(CFTimeInterval)now
|
|
379
407
|
{
|
|
380
408
|
CGFloat effectiveTime = fmax(now - _delay, 0.0);
|
|
@@ -562,20 +590,31 @@ static void _unregisterView(GleamView *view) {
|
|
|
562
590
|
_transitionElapsed = 0.0;
|
|
563
591
|
[self _registerClock];
|
|
564
592
|
} else {
|
|
565
|
-
[self
|
|
566
|
-
_lastSetChildrenAlpha = -1.0;
|
|
567
|
-
for (UIView *subview in self.subviews) {
|
|
568
|
-
subview.alpha = 1.0;
|
|
569
|
-
}
|
|
570
|
-
_lastSetChildrenAlpha = 1.0;
|
|
571
|
-
_contentAlpha = 1.0;
|
|
572
|
-
_shimmerLayer.opacity = 0.0;
|
|
573
|
-
[_shimmerLayer removeFromSuperlayer];
|
|
593
|
+
[self _forceLoadedState];
|
|
574
594
|
[self _emitTransitionEnd:YES];
|
|
575
595
|
}
|
|
576
596
|
}
|
|
577
597
|
}
|
|
578
598
|
|
|
599
|
+
- (void)_forceLoadedState
|
|
600
|
+
{
|
|
601
|
+
if (_isTransitioning) {
|
|
602
|
+
_isTransitioning = NO;
|
|
603
|
+
}
|
|
604
|
+
[self _unregisterClock];
|
|
605
|
+
_lastSetChildrenAlpha = -1.0;
|
|
606
|
+
for (UIView *subview in self.subviews) {
|
|
607
|
+
subview.alpha = 1.0;
|
|
608
|
+
}
|
|
609
|
+
_lastSetChildrenAlpha = 1.0;
|
|
610
|
+
_contentAlpha = 1.0;
|
|
611
|
+
_shimmerOpacity = 0.0;
|
|
612
|
+
_shimmerLayer.opacity = 0.0;
|
|
613
|
+
_shimmerLayer.transform = CATransform3DIdentity;
|
|
614
|
+
_shimmerLayer.mask = nil;
|
|
615
|
+
[_shimmerLayer removeFromSuperlayer];
|
|
616
|
+
}
|
|
617
|
+
|
|
579
618
|
- (void)_finishTransition
|
|
580
619
|
{
|
|
581
620
|
_isTransitioning = NO;
|