react-native-smooth-clip-view 0.2.8 → 0.3.0

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.
Files changed (132) hide show
  1. package/README.md +86 -21
  2. package/android/CMakeLists.txt +7 -0
  3. package/android/build.gradle +14 -2
  4. package/android/proguard-rules.pro +3 -3
  5. package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
  6. package/android/src/main/cpp/SmoothClipBindings.cpp +896 -6
  7. package/android/src/main/cpp/SmoothClipRegistry.cpp +1067 -46
  8. package/android/src/main/cpp/SmoothClipTrace.h +25 -0
  9. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
  11. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +385 -24
  13. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
  14. package/cpp/SmoothClipAnimationCurve.h +151 -122
  15. package/cpp/SmoothClipRegistry.h +102 -4
  16. package/cpp/SmoothClipSharedGeometry.h +146 -7
  17. package/cpp/SmoothClipVelocityTracker.h +13 -9
  18. package/ios/SmoothClipGeometry.h +184 -6
  19. package/ios/SmoothClipRegistry.mm +1033 -25
  20. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  21. package/ios/SmoothClipTurboModule.cpp +857 -2
  22. package/ios/SmoothClipTurboModule.h +310 -0
  23. package/ios/SmoothClipView.mm +714 -183
  24. package/ios/SmoothClipViewRegistry.h +1 -0
  25. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  26. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  27. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  28. package/lib/module/SmoothClipView.ios.js +22 -6
  29. package/lib/module/SmoothClipView.ios.js.map +1 -1
  30. package/lib/module/SmoothClipView.js +1 -46
  31. package/lib/module/SmoothClipView.js.map +1 -1
  32. package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
  33. package/lib/module/capabilities.js +4 -0
  34. package/lib/module/capabilities.js.map +1 -0
  35. package/lib/module/capabilities.native.js +36 -0
  36. package/lib/module/capabilities.native.js.map +1 -0
  37. package/lib/module/capabilityTypes.js +11 -0
  38. package/lib/module/capabilityTypes.js.map +1 -0
  39. package/lib/module/driverState.js +0 -28
  40. package/lib/module/driverState.js.map +1 -1
  41. package/lib/module/drivers.js +1 -281
  42. package/lib/module/drivers.js.map +1 -1
  43. package/lib/module/drivers.native.js +340 -75
  44. package/lib/module/drivers.native.js.map +1 -1
  45. package/lib/module/geometry.js +147 -17
  46. package/lib/module/geometry.js.map +1 -1
  47. package/lib/module/groupDriverTypes.js +4 -0
  48. package/lib/module/groupDriverTypes.js.map +1 -0
  49. package/lib/module/groupDrivers.android.js +4 -0
  50. package/lib/module/groupDrivers.android.js.map +1 -0
  51. package/lib/module/groupDrivers.ios.js +4 -0
  52. package/lib/module/groupDrivers.ios.js.map +1 -0
  53. package/lib/module/groupDrivers.js +4 -0
  54. package/lib/module/groupDrivers.js.map +1 -0
  55. package/lib/module/groupDrivers.native.js +799 -0
  56. package/lib/module/groupDrivers.native.js.map +1 -0
  57. package/lib/module/index.js +3 -2
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/presentationProtocol.js +15 -0
  60. package/lib/module/presentationProtocol.js.map +1 -0
  61. package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
  62. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  63. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  64. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
  66. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  67. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
  68. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  69. package/lib/typescript/src/capabilities.d.ts +3 -0
  70. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  71. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  72. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  73. package/lib/typescript/src/capabilityTypes.d.ts +10 -0
  74. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  75. package/lib/typescript/src/driverState.d.ts +1 -6
  76. package/lib/typescript/src/driverState.d.ts.map +1 -1
  77. package/lib/typescript/src/driverTypes.d.ts +39 -11
  78. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  79. package/lib/typescript/src/drivers.d.ts +2 -4
  80. package/lib/typescript/src/drivers.d.ts.map +1 -1
  81. package/lib/typescript/src/drivers.native.d.ts.map +1 -1
  82. package/lib/typescript/src/geometry.d.ts +36 -2
  83. package/lib/typescript/src/geometry.d.ts.map +1 -1
  84. package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
  85. package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
  86. package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
  87. package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
  88. package/lib/typescript/src/groupDrivers.d.ts +2 -0
  89. package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
  90. package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
  91. package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
  92. package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
  93. package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
  94. package/lib/typescript/src/index.d.ts +6 -3
  95. package/lib/typescript/src/index.d.ts.map +1 -1
  96. package/lib/typescript/src/presentationProtocol.d.ts +4 -0
  97. package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
  98. package/package.json +10 -11
  99. package/src/NativeSmoothClipModule.ts +275 -0
  100. package/src/SmoothClipView.ios.tsx +30 -11
  101. package/src/SmoothClipView.tsx +2 -76
  102. package/src/SmoothClipViewNativeComponent.ts +27 -1
  103. package/src/capabilities.native.ts +69 -0
  104. package/src/capabilities.ts +2 -0
  105. package/src/capabilityTypes.ts +17 -0
  106. package/src/driverState.ts +1 -41
  107. package/src/driverTypes.ts +56 -11
  108. package/src/drivers.native.ts +921 -151
  109. package/src/drivers.ts +2 -423
  110. package/src/geometry.ts +231 -27
  111. package/src/groupDriverTypes.ts +117 -0
  112. package/src/groupDrivers.android.ts +1 -0
  113. package/src/groupDrivers.ios.ts +1 -0
  114. package/src/groupDrivers.native.ts +1189 -0
  115. package/src/groupDrivers.ts +1 -0
  116. package/src/index.ts +30 -2
  117. package/src/presentationProtocol.ts +28 -0
  118. package/lib/module/SmoothClipView.web.js +0 -54
  119. package/lib/module/SmoothClipView.web.js.map +0 -1
  120. package/lib/module/index.web.js +0 -8
  121. package/lib/module/index.web.js.map +0 -1
  122. package/lib/module/webClipPath.js +0 -25
  123. package/lib/module/webClipPath.js.map +0 -1
  124. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  125. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  126. package/lib/typescript/src/index.web.d.ts +0 -9
  127. package/lib/typescript/src/index.web.d.ts.map +0 -1
  128. package/lib/typescript/src/webClipPath.d.ts +0 -7
  129. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  130. package/src/SmoothClipView.web.tsx +0 -66
  131. package/src/index.web.ts +0 -23
  132. package/src/webClipPath.ts +0 -30
@@ -1,6 +1,7 @@
1
1
  #include "SmoothClipAndroid.h"
2
2
  #include "SmoothClipAnimationId.h"
3
3
  #include "SmoothClipAnimationCurve.h"
4
+ #include "SmoothClipTrace.h"
4
5
  #include "SmoothClipVelocityTracker.h"
5
6
 
6
7
  #include <fbjni/fbjni.h>
