react-native-gleam 1.0.3 → 1.0.5

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.
@@ -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
@@ -171,6 +172,22 @@ class GleamView(context: Context) : ReactViewGroup(context) {
171
172
  }
172
173
  }
173
174
 
175
+ override fun onVisibilityChanged(changedView: View, visibility: Int) {
176
+ super.onVisibilityChanged(changedView, visibility)
177
+ if (!didAttach) return
178
+
179
+ if (visibility == VISIBLE) {
180
+ if (loading) {
181
+ registerClock()
182
+ invalidate()
183
+ } else if (!isTransitioning) {
184
+ contentOpacity = 1f
185
+ shimmerOpacity = 0f
186
+ invalidate()
187
+ }
188
+ }
189
+ }
190
+
174
191
  /** Called by ViewManager when the view is dropped */
175
192
  fun cleanup() {
176
193
  unregisterClock()
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
  }
@@ -190,6 +195,28 @@ static void _unregisterView(GleamView *view) {
190
195
  }
191
196
  }
192
197
 
198
+ - (void)didMoveToWindow
199
+ {
200
+ [super didMoveToWindow];
201
+ if (!self.window) return;
202
+
203
+ // View (re)joined a window — resync visual state.
204
+ // Mirrors Android's onAttachedToWindow re-attachment path.
205
+ if (_loading) {
206
+ if (_shimmerLayer.superlayer != self.layer) {
207
+ [self.layer addSublayer:_shimmerLayer];
208
+ }
209
+ [self _registerClock];
210
+ } else if (!_isTransitioning) {
211
+ // _isTransitioning=YES means ticks are actively driving it to
212
+ // completion (we no longer bail on !self.window) — let it finish.
213
+ [self _setChildrenAlphaIfNeeded:1.0];
214
+ _shimmerLayer.opacity = 0.0;
215
+ [_shimmerLayer removeFromSuperlayer];
216
+ [self _unregisterClock];
217
+ }
218
+ }
219
+
193
220
  - (void)removeFromSuperview
194
221
  {
195
222
  [self _unregisterClock];
@@ -214,6 +241,10 @@ static void _unregisterView(GleamView *view) {
214
241
 
215
242
  - (void)prepareForRecycle
216
243
  {
244
+ if (_isObservingHidden) {
245
+ [self removeObserver:self forKeyPath:@"hidden" context:kHiddenKVOContext];
246
+ _isObservingHidden = NO;
247
+ }
217
248
  [super prepareForRecycle];
218
249
  [self _unregisterClock];
219
250
  _isTransitioning = NO;
@@ -227,6 +258,9 @@ static void _unregisterView(GleamView *view) {
227
258
  _loading = YES;
228
259
  _wasLoading = YES;
229
260
  _didInitialSetup = NO;
261
+
262
+ [self addObserver:self forKeyPath:@"hidden" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:kHiddenKVOContext];
263
+ _isObservingHidden = YES;
230
264
  }
231
265
 
232
266
  // Invariant: a registered view (_isRegistered=YES) is held by the static
@@ -237,6 +271,10 @@ static void _unregisterView(GleamView *view) {
237
271
  // by the display link on the main thread. The view leaks in _views but no crash.
238
272
  - (void)dealloc
239
273
  {
274
+ if (_isObservingHidden) {
275
+ _isObservingHidden = NO;
276
+ [self removeObserver:self forKeyPath:@"hidden" context:kHiddenKVOContext];
277
+ }
240
278
  if (_isRegistered) {
241
279
  _isRegistered = NO;
242
280
  if ([NSThread isMainThread]) {
@@ -353,6 +391,36 @@ static void _unregisterView(GleamView *view) {
353
391
 
354
392
  #pragma mark - Private
355
393
 
394
+ - (void)observeValueForKeyPath:(NSString *)keyPath
395
+ ofObject:(id)object
396
+ change:(NSDictionary<NSKeyValueChangeKey,id> *)change
397
+ context:(void *)context
398
+ {
399
+ if (context != kHiddenKVOContext) {
400
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
401
+ return;
402
+ }
403
+
404
+ BOOL wasHidden = [change[NSKeyValueChangeOldKey] boolValue];
405
+ BOOL isHidden = [change[NSKeyValueChangeNewKey] boolValue];
406
+
407
+ if (wasHidden && !isHidden) {
408
+ // hidden YES→NO: ancestor removed display:'none' — resync visual state
409
+ if (_loading) {
410
+ if (_shimmerLayer.superlayer != self.layer) {
411
+ [self.layer addSublayer:_shimmerLayer];
412
+ }
413
+ [self _registerClock];
414
+ } else if (!_isTransitioning) {
415
+ [self _setChildrenAlphaIfNeeded:1.0];
416
+ _shimmerOpacity = 0.0;
417
+ _shimmerLayer.opacity = 0.0;
418
+ [_shimmerLayer removeFromSuperlayer];
419
+ [self _unregisterClock];
420
+ }
421
+ }
422
+ }
423
+
356
424
  - (CGFloat)_computeProgressWithTime:(CFTimeInterval)now
357
425
  {
358
426
  CGFloat effectiveTime = fmax(now - _delay, 0.0);
@@ -362,8 +430,10 @@ static void _unregisterView(GleamView *view) {
362
430
 
363
431
  - (void)_tickWithTime:(CFTimeInterval)now
364
432
  {
365
- if (!self.window) return;
366
-
433
+ // Transitions must always complete to avoid stuck visual state when the
434
+ // view is hidden (display:'none' → hidden=YES) or has no window.
435
+ // Cost is bounded (≤ transitionDuration) and the ops are cheap property sets.
436
+ // Shimmer animation is cosmetic and unbounded — skip when not visible.
367
437
  if (_isTransitioning) {
368
438
  _transitionElapsed += _displayLink.duration;
369
439
  CGFloat t = _transitionDuration > 0 ? fmin(_transitionElapsed / _transitionDuration, 1.0) : 1.0;
@@ -436,6 +506,7 @@ static void _unregisterView(GleamView *view) {
436
506
  [self _finishTransition];
437
507
  }
438
508
  } else if (_loading) {
509
+ if (!self.window) return;
439
510
  [self _updateGradientPositionWithTime:now];
440
511
  }
441
512
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gleam",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",