capacitor-lottie-splash 0.1.3 → 0.1.4

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
@@ -101,7 +101,7 @@ The documented HeadCount defaults are light `#f0f0f0 → #f5f5f5` and dark `#1e1
101
101
 
102
102
  ## Recovery and accessibility
103
103
 
104
- When playback completes before readiness, its final frame holds. After one second, a subtle indeterminate progress rail appears beneath the mark and remains visible until the application is ready. Reduced-motion and asset failures retain the theme-matched native cover instead of exposing a blank screen.
104
+ Each presentation fades in over 500 milliseconds before Lottie playback begins, then fades out over 500 milliseconds when the application is ready. When playback completes before readiness, its final frame holds. After one second, a subtle indeterminate progress rail appears beneath the mark and remains visible until the application is ready. Reduced-motion and asset failures retain the theme-matched native cover instead of exposing a blank screen.
105
105
 
106
106
  For a staged native/over-the-air rollout, the consuming app must keep its web bundle and native host compatible. The native cover stays visible until the app-ready latch is set.
107
107
 
@@ -79,6 +79,7 @@ object LottieSplashCoordinator {
79
79
  // incorrectly dismissing a healthy native cover during that startup work.
80
80
  private const val WEB_ADOPTION_GRACE_MS = 30_000L
81
81
  private const val PROGRESS_DELAY_MS = 1_000L
82
+ private const val COVER_FADE_IN_MS = 500L
82
83
  private const val COVER_FADE_OUT_MS = 500L
83
84
  private val main = Handler(Looper.getMainLooper())
84
85
 
@@ -88,6 +89,8 @@ object LottieSplashCoordinator {
88
89
  private var hostForeground = false
89
90
  private var systemHandoffComplete = true
90
91
  private var compositionReady = false
92
+ private var coverFadeInStarted = false
93
+ private var coverFadeInComplete = false
91
94
  private var requestedPlayback = AndroidSplashPlayback.PLAY
92
95
  private var loadingRail: LottieSplashLoadingRail? = null
93
96
  private var presentationId: String? = null
@@ -135,6 +138,8 @@ object LottieSplashCoordinator {
135
138
  hostForeground = activity.hasWindowFocus()
136
139
  systemHandoffComplete = !dismissWhenUnadopted
137
140
  compositionReady = false
141
+ coverFadeInStarted = false
142
+ coverFadeInComplete = false
138
143
  requestedPlayback = playback
139
144
  presentationId = UUID.randomUUID().toString()
140
145
  phase = AndroidSplashPhase.PREPARING
@@ -154,7 +159,7 @@ object LottieSplashCoordinator {
154
159
  isClickable = true
155
160
  isFocusable = true
156
161
  contentDescription = "Loading application"
157
- alpha = 1f
162
+ alpha = 0f
158
163
  background = GradientDrawable(
159
164
  GradientDrawable.Orientation.LEFT_RIGHT,
160
165
  if (darkTheme) intArrayOf(backgroundStart, Color.rgb(37, 37, 37))
@@ -171,6 +176,7 @@ object LottieSplashCoordinator {
171
176
  overlay.post { updateAnimationLayout(overlay, lottie) }
172
177
  cover = overlay
173
178
  animation = lottie
179
+ if (systemHandoffComplete) beginCoverFadeIn(presentationId!!, overlay, lottie)
174
180
  if (dismissWhenUnadopted) scheduleUnadoptedFallback(presentationId!!)
175
181
  emit()
176
182
 
@@ -217,6 +223,7 @@ object LottieSplashCoordinator {
217
223
  val id = presentationId ?: return
218
224
  val existingCover = cover ?: return
219
225
  val existingAnimation = animation ?: return
226
+ beginCoverFadeIn(id, existingCover, existingAnimation)
220
227
  startPlaybackWhenVisible(id, existingCover, existingAnimation)
221
228
  }
222
229
 
@@ -283,7 +290,7 @@ object LottieSplashCoordinator {
283
290
  }
284
291
 
285
292
  private fun startPlaybackWhenVisible(id: String, overlay: FrameLayout, lottie: LottieAnimationView) {
286
- if (id != presentationId || cover !== overlay || animation !== lottie || !compositionReady || !hostForeground || !systemHandoffComplete || phase != AndroidSplashPhase.PREPARING) return
293
+ if (id != presentationId || cover !== overlay || animation !== lottie || !compositionReady || !coverFadeInComplete || !hostForeground || !systemHandoffComplete || phase != AndroidSplashPhase.PREPARING) return
287
294
  val composition = lottie.composition ?: return
288
295
  phase = AndroidSplashPhase.PLAYING
289
296
  emit()
@@ -312,6 +319,22 @@ object LottieSplashCoordinator {
312
319
  }
313
320
  }
314
321
 
322
+ private fun beginCoverFadeIn(id: String, overlay: FrameLayout, lottie: LottieAnimationView) {
323
+ if (id != presentationId || cover !== overlay || animation !== lottie || coverFadeInStarted || !systemHandoffComplete) return
324
+ coverFadeInStarted = true
325
+ overlay.animate()
326
+ .alpha(1f)
327
+ .setDuration(if (ValueAnimator.areAnimatorsEnabled()) COVER_FADE_IN_MS else 0)
328
+ .setInterpolator(AccelerateDecelerateInterpolator())
329
+ .withEndAction {
330
+ if (id == presentationId && cover === overlay && animation === lottie) {
331
+ coverFadeInComplete = true
332
+ startPlaybackWhenVisible(id, overlay, lottie)
333
+ }
334
+ }
335
+ .start()
336
+ }
337
+
315
338
  private fun updateAnimationLayout(overlay: FrameLayout, lottie: LottieAnimationView) {
316
339
  if (cover !== overlay || animation !== lottie || overlay.width <= 0 || overlay.height <= 0) return
317
340
  val availableWidth = minOf(overlay.width.toFloat(), overlay.height * 0.75f)
@@ -424,6 +447,8 @@ object LottieSplashCoordinator {
424
447
  presentationId = null
425
448
  phase = AndroidSplashPhase.HIDDEN
426
449
  compositionReady = false
450
+ coverFadeInStarted = false
451
+ coverFadeInComplete = false
427
452
  emit()
428
453
  callback(null)
429
454
  }.start()
@@ -444,6 +469,8 @@ object LottieSplashCoordinator {
444
469
  presentationId = null
445
470
  phase = AndroidSplashPhase.HIDDEN
446
471
  compositionReady = false
472
+ coverFadeInStarted = false
473
+ coverFadeInComplete = false
447
474
  pendingHide = null
448
475
  completion = null
449
476
  emit()
package/dist/esm/web.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare class LottieSplashWeb extends WebPlugin implements LottieSplashPl
6
6
  private resolvePlayback?;
7
7
  private overlay?;
8
8
  private progressTimer?;
9
+ private playbackTimer?;
9
10
  show(options: SplashShowOptions): Promise<Presentation>;
10
11
  setLaunchTheme(_options: SplashLaunchThemeOptions): Promise<void>;
11
12
  waitForCompletion(options: {
package/dist/esm/web.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
2
  import { SplashStateMachine } from './state-machine';
3
3
  const PROGRESS_DELAY_MS = 1_000;
4
+ const COVER_FADE_MS = 500;
4
5
  export class LottieSplashWeb extends WebPlugin {
5
6
  stateMachine = new SplashStateMachine();
6
7
  playbackPromise;
7
8
  resolvePlayback;
8
9
  overlay;
9
10
  progressTimer;
11
+ playbackTimer;
10
12
  async show(options) {
11
13
  const state = this.stateMachine.current();
12
14
  if (state.visible && state.presentationId)
@@ -30,7 +32,7 @@ export class LottieSplashWeb extends WebPlugin {
30
32
  this.emitState();
31
33
  if (!state.playbackSettled)
32
34
  await this.waitForCompletion(options);
33
- this.removeOverlay(options.presentationId);
35
+ await this.removeOverlay(options.presentationId);
34
36
  }
35
37
  async getState() {
36
38
  const { generation: _generation, ...state } = this.stateMachine.current();
@@ -47,10 +49,14 @@ export class LottieSplashWeb extends WebPlugin {
47
49
  overlay.dataset.presentationId = presentationId;
48
50
  Object.assign(overlay.style, {
49
51
  position: 'fixed', inset: '0', zIndex: '2147483647', display: 'grid', placeItems: 'center',
50
- background: this.backgroundFor(this.stateMachine.current().theme), overflow: 'hidden'
52
+ background: this.backgroundFor(this.stateMachine.current().theme), overflow: 'hidden',
53
+ opacity: '0', transition: `opacity ${COVER_FADE_MS}ms ease-in-out`
51
54
  });
52
55
  document.body.append(overlay);
53
56
  this.overlay = overlay;
57
+ // Force the initial transparent style to paint before beginning the fade.
58
+ void overlay.offsetWidth;
59
+ overlay.style.opacity = '1';
54
60
  // A generic package cannot ship HeadCount assets. The host supplies an approved local
55
61
  // composition; absent or failed assets leave the opaque branded background in place.
56
62
  if (!options.assets?.animationData && !options.assets?.animationPath) {
@@ -61,9 +67,13 @@ export class LottieSplashWeb extends WebPlugin {
61
67
  this.settle(presentationId, 'reduced-motion', 'Reduced motion is enabled');
62
68
  return;
63
69
  }
64
- this.stateMachine.playing(presentationId);
65
- this.emitState();
66
- void this.playComposition(options, presentationId);
70
+ this.playbackTimer = window.setTimeout(() => {
71
+ if (this.stateMachine.current().presentationId !== presentationId || this.overlay !== overlay)
72
+ return;
73
+ this.stateMachine.playing(presentationId);
74
+ this.emitState();
75
+ void this.playComposition(options, presentationId);
76
+ }, COVER_FADE_MS);
67
77
  }
68
78
  async playComposition(options, presentationId) {
69
79
  try {
@@ -124,9 +134,15 @@ export class LottieSplashWeb extends WebPlugin {
124
134
  rail.append(segment);
125
135
  this.overlay?.append(style, rail);
126
136
  }
127
- removeOverlay(presentationId) {
137
+ async removeOverlay(presentationId) {
128
138
  window.clearTimeout(this.progressTimer);
129
- this.overlay?.remove();
139
+ window.clearTimeout(this.playbackTimer);
140
+ const overlay = this.overlay;
141
+ if (overlay) {
142
+ overlay.style.opacity = '0';
143
+ await new Promise(resolve => window.setTimeout(resolve, COVER_FADE_MS));
144
+ overlay.remove();
145
+ }
130
146
  this.overlay = undefined;
131
147
  this.stateMachine.hidden(presentationId);
132
148
  this.emitState();
@@ -80,12 +80,14 @@ class SplashStateMachine {
80
80
  }
81
81
 
82
82
  const PROGRESS_DELAY_MS = 1_000;
83
+ const COVER_FADE_MS = 500;
83
84
  class LottieSplashWeb extends core.WebPlugin {
84
85
  stateMachine = new SplashStateMachine();
85
86
  playbackPromise;
86
87
  resolvePlayback;
87
88
  overlay;
88
89
  progressTimer;
90
+ playbackTimer;
89
91
  async show(options) {
90
92
  const state = this.stateMachine.current();
91
93
  if (state.visible && state.presentationId)
@@ -109,7 +111,7 @@ class LottieSplashWeb extends core.WebPlugin {
109
111
  this.emitState();
110
112
  if (!state.playbackSettled)
111
113
  await this.waitForCompletion(options);
112
- this.removeOverlay(options.presentationId);
114
+ await this.removeOverlay(options.presentationId);
113
115
  }
114
116
  async getState() {
115
117
  const { generation: _generation, ...state } = this.stateMachine.current();
@@ -126,10 +128,14 @@ class LottieSplashWeb extends core.WebPlugin {
126
128
  overlay.dataset.presentationId = presentationId;
127
129
  Object.assign(overlay.style, {
128
130
  position: 'fixed', inset: '0', zIndex: '2147483647', display: 'grid', placeItems: 'center',
129
- background: this.backgroundFor(this.stateMachine.current().theme), overflow: 'hidden'
131
+ background: this.backgroundFor(this.stateMachine.current().theme), overflow: 'hidden',
132
+ opacity: '0', transition: `opacity ${COVER_FADE_MS}ms ease-in-out`
130
133
  });
131
134
  document.body.append(overlay);
132
135
  this.overlay = overlay;
136
+ // Force the initial transparent style to paint before beginning the fade.
137
+ void overlay.offsetWidth;
138
+ overlay.style.opacity = '1';
133
139
  // A generic package cannot ship HeadCount assets. The host supplies an approved local
134
140
  // composition; absent or failed assets leave the opaque branded background in place.
135
141
  if (!options.assets?.animationData && !options.assets?.animationPath) {
@@ -140,9 +146,13 @@ class LottieSplashWeb extends core.WebPlugin {
140
146
  this.settle(presentationId, 'reduced-motion', 'Reduced motion is enabled');
141
147
  return;
142
148
  }
143
- this.stateMachine.playing(presentationId);
144
- this.emitState();
145
- void this.playComposition(options, presentationId);
149
+ this.playbackTimer = window.setTimeout(() => {
150
+ if (this.stateMachine.current().presentationId !== presentationId || this.overlay !== overlay)
151
+ return;
152
+ this.stateMachine.playing(presentationId);
153
+ this.emitState();
154
+ void this.playComposition(options, presentationId);
155
+ }, COVER_FADE_MS);
146
156
  }
147
157
  async playComposition(options, presentationId) {
148
158
  try {
@@ -203,9 +213,15 @@ class LottieSplashWeb extends core.WebPlugin {
203
213
  rail.append(segment);
204
214
  this.overlay?.append(style, rail);
205
215
  }
206
- removeOverlay(presentationId) {
216
+ async removeOverlay(presentationId) {
207
217
  window.clearTimeout(this.progressTimer);
208
- this.overlay?.remove();
218
+ window.clearTimeout(this.playbackTimer);
219
+ const overlay = this.overlay;
220
+ if (overlay) {
221
+ overlay.style.opacity = '0';
222
+ await new Promise(resolve => window.setTimeout(resolve, COVER_FADE_MS));
223
+ overlay.remove();
224
+ }
209
225
  this.overlay = undefined;
210
226
  this.stateMachine.hidden(presentationId);
211
227
  this.emitState();
package/dist/plugin.js CHANGED
@@ -94,12 +94,14 @@ var capacitorLottieSplash = (function (exports, core) {
94
94
  }
95
95
 
96
96
  const PROGRESS_DELAY_MS = 1_000;
97
+ const COVER_FADE_MS = 500;
97
98
  class LottieSplashWeb extends core.WebPlugin {
98
99
  stateMachine = new SplashStateMachine();
99
100
  playbackPromise;
100
101
  resolvePlayback;
101
102
  overlay;
102
103
  progressTimer;
104
+ playbackTimer;
103
105
  async show(options) {
104
106
  const state = this.stateMachine.current();
105
107
  if (state.visible && state.presentationId)
@@ -123,7 +125,7 @@ var capacitorLottieSplash = (function (exports, core) {
123
125
  this.emitState();
124
126
  if (!state.playbackSettled)
125
127
  await this.waitForCompletion(options);
126
- this.removeOverlay(options.presentationId);
128
+ await this.removeOverlay(options.presentationId);
127
129
  }
128
130
  async getState() {
129
131
  const { generation: _generation, ...state } = this.stateMachine.current();
@@ -140,10 +142,14 @@ var capacitorLottieSplash = (function (exports, core) {
140
142
  overlay.dataset.presentationId = presentationId;
141
143
  Object.assign(overlay.style, {
142
144
  position: 'fixed', inset: '0', zIndex: '2147483647', display: 'grid', placeItems: 'center',
143
- background: this.backgroundFor(this.stateMachine.current().theme), overflow: 'hidden'
145
+ background: this.backgroundFor(this.stateMachine.current().theme), overflow: 'hidden',
146
+ opacity: '0', transition: `opacity ${COVER_FADE_MS}ms ease-in-out`
144
147
  });
145
148
  document.body.append(overlay);
146
149
  this.overlay = overlay;
150
+ // Force the initial transparent style to paint before beginning the fade.
151
+ void overlay.offsetWidth;
152
+ overlay.style.opacity = '1';
147
153
  // A generic package cannot ship HeadCount assets. The host supplies an approved local
148
154
  // composition; absent or failed assets leave the opaque branded background in place.
149
155
  if (!options.assets?.animationData && !options.assets?.animationPath) {
@@ -154,9 +160,13 @@ var capacitorLottieSplash = (function (exports, core) {
154
160
  this.settle(presentationId, 'reduced-motion', 'Reduced motion is enabled');
155
161
  return;
156
162
  }
157
- this.stateMachine.playing(presentationId);
158
- this.emitState();
159
- void this.playComposition(options, presentationId);
163
+ this.playbackTimer = window.setTimeout(() => {
164
+ if (this.stateMachine.current().presentationId !== presentationId || this.overlay !== overlay)
165
+ return;
166
+ this.stateMachine.playing(presentationId);
167
+ this.emitState();
168
+ void this.playComposition(options, presentationId);
169
+ }, COVER_FADE_MS);
160
170
  }
161
171
  async playComposition(options, presentationId) {
162
172
  try {
@@ -217,9 +227,15 @@ var capacitorLottieSplash = (function (exports, core) {
217
227
  rail.append(segment);
218
228
  this.overlay?.append(style, rail);
219
229
  }
220
- removeOverlay(presentationId) {
230
+ async removeOverlay(presentationId) {
221
231
  window.clearTimeout(this.progressTimer);
222
- this.overlay?.remove();
232
+ window.clearTimeout(this.playbackTimer);
233
+ const overlay = this.overlay;
234
+ if (overlay) {
235
+ overlay.style.opacity = '0';
236
+ await new Promise(resolve => window.setTimeout(resolve, COVER_FADE_MS));
237
+ overlay.remove();
238
+ }
223
239
  this.overlay = undefined;
224
240
  this.stateMachine.hidden(presentationId);
225
241
  this.emitState();
@@ -159,6 +159,7 @@ public final class LottieSplashCoordinator {
159
159
  /// permanently obscure that bundle during an over-the-air/native rollout mismatch.
160
160
  private let webAdoptionGracePeriod: TimeInterval = 10
161
161
  private let progressDelay: TimeInterval = 1
162
+ private let coverFadeDuration: TimeInterval = 0.5
162
163
 
163
164
  public private(set) var presentationId: String?
164
165
  public private(set) var phase: LottieSplashPhase = .hidden
@@ -176,6 +177,7 @@ public final class LottieSplashCoordinator {
176
177
  private var adoptionWorkItem: DispatchWorkItem?
177
178
  private var adoptedByWeb = false
178
179
  private var webRendererLoaded = false
180
+ private var coverFadeInComplete = false
179
181
  private var progressRemaining = TimeInterval(1)
180
182
  private var waitingClockStartedAt: Date?
181
183
  private var playbackCompletion: ((String, String?) -> Void)?
@@ -239,7 +241,7 @@ public final class LottieSplashCoordinator {
239
241
  }
240
242
 
241
243
  public func startPlaybackIfVisible(configuration: LottieSplashConfiguration = .init()) {
242
- guard phase == .preparing, animationWebView != nil else { return }
244
+ guard phase == .preparing, animationWebView != nil, coverFadeInComplete else { return }
243
245
  phase = .playing
244
246
  emitState()
245
247
  guard !UIAccessibility.isReduceMotionEnabled else {
@@ -292,6 +294,7 @@ public final class LottieSplashCoordinator {
292
294
 
293
295
  private func buildCover(in controller: UIViewController, configuration: LottieSplashConfiguration) {
294
296
  let cover = LottieSplashCoverView(theme: configuration.resolvedTheme)
297
+ cover.alpha = 0
295
298
  cover.translatesAutoresizingMaskIntoConstraints = false
296
299
  cover.isAccessibilityElement = true
297
300
  cover.accessibilityLabel = "Loading application"
@@ -330,6 +333,15 @@ public final class LottieSplashCoordinator {
330
333
  self.animationWebView = animation
331
334
  self.animationMessageBridge = messageBridge
332
335
  self.webRendererLoaded = false
336
+ self.coverFadeInComplete = false
337
+ controller.view.layoutIfNeeded()
338
+ UIView.animate(withDuration: UIAccessibility.isReduceMotionEnabled ? 0 : coverFadeDuration, delay: 0, options: [.curveEaseInOut]) {
339
+ cover.alpha = 1
340
+ } completion: { [weak self, weak cover] _ in
341
+ guard let self, let cover, self.cover === cover else { return }
342
+ self.coverFadeInComplete = true
343
+ self.startPlaybackIfVisible()
344
+ }
333
345
  }
334
346
 
335
347
  private func apply(theme: LottieSplashTheme) {
@@ -439,7 +451,7 @@ public final class LottieSplashCoordinator {
439
451
  adoptionWorkItem = nil
440
452
  loadingRail?.removeFromSuperview()
441
453
  loadingRail = nil
442
- UIView.animate(withDuration: UIAccessibility.isReduceMotionEnabled ? 0 : 0.5, animations: { cover.alpha = 0 }) { [weak self] _ in
454
+ UIView.animate(withDuration: UIAccessibility.isReduceMotionEnabled ? 0 : coverFadeDuration, animations: { cover.alpha = 0 }) { [weak self] _ in
443
455
  cover.removeFromSuperview()
444
456
  self?.cover = nil
445
457
  self?.phase = .hidden
@@ -447,6 +459,7 @@ public final class LottieSplashCoordinator {
447
459
  self?.animationWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "lottieSplash")
448
460
  self?.animationWebView = nil
449
461
  self?.animationMessageBridge = nil
462
+ self?.coverFadeInComplete = false
450
463
  self?.emitState()
451
464
  _ = id
452
465
  completion(nil)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-lottie-splash",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Native-first Lottie splash presentation for Capacitor 8",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",