@@ -56,14 +57,14 @@ struct ActiveAnimation {
56
57
  Presentation current{{0, 0, 0, 0, 0}, 0, 0};
57
58
  TimingAnimation timing{};
58
59
  SpringAnimation spring{};
59
- // Owns the keyframes and their precomputed monotone-cubic tangents; the
60
- // tangent solve happens on reset(), never per frame.
60
+ // Owns the segment-linear keyframes and a cursor into the active segment.
61
61
  KeyframeCurve keyframes;
62
62
  double durationS = 0;
63
63
  double startedAtS = 0;
64
- // Integrated spring state (channels: x, y, width, height, radius, tx, ty).
65
- std::array<double, 7> springPosition{};
66
- std::array<double, 7> springVelocity{};
64
+ // Integrated spring state (the eleven continuous V2 channels; curve is
65
+ // categorical and remains the start presentation's curve while running).
66
+ Channels springPosition{};
67
+ Channels springVelocity{};
67
68
  double lastFrameS = 0;
68
69
  bool finished = true;
69
70
  // False while the animation is latched, and held out of animatingDrivers()
@@ -103,6 +104,8 @@ struct DriverState {
103
104
  // replay, hosts mounted in another subtree). The entry is erased when the
104
105
  // last view leaves and revived by a take-ownership setPresentation.
105
106
  bool destroyed = false;
107
+ // Non-zero while this driver is an immutable member of one active group.
108
+ int32_t groupId = 0;
106
109
 
107
110
  // 'inherit' velocity samples; recording/coalescing/projection live in the
108
111
  // shared cpp/SmoothClipVelocityTracker.h (behavior-paired with iOS).
@@ -114,6 +117,31 @@ struct CompletionSink {
114
117
  CompletionCallback callback;
115
118
  };
116
119
 
120
+ struct GroupCompletionSink {
121
+ const void *owner = nullptr;
122
+ GroupCompletionCallback callback;
123
+ };
124
+
125
+ struct GroupMemberAnimation {
126
+ uint64_t driverId = 0;
127
+ ActiveAnimation animation;
128
+ };
129
+
130
+ struct ActiveGroup {
131
+ uint64_t controllerId = 0;
132
+ int32_t id = 0;
133
+ AnimationKind kind = AnimationKind::Timing;
134
+ GroupSuspensionPolicy suspensionPolicy = GroupSuspensionPolicy::Pause;
135
+ TimingAnimation timing{};
136
+ SpringAnimation spring{};
137
+ double durationS = 0;
138
+ double startedAtS = 0;
139
+ double lastFrameS = 0;
140
+ bool started = false;
141
+ bool frameClockAnchored = false;
142
+ std::vector<GroupMemberAnimation> members;
143
+ };
144
+
117
145
  std::unordered_map<uint64_t, DriverState> &registry() {
118
146
  static std::unordered_map<uint64_t, DriverState> value;
119
147
  return value;
@@ -124,6 +152,21 @@ CompletionSink &completionSink() {
124
152
  return sink;
125
153
  }
126
154
 
155
+ GroupCompletionSink &groupCompletionSink() {
156
+ static GroupCompletionSink sink;
157
+ return sink;
158
+ }
159
+
160
+ std::unordered_map<int32_t, ActiveGroup> &groupRegistry() {
161
+ static std::unordered_map<int32_t, ActiveGroup> value;
162
+ return value;
163
+ }
164
+
165
+ std::vector<int32_t> &animatingGroups() {
166
+ static std::vector<int32_t> value;
167
+ return value;
168
+ }
169
+
127
170
  // The sink is written from the JS thread (installBindings / invalidate) and
128
171
  // invoked on main. Invoking under the lock also keeps the owning bindings
129
172
  // state alive for the duration of the callback.
@@ -241,7 +284,7 @@ bool advanceSpring(ActiveAnimation &animation, double now) {
241
284
  while (dt > 0) {
242
285
  const double step = std::min(dt, kMaxStepS);
243
286
  dt -= step;
244
- for (int index = 0; index < 7; index += 1) {
287
+ for (std::size_t index = 0; index < kChannelCount; index += 1) {
245
288
  const double displacement =
246
289
  animation.springPosition[index] - target[index];
247
290
  const double acceleration =
@@ -253,7 +296,7 @@ bool advanceSpring(ActiveAnimation &animation, double now) {
253
296
  }
254
297
  }
255
298
  bool settled = true;
256
- for (int index = 0; index < 7; index += 1) {
299
+ for (std::size_t index = 0; index < kChannelCount; index += 1) {
257
300
  if (!std::isfinite(animation.springPosition[index]) ||
258
301
  !std::isfinite(animation.springVelocity[index])) {
259
302
  // A divergent configuration must never freeze the clip: end at target.
@@ -268,7 +311,8 @@ bool advanceSpring(ActiveAnimation &animation, double now) {
268
311
  settled = false;
269
312
  }
270
313
  }
271
- animation.current = fromChannels(animation.springPosition);
314
+ animation.current =
315
+ fromChannels(animation.springPosition, animation.start.clip.curve);
272
316
  return settled || now - animation.startedAtS >= kSpringMaxDurationS;
273
317
  }
274
318
 
@@ -295,32 +339,117 @@ void emitCompletion(uint64_t driverId, int32_t animationId, bool finished) {
295
339
  }
296
340
  }
297
341
 
342
+ void emitGroupCompletion(const ActiveGroup &group, bool finished) {
343
+ std::vector<uint64_t> driverIds;
344
+ driverIds.reserve(group.members.size());
345
+ for (const GroupMemberAnimation &member : group.members) {
346
+ driverIds.push_back(member.driverId);
347
+ }
348
+ std::lock_guard<std::mutex> lock(completionSinkMutex());
349
+ if (groupCompletionSink().callback) {
350
+ groupCompletionSink().callback(
351
+ group.controllerId, group.id, finished, std::move(driverIds));
352
+ }
353
+ }
354
+
355
+ const GroupMemberAnimation *findGroupMember(
356
+ int32_t groupId,
357
+ uint64_t driverId) {
358
+ const auto groupIterator = groupRegistry().find(groupId);
359
+ if (groupIterator == groupRegistry().end()) return nullptr;
360
+ for (const GroupMemberAnimation &member : groupIterator->second.members) {
361
+ if (member.driverId == driverId) return &member;
362
+ }
363
+ return nullptr;
364
+ }
365
+
366
+ Presentation visiblePresentation(
367
+ uint64_t driverId,
368
+ const DriverState &state) {
369
+ if (state.groupId != 0) {
370
+ const GroupMemberAnimation *member =
371
+ findGroupMember(state.groupId, driverId);
372
+ if (member != nullptr) return member->animation.current;
373
+ }
374
+ return state.animation.has_value() ? state.animation->current : state.latest;
375
+ }
376
+
377
+ bool needsV2Delivery(const Presentation &presentation) {
378
+ const double topLeft = SmoothClipResolvedRadius(
379
+ presentation.clip.topLeftRadius, presentation.clip.radius);
380
+ const double topRight = SmoothClipResolvedRadius(
381
+ presentation.clip.topRightRadius, presentation.clip.radius);
382
+ const double bottomRight = SmoothClipResolvedRadius(
383
+ presentation.clip.bottomRightRadius, presentation.clip.radius);
384
+ const double bottomLeft = SmoothClipResolvedRadius(
385
+ presentation.clip.bottomLeftRadius, presentation.clip.radius);
386
+ return presentation.clip.curve != ClipCurve::Circular ||
387
+ presentation.contentScale != 1.0 ||
388
+ topLeft != presentation.clip.radius ||
389
+ topRight != presentation.clip.radius ||
390
+ bottomRight != presentation.clip.radius ||
391
+ bottomLeft != presentation.clip.radius;
392
+ }
393
+
298
394
  // Terminal per-frame delivery: scale DIP -> px and normalize against the
299
395
  // pushed host metrics in C++, so the JNI call carries final pixel floats and
300
396
  // the Kotlin side reduces to field stores + invalidateOutline().
301
397
  void deliverToView(const ViewEntry &entry, const Presentation &presentation) {
398
+ const bool deliverV2 = needsV2Delivery(presentation);
302
399
  if (entry.density <= 0) {
303
- entry.view->applyClip(presentation);
400
+ if (deliverV2) {
401
+ entry.view->applyClipV2(presentation);
402
+ } else {
403
+ entry.view->applyClip(presentation);
404
+ }
304
405
  return;
305
406
  }
306
407
  const double density = entry.density;
307
408
  NormalizedClip clip;
308
- if (!SmoothClipNormalize(
309
- presentation.clip.x * density,
310
- presentation.clip.y * density,
311
- presentation.clip.width * density,
312
- presentation.clip.height * density,
313
- presentation.clip.radius * density,
314
- entry.hostWidthPx,
315
- entry.hostHeightPx,
316
- clip)) {
317
- return;
409
+ if (deliverV2) {
410
+ if (!SmoothClipNormalizeV2(
411
+ presentation.clip.x * density,
412
+ presentation.clip.y * density,
413
+ presentation.clip.width * density,
414
+ presentation.clip.height * density,
415
+ presentation.clip.radius * density,
416
+ presentation.clip.topLeftRadius * density,
417
+ presentation.clip.topRightRadius * density,
418
+ presentation.clip.bottomRightRadius * density,
419
+ presentation.clip.bottomLeftRadius * density,
420
+ presentation.clip.curve,
421
+ entry.hostWidthPx,
422
+ entry.hostHeightPx,
423
+ clip)) {
424
+ return;
425
+ }
426
+ } else {
427
+ if (!SmoothClipNormalize(
428
+ presentation.clip.x * density,
429
+ presentation.clip.y * density,
430
+ presentation.clip.width * density,
431
+ presentation.clip.height * density,
432
+ presentation.clip.radius * density,
433
+ entry.hostWidthPx,
434
+ entry.hostHeightPx,
435
+ clip)) {
436
+ return;
437
+ }
318
438
  }
319
439
  const double translateX = presentation.contentTranslateX * density;
320
440
  const double translateY = presentation.contentTranslateY * density;
321
441
  // Atomic reject, mirroring the Kotlin DIP path's all-or-nothing gate.
322
- if (!std::isfinite(translateX) || !std::isfinite(translateY)) return;
323
- entry.view->applyClipPx(clip, translateX, translateY);
442
+ if (!std::isfinite(translateX) || !std::isfinite(translateY) ||
443
+ !std::isfinite(presentation.contentScale) ||
444
+ presentation.contentScale <= 0) {
445
+ return;
446
+ }
447
+ if (deliverV2) {
448
+ entry.view->applyClipV2Px(
449
+ clip, translateX, translateY, presentation.contentScale);
450
+ } else {
451
+ entry.view->applyClipPx(clip, translateX, translateY);
452
+ }
324
453
  }
325
454
 
326
455
  void applyToViews(DriverState &state, const Presentation &presentation) {
@@ -368,8 +497,10 @@ Presentation prepareAnimation(
368
497
  Presentation target) {
369
498
  const Presentation visibleBefore =
370
499
  state.animation.has_value() ? state.animation->current : state.latest;
371
- const bool acceptsInteractiveStart =
372
- start.hasInteractiveStart && state.ownership == Ownership::Interactive;
500
+ // `from` is an explicit part of the replacement transaction and therefore
501
+ // overrides the sampled native frame even when the previous owner was a
502
+ // native animation/group.
503
+ const bool acceptsInteractiveStart = start.hasInteractiveStart;
373
504
  finishActive(driverId, state, false);
374
505
  const Presentation resolvedStart =
375
506
  acceptsInteractiveStart ? start.interactiveStart : visibleBefore;
@@ -391,6 +522,77 @@ bool entryDisplayable(const ViewEntry &entry) {
391
522
  entry.lifecycleVisible;
392
523
  }
393
524
 
525
+ Geometry geometryInPixels(
526
+ const Geometry &geometry,
527
+ double density) {
528
+ Geometry result = geometry;
529
+ result.x *= density;
530
+ result.y *= density;
531
+ result.width *= density;
532
+ result.height *= density;
533
+ result.radius *= density;
534
+ result.topLeftRadius *= density;
535
+ result.topRightRadius *= density;
536
+ result.bottomRightRadius *= density;
537
+ result.bottomLeftRadius *= density;
538
+ return result;
539
+ }
540
+
541
+ bool presentationFitsEveryKnownHostWithoutNormalization(
542
+ const DriverState &state,
543
+ const Presentation &presentation) {
544
+ for (const ViewEntry &entry : state.views) {
545
+ // Zero host metrics are still waiting on Fabric layout and remain covered
546
+ // by the existing readiness latch. Once metrics exist, density is required
547
+ // to compare the DIP presentation against the exact pixel host bounds.
548
+ if (entry.hostWidthPx <= 0 || entry.hostHeightPx <= 0) continue;
549
+ if (entry.density <= 0 ||
550
+ !SmoothClipGeometryNormalizationIsIdentity(
551
+ geometryInPixels(presentation.clip, entry.density),
552
+ entry.hostWidthPx,
553
+ entry.hostHeightPx)) {
554
+ return false;
555
+ }
556
+ }
557
+ return true;
558
+ }
559
+
560
+ bool timingPlanPreservesLinearHostNormalization(
561
+ const DriverState &state,
562
+ const Presentation &start,
563
+ const Presentation &target,
564
+ const TimingAnimation &animation) {
565
+ const double minimumProgress = std::min(
566
+ {0.0, 1.0, animation.controlPoint1Y, animation.controlPoint2Y});
567
+ const double maximumProgress = std::max(
568
+ {0.0, 1.0, animation.controlPoint1Y, animation.controlPoint2Y});
569
+ const Presentation minimum =
570
+ interpolate(start, target, minimumProgress);
571
+ const Presentation maximum =
572
+ interpolate(start, target, maximumProgress);
573
+ return isFiniteProtocolV2Presentation(minimum) &&
574
+ isFiniteProtocolV2Presentation(maximum) &&
575
+ presentationFitsEveryKnownHostWithoutNormalization(state, minimum) &&
576
+ presentationFitsEveryKnownHostWithoutNormalization(state, maximum);
577
+ }
578
+
579
+ bool keyframePlanPreservesLinearHostNormalization(
580
+ const DriverState &state,
581
+ const Presentation &resolvedStart,
582
+ const std::vector<Keyframe> &keyframes) {
583
+ if (!presentationFitsEveryKnownHostWithoutNormalization(
584
+ state, resolvedStart)) {
585
+ return false;
586
+ }
587
+ for (std::size_t index = 1; index < keyframes.size(); index += 1) {
588
+ if (!presentationFitsEveryKnownHostWithoutNormalization(
589
+ state, keyframes[index].presentation)) {
590
+ return false;
591
+ }
592
+ }
593
+ return true;
594
+ }
595
+
394
596
  bool anyDisplayableView(const DriverState &state) {
395
597
  for (const ViewEntry &entry : state.views) {
396
598
  if (entryDisplayable(entry)) return true;
@@ -398,6 +600,141 @@ bool anyDisplayableView(const DriverState &state) {
398
600
  return false;
399
601
  }
400
602
 
603
+ bool allGroupMembersReady(const ActiveGroup &group) {
604
+ for (const GroupMemberAnimation &member : group.members) {
605
+ const auto iterator = registry().find(member.driverId);
606
+ if (iterator == registry().end() || iterator->second.destroyed ||
607
+ !anyDisplayableView(iterator->second)) {
608
+ return false;
609
+ }
610
+ }
611
+ return true;
612
+ }
613
+
614
+ void removeAnimatingGroup(int32_t groupId) {
615
+ auto &active = animatingGroups();
616
+ active.erase(std::remove(active.begin(), active.end(), groupId), active.end());
617
+ }
618
+
619
+ void applyGroupCurrent(const ActiveGroup &group) {
620
+ for (const GroupMemberAnimation &member : group.members) {
621
+ const auto iterator = registry().find(member.driverId);
622
+ if (iterator != registry().end()) {
623
+ applyToViews(iterator->second, member.animation.current);
624
+ }
625
+ }
626
+ }
627
+
628
+ std::vector<DriverSnapshot> finishGroupImpl(
629
+ int32_t groupId,
630
+ bool useTarget,
631
+ bool finished) {
632
+ auto groupIterator = groupRegistry().find(groupId);
633
+ if (groupIterator == groupRegistry().end()) return {};
634
+ ActiveGroup group = std::move(groupIterator->second);
635
+ groupRegistry().erase(groupIterator);
636
+ removeAnimatingGroup(groupId);
637
+
638
+ std::vector<DriverSnapshot> snapshots;
639
+ snapshots.reserve(group.members.size());
640
+ for (GroupMemberAnimation &member : group.members) {
641
+ auto stateIterator = registry().find(member.driverId);
642
+ if (stateIterator == registry().end()) {
643
+ snapshots.push_back({member.driverId, unavailablePresentation(), false});
644
+ continue;
645
+ }
646
+ DriverState &state = stateIterator->second;
647
+ const Presentation result =
648
+ useTarget ? member.animation.target : member.animation.current;
649
+ state.groupId = 0;
650
+ state.latest = result;
651
+ state.hasLatest = true;
652
+ state.ownership = Ownership::Interactive;
653
+ applyToViews(state, result);
654
+ snapshots.push_back(
655
+ {member.driverId, result, !state.destroyed && anyDisplayableView(state)});
656
+ }
657
+ emitGroupCompletion(group, finished);
658
+ return snapshots;
659
+ }
660
+
661
+ void startLatchedGroup(ActiveGroup &group) {
662
+ const double now = nowSeconds();
663
+ group.started = true;
664
+ group.startedAtS = now;
665
+ group.lastFrameS = now;
666
+ group.frameClockAnchored = false;
667
+ for (GroupMemberAnimation &member : group.members) {
668
+ member.animation.startedAtS = now;
669
+ member.animation.lastFrameS = now;
670
+ member.animation.frameClockAnchored = false;
671
+ }
672
+ auto &active = animatingGroups();
673
+ if (std::find(active.begin(), active.end(), group.id) == active.end()) {
674
+ active.push_back(group.id);
675
+ }
676
+ applyGroupCurrent(group);
677
+ scheduleFrame();
678
+ }
679
+
680
+ void pauseGroup(ActiveGroup &group) {
681
+ if (group.kind == AnimationKind::Timing) {
682
+ const double rawProgress = timingFraction(
683
+ group.lastFrameS, group.startedAtS, group.durationS);
684
+ const TimingRemainder remainder = timingRemainder(group.timing, rawProgress);
685
+ group.timing = remainder.animation;
686
+ group.durationS = remainder.animation.durationMs / 1000.0;
687
+ for (GroupMemberAnimation &member : group.members) {
688
+ member.animation.start = member.animation.current;
689
+ member.animation.timing = remainder.animation;
690
+ }
691
+ } else if (group.kind == AnimationKind::Keyframes) {
692
+ double remainingDurationMs = 0;
693
+ for (GroupMemberAnimation &member : group.members) {
694
+ const KeyframeContinuation continuation = keyframeContinuationAtFrame(
695
+ member.animation.keyframes.frames(),
696
+ member.animation.current,
697
+ member.animation.target,
698
+ group.lastFrameS,
699
+ group.startedAtS,
700
+ group.durationS);
701
+ member.animation.start = continuation.start;
702
+ member.animation.current = continuation.start;
703
+ member.animation.keyframes.reset(continuation.frames);
704
+ remainingDurationMs = continuation.durationMs;
705
+ }
706
+ group.durationS = remainingDurationMs / 1000.0;
707
+ }
708
+ group.started = false;
709
+ group.frameClockAnchored = false;
710
+ removeAnimatingGroup(group.id);
711
+ }
712
+
713
+ void reconcileGroupReadiness(uint64_t driverId) {
714
+ const auto stateIterator = registry().find(driverId);
715
+ if (stateIterator == registry().end() || stateIterator->second.groupId == 0) {
716
+ return;
717
+ }
718
+ const int32_t groupId = stateIterator->second.groupId;
719
+ auto groupIterator = groupRegistry().find(groupId);
720
+ if (groupIterator == groupRegistry().end()) {
721
+ stateIterator->second.groupId = 0;
722
+ return;
723
+ }
724
+ ActiveGroup &group = groupIterator->second;
725
+ const bool ready = allGroupMembersReady(group);
726
+ if (!group.started) {
727
+ if (ready) startLatchedGroup(group);
728
+ return;
729
+ }
730
+ if (ready) return;
731
+ if (group.suspensionPolicy == GroupSuspensionPolicy::Finish) {
732
+ finishGroupImpl(groupId, true, true);
733
+ } else {
734
+ pauseGroup(group);
735
+ }
736
+ }
737
+
401
738
  // Starts a latched animation: rebases the clock so no progress was burned
402
739
  // while no view could display, then joins the choreographer loop.
403
740
  void startLatchedAnimation(uint64_t driverId, DriverState &state) {
@@ -614,7 +951,56 @@ void advance(uint64_t driverId, DriverState &state, double now) {
614
951
  }
615
952
  }
616
953
 
954
+ void advanceGroup(int32_t groupId, ActiveGroup &group, double now) {
955
+ if (!group.frameClockAnchored) {
956
+ group.startedAtS = anchorStartTime(group.startedAtS, now);
957
+ group.lastFrameS = group.startedAtS;
958
+ group.frameClockAnchored = true;
959
+ for (GroupMemberAnimation &member : group.members) {
960
+ member.animation.startedAtS = group.startedAtS;
961
+ member.animation.lastFrameS = group.startedAtS;
962
+ member.animation.frameClockAnchored = true;
963
+ }
964
+ }
965
+
966
+ bool done = false;
967
+ if (group.kind == AnimationKind::Spring) {
968
+ done = true;
969
+ for (GroupMemberAnimation &member : group.members) {
970
+ if (!advanceSpring(member.animation, now)) done = false;
971
+ }
972
+ } else {
973
+ group.lastFrameS = now;
974
+ const double fraction =
975
+ timingFraction(now, group.startedAtS, group.durationS);
976
+ for (GroupMemberAnimation &member : group.members) {
977
+ member.animation.lastFrameS = now;
978
+ if (group.kind == AnimationKind::Keyframes) {
979
+ member.animation.current =
980
+ member.animation.keyframes.evaluate(fraction);
981
+ } else {
982
+ const double eased = cubicBezier(
983
+ group.timing.controlPoint1X,
984
+ group.timing.controlPoint1Y,
985
+ group.timing.controlPoint2X,
986
+ group.timing.controlPoint2Y,
987
+ fraction);
988
+ member.animation.current = interpolate(
989
+ member.animation.start, member.animation.target, eased);
990
+ }
991
+ }
992
+ done = fraction >= 1.0;
993
+ }
994
+
995
+ if (done) {
996
+ finishGroupImpl(groupId, true, true);
997
+ } else {
998
+ applyGroupCurrent(group);
999
+ }
1000
+ }
1001
+
617
1002
  void onFrameImpl(double now) {
1003
+ SMOOTH_CLIP_TRACE("SmoothClip.frame");
618
1004
  gFrameScheduled = false;
619
1005
  // Snapshot: advance() clears live-list entries on completion. advance()
620
1006
  // never erases registry entries and never starts animations (completion
@@ -632,7 +1018,14 @@ void onFrameImpl(double now) {
632
1018
  }
633
1019
  advance(driverId, iterator->second, now);
634
1020
  }
635
- if (!animatingDrivers().empty()) scheduleFrame();
1021
+ static std::vector<int32_t> groupScratch;
1022
+ groupScratch = animatingGroups();
1023
+ for (const int32_t groupId : groupScratch) {
1024
+ auto iterator = groupRegistry().find(groupId);
1025
+ if (iterator == groupRegistry().end() || !iterator->second.started) continue;
1026
+ advanceGroup(groupId, iterator->second, now);
1027
+ }
1028
+ if (!animatingDrivers().empty() || !animatingGroups().empty()) scheduleFrame();
636
1029
  }
637
1030
 
638
1031
  } // namespace
@@ -671,12 +1064,415 @@ void clearCompletionCallback(const void *owner) {
671
1064
  completionSink().callback = nullptr;
672
1065
  }
673
1066
 
1067
+ void setGroupCompletionCallback(
1068
+ const void *owner,
1069
+ GroupCompletionCallback callback) {
1070
+ std::lock_guard<std::mutex> lock(completionSinkMutex());
1071
+ groupCompletionSink().owner = owner;
1072
+ groupCompletionSink().callback = std::move(callback);
1073
+ }
1074
+
1075
+ void clearGroupCompletionCallback(const void *owner) {
1076
+ std::lock_guard<std::mutex> lock(completionSinkMutex());
1077
+ if (groupCompletionSink().owner != owner) return;
1078
+ groupCompletionSink().owner = nullptr;
1079
+ groupCompletionSink().callback = nullptr;
1080
+ }
1081
+
1082
+ Presentation snapshotCurrent(uint64_t driverId) {
1083
+ if (!isOnMainThread()) return unavailablePresentation();
1084
+ const auto iterator = registry().find(driverId);
1085
+ if (iterator == registry().end() || iterator->second.destroyed ||
1086
+ !iterator->second.hasLatest) {
1087
+ return unavailablePresentation();
1088
+ }
1089
+ return visiblePresentation(driverId, iterator->second);
1090
+ }
1091
+
1092
+ Presentation snapshotCurrentAndroid(uint64_t driverId) {
1093
+ return snapshotCurrent(driverId);
1094
+ }
1095
+
1096
+ std::vector<DriverSnapshot> snapshotGroup(
1097
+ const std::vector<uint64_t> &driverIds) {
1098
+ std::vector<DriverSnapshot> snapshots;
1099
+ snapshots.reserve(driverIds.size());
1100
+ if (!isOnMainThread()) return snapshots;
1101
+ for (const uint64_t driverId : driverIds) {
1102
+ const auto iterator = registry().find(driverId);
1103
+ if (iterator == registry().end() || iterator->second.destroyed ||
1104
+ !iterator->second.hasLatest) {
1105
+ snapshots.push_back({driverId, unavailablePresentation(), false});
1106
+ continue;
1107
+ }
1108
+ const DriverState &state = iterator->second;
1109
+ snapshots.push_back(
1110
+ {driverId, visiblePresentation(driverId, state),
1111
+ anyDisplayableView(state)});
1112
+ }
1113
+ return snapshots;
1114
+ }
1115
+
1116
+ std::vector<DriverSnapshot> beginGroupInteraction(
1117
+ const std::vector<uint64_t> &driverIds) {
1118
+ if (!isOnMainThread()) return {};
1119
+ std::vector<uint64_t> unique;
1120
+ unique.reserve(driverIds.size());
1121
+ for (const uint64_t driverId : driverIds) {
1122
+ if (driverId == 0 ||
1123
+ std::find(unique.begin(), unique.end(), driverId) != unique.end()) {
1124
+ return {};
1125
+ }
1126
+ const auto iterator = registry().find(driverId);
1127
+ if (iterator == registry().end() || iterator->second.destroyed ||
1128
+ !iterator->second.hasLatest) {
1129
+ return snapshotGroup(driverIds);
1130
+ }
1131
+ unique.push_back(driverId);
1132
+ }
1133
+
1134
+ std::vector<int32_t> groupsToCancel;
1135
+ for (const uint64_t driverId : driverIds) {
1136
+ const int32_t groupId = registry().at(driverId).groupId;
1137
+ if (groupId != 0 &&
1138
+ std::find(groupsToCancel.begin(), groupsToCancel.end(), groupId) ==
1139
+ groupsToCancel.end()) {
1140
+ groupsToCancel.push_back(groupId);
1141
+ }
1142
+ }
1143
+ for (const int32_t groupId : groupsToCancel) {
1144
+ finishGroupImpl(groupId, false, false);
1145
+ }
1146
+ for (const uint64_t driverId : driverIds) {
1147
+ DriverState &state = registry().at(driverId);
1148
+ if (state.animation.has_value()) {
1149
+ const int32_t animationId = state.animation->id;
1150
+ const Presentation current = state.animation->current;
1151
+ clearActiveAnimation(driverId, state);
1152
+ state.latest = current;
1153
+ state.hasLatest = true;
1154
+ applyToViews(state, current);
1155
+ emitCompletion(driverId, animationId, false);
1156
+ }
1157
+ state.ownership = Ownership::Interactive;
1158
+ }
1159
+ return snapshotGroup(driverIds);
1160
+ }
1161
+
1162
+ bool setPresentationBatch(const std::vector<BatchEntry> &entries) {
1163
+ if (!isOnMainThread() || entries.empty()) return false;
1164
+ std::vector<uint64_t> unique;
1165
+ std::vector<int32_t> groupsToCancel;
1166
+ unique.reserve(entries.size());
1167
+ for (const BatchEntry &entry : entries) {
1168
+ if (entry.driverId == 0 ||
1169
+ std::find(unique.begin(), unique.end(), entry.driverId) != unique.end()) {
1170
+ return false;
1171
+ }
1172
+ const auto iterator = registry().find(entry.driverId);
1173
+ if (iterator == registry().end() || iterator->second.destroyed ||
1174
+ !isFiniteProtocolV2Presentation(entry.presentation)) {
1175
+ return false;
1176
+ }
1177
+ if (iterator != registry().end() && iterator->second.groupId != 0 &&
1178
+ std::find(
1179
+ groupsToCancel.begin(),
1180
+ groupsToCancel.end(),
1181
+ iterator->second.groupId) == groupsToCancel.end()) {
1182
+ groupsToCancel.push_back(iterator->second.groupId);
1183
+ }
1184
+ unique.push_back(entry.driverId);
1185
+ }
1186
+
1187
+ // Replacement is all-or-nothing: validation above is deliberately complete
1188
+ // before any old group is dissolved. An overlap dissolves the whole immutable
1189
+ // old group (including members absent from this batch) exactly once.
1190
+ for (const int32_t groupId : groupsToCancel) {
1191
+ finishGroupImpl(groupId, false, false);
1192
+ }
1193
+
1194
+ const double now = nowSeconds();
1195
+ for (const BatchEntry &entry : entries) {
1196
+ DriverState &state = registry().at(entry.driverId);
1197
+ state.destroyed = false;
1198
+ finishActive(entry.driverId, state, false);
1199
+ state.latest = entry.presentation;
1200
+ state.hasLatest = true;
1201
+ state.ownership = Ownership::Interactive;
1202
+ recordVelocitySample(state.samples, toChannels(entry.presentation), now);
1203
+ }
1204
+ for (const BatchEntry &entry : entries) {
1205
+ applyToViews(registry().at(entry.driverId), entry.presentation);
1206
+ }
1207
+ return true;
1208
+ }
1209
+
1210
+ namespace {
1211
+
1212
+ int32_t startGroupCommon(
1213
+ uint64_t controllerId,
1214
+ std::vector<GroupMotionEntry> entries,
1215
+ AnimationKind kind,
1216
+ TimingAnimation timing,
1217
+ SpringAnimation spring,
1218
+ double durationMs,
1219
+ int32_t reduceMotion,
1220
+ GroupSuspensionPolicy suspensionPolicy,
1221
+ double startedAtHintS) {
1222
+ if (!isOnMainThread() || controllerId == 0 || entries.empty() ||
1223
+ (suspensionPolicy != GroupSuspensionPolicy::Pause &&
1224
+ suspensionPolicy != GroupSuspensionPolicy::Finish)) {
1225
+ return 0;
1226
+ }
1227
+ if (kind == AnimationKind::Timing &&
1228
+ !isValidProtocolV2Timing(timing)) {
1229
+ return 0;
1230
+ }
1231
+ if (kind == AnimationKind::Spring &&
1232
+ !isValidProtocolV2Spring(spring)) {
1233
+ return 0;
1234
+ }
1235
+ if (kind == AnimationKind::Keyframes &&
1236
+ (!std::isfinite(durationMs) || durationMs < 0 ||
1237
+ !isValidReduceMotionCode(reduceMotion))) {
1238
+ return 0;
1239
+ }
1240
+ const bool reduce = shouldReduceMotion(reduceMotion) ||
1241
+ (kind != AnimationKind::Spring && durationMs <= 0);
1242
+ std::vector<uint64_t> unique;
1243
+ std::vector<int32_t> groupsToCancel;
1244
+ std::vector<Presentation> resolvedStarts;
1245
+ unique.reserve(entries.size());
1246
+ resolvedStarts.reserve(entries.size());
1247
+ for (const GroupMotionEntry &entry : entries) {
1248
+ if (entry.driverId == 0 ||
1249
+ std::find(unique.begin(), unique.end(), entry.driverId) != unique.end()) {
1250
+ return 0;
1251
+ }
1252
+ const auto iterator = registry().find(entry.driverId);
1253
+ if ((iterator == registry().end() || iterator->second.destroyed ||
1254
+ !iterator->second.hasLatest) && !entry.hasFrom) {
1255
+ return 0;
1256
+ }
1257
+ if (iterator != registry().end() && iterator->second.groupId != 0 &&
1258
+ std::find(
1259
+ groupsToCancel.begin(),
1260
+ groupsToCancel.end(),
1261
+ iterator->second.groupId) == groupsToCancel.end()) {
1262
+ groupsToCancel.push_back(iterator->second.groupId);
1263
+ }
1264
+ const Presentation resolvedStart = entry.hasFrom
1265
+ ? entry.from
1266
+ : visiblePresentation(entry.driverId, iterator->second);
1267
+ if (!isFiniteProtocolV2Presentation(resolvedStart) ||
1268
+ !isFiniteProtocolV2Presentation(entry.target) ||
1269
+ resolvedStart.clip.curve != entry.target.clip.curve) {
1270
+ return 0;
1271
+ }
1272
+ if (kind == AnimationKind::Spring &&
1273
+ !protocolV2SpringScaleIsProvablyPositive(
1274
+ resolvedStart, entry.target, spring)) {
1275
+ return 0;
1276
+ }
1277
+ if (kind == AnimationKind::Keyframes) {
1278
+ if (!isValidProtocolV2Keyframes(
1279
+ entry.keyframes,
1280
+ resolvedStart,
1281
+ entry.target,
1282
+ entry.hasFrom)) {
1283
+ return 0;
1284
+ }
1285
+ } else if (!entry.keyframes.empty()) {
1286
+ return 0;
1287
+ }
1288
+ if (!reduce && iterator != registry().end()) {
1289
+ if (kind == AnimationKind::Timing &&
1290
+ !timingPlanPreservesLinearHostNormalization(
1291
+ iterator->second, resolvedStart, entry.target, timing)) {
1292
+ return 0;
1293
+ }
1294
+ if (kind == AnimationKind::Keyframes &&
1295
+ !keyframePlanPreservesLinearHostNormalization(
1296
+ iterator->second, resolvedStart, entry.keyframes)) {
1297
+ return 0;
1298
+ }
1299
+ }
1300
+ resolvedStarts.push_back(resolvedStart);
1301
+ unique.push_back(entry.driverId);
1302
+ }
1303
+
1304
+ // Capture every implicit start above, while overlapped groups still own
1305
+ // their current frames. Only after full preflight may replacement dissolve
1306
+ // those immutable groups and emit one unfinished completion for each.
1307
+ for (const int32_t groupId : groupsToCancel) {
1308
+ finishGroupImpl(groupId, false, false);
1309
+ }
1310
+
1311
+ ActiveGroup group;
1312
+ group.controllerId = controllerId;
1313
+ group.id = allocateAnimationId();
1314
+ group.kind = kind;
1315
+ group.suspensionPolicy = suspensionPolicy;
1316
+ group.timing = timing;
1317
+ group.spring = spring;
1318
+ group.durationS = durationMs / 1000.0;
1319
+ group.members.reserve(entries.size());
1320
+ const double wallNow = nowSeconds();
1321
+
1322
+ for (std::size_t entryIndex = 0; entryIndex < entries.size(); entryIndex += 1) {
1323
+ GroupMotionEntry &entry = entries[entryIndex];
1324
+ DriverState &state = registry()[entry.driverId];
1325
+ const Presentation resolvedStart = resolvedStarts[entryIndex];
1326
+ finishActive(entry.driverId, state, false);
1327
+ state.destroyed = false;
1328
+ state.latest = entry.target;
1329
+ state.hasLatest = true;
1330
+ state.ownership = Ownership::Native;
1331
+ state.groupId = group.id;
1332
+
1333
+ GroupMemberAnimation member;
1334
+ member.driverId = entry.driverId;
1335
+ member.animation.id = group.id;
1336
+ member.animation.kind = kind;
1337
+ member.animation.start = resolvedStart;
1338
+ member.animation.target = entry.target;
1339
+ member.animation.current = resolvedStart;
1340
+ member.animation.timing = timing;
1341
+ member.animation.spring = spring;
1342
+ member.animation.durationS = group.durationS;
1343
+ if (kind == AnimationKind::Keyframes) {
1344
+ if (!entry.hasFrom) {
1345
+ entry.keyframes.front().presentation = resolvedStart;
1346
+ }
1347
+ member.animation.keyframes.reset(std::move(entry.keyframes));
1348
+ } else if (kind == AnimationKind::Spring) {
1349
+ const double velocity = spring.inheritVelocity
1350
+ ? inheritedVelocity(state.samples, toChannels(entry.target), wallNow)
1351
+ : spring.initialVelocity;
1352
+ member.animation.spring.initialVelocity = velocity;
1353
+ member.animation.spring.inheritVelocity = false;
1354
+ const Channels startChannels = toChannels(resolvedStart);
1355
+ const Channels targetChannels = toChannels(entry.target);
1356
+ for (std::size_t index = 0; index < kChannelCount; index += 1) {
1357
+ member.animation.springPosition[index] = startChannels[index];
1358
+ member.animation.springVelocity[index] =
1359
+ velocity * (targetChannels[index] - startChannels[index]);
1360
+ }
1361
+ }
1362
+ group.members.push_back(std::move(member));
1363
+ }
1364
+
1365
+ // Apply every explicit start after the whole participant set has passed
1366
+ // preflight and acquired the new immutable group id. This remains one
1367
+ // synchronous main-thread transaction even when the initial readiness
1368
+ // barrier is latched; ready hosts must not keep displaying their old frame
1369
+ // while snapshots already report `from`.
1370
+ for (std::size_t entryIndex = 0; entryIndex < entries.size(); entryIndex += 1) {
1371
+ if (!entries[entryIndex].hasFrom) continue;
1372
+ applyToViews(
1373
+ registry().at(entries[entryIndex].driverId),
1374
+ resolvedStarts[entryIndex]);
1375
+ }
1376
+
1377
+ const int32_t groupId = group.id;
1378
+ const StartStamp stamp = resolveStartStamp(startedAtHintS, wallNow);
1379
+ group.startedAtS = stamp.startedAtS;
1380
+ group.lastFrameS = stamp.startedAtS;
1381
+ group.frameClockAnchored = stamp.frameClockAnchored;
1382
+ for (GroupMemberAnimation &member : group.members) {
1383
+ member.animation.startedAtS = stamp.startedAtS;
1384
+ member.animation.lastFrameS = stamp.startedAtS;
1385
+ member.animation.frameClockAnchored = stamp.frameClockAnchored;
1386
+ }
1387
+ group.started = allGroupMembersReady(group);
1388
+ groupRegistry().emplace(groupId, std::move(group));
1389
+ if (reduce) {
1390
+ finishGroupImpl(groupId, true, true);
1391
+ return groupId;
1392
+ }
1393
+ ActiveGroup &stored = groupRegistry().at(groupId);
1394
+ if (stored.started) {
1395
+ animatingGroups().push_back(groupId);
1396
+ applyGroupCurrent(stored);
1397
+ scheduleFrame();
1398
+ }
1399
+ return groupId;
1400
+ }
1401
+
1402
+ } // namespace
1403
+
1404
+ int32_t animateTimingGroup(
1405
+ uint64_t controllerId,
1406
+ std::vector<GroupMotionEntry> entries,
1407
+ TimingAnimation animation,
1408
+ GroupSuspensionPolicy suspensionPolicy,
1409
+ double startedAtHintS) {
1410
+ return startGroupCommon(
1411
+ controllerId,
1412
+ std::move(entries),
1413
+ AnimationKind::Timing,
1414
+ animation,
1415
+ {},
1416
+ animation.durationMs,
1417
+ animation.reduceMotion,
1418
+ suspensionPolicy,
1419
+ startedAtHintS);
1420
+ }
1421
+
1422
+ int32_t animateSpringGroup(
1423
+ uint64_t controllerId,
1424
+ std::vector<GroupMotionEntry> entries,
1425
+ SpringAnimation animation,
1426
+ GroupSuspensionPolicy suspensionPolicy,
1427
+ double startedAtHintS) {
1428
+ return startGroupCommon(
1429
+ controllerId,
1430
+ std::move(entries),
1431
+ AnimationKind::Spring,
1432
+ {},
1433
+ animation,
1434
+ kSpringMaxDurationS * 1000.0,
1435
+ animation.reduceMotion,
1436
+ suspensionPolicy,
1437
+ startedAtHintS);
1438
+ }
1439
+
1440
+ int32_t animateKeyframesGroup(
1441
+ uint64_t controllerId,
1442
+ std::vector<GroupMotionEntry> entries,
1443
+ double durationMs,
1444
+ int32_t reduceMotion,
1445
+ GroupSuspensionPolicy suspensionPolicy,
1446
+ double startedAtHintS) {
1447
+ return startGroupCommon(
1448
+ controllerId,
1449
+ std::move(entries),
1450
+ AnimationKind::Keyframes,
1451
+ {},
1452
+ {},
1453
+ durationMs,
1454
+ reduceMotion,
1455
+ suspensionPolicy,
1456
+ startedAtHintS);
1457
+ }
1458
+
1459
+ std::vector<DriverSnapshot> cancelAnimationGroup(
1460
+ int32_t groupId,
1461
+ GroupCancelBehavior behavior) {
1462
+ if (!isOnMainThread()) return {};
1463
+ const bool finish = behavior == GroupCancelBehavior::Finish;
1464
+ return finishGroupImpl(
1465
+ groupId, finish, finish);
1466
+ }
1467
+
674
1468
  void setPresentation(
675
1469
  uint64_t driverId,
676
1470
  Presentation presentation,
677
1471
  bool takeOwnership,
678
- bool overridePendingAnimation) {
1472
+ bool overridePendingAnimation,
1473
+ bool recordVelocity) {
679
1474
  if (!isOnMainThread()) return;
1475
+ SMOOTH_CLIP_TRACE("SmoothClip.setPresentation");
680
1476
  auto iterator = registry().find(driverId);
681
1477
  if (!takeOwnership) {
682
1478
  if (iterator == registry().end() || iterator->second.destroyed) {
@@ -688,6 +1484,10 @@ void setPresentation(
688
1484
  iterator = registry().try_emplace(driverId).first;
689
1485
  }
690
1486
  auto &state = iterator->second;
1487
+ if (state.groupId != 0) {
1488
+ if (!takeOwnership) return;
1489
+ finishGroupImpl(state.groupId, false, false);
1490
+ }
691
1491
  if (!takeOwnership && state.ownership != Ownership::Interactive) return;
692
1492
  if (takeOwnership) {
693
1493
  state.destroyed = false;
@@ -703,7 +1503,12 @@ void setPresentation(
703
1503
  state.latest = presentation;
704
1504
  state.hasLatest = true;
705
1505
  state.ownership = Ownership::Interactive;
706
- recordVelocitySample(state.samples, toChannels(presentation), nowSeconds());
1506
+ if (recordVelocity) {
1507
+ recordVelocitySample(state.samples, toChannels(presentation), nowSeconds());
1508
+ } else if (state.samples.hasLatest) {
1509
+ // Untracked movement invalidates the recorded velocity pair.
1510
+ clearVelocitySamples(state.samples);
1511
+ }
707
1512
  applyToViews(state, presentation);
708
1513
  }
709
1514
 
@@ -712,6 +1517,9 @@ Presentation beginInteraction(uint64_t driverId) {
712
1517
  auto iterator = registry().find(driverId);
713
1518
  if (iterator == registry().end()) return unavailablePresentation();
714
1519
  auto &state = iterator->second;
1520
+ if (state.groupId != 0) {
1521
+ finishGroupImpl(state.groupId, false, false);
1522
+ }
715
1523
  state.destroyed = false;
716
1524
  if (!state.animation.has_value()) {
717
1525
  state.ownership = Ownership::Interactive;
@@ -734,21 +1542,62 @@ int32_t animateTiming(
734
1542
  uint64_t driverId,
735
1543
  AnimationStart start,
736
1544
  Presentation presentation,
737
- TimingAnimation animation) {
738
- if (!isOnMainThread()) return 0;
1545
+ TimingAnimation animation,
1546
+ AnimationValidationMode validationMode) {
1547
+ const bool strictV2 =
1548
+ validationMode == AnimationValidationMode::ProtocolV2;
1549
+ const bool validAnimation = strictV2
1550
+ ? isValidProtocolV2Timing(animation)
1551
+ : std::isfinite(animation.durationMs) &&
1552
+ std::isfinite(animation.controlPoint1X) &&
1553
+ std::isfinite(animation.controlPoint1Y) &&
1554
+ std::isfinite(animation.controlPoint2X) &&
1555
+ std::isfinite(animation.controlPoint2Y) &&
1556
+ isValidReduceMotionCode(animation.reduceMotion);
1557
+ if (!strictV2 && validAnimation) {
1558
+ animation.durationMs = std::max(0.0, animation.durationMs);
1559
+ }
1560
+ if (!isOnMainThread() || driverId == 0 ||
1561
+ !isFiniteProtocolV2Presentation(presentation) || !validAnimation) {
1562
+ return 0;
1563
+ }
739
1564
  auto iterator = registry().find(driverId);
1565
+ Presentation preflightStart;
1566
+ if (iterator == registry().end()) {
1567
+ if (!start.hasInteractiveStart ||
1568
+ !isFiniteProtocolV2Presentation(start.interactiveStart)) {
1569
+ return 0;
1570
+ }
1571
+ preflightStart = start.interactiveStart;
1572
+ } else {
1573
+ if (iterator->second.destroyed && !start.hasInteractiveStart) return 0;
1574
+ preflightStart = start.hasInteractiveStart
1575
+ ? start.interactiveStart
1576
+ : visiblePresentation(driverId, iterator->second);
1577
+ }
1578
+ if (!isFiniteProtocolV2Presentation(preflightStart) ||
1579
+ preflightStart.clip.curve != presentation.clip.curve) {
1580
+ return 0;
1581
+ }
1582
+ const bool reduce = shouldReduceMotion(animation.reduceMotion) ||
1583
+ animation.durationMs <= 0;
1584
+ if (strictV2 && !reduce && iterator != registry().end() &&
1585
+ !timingPlanPreservesLinearHostNormalization(
1586
+ iterator->second, preflightStart, presentation, animation)) {
1587
+ return 0;
1588
+ }
740
1589
  if (iterator == registry().end()) {
741
- if (!start.hasInteractiveStart) return 0;
742
1590
  iterator = registry().try_emplace(driverId).first;
743
1591
  iterator->second.latest = start.interactiveStart;
744
1592
  iterator->second.hasLatest = true;
745
1593
  }
746
1594
  auto &state = iterator->second;
1595
+ if (state.groupId != 0) finishGroupImpl(state.groupId, false, false);
747
1596
  state.destroyed = false;
748
1597
  const int32_t animationId = allocateAnimationId();
749
1598
  const Presentation resolvedStart =
750
1599
  prepareAnimation(driverId, state, start, presentation);
751
- if (shouldReduceMotion(animation.reduceMotion) || animation.durationMs <= 0) {
1600
+ if (reduce) {
752
1601
  applyPresentation(state, presentation);
753
1602
  emitCompletion(driverId, animationId, true);
754
1603
  return animationId;
@@ -767,16 +1616,53 @@ int32_t animateSpring(
767
1616
  uint64_t driverId,
768
1617
  AnimationStart start,
769
1618
  Presentation presentation,
770
- SpringAnimation animation) {
771
- if (!isOnMainThread()) return 0;
1619
+ SpringAnimation animation,
1620
+ AnimationValidationMode validationMode) {
1621
+ const bool strictV2 =
1622
+ validationMode == AnimationValidationMode::ProtocolV2;
1623
+ const bool validAnimation = strictV2
1624
+ ? isValidProtocolV2Spring(animation)
1625
+ : std::isfinite(animation.mass) &&
1626
+ std::isfinite(animation.stiffness) &&
1627
+ std::isfinite(animation.damping) &&
1628
+ std::isfinite(animation.initialVelocity) &&
1629
+ isValidReduceMotionCode(animation.reduceMotion);
1630
+ if (!strictV2 && validAnimation) {
1631
+ animation.mass = std::max(0.0001, animation.mass);
1632
+ animation.stiffness = std::max(0.0001, animation.stiffness);
1633
+ animation.damping = std::max(0.0, animation.damping);
1634
+ }
1635
+ if (!isOnMainThread() || driverId == 0 ||
1636
+ !isFiniteProtocolV2Presentation(presentation) || !validAnimation) {
1637
+ return 0;
1638
+ }
772
1639
  auto iterator = registry().find(driverId);
1640
+ Presentation preflightStart;
1641
+ if (iterator == registry().end()) {
1642
+ if (!start.hasInteractiveStart ||
1643
+ !isFiniteProtocolV2Presentation(start.interactiveStart)) {
1644
+ return 0;
1645
+ }
1646
+ preflightStart = start.interactiveStart;
1647
+ } else {
1648
+ if (iterator->second.destroyed && !start.hasInteractiveStart) return 0;
1649
+ preflightStart = start.hasInteractiveStart
1650
+ ? start.interactiveStart
1651
+ : visiblePresentation(driverId, iterator->second);
1652
+ }
1653
+ if (!isFiniteProtocolV2Presentation(preflightStart) ||
1654
+ preflightStart.clip.curve != presentation.clip.curve ||
1655
+ (strictV2 && !protocolV2SpringScaleIsProvablyPositive(
1656
+ preflightStart, presentation, animation))) {
1657
+ return 0;
1658
+ }
773
1659
  if (iterator == registry().end()) {
774
- if (!start.hasInteractiveStart) return 0;
775
1660
  iterator = registry().try_emplace(driverId).first;
776
1661
  iterator->second.latest = start.interactiveStart;
777
1662
  iterator->second.hasLatest = true;
778
1663
  }
779
1664
  auto &state = iterator->second;
1665
+ if (state.groupId != 0) finishGroupImpl(state.groupId, false, false);
780
1666
  state.destroyed = false;
781
1667
  const int32_t animationId = allocateAnimationId();
782
1668
  // Scalar velocity along the current-to-target trajectory; each channel is
@@ -803,7 +1689,7 @@ int32_t animateSpring(
803
1689
  active.durationS = kSpringMaxDurationS;
804
1690
  const auto startChannels = toChannels(resolvedStart);
805
1691
  const auto targetChannels = toChannels(presentation);
806
- for (int index = 0; index < 7; index += 1) {
1692
+ for (std::size_t index = 0; index < kChannelCount; index += 1) {
807
1693
  active.springPosition[index] = startChannels[index];
808
1694
  active.springVelocity[index] =
809
1695
  velocity * (targetChannels[index] - startChannels[index]);
@@ -817,26 +1703,74 @@ int32_t animateKeyframes(
817
1703
  Presentation presentation,
818
1704
  double durationMs,
819
1705
  std::vector<Keyframe> keyframes,
820
- int32_t reduceMotion) {
821
- if (!isOnMainThread()) return 0;
1706
+ int32_t reduceMotion,
1707
+ AnimationValidationMode validationMode) {
1708
+ const bool strictV2 =
1709
+ validationMode == AnimationValidationMode::ProtocolV2;
1710
+ const bool finiteDuration = std::isfinite(durationMs);
1711
+ if (!strictV2 && finiteDuration) durationMs = std::max(0.0, durationMs);
1712
+ if (!isOnMainThread() || driverId == 0 ||
1713
+ !isFiniteProtocolV2Presentation(presentation) ||
1714
+ !finiteDuration || (strictV2 && durationMs < 0) ||
1715
+ !isValidReduceMotionCode(reduceMotion)) {
1716
+ return 0;
1717
+ }
822
1718
  auto iterator = registry().find(driverId);
1719
+ Presentation preflightStart;
1720
+ if (iterator == registry().end()) {
1721
+ if (!start.hasInteractiveStart ||
1722
+ !isFiniteProtocolV2Presentation(start.interactiveStart)) {
1723
+ return 0;
1724
+ }
1725
+ preflightStart = start.interactiveStart;
1726
+ } else {
1727
+ if (iterator->second.destroyed && !start.hasInteractiveStart) return 0;
1728
+ preflightStart = start.hasInteractiveStart
1729
+ ? start.interactiveStart
1730
+ : visiblePresentation(driverId, iterator->second);
1731
+ }
1732
+ bool validKeyframes = keyframes.size() >= 2 &&
1733
+ keyframes.front().offset == 0 && keyframes.back().offset == 1;
1734
+ double previousOffset = -1;
1735
+ for (const Keyframe &keyframe : keyframes) {
1736
+ validKeyframes = validKeyframes && std::isfinite(keyframe.offset) &&
1737
+ keyframe.offset > previousOffset && keyframe.offset >= 0 &&
1738
+ keyframe.offset <= 1 &&
1739
+ isFiniteProtocolV2Presentation(keyframe.presentation);
1740
+ previousOffset = keyframe.offset;
1741
+ }
1742
+ if (strictV2) {
1743
+ validKeyframes = isValidProtocolV2Keyframes(
1744
+ keyframes, preflightStart, presentation, start.hasInteractiveStart);
1745
+ }
1746
+ if (!isFiniteProtocolV2Presentation(preflightStart) || !validKeyframes) {
1747
+ return 0;
1748
+ }
1749
+ const bool reduce = shouldReduceMotion(reduceMotion) || durationMs <= 0;
1750
+ if (strictV2 && !reduce && iterator != registry().end() &&
1751
+ !keyframePlanPreservesLinearHostNormalization(
1752
+ iterator->second, preflightStart, keyframes)) {
1753
+ return 0;
1754
+ }
823
1755
  if (iterator == registry().end()) {
824
- if (!start.hasInteractiveStart) return 0;
825
1756
  iterator = registry().try_emplace(driverId).first;
826
1757
  iterator->second.latest = start.interactiveStart;
827
1758
  iterator->second.hasLatest = true;
828
1759
  }
829
1760
  auto &state = iterator->second;
1761
+ if (state.groupId != 0) finishGroupImpl(state.groupId, false, false);
830
1762
  state.destroyed = false;
831
1763
  const int32_t animationId = allocateAnimationId();
832
1764
  const Presentation resolvedStart =
833
1765
  prepareAnimation(driverId, state, start, presentation);
834
- if (shouldReduceMotion(reduceMotion) || durationMs <= 0 ||
835
- keyframes.size() < 2) {
1766
+ if (reduce) {
836
1767
  applyPresentation(state, presentation);
837
1768
  emitCompletion(driverId, animationId, true);
838
1769
  return animationId;
839
1770
  }
1771
+ if (strictV2 && !start.hasInteractiveStart) {
1772
+ keyframes.front().presentation = resolvedStart;
1773
+ }
840
1774
  ActiveAnimation active;
841
1775
  active.id = animationId;
842
1776
  active.kind = AnimationKind::Keyframes;
@@ -866,6 +1800,18 @@ CancelResult cancelAnimation(
866
1800
  return {false, {{0, 0, 0, 0, 0}, 0, 0}};
867
1801
  }
868
1802
  auto &state = iterator->second;
1803
+ if (state.groupId != 0) {
1804
+ const int32_t activeGroupId = state.groupId;
1805
+ if (animationId > 0 && animationId != activeGroupId) {
1806
+ return {false, visiblePresentation(driverId, state)};
1807
+ }
1808
+ const std::vector<DriverSnapshot> snapshots =
1809
+ finishGroupImpl(activeGroupId, useTarget, false);
1810
+ for (const DriverSnapshot &snapshot : snapshots) {
1811
+ if (snapshot.driverId == driverId) return {true, snapshot.presentation};
1812
+ }
1813
+ return {false, unavailablePresentation()};
1814
+ }
869
1815
  if (!state.animation.has_value() ||
870
1816
  (animationId > 0 && animationId != state.animation->id)) {
871
1817
  return {false, state.animation.has_value() ? state.animation->current
@@ -888,6 +1834,7 @@ void destroyDriver(uint64_t driverId) {
888
1834
  auto iterator = registry().find(driverId);
889
1835
  if (iterator == registry().end()) return;
890
1836
  auto &state = iterator->second;
1837
+ if (state.groupId != 0) finishGroupImpl(state.groupId, false, false);
891
1838
  state.ownership = Ownership::Interactive;
892
1839
  finishActive(driverId, state, false);
893
1840
  // A destroyed driver's interaction history must not seed a revived
@@ -925,6 +1872,42 @@ void JSmoothClipView::applyClip(const Presentation &presentation) const {
925
1872
  presentation.contentTranslateY);
926
1873
  }
927
1874
 
1875
+ void JSmoothClipView::applyClipV2(const Presentation &presentation) const {
1876
+ static const auto method =
1877
+ javaClassStatic()
1878
+ ->getMethod<void(
1879
+ jdouble,
1880
+ jdouble,
1881
+ jdouble,
1882
+ jdouble,
1883
+ jdouble,
1884
+ jdouble,
1885
+ jdouble,
1886
+ jdouble,
1887
+ jint,
1888
+ jdouble,
1889
+ jdouble,
1890
+ jdouble)>("setClipPresentationV2Dip");
1891
+ method(
1892
+ self(),
1893
+ presentation.clip.x,
1894
+ presentation.clip.y,
1895
+ presentation.clip.width,
1896
+ presentation.clip.height,
1897
+ SmoothClipResolvedRadius(
1898
+ presentation.clip.topLeftRadius, presentation.clip.radius),
1899
+ SmoothClipResolvedRadius(
1900
+ presentation.clip.topRightRadius, presentation.clip.radius),
1901
+ SmoothClipResolvedRadius(
1902
+ presentation.clip.bottomRightRadius, presentation.clip.radius),
1903
+ SmoothClipResolvedRadius(
1904
+ presentation.clip.bottomLeftRadius, presentation.clip.radius),
1905
+ static_cast<jint>(presentation.clip.curve),
1906
+ presentation.contentTranslateX,
1907
+ presentation.contentTranslateY,
1908
+ presentation.contentScale);
1909
+ }
1910
+
928
1911
  void JSmoothClipView::applyClipPx(
929
1912
  const NormalizedClip &clip,
930
1913
  double contentTranslateXPx,
@@ -945,6 +1928,42 @@ void JSmoothClipView::applyClipPx(
945
1928
  static_cast<jfloat>(contentTranslateYPx));
946
1929
  }
947
1930
 
1931
+ void JSmoothClipView::applyClipV2Px(
1932
+ const NormalizedClip &clip,
1933
+ double contentTranslateXPx,
1934
+ double contentTranslateYPx,
1935
+ double contentScale) const {
1936
+ static const auto method =
1937
+ javaClassStatic()
1938
+ ->getMethod<void(
1939
+ jfloat,
1940
+ jfloat,
1941
+ jfloat,
1942
+ jfloat,
1943
+ jfloat,
1944
+ jfloat,
1945
+ jfloat,
1946
+ jfloat,
1947
+ jint,
1948
+ jfloat,
1949
+ jfloat,
1950
+ jfloat)>("setClipPresentationV2Px");
1951
+ method(
1952
+ self(),
1953
+ static_cast<jfloat>(clip.left),
1954
+ static_cast<jfloat>(clip.top),
1955
+ static_cast<jfloat>(clip.right),
1956
+ static_cast<jfloat>(clip.bottom),
1957
+ static_cast<jfloat>(clip.topLeftRadius),
1958
+ static_cast<jfloat>(clip.topRightRadius),
1959
+ static_cast<jfloat>(clip.bottomRightRadius),
1960
+ static_cast<jfloat>(clip.bottomLeftRadius),
1961
+ static_cast<jint>(clip.curve),
1962
+ static_cast<jfloat>(contentTranslateXPx),
1963
+ static_cast<jfloat>(contentTranslateYPx),
1964
+ static_cast<jfloat>(contentScale));
1965
+ }
1966
+
948
1967
  void registerViewAndroid(
949
1968
  uint64_t driverId,
950
1969
  alias_ref<JSmoothClipView> view,
@@ -959,9 +1978,7 @@ void registerViewAndroid(
959
1978
  state.latest = initialPresentation;
960
1979
  state.hasLatest = true;
961
1980
  }
962
- const Presentation visible = state.animation.has_value()
963
- ? state.animation->current
964
- : state.latest;
1981
+ const Presentation visible = visiblePresentation(driverId, state);
965
1982
  JNIEnv *env = facebook::jni::Environment::current();
966
1983
  for (auto &existing : state.views) {
967
1984
  if (env->IsSameObject(existing.view.get(), view.get())) {
@@ -971,6 +1988,7 @@ void registerViewAndroid(
971
1988
  existing.lifecycleVisible = lifecycleVisible;
972
1989
  deliverToView(existing, visible);
973
1990
  reconcileViewDisplayability(driverId, state, existing);
1991
+ reconcileGroupReadiness(driverId);
974
1992
  return;
975
1993
  }
976
1994
  }
@@ -986,6 +2004,7 @@ void registerViewAndroid(
986
2004
  deliverToView(entry, visible);
987
2005
  state.views.push_back(std::move(entry));
988
2006
  reconcileViewDisplayability(driverId, state, state.views.back());
2007
+ reconcileGroupReadiness(driverId);
989
2008
  }
990
2009
 
991
2010
  void setViewHostGeometryAndroid(
@@ -1005,12 +2024,10 @@ void setViewHostGeometryAndroid(
1005
2024
  entry.hostWidthPx = hostWidthPx;
1006
2025
  entry.hostHeightPx = hostHeightPx;
1007
2026
  if (state.hasLatest) {
1008
- deliverToView(
1009
- entry,
1010
- state.animation.has_value() ? state.animation->current
1011
- : state.latest);
2027
+ deliverToView(entry, visiblePresentation(driverId, state));
1012
2028
  }
1013
2029
  reconcileViewDisplayability(driverId, state, entry);
2030
+ reconcileGroupReadiness(driverId);
1014
2031
  return;
1015
2032
  }
1016
2033
  }
@@ -1028,6 +2045,7 @@ void setViewLifecycleVisibilityAndroid(
1028
2045
  if (!env->IsSameObject(entry.view.get(), view.get())) continue;
1029
2046
  entry.lifecycleVisible = lifecycleVisible;
1030
2047
  reconcileViewDisplayability(driverId, state, entry);
2048
+ reconcileGroupReadiness(driverId);
1031
2049
  return;
1032
2050
  }
1033
2051
  }
@@ -1072,6 +2090,9 @@ void unregisterViewAndroid(
1072
2090
  relatchIfNoDisplayable(driverId, state);
1073
2091
  }
1074
2092
  }
2093
+ if (removed && state.groupId != 0 && isOnMainThread()) {
2094
+ reconcileGroupReadiness(driverId);
2095
+ }
1075
2096
  if (state.destroyed && state.views.empty()) {
1076
2097
  registry().erase(iterator);
1077
2098
  }