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 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
- contentOpacity = 1f
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
- contentOpacity = 1f
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
- unregisterClock()
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 finishTransition() {
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 _setChildrenAlphaIfNeeded:1.0];
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 _setChildrenAlphaIfNeeded:1.0];
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
- if (oldViewProps.speed != newViewProps.speed) {
294
- double s = newViewProps.speed / 1000.0;
295
- _speed = (isnan(s) || isinf(s) || s <= 0) ? 1.0 : fmax(s, 0.001);
296
- }
297
-
298
- if (oldViewProps.delay != newViewProps.delay) {
299
- double d = newViewProps.delay / 1000.0;
300
- _delay = (isnan(d) || isinf(d)) ? 0.0 : d;
301
- }
302
-
303
- if (oldViewProps.transitionDuration != newViewProps.transitionDuration) {
304
- double td = newViewProps.transitionDuration / 1000.0;
305
- _transitionDuration = (isnan(td) || isinf(td) || td < 0) ? 0.3 : td;
306
- }
307
-
308
- if (oldViewProps.transitionType != newViewProps.transitionType) {
309
- auto tt = newViewProps.transitionType;
310
- if (tt == GleamViewTransitionType::Shrink) _transitionTypeValue = 1;
311
- else if (tt == GleamViewTransitionType::Collapse) _transitionTypeValue = 2;
312
- else _transitionTypeValue = 0;
313
- }
314
-
315
- if (oldViewProps.direction != newViewProps.direction) {
316
- auto dir = newViewProps.direction;
317
- if (dir == GleamViewDirection::Rtl) {
318
- _direction = GleamDirectionRTL;
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
- if (oldViewProps.intensity != newViewProps.intensity) {
328
- _intensity = fmin(fmax(newViewProps.intensity, 0.0), 1.0);
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
- if (oldViewProps.baseColor != newViewProps.baseColor) {
333
- UIColor *color = RCTUIColorFromSharedColor(newViewProps.baseColor);
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
- if (oldViewProps.highlightColor != newViewProps.highlightColor) {
339
- UIColor *color = RCTUIColorFromSharedColor(newViewProps.highlightColor);
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
- if (oldViewProps.loading != newViewProps.loading) {
345
- _loading = newViewProps.loading;
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
- _contentAlpha = 1.0;
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 (oldViewProps.loading != newViewProps.loading) {
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 _unregisterClock];
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gleam",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Native-powered shimmer loading effect for React Native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",