react-native-smooth-clip-view 0.2.9 → 0.4.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.
- package/README.md +132 -349
- package/android/build.gradle +8 -4
- package/android/proguard-rules.pro +3 -3
- package/android/src/main/cpp/SmoothClipAndroid.h +13 -6
- package/android/src/main/cpp/SmoothClipBindings.cpp +605 -123
- package/android/src/main/cpp/SmoothClipRegistry.cpp +975 -209
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +192 -118
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +22 -4
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +81 -56
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +449 -240
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +150 -38
- package/cpp/SmoothClipAnimationCurve.h +317 -397
- package/cpp/SmoothClipRegistry.h +84 -20
- package/cpp/SmoothClipSharedGeometry.h +131 -22
- package/cpp/SmoothClipVelocityTracker.h +16 -12
- package/ios/SmoothClipGeometry.h +188 -21
- package/ios/SmoothClipRegistry.mm +869 -368
- package/ios/SmoothClipRegistrySnapshot.h +13 -0
- package/ios/SmoothClipTurboModule.cpp +513 -156
- package/ios/SmoothClipTurboModule.h +123 -56
- package/ios/SmoothClipView.mm +1074 -449
- package/ios/SmoothClipViewRegistry.h +3 -7
- package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/SmoothClipView.ios.js +94 -11
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipView.js +1 -46
- package/lib/module/SmoothClipView.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +32 -12
- package/lib/module/capabilities.js +4 -0
- package/lib/module/capabilities.js.map +1 -0
- package/lib/module/capabilities.native.js +18 -0
- package/lib/module/capabilities.native.js.map +1 -0
- package/lib/module/capabilityTypes.js +2 -0
- package/lib/module/capabilityTypes.js.map +1 -0
- package/lib/module/controllerInternals.js +28 -0
- package/lib/module/controllerInternals.js.map +1 -0
- package/lib/module/controllerLifecycle.js +9 -0
- package/lib/module/controllerLifecycle.js.map +1 -0
- package/lib/module/controllerTypes.js +4 -0
- package/lib/module/controllerTypes.js.map +1 -0
- package/lib/module/controllers.android.js +4 -0
- package/lib/module/controllers.android.js.map +1 -0
- package/lib/module/controllers.ios.js +4 -0
- package/lib/module/controllers.ios.js.map +1 -0
- package/lib/module/controllers.js +4 -0
- package/lib/module/controllers.js.map +1 -0
- package/lib/module/controllers.native.js +89 -0
- package/lib/module/controllers.native.js.map +1 -0
- package/lib/module/geometry.js +180 -30
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groupTypes.js +4 -0
- package/lib/module/groupTypes.js.map +1 -0
- package/lib/module/groups.android.js +4 -0
- package/lib/module/groups.android.js.map +1 -0
- package/lib/module/groups.ios.js +4 -0
- package/lib/module/groups.ios.js.map +1 -0
- package/lib/module/groups.js +4 -0
- package/lib/module/groups.js.map +1 -0
- package/lib/module/groups.native.js +399 -0
- package/lib/module/groups.native.js.map +1 -0
- package/lib/module/ids.js +8 -0
- package/lib/module/ids.js.map +1 -0
- package/lib/module/index.js +4 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/nativeCompletion.js +37 -16
- package/lib/module/nativeCompletion.js.map +1 -1
- package/lib/module/presentationCodec.js +81 -0
- package/lib/module/presentationCodec.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +20 -4
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +2 -10
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +139 -4
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +16 -3
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/capabilities.d.ts +3 -0
- package/lib/typescript/src/capabilities.d.ts.map +1 -0
- package/lib/typescript/src/capabilities.native.d.ts +3 -0
- package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
- package/lib/typescript/src/capabilityTypes.d.ts +4 -0
- package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
- package/lib/typescript/src/controllerInternals.d.ts +14 -0
- package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
- package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
- package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/controllerTypes.d.ts +62 -0
- package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
- package/lib/typescript/src/controllers.android.d.ts +2 -0
- package/lib/typescript/src/controllers.android.d.ts.map +1 -0
- package/lib/typescript/src/controllers.d.ts +2 -0
- package/lib/typescript/src/controllers.d.ts.map +1 -0
- package/lib/typescript/src/controllers.ios.d.ts +2 -0
- package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
- package/lib/typescript/src/controllers.native.d.ts +3 -0
- package/lib/typescript/src/controllers.native.d.ts.map +1 -0
- package/lib/typescript/src/geometry.d.ts +49 -5
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groupTypes.d.ts +31 -0
- package/lib/typescript/src/groupTypes.d.ts.map +1 -0
- package/lib/typescript/src/groups.android.d.ts +2 -0
- package/lib/typescript/src/groups.android.d.ts.map +1 -0
- package/lib/typescript/src/groups.d.ts +2 -0
- package/lib/typescript/src/groups.d.ts.map +1 -0
- package/lib/typescript/src/groups.ios.d.ts +2 -0
- package/lib/typescript/src/groups.ios.d.ts.map +1 -0
- package/lib/typescript/src/groups.native.d.ts +3 -0
- package/lib/typescript/src/groups.native.d.ts.map +1 -0
- package/lib/typescript/src/ids.d.ts +2 -0
- package/lib/typescript/src/ids.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -5
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/nativeCompletion.d.ts +16 -6
- package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +10 -0
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
- package/package.json +17 -13
- package/src/NativeSmoothClipModule.ts +80 -56
- package/src/SmoothClipView.ios.tsx +142 -16
- package/src/SmoothClipView.tsx +6 -76
- package/src/SmoothClipViewNativeComponent.ts +32 -12
- package/src/capabilities.native.ts +19 -0
- package/src/capabilities.ts +2 -0
- package/src/capabilityTypes.ts +3 -0
- package/src/controllerInternals.ts +52 -0
- package/src/controllerLifecycle.ts +6 -0
- package/src/controllerTypes.ts +87 -0
- package/src/controllers.android.ts +1 -0
- package/src/controllers.ios.ts +1 -0
- package/src/controllers.native.ts +104 -0
- package/src/controllers.ts +1 -0
- package/src/geometry.ts +293 -45
- package/src/groupTypes.ts +53 -0
- package/src/groups.android.ts +1 -0
- package/src/groups.ios.ts +1 -0
- package/src/groups.native.ts +604 -0
- package/src/groups.ts +1 -0
- package/src/ids.ts +6 -0
- package/src/index.ts +32 -12
- package/src/nativeCompletion.ts +67 -32
- package/src/presentationCodec.ts +134 -0
- package/lib/module/SmoothClipView.web.js +0 -54
- package/lib/module/SmoothClipView.web.js.map +0 -1
- package/lib/module/driverState.js +0 -110
- package/lib/module/driverState.js.map +0 -1
- package/lib/module/driverTypes.js +0 -4
- package/lib/module/driverTypes.js.map +0 -1
- package/lib/module/drivers.android.js +0 -6
- package/lib/module/drivers.android.js.map +0 -1
- package/lib/module/drivers.ios.js +0 -6
- package/lib/module/drivers.ios.js.map +0 -1
- package/lib/module/drivers.js +0 -284
- package/lib/module/drivers.js.map +0 -1
- package/lib/module/drivers.native.js +0 -556
- package/lib/module/drivers.native.js.map +0 -1
- package/lib/module/index.web.js +0 -8
- package/lib/module/index.web.js.map +0 -1
- package/lib/module/reactRequests.js +0 -56
- package/lib/module/reactRequests.js.map +0 -1
- package/lib/module/webClipPath.js +0 -25
- package/lib/module/webClipPath.js.map +0 -1
- package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
- package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
- package/lib/typescript/src/driverState.d.ts +0 -31
- package/lib/typescript/src/driverState.d.ts.map +0 -1
- package/lib/typescript/src/driverTypes.d.ts +0 -107
- package/lib/typescript/src/driverTypes.d.ts.map +0 -1
- package/lib/typescript/src/drivers.android.d.ts +0 -3
- package/lib/typescript/src/drivers.android.d.ts.map +0 -1
- package/lib/typescript/src/drivers.d.ts +0 -5
- package/lib/typescript/src/drivers.d.ts.map +0 -1
- package/lib/typescript/src/drivers.ios.d.ts +0 -3
- package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
- package/lib/typescript/src/drivers.native.d.ts +0 -5
- package/lib/typescript/src/drivers.native.d.ts.map +0 -1
- package/lib/typescript/src/index.web.d.ts +0 -9
- package/lib/typescript/src/index.web.d.ts.map +0 -1
- package/lib/typescript/src/reactRequests.d.ts +0 -9
- package/lib/typescript/src/reactRequests.d.ts.map +0 -1
- package/lib/typescript/src/webClipPath.d.ts +0 -7
- package/lib/typescript/src/webClipPath.d.ts.map +0 -1
- package/src/SmoothClipView.web.tsx +0 -66
- package/src/driverState.ts +0 -158
- package/src/driverTypes.ts +0 -140
- package/src/drivers.android.ts +0 -8
- package/src/drivers.ios.ts +0 -8
- package/src/drivers.native.ts +0 -899
- package/src/drivers.ts +0 -427
- package/src/index.web.ts +0 -23
- package/src/reactRequests.ts +0 -76
- package/src/webClipPath.ts +0 -30
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
#include "SmoothClipAnimationId.h"
|
|
23
23
|
#include "SmoothClipAnimationCurve.h"
|
|
24
|
+
#include "SmoothClipSharedGeometry.h"
|
|
24
25
|
|
|
25
26
|
@interface SmoothClipView (Registry)
|
|
26
27
|
- (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation;
|
|
@@ -31,18 +32,22 @@
|
|
|
31
32
|
- (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
|
|
32
33
|
animation:(smoothclip::TimingAnimation)animation
|
|
33
34
|
animationId:(int32_t)animationId;
|
|
35
|
+
- (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
|
|
36
|
+
animation:(smoothclip::TimingAnimation)animation
|
|
37
|
+
animationId:(int32_t)animationId
|
|
38
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime;
|
|
34
39
|
- (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
|
|
35
40
|
animation:(smoothclip::SpringAnimation)animation
|
|
36
41
|
animationId:(int32_t)animationId;
|
|
37
|
-
- (BOOL)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
- (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
|
|
43
|
+
animation:(smoothclip::SpringAnimation)animation
|
|
44
|
+
animationId:(int32_t)animationId
|
|
45
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime;
|
|
41
46
|
- (void)smoothClipCancelAnimationUsingTarget:(BOOL)useTarget;
|
|
42
47
|
- (BOOL)smoothClipIsJoinable;
|
|
43
48
|
- (BOOL)smoothClipCanDisplay;
|
|
44
|
-
- (
|
|
45
|
-
|
|
49
|
+
- (double)smoothClipInheritedVelocityToPresentation:
|
|
50
|
+
(smoothclip::Presentation)presentation;
|
|
46
51
|
- (void)smoothClipClearVelocitySamples;
|
|
47
52
|
@end
|
|
48
53
|
|
|
@@ -52,15 +57,15 @@ namespace {
|
|
|
52
57
|
using ViewKey = uintptr_t;
|
|
53
58
|
|
|
54
59
|
enum class Ownership { Interactive, Native };
|
|
55
|
-
enum class AnimationKind { Timing, Spring
|
|
60
|
+
enum class AnimationKind { Timing, Spring };
|
|
56
61
|
|
|
57
62
|
struct ActiveAnimation {
|
|
58
63
|
int32_t animationId;
|
|
64
|
+
int32_t completionTag = 0;
|
|
65
|
+
// Zero identifies a single-driver animation. A non-zero value binds
|
|
66
|
+
// the driver to an immutable group whose completion is aggregated once.
|
|
67
|
+
int32_t groupId = 0;
|
|
59
68
|
std::unordered_set<ViewKey> participants;
|
|
60
|
-
// Hosts that installed this id and temporarily lost displayability. They
|
|
61
|
-
// may rejoin the same remainder; completion is false if any remain
|
|
62
|
-
// unresolved when the run ends or unregister.
|
|
63
|
-
std::unordered_set<ViewKey> suspendedParticipants;
|
|
64
69
|
bool finished = true;
|
|
65
70
|
AnimationKind kind = AnimationKind::Timing;
|
|
66
71
|
// Geometry the transition started from. Used when a view must join the
|
|
@@ -68,16 +73,8 @@ struct ActiveAnimation {
|
|
|
68
73
|
Presentation start{{0, 0, 0, 0, 0}, 0, 0};
|
|
69
74
|
TimingAnimation timing{};
|
|
70
75
|
SpringAnimation spring{};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
CFTimeInterval startedAt = 0;
|
|
74
|
-
// False while the animation is latched. Two ways in: built before any host
|
|
75
|
-
// view could display (pre-registration, or a host in a detached subtree), or
|
|
76
|
-
// re-latched by unregisterView when the last DISPLAYABLE host left
|
|
77
|
-
// mid-flight. Started by whichever registration/displayability update or
|
|
78
|
-
// joinActiveAnimation first finds a host that can produce a frame; each
|
|
79
|
-
// rebases startedAt through startLatchedAnimation. startedAt is only
|
|
80
|
-
// meaningful once started.
|
|
76
|
+
// False while a pre-ready run waits for its first displayable host. Host
|
|
77
|
+
// loss after the run starts finishes it at the target.
|
|
81
78
|
bool started = false;
|
|
82
79
|
};
|
|
83
80
|
|
|
@@ -98,11 +95,31 @@ struct CompletionSink {
|
|
|
98
95
|
CompletionCallback callback;
|
|
99
96
|
};
|
|
100
97
|
|
|
98
|
+
struct GroupCompletionSink {
|
|
99
|
+
const void *owner = nullptr;
|
|
100
|
+
GroupCompletionCallback callback;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
struct GroupState {
|
|
104
|
+
uint64_t controllerId;
|
|
105
|
+
int32_t groupId;
|
|
106
|
+
int32_t completionTag;
|
|
107
|
+
std::vector<uint64_t> driverIds;
|
|
108
|
+
std::unordered_set<uint64_t> remainingDriverIds;
|
|
109
|
+
bool finished = true;
|
|
110
|
+
bool mutating = false;
|
|
111
|
+
};
|
|
112
|
+
|
|
101
113
|
std::unordered_map<uint64_t, DriverState> ®istry() {
|
|
102
114
|
static std::unordered_map<uint64_t, DriverState> value;
|
|
103
115
|
return value;
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
std::unordered_map<int32_t, GroupState> &groupRegistry() {
|
|
119
|
+
static std::unordered_map<int32_t, GroupState> value;
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
106
123
|
// The transitions must be observed for the whole process lifetime, not per
|
|
107
124
|
// view: the state below is process-global, and a notification delivered while
|
|
108
125
|
// no SmoothClipView happens to be alive (the last host unmounted during
|
|
@@ -143,6 +160,11 @@ CompletionSink &completionSink() {
|
|
|
143
160
|
return sink;
|
|
144
161
|
}
|
|
145
162
|
|
|
163
|
+
GroupCompletionSink &groupCompletionSink() {
|
|
164
|
+
static GroupCompletionSink sink;
|
|
165
|
+
return sink;
|
|
166
|
+
}
|
|
167
|
+
|
|
146
168
|
// The sink is written from the TurboModule ctor/dtor on the JS thread and
|
|
147
169
|
// invoked on main. Invoking under the lock also keeps the owning module
|
|
148
170
|
// alive for the duration of the callback.
|
|
@@ -151,6 +173,19 @@ std::mutex &completionSinkMutex() {
|
|
|
151
173
|
return value;
|
|
152
174
|
}
|
|
153
175
|
|
|
176
|
+
|
|
177
|
+
void groupMemberCompleted(
|
|
178
|
+
int32_t groupId,
|
|
179
|
+
uint64_t driverId,
|
|
180
|
+
bool finished);
|
|
181
|
+
std::vector<DriverSnapshot> cancelGroupInternal(
|
|
182
|
+
int32_t groupId,
|
|
183
|
+
GroupCancelBehavior behavior,
|
|
184
|
+
bool completionFinished);
|
|
185
|
+
void tryStartGroup(int32_t groupId);
|
|
186
|
+
void finishGroupForHostLoss(int32_t groupId);
|
|
187
|
+
DriverSnapshot snapshotForDriver(uint64_t driverId);
|
|
188
|
+
|
|
154
189
|
Presentation unavailablePresentation() {
|
|
155
190
|
// Non-finite geometry makes the JS side fall back to its current value
|
|
156
191
|
// instead of applying zeros.
|
|
@@ -183,11 +218,22 @@ void emitCompletion(
|
|
|
183
218
|
DriverState &state,
|
|
184
219
|
int32_t animationId,
|
|
185
220
|
bool finished) {
|
|
221
|
+
const int32_t groupId = state.animation.has_value()
|
|
222
|
+
? state.animation->groupId
|
|
223
|
+
: 0;
|
|
224
|
+
const int32_t completionTag = state.animation.has_value()
|
|
225
|
+
? state.animation->completionTag
|
|
226
|
+
: 0;
|
|
186
227
|
state.animation.reset();
|
|
228
|
+
if (groupId != 0) {
|
|
229
|
+
groupMemberCompleted(groupId, driverId, finished);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
187
232
|
{
|
|
188
233
|
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
189
234
|
if (completionSink().callback) {
|
|
190
|
-
completionSink().callback(
|
|
235
|
+
completionSink().callback(
|
|
236
|
+
driverId, animationId, completionTag, finished);
|
|
191
237
|
}
|
|
192
238
|
}
|
|
193
239
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
@@ -198,14 +244,62 @@ void emitCompletion(
|
|
|
198
244
|
#endif
|
|
199
245
|
}
|
|
200
246
|
|
|
247
|
+
void emitGroupCompletion(
|
|
248
|
+
uint64_t controllerId,
|
|
249
|
+
int32_t groupId,
|
|
250
|
+
int32_t completionTag,
|
|
251
|
+
bool finished,
|
|
252
|
+
const std::vector<uint64_t> &driverIds) {
|
|
253
|
+
std::vector<DriverSnapshot> snapshots;
|
|
254
|
+
snapshots.reserve(driverIds.size());
|
|
255
|
+
for (const uint64_t driverId : driverIds) {
|
|
256
|
+
snapshots.push_back(snapshotForDriver(driverId));
|
|
257
|
+
}
|
|
258
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
259
|
+
if (groupCompletionSink().callback) {
|
|
260
|
+
groupCompletionSink().callback(
|
|
261
|
+
controllerId,
|
|
262
|
+
groupId,
|
|
263
|
+
completionTag,
|
|
264
|
+
finished,
|
|
265
|
+
std::move(snapshots));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
void groupMemberCompleted(
|
|
270
|
+
int32_t groupId,
|
|
271
|
+
uint64_t driverId,
|
|
272
|
+
bool finished) {
|
|
273
|
+
auto iterator = groupRegistry().find(groupId);
|
|
274
|
+
if (iterator == groupRegistry().end()) return;
|
|
275
|
+
GroupState &group = iterator->second;
|
|
276
|
+
if (group.remainingDriverIds.erase(driverId) == 0) return;
|
|
277
|
+
group.finished = group.finished && finished;
|
|
278
|
+
if (!group.remainingDriverIds.empty() || group.mutating) return;
|
|
279
|
+
|
|
280
|
+
const uint64_t controllerId = group.controllerId;
|
|
281
|
+
const int32_t completionTag = group.completionTag;
|
|
282
|
+
const bool aggregateFinished = group.finished;
|
|
283
|
+
const std::vector<uint64_t> driverIds = group.driverIds;
|
|
284
|
+
groupRegistry().erase(iterator);
|
|
285
|
+
emitGroupCompletion(
|
|
286
|
+
controllerId,
|
|
287
|
+
groupId,
|
|
288
|
+
completionTag,
|
|
289
|
+
aggregateFinished,
|
|
290
|
+
driverIds);
|
|
291
|
+
}
|
|
292
|
+
|
|
201
293
|
void emitStandaloneCompletion(
|
|
202
294
|
uint64_t driverId,
|
|
203
295
|
int32_t animationId,
|
|
296
|
+
int32_t completionTag,
|
|
204
297
|
bool finished) {
|
|
205
298
|
{
|
|
206
299
|
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
207
300
|
if (completionSink().callback) {
|
|
208
|
-
completionSink().callback(
|
|
301
|
+
completionSink().callback(
|
|
302
|
+
driverId, animationId, completionTag, finished);
|
|
209
303
|
}
|
|
210
304
|
}
|
|
211
305
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
@@ -219,7 +313,7 @@ void emitStandaloneCompletion(
|
|
|
219
313
|
Presentation canonicalFrozenPresentation(
|
|
220
314
|
DriverState &state,
|
|
221
315
|
SmoothClipView *preferred = nil) {
|
|
222
|
-
// A
|
|
316
|
+
// A pre-ready animation never rendered, so state.latest already holds its
|
|
223
317
|
// target; freezing there would jump the clip. Freeze at the start instead.
|
|
224
318
|
Presentation canonical =
|
|
225
319
|
state.animation.has_value() && !state.animation->started
|
|
@@ -241,13 +335,12 @@ Presentation canonicalFrozenPresentation(
|
|
|
241
335
|
canonical = frozen;
|
|
242
336
|
hasCanonical = true;
|
|
243
337
|
} else if (!hasParticipantFallback && state.animation.has_value() &&
|
|
244
|
-
|
|
245
|
-
state.animation->suspendedParticipants.count(key) > 0)) {
|
|
338
|
+
state.animation->participants.count(key) > 0) {
|
|
246
339
|
// An installed participant whose HOST just lost its size still holds
|
|
247
340
|
// real mid-flight geometry on its clip layer (host bounds do not feed
|
|
248
341
|
// the clip container). Without this fallback, a sole running host
|
|
249
|
-
// resized to zero
|
|
250
|
-
//
|
|
342
|
+
// resized to zero must still freeze from its real clip layer rather
|
|
343
|
+
// than state.latest, which already holds the target.
|
|
251
344
|
hasParticipantFallback = true;
|
|
252
345
|
participantFallback = frozen;
|
|
253
346
|
}
|
|
@@ -258,11 +351,142 @@ Presentation canonicalFrozenPresentation(
|
|
|
258
351
|
return canonical;
|
|
259
352
|
}
|
|
260
353
|
|
|
354
|
+
Presentation canonicalVisiblePresentation(const DriverState &state) {
|
|
355
|
+
if (state.animation.has_value() && !state.animation->started) {
|
|
356
|
+
return state.animation->start;
|
|
357
|
+
}
|
|
358
|
+
for (const ViewKey key : state.views) {
|
|
359
|
+
SmoothClipView *candidate = viewForKey(key);
|
|
360
|
+
if ([candidate smoothClipIsJoinable]) {
|
|
361
|
+
return [candidate smoothClipCurrentPresentation];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return state.latest;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
bool driverReady(const DriverState &state) {
|
|
368
|
+
if (state.destroyed) return false;
|
|
369
|
+
for (const ViewKey key : state.views) {
|
|
370
|
+
if ([viewForKey(key) smoothClipCanDisplay]) return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
double resolvedSpringVelocity(
|
|
376
|
+
const DriverState &state,
|
|
377
|
+
const Presentation &target,
|
|
378
|
+
const SpringAnimation &spring) {
|
|
379
|
+
if (!spring.inheritVelocity) return spring.initialVelocity;
|
|
380
|
+
for (const ViewKey key : state.views) {
|
|
381
|
+
SmoothClipView *view = viewForKey(key);
|
|
382
|
+
if ([view smoothClipIsJoinable]) {
|
|
383
|
+
return [view smoothClipInheritedVelocityToPresentation:target];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
DriverSnapshot snapshotForDriver(uint64_t driverId) {
|
|
390
|
+
const auto iterator = registry().find(driverId);
|
|
391
|
+
if (iterator == registry().end() || iterator->second.destroyed) {
|
|
392
|
+
return {driverId, unavailablePresentation(), false};
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
driverId,
|
|
396
|
+
canonicalVisiblePresentation(iterator->second),
|
|
397
|
+
driverReady(iterator->second),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
std::vector<DriverSnapshot> cancelGroupInternal(
|
|
402
|
+
int32_t groupId,
|
|
403
|
+
GroupCancelBehavior behavior,
|
|
404
|
+
bool completionFinished) {
|
|
405
|
+
auto groupIterator = groupRegistry().find(groupId);
|
|
406
|
+
if (groupIterator == groupRegistry().end()) return {};
|
|
407
|
+
GroupState &group = groupIterator->second;
|
|
408
|
+
if (group.mutating) return {};
|
|
409
|
+
group.mutating = true;
|
|
410
|
+
|
|
411
|
+
const std::vector<uint64_t> driverIds = group.driverIds;
|
|
412
|
+
std::vector<DriverSnapshot> snapshots;
|
|
413
|
+
snapshots.reserve(driverIds.size());
|
|
414
|
+
for (const uint64_t driverId : driverIds) {
|
|
415
|
+
auto stateIterator = registry().find(driverId);
|
|
416
|
+
if (stateIterator == registry().end()) {
|
|
417
|
+
snapshots.push_back({driverId, unavailablePresentation(), false});
|
|
418
|
+
groupMemberCompleted(groupId, driverId, false);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
DriverState &state = stateIterator->second;
|
|
422
|
+
if (!state.animation.has_value() ||
|
|
423
|
+
state.animation->groupId != groupId) {
|
|
424
|
+
snapshots.push_back(snapshotForDriver(driverId));
|
|
425
|
+
groupMemberCompleted(groupId, driverId, false);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const int32_t animationId = state.animation->animationId;
|
|
430
|
+
Presentation resolved;
|
|
431
|
+
if (behavior == GroupCancelBehavior::Finish) {
|
|
432
|
+
resolved = state.latest;
|
|
433
|
+
const bool wasPending = !state.animation->started;
|
|
434
|
+
for (const ViewKey key : state.views) {
|
|
435
|
+
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
436
|
+
}
|
|
437
|
+
if (wasPending) {
|
|
438
|
+
for (const ViewKey key : state.views) {
|
|
439
|
+
[viewForKey(key) smoothClipApplyPresentation:resolved
|
|
440
|
+
recordVelocitySample:NO];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
resolved = canonicalFrozenPresentation(state);
|
|
445
|
+
}
|
|
446
|
+
state.latest = resolved;
|
|
447
|
+
state.hasLatest = true;
|
|
448
|
+
state.ownership = Ownership::Interactive;
|
|
449
|
+
const bool ready = driverReady(state);
|
|
450
|
+
emitCompletion(
|
|
451
|
+
driverId, state, animationId, completionFinished);
|
|
452
|
+
snapshots.push_back({driverId, resolved, ready});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
groupIterator = groupRegistry().find(groupId);
|
|
456
|
+
if (groupIterator != groupRegistry().end()) {
|
|
457
|
+
GroupState &remainingGroup = groupIterator->second;
|
|
458
|
+
remainingGroup.mutating = false;
|
|
459
|
+
if (remainingGroup.remainingDriverIds.empty()) {
|
|
460
|
+
const uint64_t controllerId = remainingGroup.controllerId;
|
|
461
|
+
const int32_t completionTag = remainingGroup.completionTag;
|
|
462
|
+
const bool finished = remainingGroup.finished;
|
|
463
|
+
const std::vector<uint64_t> completedDrivers =
|
|
464
|
+
remainingGroup.driverIds;
|
|
465
|
+
groupRegistry().erase(groupIterator);
|
|
466
|
+
emitGroupCompletion(
|
|
467
|
+
controllerId,
|
|
468
|
+
groupId,
|
|
469
|
+
completionTag,
|
|
470
|
+
finished,
|
|
471
|
+
completedDrivers);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return snapshots;
|
|
475
|
+
}
|
|
476
|
+
|
|
261
477
|
void cancelActive(
|
|
262
478
|
uint64_t driverId,
|
|
263
479
|
DriverState &state,
|
|
264
480
|
bool useTarget) {
|
|
265
481
|
if (!state.animation.has_value()) return;
|
|
482
|
+
if (state.animation->groupId != 0) {
|
|
483
|
+
cancelGroupInternal(
|
|
484
|
+
state.animation->groupId,
|
|
485
|
+
useTarget ? GroupCancelBehavior::Finish
|
|
486
|
+
: GroupCancelBehavior::Freeze,
|
|
487
|
+
false);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
266
490
|
const int32_t animationId = state.animation->animationId;
|
|
267
491
|
// Deferred views are deliberately not completion participants, but they do
|
|
268
492
|
// hold a pending install and requested target. Cancellation must still reach
|
|
@@ -273,14 +497,28 @@ void cancelActive(
|
|
|
273
497
|
emitCompletion(driverId, state, animationId, false);
|
|
274
498
|
}
|
|
275
499
|
|
|
500
|
+
void finishStandaloneAtTarget(uint64_t driverId, DriverState &state) {
|
|
501
|
+
if (!state.animation.has_value()) return;
|
|
502
|
+
if (state.animation->groupId != 0) {
|
|
503
|
+
cancelGroupInternal(
|
|
504
|
+
state.animation->groupId, GroupCancelBehavior::Finish, true);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const int32_t animationId = state.animation->animationId;
|
|
508
|
+
for (const ViewKey key : state.views) {
|
|
509
|
+
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
510
|
+
}
|
|
511
|
+
state.ownership = Ownership::Interactive;
|
|
512
|
+
emitCompletion(driverId, state, animationId, true);
|
|
513
|
+
}
|
|
514
|
+
|
|
276
515
|
void finishIfNoInstalledParticipants(uint64_t driverId, DriverState &state) {
|
|
277
516
|
if (!state.animation.has_value() || !state.animation->started ||
|
|
278
517
|
!state.animation->participants.empty()) {
|
|
279
518
|
return;
|
|
280
519
|
}
|
|
281
520
|
const int32_t animationId = state.animation->animationId;
|
|
282
|
-
const bool finished = state.animation->finished
|
|
283
|
-
state.animation->suspendedParticipants.empty();
|
|
521
|
+
const bool finished = state.animation->finished;
|
|
284
522
|
// A detached/unlaid-out view can have a requested animation without any CA
|
|
285
523
|
// delegate capable of completing it. Once every installed animation ends,
|
|
286
524
|
// make those deferred views statically correct and invalidate their pending
|
|
@@ -296,6 +534,7 @@ void applyPresentation(
|
|
|
296
534
|
DriverState &state,
|
|
297
535
|
Presentation presentation,
|
|
298
536
|
bool recordVelocitySample = true) {
|
|
537
|
+
if (!canonicalizePresentation(presentation)) return;
|
|
299
538
|
state.latest = presentation;
|
|
300
539
|
state.hasLatest = true;
|
|
301
540
|
state.ownership = Ownership::Interactive;
|
|
@@ -310,8 +549,10 @@ void prepareAnimation(
|
|
|
310
549
|
DriverState &state,
|
|
311
550
|
AnimationStart start,
|
|
312
551
|
Presentation target) {
|
|
313
|
-
|
|
314
|
-
|
|
552
|
+
// An explicit `from` is part of the new animation's start transaction. It
|
|
553
|
+
// intentionally overrides a sampled native frame even when this operation
|
|
554
|
+
// is replacing another native-owned animation.
|
|
555
|
+
const bool acceptsInteractiveStart = start.hasInteractiveStart;
|
|
315
556
|
cancelActive(driverId, state, false);
|
|
316
557
|
if (acceptsInteractiveStart) {
|
|
317
558
|
applyPresentation(state, start.interactiveStart);
|
|
@@ -326,72 +567,42 @@ void prepareAnimation(
|
|
|
326
567
|
Presentation resolvedAnimationStart(
|
|
327
568
|
const DriverState &state,
|
|
328
569
|
const AnimationStart &start) {
|
|
329
|
-
if (start.hasInteractiveStart
|
|
570
|
+
if (start.hasInteractiveStart) {
|
|
330
571
|
return start.interactiveStart;
|
|
331
572
|
}
|
|
332
|
-
// Replacing a
|
|
573
|
+
// Replacing a pre-ready animation (e.g. open→close before the host mounted):
|
|
333
574
|
// it never rendered, and state.latest is its target — start from where the
|
|
334
|
-
// clip actually is, the
|
|
575
|
+
// clip actually is, the pending run's own start.
|
|
335
576
|
if (state.animation.has_value() && !state.animation->started) {
|
|
336
577
|
return state.animation->start;
|
|
337
578
|
}
|
|
338
|
-
return state.
|
|
579
|
+
return state.animation.has_value()
|
|
580
|
+
? canonicalVisiblePresentation(state)
|
|
581
|
+
: state.latest;
|
|
339
582
|
}
|
|
340
583
|
|
|
341
|
-
// Installs
|
|
342
|
-
|
|
343
|
-
// animation) and joinActiveAnimation (first layout after such a mount).
|
|
344
|
-
void dispatchActiveAnimationJoin(
|
|
584
|
+
// Installs a run that waited for its host to become displayable.
|
|
585
|
+
void installPendingAnimation(
|
|
345
586
|
DriverState &state,
|
|
346
587
|
SmoothClipView *view,
|
|
347
588
|
ViewKey key,
|
|
348
589
|
const Presentation &visible,
|
|
349
|
-
|
|
590
|
+
CFTimeInterval sharedBeginTime = 0) {
|
|
350
591
|
auto &active = *state.animation;
|
|
351
|
-
const double elapsedMs = (CACurrentMediaTime() - active.startedAt) * 1000.0;
|
|
352
|
-
const double remainingMs = MAX(0, active.durationMs - elapsedMs);
|
|
353
592
|
BOOL installed = NO;
|
|
354
593
|
if (active.kind == AnimationKind::Timing) {
|
|
355
|
-
const double progress = active.durationMs <= 0
|
|
356
|
-
? 1
|
|
357
|
-
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
358
|
-
TimingAnimation timing = timingRemainder(active.timing, progress).animation;
|
|
359
594
|
installed = [view smoothClipAnimateTiming:state.latest
|
|
360
|
-
animation:timing
|
|
361
|
-
animationId:active.animationId
|
|
595
|
+
animation:active.timing
|
|
596
|
+
animationId:active.animationId
|
|
597
|
+
sharedBeginTime:sharedBeginTime];
|
|
362
598
|
} else if (active.kind == AnimationKind::Spring) {
|
|
363
|
-
SpringAnimation spring = active.spring;
|
|
364
|
-
if (reference != nil) {
|
|
365
|
-
// A late host must join the spring's CURRENT physical state. Reusing the
|
|
366
|
-
// original launch velocity (or resolving `inherit` from the joining
|
|
367
|
-
// host's unrelated samples) creates a different spring at the seam.
|
|
368
|
-
spring.inheritVelocity = false;
|
|
369
|
-
spring.initialVelocity =
|
|
370
|
-
[reference smoothClipSpringContinuationVelocity];
|
|
371
|
-
}
|
|
372
599
|
installed = [view smoothClipAnimateSpring:state.latest
|
|
373
|
-
animation:spring
|
|
374
|
-
animationId:active.animationId
|
|
375
|
-
|
|
376
|
-
const double progress = active.durationMs <= 0
|
|
377
|
-
? 1
|
|
378
|
-
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
379
|
-
std::vector<Keyframe> remaining;
|
|
380
|
-
remaining.push_back({0, visible});
|
|
381
|
-
for (const Keyframe &frame : active.keyframes) {
|
|
382
|
-
if (frame.offset <= progress) continue;
|
|
383
|
-
const double offset = (frame.offset - progress) / (1 - progress);
|
|
384
|
-
remaining.push_back({offset, frame.presentation});
|
|
385
|
-
}
|
|
386
|
-
if (remaining.size() == 1) remaining.push_back({1, state.latest});
|
|
387
|
-
installed = [view smoothClipAnimateKeyframes:state.latest
|
|
388
|
-
keyframes:remaining
|
|
389
|
-
durationMs:remainingMs
|
|
390
|
-
animationId:active.animationId];
|
|
600
|
+
animation:active.spring
|
|
601
|
+
animationId:active.animationId
|
|
602
|
+
sharedBeginTime:sharedBeginTime];
|
|
391
603
|
}
|
|
392
604
|
if (installed) {
|
|
393
605
|
active.participants.insert(key);
|
|
394
|
-
active.suspendedParticipants.erase(key);
|
|
395
606
|
}
|
|
396
607
|
}
|
|
397
608
|
|
|
@@ -407,85 +618,11 @@ bool anyDisplayableView(const DriverState &state) {
|
|
|
407
618
|
return false;
|
|
408
619
|
}
|
|
409
620
|
|
|
410
|
-
void
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
SmoothClipView *preferred = nil) {
|
|
414
|
-
if (!state.animation.has_value() || !state.animation->started) return;
|
|
415
|
-
auto &active = *state.animation;
|
|
416
|
-
double springVelocity = 0;
|
|
417
|
-
if (active.kind == AnimationKind::Spring) {
|
|
418
|
-
SmoothClipView *source = preferred;
|
|
419
|
-
if (source == nil ||
|
|
420
|
-
active.participants.count(keyForView(source)) == 0) {
|
|
421
|
-
source = active.participants.empty()
|
|
422
|
-
? nil
|
|
423
|
-
: viewForKey(*active.participants.begin());
|
|
424
|
-
}
|
|
425
|
-
if (source != nil) {
|
|
426
|
-
springVelocity = [source smoothClipSpringContinuationVelocity];
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
const Presentation frozen = canonicalFrozenPresentation(state, preferred);
|
|
430
|
-
// Captured before the migration below: only hosts that were ALREADY
|
|
431
|
-
// suspended are unresolved. The active participants being frozen right now
|
|
432
|
-
// rendered every frame up to this instant.
|
|
433
|
-
const bool hadUnresolvedSuspended = !active.suspendedParticipants.empty();
|
|
434
|
-
active.suspendedParticipants.insert(
|
|
435
|
-
active.participants.begin(), active.participants.end());
|
|
436
|
-
active.participants.clear();
|
|
437
|
-
|
|
438
|
-
const double elapsedMs = MAX(
|
|
439
|
-
0, (CACurrentMediaTime() - active.startedAt) * 1000.0);
|
|
440
|
-
const double progress = active.durationMs <= 0
|
|
441
|
-
? 1
|
|
442
|
-
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
443
|
-
if (active.kind == AnimationKind::Timing) {
|
|
444
|
-
active.timing = timingRemainder(active.timing, progress).animation;
|
|
445
|
-
active.durationMs = active.timing.durationMs;
|
|
446
|
-
} else if (active.kind == AnimationKind::Keyframes) {
|
|
447
|
-
const KeyframeContinuation continuation = keyframeContinuation(
|
|
448
|
-
active.keyframes,
|
|
449
|
-
frozen,
|
|
450
|
-
state.latest,
|
|
451
|
-
active.durationMs,
|
|
452
|
-
progress);
|
|
453
|
-
active.keyframes = continuation.frames;
|
|
454
|
-
active.durationMs = continuation.durationMs;
|
|
455
|
-
} else {
|
|
456
|
-
active.spring.inheritVelocity = false;
|
|
457
|
-
active.spring.initialVelocity = springVelocity;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (state.destroyed ||
|
|
461
|
-
(active.kind != AnimationKind::Spring && active.durationMs <= 0)) {
|
|
462
|
-
// A residual trimmed to zero means the curve fully elapsed while a host
|
|
463
|
-
// displayed every frame — only the asynchronous didStop was outstanding
|
|
464
|
-
// when this freeze arrived. Report that run honestly instead of stamping
|
|
465
|
-
// the freeze-path false; hosts that were suspended before the freeze are
|
|
466
|
-
// unresolved and still poison it.
|
|
467
|
-
const bool ranToEnd = !state.destroyed && progress >= 1 &&
|
|
468
|
-
active.finished && !hadUnresolvedSuspended;
|
|
469
|
-
// Deferred peers hold pending installs for this id; invalidate them and
|
|
470
|
-
// finalize statically at the target, exactly as the didStop completion
|
|
471
|
-
// path does. Frozen participants already have no animation to cancel.
|
|
472
|
-
for (const ViewKey key : state.views) {
|
|
473
|
-
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
474
|
-
}
|
|
475
|
-
state.ownership = Ownership::Interactive;
|
|
476
|
-
emitCompletion(driverId, state, active.animationId, ranToEnd);
|
|
621
|
+
void startPendingAnimation(DriverState &state) {
|
|
622
|
+
if (state.animation->groupId != 0) {
|
|
623
|
+
tryStartGroup(state.animation->groupId);
|
|
477
624
|
return;
|
|
478
625
|
}
|
|
479
|
-
active.start = frozen;
|
|
480
|
-
active.started = false;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Starts a latched animation: rebases the clock so no progress was burned
|
|
484
|
-
// while no view could display, then dispatches the join to every registered
|
|
485
|
-
// view. Views that still cannot display defer their install per-view and
|
|
486
|
-
// resume via a later positive displayability update.
|
|
487
|
-
void startLatchedAnimation(DriverState &state) {
|
|
488
|
-
state.animation->startedAt = CACurrentMediaTime();
|
|
489
626
|
state.animation->started = true;
|
|
490
627
|
// Participants describe live CA delegates, not registered views. Anything
|
|
491
628
|
// left from the frozen run can only produce a stale callback for that run.
|
|
@@ -494,10 +631,55 @@ void startLatchedAnimation(DriverState &state) {
|
|
|
494
631
|
for (const ViewKey key : state.views) {
|
|
495
632
|
SmoothClipView *participant = viewForKey(key);
|
|
496
633
|
[participant smoothClipApplyPresentation:start recordVelocitySample:NO];
|
|
497
|
-
|
|
634
|
+
installPendingAnimation(state, participant, key, start);
|
|
498
635
|
}
|
|
499
636
|
}
|
|
500
637
|
|
|
638
|
+
void tryStartGroup(int32_t groupId) {
|
|
639
|
+
auto groupIterator = groupRegistry().find(groupId);
|
|
640
|
+
if (groupIterator == groupRegistry().end() ||
|
|
641
|
+
groupIterator->second.mutating) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const std::vector<uint64_t> driverIds = groupIterator->second.driverIds;
|
|
645
|
+
for (const uint64_t driverId : driverIds) {
|
|
646
|
+
const auto stateIterator = registry().find(driverId);
|
|
647
|
+
if (stateIterator == registry().end() ||
|
|
648
|
+
!stateIterator->second.animation.has_value() ||
|
|
649
|
+
stateIterator->second.animation->groupId != groupId ||
|
|
650
|
+
!driverReady(stateIterator->second)) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const CFTimeInterval sharedBeginTime = CACurrentMediaTime();
|
|
656
|
+
[CATransaction begin];
|
|
657
|
+
[CATransaction setDisableActions:YES];
|
|
658
|
+
for (const uint64_t driverId : driverIds) {
|
|
659
|
+
DriverState &state = registry().find(driverId)->second;
|
|
660
|
+
ActiveAnimation &active = *state.animation;
|
|
661
|
+
active.started = true;
|
|
662
|
+
active.participants.clear();
|
|
663
|
+
const Presentation start = active.start;
|
|
664
|
+
for (const ViewKey key : state.views) {
|
|
665
|
+
SmoothClipView *participant = viewForKey(key);
|
|
666
|
+
[participant smoothClipApplyPresentation:start recordVelocitySample:NO];
|
|
667
|
+
installPendingAnimation(
|
|
668
|
+
state, participant, key, start, sharedBeginTime);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
[CATransaction commit];
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
void finishGroupForHostLoss(int32_t groupId) {
|
|
675
|
+
auto groupIterator = groupRegistry().find(groupId);
|
|
676
|
+
if (groupIterator == groupRegistry().end() ||
|
|
677
|
+
groupIterator->second.mutating) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
cancelGroupInternal(groupId, GroupCancelBehavior::Freeze, false);
|
|
681
|
+
}
|
|
682
|
+
|
|
501
683
|
} // namespace
|
|
502
684
|
|
|
503
685
|
bool applicationIsActive() {
|
|
@@ -508,8 +690,19 @@ void applicationWillResignActive() {
|
|
|
508
690
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
509
691
|
if (!applicationActiveState()) return;
|
|
510
692
|
applicationActiveState() = false;
|
|
693
|
+
std::vector<int32_t> groupIds;
|
|
694
|
+
groupIds.reserve(groupRegistry().size());
|
|
695
|
+
for (const auto &[groupId, group] : groupRegistry()) {
|
|
696
|
+
(void)group;
|
|
697
|
+
groupIds.push_back(groupId);
|
|
698
|
+
}
|
|
699
|
+
for (const int32_t groupId : groupIds) {
|
|
700
|
+
finishGroupForHostLoss(groupId);
|
|
701
|
+
}
|
|
511
702
|
for (auto &[driverId, state] : registry()) {
|
|
512
|
-
|
|
703
|
+
if (!state.animation.has_value() || state.animation->groupId == 0) {
|
|
704
|
+
finishStandaloneAtTarget(driverId, state);
|
|
705
|
+
}
|
|
513
706
|
}
|
|
514
707
|
}
|
|
515
708
|
|
|
@@ -517,11 +710,20 @@ void applicationDidBecomeActive() {
|
|
|
517
710
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
518
711
|
if (applicationActiveState()) return;
|
|
519
712
|
applicationActiveState() = true;
|
|
713
|
+
std::vector<int32_t> groupIds;
|
|
714
|
+
groupIds.reserve(groupRegistry().size());
|
|
715
|
+
for (const auto &[groupId, group] : groupRegistry()) {
|
|
716
|
+
(void)group;
|
|
717
|
+
groupIds.push_back(groupId);
|
|
718
|
+
}
|
|
719
|
+
for (const int32_t groupId : groupIds) {
|
|
720
|
+
tryStartGroup(groupId);
|
|
721
|
+
}
|
|
520
722
|
for (auto &[driverId, state] : registry()) {
|
|
521
723
|
(void)driverId;
|
|
522
724
|
if (state.animation.has_value() && !state.animation->started &&
|
|
523
|
-
anyDisplayableView(state)) {
|
|
524
|
-
|
|
725
|
+
state.animation->groupId == 0 && anyDisplayableView(state)) {
|
|
726
|
+
startPendingAnimation(state);
|
|
525
727
|
}
|
|
526
728
|
}
|
|
527
729
|
}
|
|
@@ -541,13 +743,37 @@ void clearCompletionCallback(const void *owner) {
|
|
|
541
743
|
completionSink().callback = nullptr;
|
|
542
744
|
}
|
|
543
745
|
|
|
746
|
+
void setGroupCompletionCallback(
|
|
747
|
+
const void *owner,
|
|
748
|
+
GroupCompletionCallback callback) {
|
|
749
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
750
|
+
groupCompletionSink().owner = owner;
|
|
751
|
+
groupCompletionSink().callback = std::move(callback);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
void clearGroupCompletionCallback(const void *owner) {
|
|
755
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
756
|
+
if (groupCompletionSink().owner != owner) return;
|
|
757
|
+
groupCompletionSink().owner = nullptr;
|
|
758
|
+
groupCompletionSink().callback = nullptr;
|
|
759
|
+
}
|
|
760
|
+
|
|
544
761
|
void registerView(
|
|
545
762
|
uint64_t driverId,
|
|
546
763
|
SmoothClipView *view,
|
|
547
764
|
Presentation initialPresentation) {
|
|
548
765
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
766
|
+
if (!canonicalizePresentation(initialPresentation)) return;
|
|
549
767
|
auto &state = registry()[driverId];
|
|
550
768
|
state.destroyed = false;
|
|
769
|
+
const ViewKey key = keyForView(view);
|
|
770
|
+
const auto existing = std::find(state.views.begin(), state.views.end(), key);
|
|
771
|
+
if (existing == state.views.end() && !state.views.empty()) {
|
|
772
|
+
#if DEBUG
|
|
773
|
+
NSCAssert(NO, @"One SmoothClip controller cannot drive two mounted hosts");
|
|
774
|
+
#endif
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
551
777
|
// Seed canonical geometry from the mounting view's initial props BEFORE
|
|
552
778
|
// sampling `visible`; otherwise the first registration captures the default
|
|
553
779
|
// empty {0,0,0,0} rect, seeding the view (and the presentation layer the
|
|
@@ -557,57 +783,34 @@ void registerView(
|
|
|
557
783
|
state.latest = initialPresentation;
|
|
558
784
|
state.hasLatest = true;
|
|
559
785
|
}
|
|
560
|
-
const bool
|
|
561
|
-
|
|
562
|
-
// A
|
|
786
|
+
const bool hasPendingRun =
|
|
787
|
+
state.animation.has_value() && !state.animation->started;
|
|
788
|
+
// A pending run may only start once this view can produce a visible frame; a
|
|
563
789
|
// registration from a detached subtree (transparentModal before its view
|
|
564
|
-
// controller is presented) keeps
|
|
790
|
+
// controller is presented) keeps it waiting; a positive
|
|
565
791
|
// displayability update starts it inside the attach commit.
|
|
566
|
-
const bool
|
|
567
|
-
|
|
568
|
-
hasAnimation && state.animation->started && !state.views.empty();
|
|
792
|
+
const bool startsPendingRun =
|
|
793
|
+
hasPendingRun && [view smoothClipCanDisplay];
|
|
569
794
|
Presentation visible = state.latest;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
// A registered peer that never laid out reports zero geometry; joining
|
|
573
|
-
// from it would visibly collapse the clip. Prefer the first laid-out
|
|
574
|
-
// peer and fall back to the animation's start.
|
|
575
|
-
for (const ViewKey candidate : state.animation->participants) {
|
|
576
|
-
SmoothClipView *candidateView = viewForKey(candidate);
|
|
577
|
-
if ([candidateView smoothClipIsJoinable]) {
|
|
578
|
-
reference = candidateView;
|
|
579
|
-
break;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
visible = reference != nil
|
|
583
|
-
? [reference smoothClipCurrentPresentation]
|
|
584
|
-
: state.animation->start;
|
|
585
|
-
} else if (hasLatch) {
|
|
586
|
-
// state.latest already holds the latch's TARGET; applying it here is
|
|
795
|
+
if (hasPendingRun) {
|
|
796
|
+
// state.latest already holds the target; applying it here is
|
|
587
797
|
// exactly the jump this path exists to avoid.
|
|
588
798
|
visible = state.animation->start;
|
|
589
799
|
}
|
|
590
|
-
|
|
591
|
-
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
592
|
-
state.views.end()) {
|
|
800
|
+
if (existing == state.views.end()) {
|
|
593
801
|
state.views.push_back(key);
|
|
594
802
|
}
|
|
595
803
|
[view smoothClipApplyPresentation:visible recordVelocitySample:NO];
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
// that actually install their CA groups become completion participants.
|
|
599
|
-
startLatchedAnimation(state);
|
|
600
|
-
return;
|
|
804
|
+
if (startsPendingRun) {
|
|
805
|
+
startPendingAnimation(state);
|
|
601
806
|
}
|
|
602
|
-
if (!shouldReplay) return;
|
|
603
|
-
dispatchActiveAnimationJoin(state, view, key, visible, reference);
|
|
604
807
|
}
|
|
605
808
|
|
|
606
809
|
void viewDisplayabilityChanged(uint64_t driverId, SmoothClipView *view) {
|
|
607
810
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
608
811
|
auto iterator = registry().find(driverId);
|
|
609
812
|
if (iterator == registry().end()) return;
|
|
610
|
-
|
|
813
|
+
DriverState &state = iterator->second;
|
|
611
814
|
if (!state.animation.has_value()) return;
|
|
612
815
|
const ViewKey key = keyForView(view);
|
|
613
816
|
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
@@ -615,116 +818,39 @@ void viewDisplayabilityChanged(uint64_t driverId, SmoothClipView *view) {
|
|
|
615
818
|
return;
|
|
616
819
|
}
|
|
617
820
|
if (![view smoothClipCanDisplay]) {
|
|
618
|
-
if (
|
|
619
|
-
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
return;
|
|
821
|
+
if (state.animation->groupId != 0 && state.animation->started) {
|
|
822
|
+
finishGroupForHostLoss(state.animation->groupId);
|
|
823
|
+
} else if (state.animation->started) {
|
|
824
|
+
finishStandaloneAtTarget(driverId, state);
|
|
623
825
|
}
|
|
624
|
-
[view smoothClipFreezePresentation];
|
|
625
|
-
state.animation->participants.erase(key);
|
|
626
|
-
state.animation->suspendedParticipants.insert(key);
|
|
627
826
|
return;
|
|
628
827
|
}
|
|
629
828
|
if (!state.animation->started) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if ([view smoothClipHasPendingInstall] ||
|
|
635
|
-
state.animation->suspendedParticipants.count(key) > 0) {
|
|
636
|
-
joinActiveAnimation(driverId, view);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
bool joinActiveAnimation(uint64_t driverId, SmoothClipView *view) {
|
|
641
|
-
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
642
|
-
auto iterator = registry().find(driverId);
|
|
643
|
-
if (iterator == registry().end()) {
|
|
644
|
-
return false;
|
|
645
|
-
}
|
|
646
|
-
auto &state = iterator->second;
|
|
647
|
-
if (!state.animation.has_value() || state.views.empty()) {
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
const ViewKey key = keyForView(view);
|
|
651
|
-
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
652
|
-
state.views.end()) {
|
|
653
|
-
return false;
|
|
654
|
-
}
|
|
655
|
-
// A pending install used to imply the animation was dispatched, which
|
|
656
|
-
// implied started. Re-latching on displayability broke that: when the last
|
|
657
|
-
// displayable host leaves, a peer that deferred its install keeps
|
|
658
|
-
// `_pendingAnimationInstall` while the animation goes back to un-started, and
|
|
659
|
-
// its first layout lands here. Start the latch — returning false would make
|
|
660
|
-
// the caller clear `_activeAnimationId` and apply the requested geometry,
|
|
661
|
-
// snapping this view straight to the target. A *join* must still never
|
|
662
|
-
// compute elapsed time against an un-rebased latch clock, so this returns
|
|
663
|
-
// instead of falling through.
|
|
664
|
-
if (!state.animation->started) {
|
|
665
|
-
if (![view smoothClipCanDisplay]) return false;
|
|
666
|
-
startLatchedAnimation(state);
|
|
667
|
-
return true;
|
|
668
|
-
}
|
|
669
|
-
// Join from a laid-out peer's live presentation, exactly as a view that had
|
|
670
|
-
// registered at this moment would. When the joining view is the only
|
|
671
|
-
// registered host, its own layer never received geometry (the install was
|
|
672
|
-
// deferred for lack of layout), so fall back to the animation's start.
|
|
673
|
-
SmoothClipView *reference = nil;
|
|
674
|
-
for (const ViewKey candidate : state.animation->participants) {
|
|
675
|
-
if (candidate != key && [viewForKey(candidate) smoothClipIsJoinable]) {
|
|
676
|
-
reference = viewForKey(candidate);
|
|
677
|
-
break;
|
|
829
|
+
if (state.animation->groupId != 0) {
|
|
830
|
+
tryStartGroup(state.animation->groupId);
|
|
831
|
+
} else {
|
|
832
|
+
startPendingAnimation(state);
|
|
678
833
|
}
|
|
679
834
|
}
|
|
680
|
-
const Presentation visible = reference != nil
|
|
681
|
-
? [reference smoothClipCurrentPresentation]
|
|
682
|
-
: state.animation->start;
|
|
683
|
-
[view smoothClipApplyPresentation:visible recordVelocitySample:NO];
|
|
684
|
-
dispatchActiveAnimationJoin(state, view, key, visible, reference);
|
|
685
|
-
return true;
|
|
686
835
|
}
|
|
687
836
|
|
|
688
837
|
void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
689
838
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
690
839
|
auto iterator = registry().find(driverId);
|
|
691
840
|
if (iterator == registry().end()) return;
|
|
692
|
-
|
|
693
|
-
auto &state = iterator->second;
|
|
841
|
+
DriverState &state = iterator->second;
|
|
694
842
|
const ViewKey key = keyForView(view);
|
|
695
|
-
bool installed = false;
|
|
696
843
|
if (state.animation.has_value()) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (state.animation->started
|
|
700
|
-
|
|
701
|
-
bool otherDisplayable = false;
|
|
702
|
-
for (const ViewKey candidate : state.views) {
|
|
703
|
-
if (candidate != key && [viewForKey(candidate) smoothClipCanDisplay]) {
|
|
704
|
-
otherDisplayable = true;
|
|
705
|
-
break;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (!otherDisplayable) {
|
|
709
|
-
relatchActiveAnimation(driverId, state, view);
|
|
710
|
-
}
|
|
844
|
+
if (state.animation->groupId != 0 && state.animation->started) {
|
|
845
|
+
finishGroupForHostLoss(state.animation->groupId);
|
|
846
|
+
} else if (state.animation->groupId == 0 && state.animation->started) {
|
|
847
|
+
finishStandaloneAtTarget(driverId, state);
|
|
711
848
|
}
|
|
712
849
|
}
|
|
713
|
-
|
|
714
850
|
state.views.erase(
|
|
715
851
|
std::remove(state.views.begin(), state.views.end(), key),
|
|
716
852
|
state.views.end());
|
|
717
|
-
if (state.
|
|
718
|
-
state.animation->participants.erase(key);
|
|
719
|
-
state.animation->suspendedParticipants.erase(key);
|
|
720
|
-
if (installed) state.animation->finished = false;
|
|
721
|
-
if (state.animation->started && state.animation->participants.empty()) {
|
|
722
|
-
finishIfNoInstalledParticipants(driverId, state);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (state.destroyed && state.views.empty()) {
|
|
726
|
-
registry().erase(iterator);
|
|
727
|
-
}
|
|
853
|
+
if (state.destroyed && state.views.empty()) registry().erase(iterator);
|
|
728
854
|
}
|
|
729
855
|
|
|
730
856
|
void setPresentation(
|
|
@@ -733,6 +859,7 @@ void setPresentation(
|
|
|
733
859
|
bool takeOwnership,
|
|
734
860
|
bool overridePendingAnimation,
|
|
735
861
|
bool recordVelocity) {
|
|
862
|
+
if (!canonicalizePresentation(presentation)) return;
|
|
736
863
|
// CALayer writes require the main thread (not main-queue drain context).
|
|
737
864
|
if (!NSThread.isMainThread) {
|
|
738
865
|
// Never block an off-main caller: a synchronous hop can deadlock against
|
|
@@ -784,7 +911,7 @@ void setPresentation(
|
|
|
784
911
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
785
912
|
if (signpostsEnabled) {
|
|
786
913
|
os_signpost_interval_end(
|
|
787
|
-
signpostLog(), identifier, "lookup-fanout", "ignored=
|
|
914
|
+
signpostLog(), identifier, "lookup-fanout", "ignored=pending-run");
|
|
788
915
|
}
|
|
789
916
|
#endif
|
|
790
917
|
return;
|
|
@@ -806,7 +933,8 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
806
933
|
// See setPresentation: blocking off-main risks a deadlock, so the call
|
|
807
934
|
// fails defined and the JS side keeps its current value.
|
|
808
935
|
return unavailablePresentation();
|
|
809
|
-
}
|
|
936
|
+
}
|
|
937
|
+
auto iterator = registry().find(driverId);
|
|
810
938
|
if (iterator == registry().end()) return unavailablePresentation();
|
|
811
939
|
auto &state = iterator->second;
|
|
812
940
|
state.destroyed = false;
|
|
@@ -815,6 +943,16 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
815
943
|
return state.latest;
|
|
816
944
|
}
|
|
817
945
|
|
|
946
|
+
if (state.animation->groupId != 0) {
|
|
947
|
+
const int32_t groupId = state.animation->groupId;
|
|
948
|
+
cancelGroupInternal(
|
|
949
|
+
groupId, GroupCancelBehavior::Freeze, false);
|
|
950
|
+
const auto current = registry().find(driverId);
|
|
951
|
+
return current == registry().end()
|
|
952
|
+
? unavailablePresentation()
|
|
953
|
+
: current->second.latest;
|
|
954
|
+
}
|
|
955
|
+
|
|
818
956
|
const int32_t animationId = state.animation->animationId;
|
|
819
957
|
const Presentation canonical = canonicalFrozenPresentation(state);
|
|
820
958
|
state.latest = canonical;
|
|
@@ -824,19 +962,393 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
824
962
|
return canonical;
|
|
825
963
|
}
|
|
826
964
|
|
|
965
|
+
Presentation snapshotCurrent(uint64_t driverId) {
|
|
966
|
+
if (!NSThread.isMainThread) return unavailablePresentation();
|
|
967
|
+
const auto iterator = registry().find(driverId);
|
|
968
|
+
if (iterator == registry().end() || iterator->second.destroyed) {
|
|
969
|
+
return unavailablePresentation();
|
|
970
|
+
}
|
|
971
|
+
return canonicalVisiblePresentation(iterator->second);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
std::vector<DriverSnapshot> snapshotGroup(
|
|
975
|
+
const std::vector<uint64_t> &driverIds) {
|
|
976
|
+
if (!NSThread.isMainThread) return {};
|
|
977
|
+
std::vector<DriverSnapshot> snapshots;
|
|
978
|
+
snapshots.reserve(driverIds.size());
|
|
979
|
+
for (const uint64_t driverId : driverIds) {
|
|
980
|
+
snapshots.push_back(snapshotForDriver(driverId));
|
|
981
|
+
}
|
|
982
|
+
return snapshots;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
std::vector<DriverSnapshot> beginGroupInteraction(
|
|
986
|
+
const std::vector<uint64_t> &driverIds) {
|
|
987
|
+
if (!NSThread.isMainThread) return {};
|
|
988
|
+
std::unordered_set<uint64_t> unique;
|
|
989
|
+
for (const uint64_t driverId : driverIds) {
|
|
990
|
+
const auto iterator = registry().find(driverId);
|
|
991
|
+
if (driverId == 0 || !unique.insert(driverId).second ||
|
|
992
|
+
iterator == registry().end() || iterator->second.destroyed) {
|
|
993
|
+
std::vector<DriverSnapshot> unavailable;
|
|
994
|
+
unavailable.reserve(driverIds.size());
|
|
995
|
+
for (const uint64_t requested : driverIds) {
|
|
996
|
+
unavailable.push_back(
|
|
997
|
+
{requested, unavailablePresentation(), false});
|
|
998
|
+
}
|
|
999
|
+
return unavailable;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
std::unordered_set<int32_t> cancelledGroups;
|
|
1004
|
+
for (const uint64_t driverId : driverIds) {
|
|
1005
|
+
DriverState &state = registry().find(driverId)->second;
|
|
1006
|
+
if (!state.animation.has_value()) continue;
|
|
1007
|
+
const int32_t groupId = state.animation->groupId;
|
|
1008
|
+
if (groupId != 0) {
|
|
1009
|
+
if (cancelledGroups.insert(groupId).second) {
|
|
1010
|
+
cancelGroupInternal(
|
|
1011
|
+
groupId, GroupCancelBehavior::Freeze, false);
|
|
1012
|
+
}
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const int32_t animationId = state.animation->animationId;
|
|
1016
|
+
const Presentation frozen = canonicalFrozenPresentation(state);
|
|
1017
|
+
state.latest = frozen;
|
|
1018
|
+
state.hasLatest = true;
|
|
1019
|
+
state.ownership = Ownership::Interactive;
|
|
1020
|
+
emitCompletion(driverId, state, animationId, false);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
std::vector<DriverSnapshot> snapshots;
|
|
1024
|
+
snapshots.reserve(driverIds.size());
|
|
1025
|
+
for (const uint64_t driverId : driverIds) {
|
|
1026
|
+
DriverState &state = registry().find(driverId)->second;
|
|
1027
|
+
state.destroyed = false;
|
|
1028
|
+
state.ownership = Ownership::Interactive;
|
|
1029
|
+
snapshots.push_back(
|
|
1030
|
+
{driverId, state.latest, driverReady(state)});
|
|
1031
|
+
}
|
|
1032
|
+
return snapshots;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
bool setPresentationBatch(const std::vector<BatchEntry> &entries) {
|
|
1036
|
+
if (!NSThread.isMainThread) return false;
|
|
1037
|
+
std::vector<BatchEntry> canonicalEntries = entries;
|
|
1038
|
+
for (BatchEntry &entry : canonicalEntries) {
|
|
1039
|
+
if (!canonicalizePresentation(entry.presentation)) return false;
|
|
1040
|
+
}
|
|
1041
|
+
std::unordered_set<uint64_t> unique;
|
|
1042
|
+
for (const BatchEntry &entry : canonicalEntries) {
|
|
1043
|
+
const auto iterator = registry().find(entry.driverId);
|
|
1044
|
+
if (entry.driverId == 0 || !unique.insert(entry.driverId).second ||
|
|
1045
|
+
iterator == registry().end() || iterator->second.destroyed ||
|
|
1046
|
+
!isFinitePresentation(entry.presentation)) {
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
std::unordered_set<int32_t> cancelledGroups;
|
|
1052
|
+
for (const BatchEntry &entry : canonicalEntries) {
|
|
1053
|
+
DriverState &state = registry().find(entry.driverId)->second;
|
|
1054
|
+
if (!state.animation.has_value()) continue;
|
|
1055
|
+
const int32_t groupId = state.animation->groupId;
|
|
1056
|
+
if (groupId != 0) {
|
|
1057
|
+
if (cancelledGroups.insert(groupId).second) {
|
|
1058
|
+
cancelGroupInternal(
|
|
1059
|
+
groupId, GroupCancelBehavior::Freeze, false);
|
|
1060
|
+
}
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const int32_t animationId = state.animation->animationId;
|
|
1064
|
+
const Presentation frozen = canonicalFrozenPresentation(state);
|
|
1065
|
+
state.latest = frozen;
|
|
1066
|
+
state.hasLatest = true;
|
|
1067
|
+
state.ownership = Ownership::Interactive;
|
|
1068
|
+
emitCompletion(entry.driverId, state, animationId, false);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
[CATransaction begin];
|
|
1072
|
+
[CATransaction setDisableActions:YES];
|
|
1073
|
+
for (const BatchEntry &entry : canonicalEntries) {
|
|
1074
|
+
applyPresentation(
|
|
1075
|
+
registry().find(entry.driverId)->second, entry.presentation, true);
|
|
1076
|
+
}
|
|
1077
|
+
[CATransaction commit];
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
namespace {
|
|
1082
|
+
|
|
1083
|
+
bool preflightGroupEntries(
|
|
1084
|
+
std::vector<GroupMotionEntry> &entries,
|
|
1085
|
+
AnimationKind kind,
|
|
1086
|
+
const TimingAnimation &timing,
|
|
1087
|
+
const SpringAnimation &spring,
|
|
1088
|
+
double durationMs,
|
|
1089
|
+
int32_t reduceMotion,
|
|
1090
|
+
bool &reduced,
|
|
1091
|
+
std::vector<Presentation> &resolvedStarts) {
|
|
1092
|
+
if (entries.empty()) return false;
|
|
1093
|
+
for (GroupMotionEntry &entry : entries) {
|
|
1094
|
+
if ((entry.hasFrom && !canonicalizePresentation(entry.from)) ||
|
|
1095
|
+
!canonicalizePresentation(entry.target)) {
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (kind == AnimationKind::Timing &&
|
|
1100
|
+
!isValidTiming(timing)) {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
if (kind == AnimationKind::Spring &&
|
|
1104
|
+
(!isValidSpring(spring) || spring.inheritVelocity)) {
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
reduced = shouldReduceMotion(reduceMotion) ||
|
|
1108
|
+
(kind != AnimationKind::Spring && durationMs <= 0);
|
|
1109
|
+
std::unordered_set<uint64_t> unique;
|
|
1110
|
+
resolvedStarts.clear();
|
|
1111
|
+
resolvedStarts.reserve(entries.size());
|
|
1112
|
+
for (const GroupMotionEntry &entry : entries) {
|
|
1113
|
+
if (entry.driverId == 0 || !unique.insert(entry.driverId).second) {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
const auto iterator = registry().find(entry.driverId);
|
|
1117
|
+
if ((iterator == registry().end() || iterator->second.destroyed ||
|
|
1118
|
+
!iterator->second.hasLatest) && !entry.hasFrom) {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
const Presentation resolvedStart = entry.hasFrom
|
|
1122
|
+
? entry.from
|
|
1123
|
+
: canonicalVisiblePresentation(iterator->second);
|
|
1124
|
+
if (!isFinitePresentation(resolvedStart) ||
|
|
1125
|
+
!isFinitePresentation(entry.target) ||
|
|
1126
|
+
!isAutonomousUniformCircular(resolvedStart) ||
|
|
1127
|
+
!isAutonomousUniformCircular(entry.target)) {
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
if (kind == AnimationKind::Spring) {
|
|
1131
|
+
const double velocity = spring.initialVelocity;
|
|
1132
|
+
if (!springScaleStaysPositive(
|
|
1133
|
+
resolvedStart, entry.target, spring, velocity)) {
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
resolvedStarts.push_back(resolvedStart);
|
|
1138
|
+
}
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
void freezeReplacedAnimations(
|
|
1143
|
+
const std::vector<GroupMotionEntry> &entries) {
|
|
1144
|
+
std::unordered_set<int32_t> cancelledGroups;
|
|
1145
|
+
for (const GroupMotionEntry &entry : entries) {
|
|
1146
|
+
auto iterator = registry().find(entry.driverId);
|
|
1147
|
+
if (iterator == registry().end() ||
|
|
1148
|
+
!iterator->second.animation.has_value()) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
DriverState &state = iterator->second;
|
|
1152
|
+
const int32_t oldGroupId = state.animation->groupId;
|
|
1153
|
+
if (oldGroupId != 0) {
|
|
1154
|
+
if (cancelledGroups.insert(oldGroupId).second) {
|
|
1155
|
+
cancelGroupInternal(
|
|
1156
|
+
oldGroupId, GroupCancelBehavior::Freeze, false);
|
|
1157
|
+
}
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
const int32_t animationId = state.animation->animationId;
|
|
1161
|
+
const Presentation frozen = canonicalFrozenPresentation(state);
|
|
1162
|
+
state.latest = frozen;
|
|
1163
|
+
state.hasLatest = true;
|
|
1164
|
+
state.ownership = Ownership::Interactive;
|
|
1165
|
+
emitCompletion(entry.driverId, state, animationId, false);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
int32_t createGroupAnimation(
|
|
1170
|
+
uint64_t controllerId,
|
|
1171
|
+
std::vector<GroupMotionEntry> entries,
|
|
1172
|
+
AnimationKind kind,
|
|
1173
|
+
TimingAnimation timing,
|
|
1174
|
+
SpringAnimation spring,
|
|
1175
|
+
double durationMs,
|
|
1176
|
+
int32_t reduceMotion,
|
|
1177
|
+
int32_t completionTag) {
|
|
1178
|
+
std::vector<Presentation> resolvedStarts;
|
|
1179
|
+
bool reduced = false;
|
|
1180
|
+
if (!NSThread.isMainThread || controllerId == 0 ||
|
|
1181
|
+
!preflightGroupEntries(
|
|
1182
|
+
entries,
|
|
1183
|
+
kind,
|
|
1184
|
+
timing,
|
|
1185
|
+
spring,
|
|
1186
|
+
durationMs,
|
|
1187
|
+
reduceMotion,
|
|
1188
|
+
reduced,
|
|
1189
|
+
resolvedStarts)) {
|
|
1190
|
+
return 0;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// No state changes occur before every entry is known to be viable. Once
|
|
1194
|
+
// preflight succeeds, replacement freezes each overlapping group at most
|
|
1195
|
+
// once and reports its stale completion before installing the new epoch.
|
|
1196
|
+
freezeReplacedAnimations(entries);
|
|
1197
|
+
const int32_t groupId = allocateAnimationId();
|
|
1198
|
+
std::vector<uint64_t> driverIds;
|
|
1199
|
+
driverIds.reserve(entries.size());
|
|
1200
|
+
for (const GroupMotionEntry &entry : entries) {
|
|
1201
|
+
driverIds.push_back(entry.driverId);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (reduced) {
|
|
1205
|
+
[CATransaction begin];
|
|
1206
|
+
[CATransaction setDisableActions:YES];
|
|
1207
|
+
for (const GroupMotionEntry &entry : entries) {
|
|
1208
|
+
auto iterator = registry().find(entry.driverId);
|
|
1209
|
+
if (iterator == registry().end()) {
|
|
1210
|
+
iterator = registry().try_emplace(entry.driverId).first;
|
|
1211
|
+
}
|
|
1212
|
+
DriverState &state = iterator->second;
|
|
1213
|
+
state.destroyed = false;
|
|
1214
|
+
applyPresentation(state, entry.target, false);
|
|
1215
|
+
}
|
|
1216
|
+
[CATransaction commit];
|
|
1217
|
+
emitGroupCompletion(
|
|
1218
|
+
controllerId, groupId, completionTag, true, driverIds);
|
|
1219
|
+
return groupId;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
GroupState group{
|
|
1223
|
+
controllerId,
|
|
1224
|
+
groupId,
|
|
1225
|
+
completionTag,
|
|
1226
|
+
driverIds,
|
|
1227
|
+
std::unordered_set<uint64_t>(driverIds.begin(), driverIds.end()),
|
|
1228
|
+
};
|
|
1229
|
+
groupRegistry().emplace(groupId, std::move(group));
|
|
1230
|
+
|
|
1231
|
+
for (std::size_t entryIndex = 0; entryIndex < entries.size(); entryIndex += 1) {
|
|
1232
|
+
GroupMotionEntry &entry = entries[entryIndex];
|
|
1233
|
+
auto iterator = registry().find(entry.driverId);
|
|
1234
|
+
if (iterator == registry().end()) {
|
|
1235
|
+
iterator = registry().try_emplace(entry.driverId).first;
|
|
1236
|
+
}
|
|
1237
|
+
DriverState &state = iterator->second;
|
|
1238
|
+
state.destroyed = false;
|
|
1239
|
+
if (entry.hasFrom) {
|
|
1240
|
+
applyPresentation(state, entry.from, true);
|
|
1241
|
+
}
|
|
1242
|
+
const Presentation start = resolvedStarts[entryIndex];
|
|
1243
|
+
state.latest = entry.target;
|
|
1244
|
+
state.hasLatest = true;
|
|
1245
|
+
state.ownership = Ownership::Native;
|
|
1246
|
+
|
|
1247
|
+
ActiveAnimation active{groupId};
|
|
1248
|
+
active.groupId = groupId;
|
|
1249
|
+
active.kind = kind;
|
|
1250
|
+
active.start = start;
|
|
1251
|
+
active.timing = timing;
|
|
1252
|
+
active.spring = spring;
|
|
1253
|
+
if (kind == AnimationKind::Spring) {
|
|
1254
|
+
active.spring.initialVelocity = spring.initialVelocity;
|
|
1255
|
+
active.spring.inheritVelocity = false;
|
|
1256
|
+
}
|
|
1257
|
+
active.started = false;
|
|
1258
|
+
state.animation = std::move(active);
|
|
1259
|
+
}
|
|
1260
|
+
tryStartGroup(groupId);
|
|
1261
|
+
return groupId;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
} // namespace
|
|
1265
|
+
|
|
1266
|
+
int32_t animateTimingGroup(
|
|
1267
|
+
uint64_t controllerId,
|
|
1268
|
+
std::vector<GroupMotionEntry> entries,
|
|
1269
|
+
TimingAnimation animation,
|
|
1270
|
+
int32_t completionTag,
|
|
1271
|
+
double startedAtHintS) {
|
|
1272
|
+
(void)startedAtHintS;
|
|
1273
|
+
return createGroupAnimation(
|
|
1274
|
+
controllerId,
|
|
1275
|
+
std::move(entries),
|
|
1276
|
+
AnimationKind::Timing,
|
|
1277
|
+
animation,
|
|
1278
|
+
{},
|
|
1279
|
+
animation.durationMs,
|
|
1280
|
+
animation.reduceMotion,
|
|
1281
|
+
completionTag);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
int32_t animateSpringGroup(
|
|
1285
|
+
uint64_t controllerId,
|
|
1286
|
+
std::vector<GroupMotionEntry> entries,
|
|
1287
|
+
SpringAnimation animation,
|
|
1288
|
+
int32_t completionTag,
|
|
1289
|
+
double startedAtHintS) {
|
|
1290
|
+
(void)startedAtHintS;
|
|
1291
|
+
return createGroupAnimation(
|
|
1292
|
+
controllerId,
|
|
1293
|
+
std::move(entries),
|
|
1294
|
+
AnimationKind::Spring,
|
|
1295
|
+
{},
|
|
1296
|
+
animation,
|
|
1297
|
+
0,
|
|
1298
|
+
animation.reduceMotion,
|
|
1299
|
+
completionTag);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
std::vector<DriverSnapshot> cancelAnimationGroup(
|
|
1303
|
+
int32_t groupId,
|
|
1304
|
+
GroupCancelBehavior behavior) {
|
|
1305
|
+
if (!NSThread.isMainThread || groupId <= 0) return {};
|
|
1306
|
+
return cancelGroupInternal(
|
|
1307
|
+
groupId,
|
|
1308
|
+
behavior,
|
|
1309
|
+
behavior == GroupCancelBehavior::Finish);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
827
1312
|
int32_t animateTiming(
|
|
828
1313
|
uint64_t driverId,
|
|
829
1314
|
AnimationStart start,
|
|
830
1315
|
Presentation presentation,
|
|
831
|
-
TimingAnimation animation
|
|
1316
|
+
TimingAnimation animation,
|
|
1317
|
+
int32_t completionTag) {
|
|
832
1318
|
if (!NSThread.isMainThread) {
|
|
833
1319
|
// See setPresentation: blocking off-main risks a deadlock. 0 is the
|
|
834
1320
|
// documented rejection sentinel.
|
|
835
1321
|
return 0;
|
|
836
1322
|
}
|
|
1323
|
+
if (!canonicalizePresentation(presentation) ||
|
|
1324
|
+
(start.hasInteractiveStart &&
|
|
1325
|
+
!canonicalizePresentation(start.interactiveStart))) {
|
|
1326
|
+
return 0;
|
|
1327
|
+
}
|
|
1328
|
+
const bool validAnimation = isValidTiming(animation);
|
|
1329
|
+
if (driverId == 0 || !isFinitePresentation(presentation) ||
|
|
1330
|
+
!validAnimation) {
|
|
1331
|
+
return 0;
|
|
1332
|
+
}
|
|
837
1333
|
auto iterator = registry().find(driverId);
|
|
1334
|
+
Presentation preflightStart;
|
|
1335
|
+
if (iterator == registry().end()) {
|
|
1336
|
+
if (!start.hasInteractiveStart ||
|
|
1337
|
+
!isFinitePresentation(start.interactiveStart)) {
|
|
1338
|
+
return 0;
|
|
1339
|
+
}
|
|
1340
|
+
preflightStart = start.interactiveStart;
|
|
1341
|
+
} else {
|
|
1342
|
+
if (iterator->second.destroyed && !start.hasInteractiveStart) return 0;
|
|
1343
|
+
preflightStart = resolvedAnimationStart(iterator->second, start);
|
|
1344
|
+
}
|
|
1345
|
+
if (!isFinitePresentation(preflightStart) ||
|
|
1346
|
+
preflightStart.clip.curve != presentation.clip.curve) {
|
|
1347
|
+
return 0;
|
|
1348
|
+
}
|
|
1349
|
+
const bool reduced = shouldReduceMotion(animation.reduceMotion) ||
|
|
1350
|
+
animation.durationMs <= 0;
|
|
838
1351
|
if (iterator == registry().end()) {
|
|
839
|
-
if (!start.hasInteractiveStart) return 0;
|
|
840
1352
|
iterator = registry().try_emplace(driverId).first;
|
|
841
1353
|
iterator->second.latest = start.interactiveStart;
|
|
842
1354
|
iterator->second.hasLatest = true;
|
|
@@ -844,30 +1356,27 @@ int32_t animateTiming(
|
|
|
844
1356
|
auto &state = iterator->second;
|
|
845
1357
|
state.destroyed = false;
|
|
846
1358
|
const int32_t animationId = allocateAnimationId();
|
|
847
|
-
const Presentation resolvedStart =
|
|
1359
|
+
const Presentation resolvedStart = preflightStart;
|
|
848
1360
|
prepareAnimation(driverId, state, start, presentation);
|
|
849
1361
|
|
|
850
|
-
if (
|
|
1362
|
+
if (reduced) {
|
|
851
1363
|
applyPresentation(state, presentation, false);
|
|
852
|
-
|
|
1364
|
+
emitStandaloneCompletion(
|
|
1365
|
+
driverId, animationId, completionTag, true);
|
|
853
1366
|
return animationId;
|
|
854
1367
|
}
|
|
855
1368
|
|
|
856
1369
|
ActiveAnimation active{animationId};
|
|
1370
|
+
active.completionTag = completionTag;
|
|
857
1371
|
active.kind = AnimationKind::Timing;
|
|
858
1372
|
active.start = resolvedStart;
|
|
859
1373
|
active.timing = animation;
|
|
860
|
-
active.durationMs = animation.durationMs;
|
|
861
|
-
active.startedAt = CACurrentMediaTime();
|
|
862
1374
|
active.started = anyDisplayableView(state);
|
|
863
1375
|
state.animation = std::move(active);
|
|
864
1376
|
if (!state.animation->started) {
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
// displayable registration/attach rebases startedAt and starts it. The
|
|
869
|
-
// non-zero id is still returned synchronously so the JS side does not
|
|
870
|
-
// treat this as rejection.
|
|
1377
|
+
// No host can display yet. Ownership stays Native and the first
|
|
1378
|
+
// displayable registration starts the full run. A non-zero ID still
|
|
1379
|
+
// distinguishes this pending run from rejection.
|
|
871
1380
|
return animationId;
|
|
872
1381
|
}
|
|
873
1382
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
@@ -891,14 +1400,44 @@ int32_t animateSpring(
|
|
|
891
1400
|
uint64_t driverId,
|
|
892
1401
|
AnimationStart start,
|
|
893
1402
|
Presentation presentation,
|
|
894
|
-
SpringAnimation animation
|
|
1403
|
+
SpringAnimation animation,
|
|
1404
|
+
int32_t completionTag) {
|
|
895
1405
|
if (!NSThread.isMainThread) {
|
|
896
1406
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
897
1407
|
return 0;
|
|
898
1408
|
}
|
|
1409
|
+
if (!canonicalizePresentation(presentation) ||
|
|
1410
|
+
(start.hasInteractiveStart &&
|
|
1411
|
+
!canonicalizePresentation(start.interactiveStart))) {
|
|
1412
|
+
return 0;
|
|
1413
|
+
}
|
|
1414
|
+
const bool validAnimation = isValidSpring(animation);
|
|
1415
|
+
if (driverId == 0 || !isFinitePresentation(presentation) ||
|
|
1416
|
+
!validAnimation) {
|
|
1417
|
+
return 0;
|
|
1418
|
+
}
|
|
899
1419
|
auto iterator = registry().find(driverId);
|
|
1420
|
+
Presentation preflightStart;
|
|
1421
|
+
if (iterator == registry().end()) {
|
|
1422
|
+
if (!start.hasInteractiveStart ||
|
|
1423
|
+
!isFinitePresentation(start.interactiveStart)) {
|
|
1424
|
+
return 0;
|
|
1425
|
+
}
|
|
1426
|
+
preflightStart = start.interactiveStart;
|
|
1427
|
+
} else {
|
|
1428
|
+
if (iterator->second.destroyed && !start.hasInteractiveStart) return 0;
|
|
1429
|
+
preflightStart = resolvedAnimationStart(iterator->second, start);
|
|
1430
|
+
}
|
|
1431
|
+
const double velocity = iterator == registry().end()
|
|
1432
|
+
? animation.initialVelocity
|
|
1433
|
+
: resolvedSpringVelocity(iterator->second, presentation, animation);
|
|
1434
|
+
if (!isFinitePresentation(preflightStart) ||
|
|
1435
|
+
preflightStart.clip.curve != presentation.clip.curve ||
|
|
1436
|
+
!springScaleStaysPositive(
|
|
1437
|
+
preflightStart, presentation, animation, velocity)) {
|
|
1438
|
+
return 0;
|
|
1439
|
+
}
|
|
900
1440
|
if (iterator == registry().end()) {
|
|
901
|
-
if (!start.hasInteractiveStart) return 0;
|
|
902
1441
|
iterator = registry().try_emplace(driverId).first;
|
|
903
1442
|
iterator->second.latest = start.interactiveStart;
|
|
904
1443
|
iterator->second.hasLatest = true;
|
|
@@ -906,24 +1445,27 @@ int32_t animateSpring(
|
|
|
906
1445
|
auto &state = iterator->second;
|
|
907
1446
|
state.destroyed = false;
|
|
908
1447
|
const int32_t animationId = allocateAnimationId();
|
|
909
|
-
const Presentation resolvedStart =
|
|
1448
|
+
const Presentation resolvedStart = preflightStart;
|
|
910
1449
|
prepareAnimation(driverId, state, start, presentation);
|
|
911
1450
|
|
|
912
1451
|
if (shouldReduceMotion(animation.reduceMotion)) {
|
|
913
1452
|
applyPresentation(state, presentation, false);
|
|
914
|
-
|
|
1453
|
+
emitStandaloneCompletion(
|
|
1454
|
+
driverId, animationId, completionTag, true);
|
|
915
1455
|
return animationId;
|
|
916
1456
|
}
|
|
917
1457
|
|
|
918
1458
|
ActiveAnimation active{animationId};
|
|
1459
|
+
active.completionTag = completionTag;
|
|
919
1460
|
active.kind = AnimationKind::Spring;
|
|
920
1461
|
active.start = resolvedStart;
|
|
921
1462
|
active.spring = animation;
|
|
922
|
-
active.
|
|
1463
|
+
active.spring.initialVelocity = velocity;
|
|
1464
|
+
active.spring.inheritVelocity = false;
|
|
923
1465
|
active.started = anyDisplayableView(state);
|
|
924
1466
|
state.animation = std::move(active);
|
|
925
1467
|
if (!state.animation->started) {
|
|
926
|
-
//
|
|
1468
|
+
// Pending until the first displayable view — see animateTiming.
|
|
927
1469
|
return animationId;
|
|
928
1470
|
}
|
|
929
1471
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
@@ -935,7 +1477,7 @@ int32_t animateSpring(
|
|
|
935
1477
|
#endif
|
|
936
1478
|
for (const ViewKey key : state.views) {
|
|
937
1479
|
if ([viewForKey(key) smoothClipAnimateSpring:presentation
|
|
938
|
-
animation:animation
|
|
1480
|
+
animation:state.animation->spring
|
|
939
1481
|
animationId:animationId]) {
|
|
940
1482
|
state.animation->participants.insert(key);
|
|
941
1483
|
}
|
|
@@ -943,58 +1485,6 @@ int32_t animateSpring(
|
|
|
943
1485
|
return animationId;
|
|
944
1486
|
}
|
|
945
1487
|
|
|
946
|
-
int32_t animateKeyframes(
|
|
947
|
-
uint64_t driverId,
|
|
948
|
-
AnimationStart start,
|
|
949
|
-
Presentation presentation,
|
|
950
|
-
double durationMs,
|
|
951
|
-
std::vector<Keyframe> keyframes,
|
|
952
|
-
int32_t reduceMotion) {
|
|
953
|
-
if (!NSThread.isMainThread) {
|
|
954
|
-
// See setPresentation: blocking off-main risks a deadlock.
|
|
955
|
-
return 0;
|
|
956
|
-
}
|
|
957
|
-
auto iterator = registry().find(driverId);
|
|
958
|
-
if (iterator == registry().end()) {
|
|
959
|
-
if (!start.hasInteractiveStart) return 0;
|
|
960
|
-
iterator = registry().try_emplace(driverId).first;
|
|
961
|
-
iterator->second.latest = start.interactiveStart;
|
|
962
|
-
iterator->second.hasLatest = true;
|
|
963
|
-
}
|
|
964
|
-
auto &state = iterator->second;
|
|
965
|
-
state.destroyed = false;
|
|
966
|
-
const int32_t animationId = allocateAnimationId();
|
|
967
|
-
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
968
|
-
prepareAnimation(driverId, state, start, presentation);
|
|
969
|
-
if (shouldReduceMotion(reduceMotion) || durationMs <= 0) {
|
|
970
|
-
applyPresentation(state, presentation, false);
|
|
971
|
-
emitCompletion(driverId, state, animationId, true);
|
|
972
|
-
return animationId;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
ActiveAnimation active{animationId};
|
|
976
|
-
active.kind = AnimationKind::Keyframes;
|
|
977
|
-
active.start = resolvedStart;
|
|
978
|
-
active.keyframes = keyframes;
|
|
979
|
-
active.durationMs = durationMs;
|
|
980
|
-
active.startedAt = CACurrentMediaTime();
|
|
981
|
-
active.started = anyDisplayableView(state);
|
|
982
|
-
state.animation = std::move(active);
|
|
983
|
-
if (!state.animation->started) {
|
|
984
|
-
// Latched until the first displayable view — see animateTiming.
|
|
985
|
-
return animationId;
|
|
986
|
-
}
|
|
987
|
-
for (const ViewKey key : state.views) {
|
|
988
|
-
if ([viewForKey(key) smoothClipAnimateKeyframes:presentation
|
|
989
|
-
keyframes:keyframes
|
|
990
|
-
durationMs:durationMs
|
|
991
|
-
animationId:animationId]) {
|
|
992
|
-
state.animation->participants.insert(key);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
return animationId;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
1488
|
int32_t rejectAnimation(uint64_t driverId) {
|
|
999
1489
|
if (!NSThread.isMainThread) {
|
|
1000
1490
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
@@ -1002,7 +1492,7 @@ int32_t rejectAnimation(uint64_t driverId) {
|
|
|
1002
1492
|
} auto iterator = registry().find(driverId);
|
|
1003
1493
|
if (iterator == registry().end()) return 0;
|
|
1004
1494
|
const int32_t animationId = allocateAnimationId();
|
|
1005
|
-
emitStandaloneCompletion(driverId, animationId, false);
|
|
1495
|
+
emitStandaloneCompletion(driverId, animationId, 0, false);
|
|
1006
1496
|
return animationId;
|
|
1007
1497
|
}
|
|
1008
1498
|
|
|
@@ -1022,6 +1512,20 @@ CancelResult cancelAnimation(
|
|
|
1022
1512
|
(animationId > 0 && animationId != state.animation->animationId)) {
|
|
1023
1513
|
return {false, state.latest};
|
|
1024
1514
|
}
|
|
1515
|
+
if (state.animation->groupId != 0) {
|
|
1516
|
+
const int32_t groupId = state.animation->groupId;
|
|
1517
|
+
const std::vector<DriverSnapshot> snapshots = cancelGroupInternal(
|
|
1518
|
+
groupId,
|
|
1519
|
+
useTarget ? GroupCancelBehavior::Finish
|
|
1520
|
+
: GroupCancelBehavior::Freeze,
|
|
1521
|
+
useTarget);
|
|
1522
|
+
for (const DriverSnapshot &snapshot : snapshots) {
|
|
1523
|
+
if (snapshot.driverId == driverId) {
|
|
1524
|
+
return {true, snapshot.presentation};
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return {false, state.latest};
|
|
1528
|
+
}
|
|
1025
1529
|
|
|
1026
1530
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
1027
1531
|
os_signpost_event_emit(
|
|
@@ -1031,7 +1535,7 @@ CancelResult cancelAnimation(
|
|
|
1031
1535
|
#endif
|
|
1032
1536
|
if (useTarget) {
|
|
1033
1537
|
const Presentation target = state.latest;
|
|
1034
|
-
// A
|
|
1538
|
+
// A pending run never dispatched to its view, so the host still has
|
|
1035
1539
|
// _activeAnimationId == 0 and smoothClipCancelAnimationUsingTarget
|
|
1036
1540
|
// early-returns without writing anything: state.latest would report the
|
|
1037
1541
|
// target while every layer still showed the pre-animation geometry, and
|
|
@@ -1039,15 +1543,15 @@ CancelResult cancelAnimation(
|
|
|
1039
1543
|
// unconditionally — match that. Read `started` before cancelActive, which
|
|
1040
1544
|
// resets state.animation.
|
|
1041
1545
|
//
|
|
1042
|
-
// Gated on the
|
|
1546
|
+
// Gated on the pending case so a started animation is not written twice
|
|
1043
1547
|
// (smoothClipCancelAnimationUsingTarget already applied the target there),
|
|
1044
1548
|
// and applied WITHOUT a velocity sample: a jump to the target is not
|
|
1045
1549
|
// interactive motion, and the cancel-to-target path deliberately keeps it
|
|
1046
1550
|
// out of the 'inherit' history on both platforms — Android's cancel
|
|
1047
1551
|
// fan-out records nothing either.
|
|
1048
|
-
const bool
|
|
1552
|
+
const bool wasPending = !state.animation->started;
|
|
1049
1553
|
cancelActive(driverId, state, true);
|
|
1050
|
-
if (
|
|
1554
|
+
if (wasPending) {
|
|
1051
1555
|
// state.latest already holds the target (set at animate time); only the
|
|
1052
1556
|
// view fan-out is missing.
|
|
1053
1557
|
for (const ViewKey key : state.views) {
|
|
@@ -1101,11 +1605,8 @@ void viewAnimationDidStop(
|
|
|
1101
1605
|
auto &state = iterator->second;
|
|
1102
1606
|
if (!state.animation.has_value() ||
|
|
1103
1607
|
state.animation->animationId != animationId) return;
|
|
1104
|
-
// A
|
|
1105
|
-
//
|
|
1106
|
-
// complete an animation that is waiting to resume. Only a running animation
|
|
1107
|
-
// may be completed by its installed delegates; a latch is completed by
|
|
1108
|
-
// replacement, cancel or destroy.
|
|
1608
|
+
// A pre-ready run has no installed delegate; it completes only through
|
|
1609
|
+
// replacement, cancellation, host loss, or controller destruction.
|
|
1109
1610
|
if (!state.animation->started) return;
|
|
1110
1611
|
|
|
1111
1612
|
const ViewKey key = keyForView(view);
|