react-native-smooth-clip-view 0.1.0 → 0.2.1
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 +171 -43
- package/SmoothClipView.podspec +8 -2
- package/android/CMakeLists.txt +39 -0
- package/android/build.gradle +69 -2
- package/android/proguard-rules.pro +6 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +59 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +428 -0
- package/android/src/main/cpp/SmoothClipRegistry.cpp +945 -0
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +25 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +55 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +128 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +164 -30
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +107 -10
- package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +18 -2
- package/cpp/SmoothClipRegistry.h +93 -0
- package/cpp/SmoothClipSharedGeometry.h +50 -0
- package/ios/SmoothClipModuleProvider.h +9 -0
- package/ios/SmoothClipModuleProvider.mm +16 -0
- package/ios/SmoothClipRegistry.mm +754 -0
- package/ios/SmoothClipTurboModule.cpp +290 -0
- package/ios/SmoothClipTurboModule.h +132 -0
- package/ios/SmoothClipView.mm +976 -85
- package/ios/SmoothClipViewRegistry.h +27 -0
- package/lib/module/NativeSmoothClipModule.js +5 -0
- package/lib/module/NativeSmoothClipModule.js.map +1 -0
- package/lib/module/SmoothClipView.android.js +4 -0
- package/lib/module/SmoothClipView.android.js.map +1 -0
- package/lib/module/SmoothClipView.ios.js +34 -0
- package/lib/module/SmoothClipView.ios.js.map +1 -0
- package/lib/module/SmoothClipView.js +26 -12
- package/lib/module/SmoothClipView.js.map +1 -1
- package/lib/module/SmoothClipView.web.js +28 -12
- package/lib/module/SmoothClipView.web.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +14 -1
- package/lib/module/driverState.js +102 -0
- package/lib/module/driverState.js.map +1 -0
- package/lib/module/driverTypes.js +4 -0
- package/lib/module/driverTypes.js.map +1 -0
- package/lib/module/drivers.android.js +4 -0
- package/lib/module/drivers.android.js.map +1 -0
- package/lib/module/drivers.ios.js +380 -0
- package/lib/module/drivers.ios.js.map +1 -0
- package/lib/module/drivers.js +276 -0
- package/lib/module/drivers.js.map +1 -0
- package/lib/module/geometry.js +19 -0
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/nativeCompletion.js +21 -0
- package/lib/module/nativeCompletion.js.map +1 -0
- package/lib/module/reactRequests.js +50 -0
- package/lib/module/reactRequests.js.map +1 -0
- package/lib/module/smoothClipNative.android.js +15 -0
- package/lib/module/smoothClipNative.android.js.map +1 -0
- package/lib/module/smoothClipNative.js +8 -0
- package/lib/module/smoothClipNative.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +19 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts +3 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.d.ts +5 -6
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +9 -0
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.web.d.ts +5 -7
- package/lib/typescript/src/SmoothClipView.web.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/driverState.d.ts +31 -0
- package/lib/typescript/src/driverState.d.ts.map +1 -0
- package/lib/typescript/src/driverTypes.d.ts +61 -0
- package/lib/typescript/src/driverTypes.d.ts.map +1 -0
- package/lib/typescript/src/drivers.android.d.ts +3 -0
- package/lib/typescript/src/drivers.android.d.ts.map +1 -0
- package/lib/typescript/src/drivers.d.ts +5 -0
- package/lib/typescript/src/drivers.d.ts.map +1 -0
- package/lib/typescript/src/drivers.ios.d.ts +5 -0
- package/lib/typescript/src/drivers.ios.d.ts.map +1 -0
- package/lib/typescript/src/geometry.d.ts +8 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +4 -1
- package/lib/typescript/src/index.web.d.ts.map +1 -1
- package/lib/typescript/src/nativeCompletion.d.ts +7 -0
- package/lib/typescript/src/nativeCompletion.d.ts.map +1 -0
- package/lib/typescript/src/reactRequests.d.ts +7 -0
- package/lib/typescript/src/reactRequests.d.ts.map +1 -0
- package/lib/typescript/src/smoothClipNative.android.d.ts +4 -0
- package/lib/typescript/src/smoothClipNative.android.d.ts.map +1 -0
- package/lib/typescript/src/smoothClipNative.d.ts +3 -0
- package/lib/typescript/src/smoothClipNative.d.ts.map +1 -0
- package/package.json +12 -7
- package/src/NativeSmoothClipModule.ts +104 -0
- package/src/SmoothClipView.android.tsx +2 -0
- package/src/SmoothClipView.ios.tsx +36 -0
- package/src/SmoothClipView.tsx +31 -27
- package/src/SmoothClipView.web.tsx +26 -22
- package/src/SmoothClipViewNativeComponent.ts +14 -1
- package/src/driverState.ts +150 -0
- package/src/driverTypes.ts +91 -0
- package/src/drivers.android.ts +6 -0
- package/src/drivers.ios.ts +665 -0
- package/src/drivers.ts +416 -0
- package/src/geometry.ts +39 -0
- package/src/index.ts +19 -1
- package/src/index.web.ts +19 -1
- package/src/nativeCompletion.ts +37 -0
- package/src/reactRequests.ts +64 -0
- package/src/smoothClipNative.android.ts +20 -0
- package/src/smoothClipNative.ts +5 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
#import "SmoothClipViewRegistry.h"
|
|
2
|
+
|
|
3
|
+
#import "SmoothClipView.h"
|
|
4
|
+
|
|
5
|
+
#import <React/RCTUtils.h>
|
|
6
|
+
#import <QuartzCore/QuartzCore.h>
|
|
7
|
+
#import <UIKit/UIKit.h>
|
|
8
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
9
|
+
#import <os/signpost.h>
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <cmath>
|
|
14
|
+
#include <limits>
|
|
15
|
+
#include <mutex>
|
|
16
|
+
#include <optional>
|
|
17
|
+
#include <unordered_map>
|
|
18
|
+
#include <unordered_set>
|
|
19
|
+
#include <utility>
|
|
20
|
+
#include <vector>
|
|
21
|
+
|
|
22
|
+
@interface SmoothClipView (Registry)
|
|
23
|
+
- (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation;
|
|
24
|
+
- (smoothclip::Presentation)smoothClipFreezePresentation;
|
|
25
|
+
- (smoothclip::Presentation)smoothClipCurrentPresentation;
|
|
26
|
+
- (void)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
|
|
27
|
+
animation:(smoothclip::TimingAnimation)animation
|
|
28
|
+
animationId:(int32_t)animationId;
|
|
29
|
+
- (void)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
|
|
30
|
+
animation:(smoothclip::SpringAnimation)animation
|
|
31
|
+
animationId:(int32_t)animationId;
|
|
32
|
+
- (void)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
|
|
33
|
+
keyframes:(const std::vector<smoothclip::Keyframe> &)keyframes
|
|
34
|
+
durationMs:(double)durationMs
|
|
35
|
+
animationId:(int32_t)animationId;
|
|
36
|
+
- (void)smoothClipCancelAnimationUsingTarget:(BOOL)useTarget;
|
|
37
|
+
- (BOOL)smoothClipIsJoinable;
|
|
38
|
+
@end
|
|
39
|
+
|
|
40
|
+
namespace smoothclip {
|
|
41
|
+
namespace {
|
|
42
|
+
|
|
43
|
+
using ViewKey = uintptr_t;
|
|
44
|
+
|
|
45
|
+
enum class Ownership { Interactive, Native };
|
|
46
|
+
enum class AnimationKind { Timing, Spring, Keyframes };
|
|
47
|
+
|
|
48
|
+
struct ActiveAnimation {
|
|
49
|
+
int32_t animationId;
|
|
50
|
+
std::unordered_set<ViewKey> participants;
|
|
51
|
+
bool finished = true;
|
|
52
|
+
AnimationKind kind = AnimationKind::Timing;
|
|
53
|
+
// Geometry the transition started from. Used when a view must join the
|
|
54
|
+
// animation but there is no laid-out peer to sample presentation from.
|
|
55
|
+
Presentation start{{0, 0, 0, 0, 0}, 0, 0};
|
|
56
|
+
TimingAnimation timing{};
|
|
57
|
+
SpringAnimation spring{};
|
|
58
|
+
std::vector<Keyframe> keyframes;
|
|
59
|
+
double durationMs = 0;
|
|
60
|
+
CFTimeInterval startedAt = 0;
|
|
61
|
+
// False while the animation is latched: built before any host view
|
|
62
|
+
// registered, held un-rendered until the first registerView rebases
|
|
63
|
+
// startedAt and starts it. startedAt is only meaningful once started.
|
|
64
|
+
bool started = false;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
struct DriverState {
|
|
68
|
+
Presentation latest{{0, 0, 0, 0, 0}, 0, 0};
|
|
69
|
+
bool hasLatest = false;
|
|
70
|
+
Ownership ownership = Ownership::Interactive;
|
|
71
|
+
std::vector<ViewKey> views;
|
|
72
|
+
std::optional<ActiveAnimation> animation;
|
|
73
|
+
int32_t nextAnimationId = 0;
|
|
74
|
+
// Set by destroyDriver while views are still registered (StrictMode effect
|
|
75
|
+
// replay, hosts mounted in another subtree). The entry is erased when the
|
|
76
|
+
// last view leaves and revived by a take-ownership setPresentation.
|
|
77
|
+
bool destroyed = false;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
struct CompletionSink {
|
|
81
|
+
const void *owner = nullptr;
|
|
82
|
+
CompletionCallback callback;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
std::unordered_map<uint64_t, DriverState> ®istry() {
|
|
86
|
+
static std::unordered_map<uint64_t, DriverState> value;
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
CompletionSink &completionSink() {
|
|
91
|
+
static CompletionSink sink;
|
|
92
|
+
return sink;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The sink is written from the TurboModule ctor/dtor on the JS thread and
|
|
96
|
+
// invoked on main. Invoking under the lock also keeps the owning module
|
|
97
|
+
// alive for the duration of the callback.
|
|
98
|
+
std::mutex &completionSinkMutex() {
|
|
99
|
+
static std::mutex value;
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Presentation unavailablePresentation() {
|
|
104
|
+
// Non-finite geometry makes the JS side fall back to its current value
|
|
105
|
+
// instead of applying zeros.
|
|
106
|
+
return {{NAN, NAN, NAN, NAN, NAN}, NAN, NAN};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
110
|
+
os_log_t signpostLog() {
|
|
111
|
+
static os_log_t log = os_log_create(
|
|
112
|
+
"com.smoothclipview", "clip-driver");
|
|
113
|
+
return log;
|
|
114
|
+
}
|
|
115
|
+
#endif
|
|
116
|
+
|
|
117
|
+
ViewKey keyForView(SmoothClipView *view) {
|
|
118
|
+
return reinterpret_cast<ViewKey>((__bridge void *)view);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
SmoothClipView *viewForKey(ViewKey key) {
|
|
122
|
+
return (__bridge SmoothClipView *)(reinterpret_cast<void *>(key));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
bool shouldReduceMotion(int32_t setting) {
|
|
126
|
+
return setting == 1 ||
|
|
127
|
+
(setting == 0 && UIAccessibilityIsReduceMotionEnabled());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
void emitCompletion(
|
|
131
|
+
uint64_t driverId,
|
|
132
|
+
DriverState &state,
|
|
133
|
+
int32_t animationId,
|
|
134
|
+
bool finished) {
|
|
135
|
+
state.animation.reset();
|
|
136
|
+
{
|
|
137
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
138
|
+
if (completionSink().callback) {
|
|
139
|
+
completionSink().callback(driverId, animationId, finished);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
143
|
+
os_signpost_event_emit(
|
|
144
|
+
signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "completion",
|
|
145
|
+
"driver=%llu animation=%d finished=%{public}d",
|
|
146
|
+
driverId, animationId, finished);
|
|
147
|
+
#endif
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void emitStandaloneCompletion(
|
|
151
|
+
uint64_t driverId,
|
|
152
|
+
int32_t animationId,
|
|
153
|
+
bool finished) {
|
|
154
|
+
{
|
|
155
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
156
|
+
if (completionSink().callback) {
|
|
157
|
+
completionSink().callback(driverId, animationId, finished);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
161
|
+
os_signpost_event_emit(
|
|
162
|
+
signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "completion",
|
|
163
|
+
"driver=%llu animation=%d finished=%{public}d",
|
|
164
|
+
driverId, animationId, finished);
|
|
165
|
+
#endif
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
int32_t allocateAnimationId(DriverState &state) {
|
|
169
|
+
state.nextAnimationId =
|
|
170
|
+
state.nextAnimationId == std::numeric_limits<int32_t>::max()
|
|
171
|
+
? 1
|
|
172
|
+
: state.nextAnimationId + 1;
|
|
173
|
+
return state.nextAnimationId;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
Presentation canonicalFrozenPresentation(DriverState &state) {
|
|
177
|
+
// A latched animation never rendered, so state.latest already holds its
|
|
178
|
+
// target; freezing there would jump the clip. Freeze at the start instead.
|
|
179
|
+
Presentation canonical =
|
|
180
|
+
state.animation.has_value() && !state.animation->started
|
|
181
|
+
? state.animation->start
|
|
182
|
+
: state.latest;
|
|
183
|
+
bool hasCanonical = false;
|
|
184
|
+
for (const ViewKey key : state.views) {
|
|
185
|
+
const Presentation frozen = [viewForKey(key) smoothClipFreezePresentation];
|
|
186
|
+
if (!hasCanonical) {
|
|
187
|
+
canonical = frozen;
|
|
188
|
+
hasCanonical = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return canonical;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
void cancelActive(
|
|
195
|
+
uint64_t driverId,
|
|
196
|
+
DriverState &state,
|
|
197
|
+
bool useTarget) {
|
|
198
|
+
if (!state.animation.has_value()) return;
|
|
199
|
+
const int32_t animationId = state.animation->animationId;
|
|
200
|
+
for (const ViewKey key : state.animation->participants) {
|
|
201
|
+
[viewForKey(key) smoothClipCancelAnimationUsingTarget:useTarget];
|
|
202
|
+
}
|
|
203
|
+
emitCompletion(driverId, state, animationId, false);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
void applyPresentation(DriverState &state, Presentation presentation) {
|
|
207
|
+
state.latest = presentation;
|
|
208
|
+
state.hasLatest = true;
|
|
209
|
+
state.ownership = Ownership::Interactive;
|
|
210
|
+
for (const ViewKey key : state.views) {
|
|
211
|
+
[viewForKey(key) smoothClipApplyPresentation:presentation];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
void prepareAnimation(
|
|
216
|
+
uint64_t driverId,
|
|
217
|
+
DriverState &state,
|
|
218
|
+
AnimationStart start,
|
|
219
|
+
Presentation target) {
|
|
220
|
+
const bool acceptsInteractiveStart =
|
|
221
|
+
start.hasInteractiveStart && state.ownership == Ownership::Interactive;
|
|
222
|
+
cancelActive(driverId, state, false);
|
|
223
|
+
if (acceptsInteractiveStart) {
|
|
224
|
+
applyPresentation(state, start.interactiveStart);
|
|
225
|
+
}
|
|
226
|
+
state.latest = target;
|
|
227
|
+
state.hasLatest = true;
|
|
228
|
+
state.ownership = Ownership::Native;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Mirrors prepareAnimation's start resolution. Must be evaluated before
|
|
232
|
+
// prepareAnimation overwrites state.latest with the target.
|
|
233
|
+
Presentation resolvedAnimationStart(
|
|
234
|
+
const DriverState &state,
|
|
235
|
+
const AnimationStart &start) {
|
|
236
|
+
if (start.hasInteractiveStart && state.ownership == Ownership::Interactive) {
|
|
237
|
+
return start.interactiveStart;
|
|
238
|
+
}
|
|
239
|
+
// Replacing a latched animation (e.g. open→close before the host mounted):
|
|
240
|
+
// it never rendered, and state.latest is its target — start from where the
|
|
241
|
+
// clip actually is, the latch's own start.
|
|
242
|
+
if (state.animation.has_value() && !state.animation->started) {
|
|
243
|
+
return state.animation->start;
|
|
244
|
+
}
|
|
245
|
+
return state.latest;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Installs the remaining part of the active animation on a view that joins
|
|
249
|
+
// mid-flight, starting from `visible`. Shared by registerView (mount during
|
|
250
|
+
// animation) and joinActiveAnimation (first layout after such a mount).
|
|
251
|
+
void dispatchActiveAnimationJoin(
|
|
252
|
+
DriverState &state,
|
|
253
|
+
SmoothClipView *view,
|
|
254
|
+
ViewKey key,
|
|
255
|
+
const Presentation &visible) {
|
|
256
|
+
auto &active = *state.animation;
|
|
257
|
+
active.participants.insert(key);
|
|
258
|
+
const double elapsedMs = (CACurrentMediaTime() - active.startedAt) * 1000.0;
|
|
259
|
+
const double remainingMs = MAX(0, active.durationMs - elapsedMs);
|
|
260
|
+
if (active.kind == AnimationKind::Timing) {
|
|
261
|
+
TimingAnimation timing = active.timing;
|
|
262
|
+
timing.durationMs = remainingMs;
|
|
263
|
+
[view smoothClipAnimateTiming:state.latest
|
|
264
|
+
animation:timing
|
|
265
|
+
animationId:active.animationId];
|
|
266
|
+
} else if (active.kind == AnimationKind::Spring) {
|
|
267
|
+
[view smoothClipAnimateSpring:state.latest
|
|
268
|
+
animation:active.spring
|
|
269
|
+
animationId:active.animationId];
|
|
270
|
+
} else {
|
|
271
|
+
const double progress = active.durationMs <= 0
|
|
272
|
+
? 1
|
|
273
|
+
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
274
|
+
std::vector<Keyframe> remaining;
|
|
275
|
+
remaining.push_back({0, visible});
|
|
276
|
+
for (const Keyframe &frame : active.keyframes) {
|
|
277
|
+
if (frame.offset <= progress) continue;
|
|
278
|
+
const double offset = (frame.offset - progress) / (1 - progress);
|
|
279
|
+
remaining.push_back({offset, frame.presentation});
|
|
280
|
+
}
|
|
281
|
+
if (remaining.size() == 1) remaining.push_back({1, state.latest});
|
|
282
|
+
[view smoothClipAnimateKeyframes:state.latest
|
|
283
|
+
keyframes:remaining
|
|
284
|
+
durationMs:remainingMs
|
|
285
|
+
animationId:active.animationId];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
} // namespace
|
|
290
|
+
|
|
291
|
+
void setCompletionCallback(
|
|
292
|
+
const void *owner,
|
|
293
|
+
CompletionCallback callback) {
|
|
294
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
295
|
+
completionSink().owner = owner;
|
|
296
|
+
completionSink().callback = std::move(callback);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
void clearCompletionCallback(const void *owner) {
|
|
300
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
301
|
+
if (completionSink().owner != owner) return;
|
|
302
|
+
completionSink().owner = nullptr;
|
|
303
|
+
completionSink().callback = nullptr;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
void registerView(
|
|
307
|
+
uint64_t driverId,
|
|
308
|
+
SmoothClipView *view,
|
|
309
|
+
Presentation initialPresentation) {
|
|
310
|
+
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
311
|
+
auto &state = registry()[driverId];
|
|
312
|
+
state.destroyed = false;
|
|
313
|
+
// Seed canonical geometry from the mounting view's initial props BEFORE
|
|
314
|
+
// sampling `visible`; otherwise the first registration captures the default
|
|
315
|
+
// empty {0,0,0,0} rect, seeding the view (and the presentation layer the
|
|
316
|
+
// opening timing animation samples as its "from") to the top-left instead of
|
|
317
|
+
// the real origin rectangle.
|
|
318
|
+
if (!state.hasLatest) {
|
|
319
|
+
state.latest = initialPresentation;
|
|
320
|
+
state.hasLatest = true;
|
|
321
|
+
}
|
|
322
|
+
const bool hasAnimation = state.animation.has_value();
|
|
323
|
+
const bool startsLatch = hasAnimation && !state.animation->started;
|
|
324
|
+
const bool shouldReplay =
|
|
325
|
+
hasAnimation && state.animation->started && !state.views.empty();
|
|
326
|
+
Presentation visible = state.latest;
|
|
327
|
+
if (shouldReplay) {
|
|
328
|
+
// A registered peer that never laid out reports zero geometry; joining
|
|
329
|
+
// from it would visibly collapse the clip. Prefer the first laid-out
|
|
330
|
+
// peer and fall back to the animation's start.
|
|
331
|
+
SmoothClipView *reference = nil;
|
|
332
|
+
for (const ViewKey candidate : state.views) {
|
|
333
|
+
SmoothClipView *candidateView = viewForKey(candidate);
|
|
334
|
+
if ([candidateView smoothClipIsJoinable]) {
|
|
335
|
+
reference = candidateView;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
visible = reference != nil
|
|
340
|
+
? [reference smoothClipCurrentPresentation]
|
|
341
|
+
: state.animation->start;
|
|
342
|
+
} else if (startsLatch) {
|
|
343
|
+
// state.latest already holds the latch's TARGET; applying it here is
|
|
344
|
+
// exactly the jump this path exists to avoid.
|
|
345
|
+
visible = state.animation->start;
|
|
346
|
+
}
|
|
347
|
+
const ViewKey key = keyForView(view);
|
|
348
|
+
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
349
|
+
state.views.end()) {
|
|
350
|
+
state.views.push_back(key);
|
|
351
|
+
}
|
|
352
|
+
[view smoothClipApplyPresentation:visible];
|
|
353
|
+
if (startsLatch) {
|
|
354
|
+
// Start the latched animation: rebase the join clock before any elapsed
|
|
355
|
+
// math runs (dispatch below and the first-layout joinActiveAnimation).
|
|
356
|
+
state.animation->startedAt = CACurrentMediaTime();
|
|
357
|
+
state.animation->started = true;
|
|
358
|
+
}
|
|
359
|
+
if (!(shouldReplay || startsLatch)) return;
|
|
360
|
+
|
|
361
|
+
// The participant insert inside the join is also what keeps unregisterView
|
|
362
|
+
// orphan-safe for a mount-then-unmount before first layout.
|
|
363
|
+
dispatchActiveAnimationJoin(state, view, key, visible);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
bool joinActiveAnimation(uint64_t driverId, SmoothClipView *view) {
|
|
367
|
+
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
368
|
+
auto iterator = registry().find(driverId);
|
|
369
|
+
if (iterator == registry().end()) return false;
|
|
370
|
+
auto &state = iterator->second;
|
|
371
|
+
if (!state.animation.has_value() || state.views.empty()) return false;
|
|
372
|
+
// Unreachable in the normal flow (a pending install implies the animation
|
|
373
|
+
// was dispatched, which implies started), but a join must never compute
|
|
374
|
+
// elapsed time against an un-rebased latch clock.
|
|
375
|
+
if (!state.animation->started) return false;
|
|
376
|
+
const ViewKey key = keyForView(view);
|
|
377
|
+
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
378
|
+
state.views.end()) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
// Join from a laid-out peer's live presentation, exactly as a view that had
|
|
382
|
+
// registered at this moment would. When the joining view is the only
|
|
383
|
+
// registered host, its own layer never received geometry (the install was
|
|
384
|
+
// deferred for lack of layout), so fall back to the animation's start.
|
|
385
|
+
ViewKey canonicalKey = 0;
|
|
386
|
+
for (const ViewKey candidate : state.views) {
|
|
387
|
+
if (candidate != key && [viewForKey(candidate) smoothClipIsJoinable]) {
|
|
388
|
+
canonicalKey = candidate;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const Presentation visible = canonicalKey != 0
|
|
393
|
+
? [viewForKey(canonicalKey) smoothClipCurrentPresentation]
|
|
394
|
+
: state.animation->start;
|
|
395
|
+
[view smoothClipApplyPresentation:visible];
|
|
396
|
+
dispatchActiveAnimationJoin(state, view, key, visible);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
401
|
+
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
402
|
+
auto iterator = registry().find(driverId);
|
|
403
|
+
if (iterator == registry().end()) return;
|
|
404
|
+
|
|
405
|
+
auto &state = iterator->second;
|
|
406
|
+
const ViewKey key = keyForView(view);
|
|
407
|
+
if (state.animation.has_value() &&
|
|
408
|
+
state.animation->participants.erase(key) > 0) {
|
|
409
|
+
state.animation->finished = false;
|
|
410
|
+
if (state.animation->participants.empty()) {
|
|
411
|
+
// The animation ends here; without releasing ownership a host that
|
|
412
|
+
// remounts on this driver later would drop every interactive update.
|
|
413
|
+
state.ownership = Ownership::Interactive;
|
|
414
|
+
emitCompletion(
|
|
415
|
+
driverId, state, state.animation->animationId, false);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
state.views.erase(
|
|
419
|
+
std::remove(state.views.begin(), state.views.end(), key),
|
|
420
|
+
state.views.end());
|
|
421
|
+
if (state.destroyed && state.views.empty()) {
|
|
422
|
+
registry().erase(iterator);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
void setPresentation(
|
|
427
|
+
uint64_t driverId,
|
|
428
|
+
Presentation presentation,
|
|
429
|
+
bool takeOwnership) {
|
|
430
|
+
// CALayer writes require the main thread (not main-queue drain context).
|
|
431
|
+
if (!NSThread.isMainThread) {
|
|
432
|
+
// Never block an off-main caller: a synchronous hop can deadlock against
|
|
433
|
+
// the worklets UI-runtime mutex (runOnUISync executes on the calling
|
|
434
|
+
// thread while holding it, and the main thread routinely waits for the
|
|
435
|
+
// same mutex when draining scheduled worklets).
|
|
436
|
+
RCTExecuteOnMainQueue(^{
|
|
437
|
+
setPresentation(driverId, presentation, takeOwnership);
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
auto iterator = registry().find(driverId);
|
|
442
|
+
if (!takeOwnership) {
|
|
443
|
+
if (iterator == registry().end() || iterator->second.destroyed) {
|
|
444
|
+
// A stale interactive delivery must not resurrect a destroyed driver.
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
} else if (iterator == registry().end()) {
|
|
448
|
+
// Same create-or-revive semantics operator[] had; a single hash lookup.
|
|
449
|
+
iterator = registry().try_emplace(driverId).first;
|
|
450
|
+
}
|
|
451
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
452
|
+
const bool signpostsEnabled = os_signpost_enabled(signpostLog());
|
|
453
|
+
os_signpost_id_t identifier = OS_SIGNPOST_ID_NULL;
|
|
454
|
+
if (signpostsEnabled) {
|
|
455
|
+
identifier = os_signpost_id_generate(signpostLog());
|
|
456
|
+
os_signpost_interval_begin(
|
|
457
|
+
signpostLog(), identifier, "lookup-fanout", "driver=%llu", driverId);
|
|
458
|
+
}
|
|
459
|
+
#endif
|
|
460
|
+
auto &state = iterator->second;
|
|
461
|
+
if (!takeOwnership && state.ownership != Ownership::Interactive) {
|
|
462
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
463
|
+
if (signpostsEnabled) {
|
|
464
|
+
os_signpost_interval_end(
|
|
465
|
+
signpostLog(), identifier, "lookup-fanout", "ignored=native-owner");
|
|
466
|
+
}
|
|
467
|
+
#endif
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (takeOwnership) {
|
|
471
|
+
state.destroyed = false;
|
|
472
|
+
cancelActive(driverId, state, false);
|
|
473
|
+
}
|
|
474
|
+
applyPresentation(state, presentation);
|
|
475
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
476
|
+
if (signpostsEnabled) {
|
|
477
|
+
os_signpost_interval_end(
|
|
478
|
+
signpostLog(), identifier, "lookup-fanout", "views=%lu",
|
|
479
|
+
static_cast<unsigned long>(state.views.size()));
|
|
480
|
+
}
|
|
481
|
+
#endif
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
Presentation beginInteraction(uint64_t driverId) {
|
|
485
|
+
if (!NSThread.isMainThread) {
|
|
486
|
+
// See setPresentation: blocking off-main risks a deadlock, so the call
|
|
487
|
+
// fails defined and the JS side keeps its current value.
|
|
488
|
+
return unavailablePresentation();
|
|
489
|
+
} auto iterator = registry().find(driverId);
|
|
490
|
+
if (iterator == registry().end()) return unavailablePresentation();
|
|
491
|
+
auto &state = iterator->second;
|
|
492
|
+
state.destroyed = false;
|
|
493
|
+
if (!state.animation.has_value()) {
|
|
494
|
+
state.ownership = Ownership::Interactive;
|
|
495
|
+
return state.latest;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const int32_t animationId = state.animation->animationId;
|
|
499
|
+
const Presentation canonical = canonicalFrozenPresentation(state);
|
|
500
|
+
state.latest = canonical;
|
|
501
|
+
state.hasLatest = true;
|
|
502
|
+
state.ownership = Ownership::Interactive;
|
|
503
|
+
emitCompletion(driverId, state, animationId, false);
|
|
504
|
+
return canonical;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
int32_t animateTiming(
|
|
508
|
+
uint64_t driverId,
|
|
509
|
+
AnimationStart start,
|
|
510
|
+
Presentation presentation,
|
|
511
|
+
TimingAnimation animation) {
|
|
512
|
+
if (!NSThread.isMainThread) {
|
|
513
|
+
// See setPresentation: blocking off-main risks a deadlock. 0 is the
|
|
514
|
+
// documented rejection sentinel.
|
|
515
|
+
return 0;
|
|
516
|
+
} auto iterator = registry().find(driverId);
|
|
517
|
+
if (iterator == registry().end()) return 0;
|
|
518
|
+
auto &state = iterator->second;
|
|
519
|
+
state.destroyed = false;
|
|
520
|
+
const int32_t animationId = allocateAnimationId(state);
|
|
521
|
+
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
522
|
+
prepareAnimation(driverId, state, start, presentation);
|
|
523
|
+
|
|
524
|
+
if (shouldReduceMotion(animation.reduceMotion) || animation.durationMs <= 0) {
|
|
525
|
+
applyPresentation(state, presentation);
|
|
526
|
+
emitCompletion(driverId, state, animationId, true);
|
|
527
|
+
return animationId;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
ActiveAnimation active{animationId};
|
|
531
|
+
active.kind = AnimationKind::Timing;
|
|
532
|
+
active.start = resolvedStart;
|
|
533
|
+
active.timing = animation;
|
|
534
|
+
active.durationMs = animation.durationMs;
|
|
535
|
+
active.startedAt = CACurrentMediaTime();
|
|
536
|
+
active.participants.insert(state.views.begin(), state.views.end());
|
|
537
|
+
active.started = !state.views.empty();
|
|
538
|
+
state.animation = std::move(active);
|
|
539
|
+
if (!state.animation->started) {
|
|
540
|
+
// Latch: no host view yet (animateTo raced the mount). Ownership stays
|
|
541
|
+
// Native — the pending animation owns rendering intent — and the first
|
|
542
|
+
// registerView rebases startedAt and starts it. The non-zero id is still
|
|
543
|
+
// returned synchronously so the JS side does not treat this as rejection.
|
|
544
|
+
return animationId;
|
|
545
|
+
}
|
|
546
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
547
|
+
os_signpost_event_emit(
|
|
548
|
+
signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "transition-setup",
|
|
549
|
+
"driver=%llu animation=%d views=%lu type=timing",
|
|
550
|
+
driverId, animationId,
|
|
551
|
+
static_cast<unsigned long>(state.views.size()));
|
|
552
|
+
#endif
|
|
553
|
+
for (const ViewKey key : state.views) {
|
|
554
|
+
[viewForKey(key) smoothClipAnimateTiming:presentation
|
|
555
|
+
animation:animation
|
|
556
|
+
animationId:animationId];
|
|
557
|
+
}
|
|
558
|
+
return animationId;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
int32_t animateSpring(
|
|
562
|
+
uint64_t driverId,
|
|
563
|
+
AnimationStart start,
|
|
564
|
+
Presentation presentation,
|
|
565
|
+
SpringAnimation animation) {
|
|
566
|
+
if (!NSThread.isMainThread) {
|
|
567
|
+
// See setPresentation: blocking off-main risks a deadlock.
|
|
568
|
+
return 0;
|
|
569
|
+
} auto iterator = registry().find(driverId);
|
|
570
|
+
if (iterator == registry().end()) return 0;
|
|
571
|
+
auto &state = iterator->second;
|
|
572
|
+
state.destroyed = false;
|
|
573
|
+
const int32_t animationId = allocateAnimationId(state);
|
|
574
|
+
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
575
|
+
prepareAnimation(driverId, state, start, presentation);
|
|
576
|
+
|
|
577
|
+
if (shouldReduceMotion(animation.reduceMotion)) {
|
|
578
|
+
applyPresentation(state, presentation);
|
|
579
|
+
emitCompletion(driverId, state, animationId, true);
|
|
580
|
+
return animationId;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
ActiveAnimation active{animationId};
|
|
584
|
+
active.kind = AnimationKind::Spring;
|
|
585
|
+
active.start = resolvedStart;
|
|
586
|
+
active.spring = animation;
|
|
587
|
+
active.startedAt = CACurrentMediaTime();
|
|
588
|
+
active.participants.insert(state.views.begin(), state.views.end());
|
|
589
|
+
active.started = !state.views.empty();
|
|
590
|
+
state.animation = std::move(active);
|
|
591
|
+
if (!state.animation->started) {
|
|
592
|
+
// Latched until the first registerView — see animateTiming.
|
|
593
|
+
return animationId;
|
|
594
|
+
}
|
|
595
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
596
|
+
os_signpost_event_emit(
|
|
597
|
+
signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "transition-setup",
|
|
598
|
+
"driver=%llu animation=%d views=%lu type=spring",
|
|
599
|
+
driverId, animationId,
|
|
600
|
+
static_cast<unsigned long>(state.views.size()));
|
|
601
|
+
#endif
|
|
602
|
+
for (const ViewKey key : state.views) {
|
|
603
|
+
[viewForKey(key) smoothClipAnimateSpring:presentation
|
|
604
|
+
animation:animation
|
|
605
|
+
animationId:animationId];
|
|
606
|
+
}
|
|
607
|
+
return animationId;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
int32_t animateKeyframes(
|
|
611
|
+
uint64_t driverId,
|
|
612
|
+
AnimationStart start,
|
|
613
|
+
Presentation presentation,
|
|
614
|
+
double durationMs,
|
|
615
|
+
std::vector<Keyframe> keyframes,
|
|
616
|
+
int32_t reduceMotion) {
|
|
617
|
+
if (!NSThread.isMainThread) {
|
|
618
|
+
// See setPresentation: blocking off-main risks a deadlock.
|
|
619
|
+
return 0;
|
|
620
|
+
} auto iterator = registry().find(driverId);
|
|
621
|
+
if (iterator == registry().end()) return 0;
|
|
622
|
+
auto &state = iterator->second;
|
|
623
|
+
state.destroyed = false;
|
|
624
|
+
const int32_t animationId = allocateAnimationId(state);
|
|
625
|
+
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
626
|
+
prepareAnimation(driverId, state, start, presentation);
|
|
627
|
+
if (shouldReduceMotion(reduceMotion) || durationMs <= 0) {
|
|
628
|
+
applyPresentation(state, presentation);
|
|
629
|
+
emitCompletion(driverId, state, animationId, true);
|
|
630
|
+
return animationId;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
ActiveAnimation active{animationId};
|
|
634
|
+
active.kind = AnimationKind::Keyframes;
|
|
635
|
+
active.start = resolvedStart;
|
|
636
|
+
active.keyframes = keyframes;
|
|
637
|
+
active.durationMs = durationMs;
|
|
638
|
+
active.startedAt = CACurrentMediaTime();
|
|
639
|
+
active.participants.insert(state.views.begin(), state.views.end());
|
|
640
|
+
active.started = !state.views.empty();
|
|
641
|
+
state.animation = std::move(active);
|
|
642
|
+
if (!state.animation->started) {
|
|
643
|
+
// Latched until the first registerView — see animateTiming.
|
|
644
|
+
return animationId;
|
|
645
|
+
}
|
|
646
|
+
for (const ViewKey key : state.views) {
|
|
647
|
+
[viewForKey(key) smoothClipAnimateKeyframes:presentation
|
|
648
|
+
keyframes:keyframes
|
|
649
|
+
durationMs:durationMs
|
|
650
|
+
animationId:animationId];
|
|
651
|
+
}
|
|
652
|
+
return animationId;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
int32_t rejectAnimation(uint64_t driverId) {
|
|
656
|
+
if (!NSThread.isMainThread) {
|
|
657
|
+
// See setPresentation: blocking off-main risks a deadlock.
|
|
658
|
+
return 0;
|
|
659
|
+
} auto iterator = registry().find(driverId);
|
|
660
|
+
if (iterator == registry().end()) return 0;
|
|
661
|
+
const int32_t animationId = allocateAnimationId(iterator->second);
|
|
662
|
+
emitStandaloneCompletion(driverId, animationId, false);
|
|
663
|
+
return animationId;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
CancelResult cancelAnimation(
|
|
667
|
+
uint64_t driverId,
|
|
668
|
+
int32_t animationId,
|
|
669
|
+
bool useTarget) {
|
|
670
|
+
if (!NSThread.isMainThread) {
|
|
671
|
+
// See setPresentation: blocking off-main risks a deadlock.
|
|
672
|
+
return {false, unavailablePresentation()};
|
|
673
|
+
} auto iterator = registry().find(driverId);
|
|
674
|
+
if (iterator == registry().end()) {
|
|
675
|
+
return {false, {{0, 0, 0, 0, 0}, 0, 0}};
|
|
676
|
+
}
|
|
677
|
+
auto &state = iterator->second;
|
|
678
|
+
if (!state.animation.has_value() ||
|
|
679
|
+
(animationId > 0 && animationId != state.animation->animationId)) {
|
|
680
|
+
return {false, state.latest};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
684
|
+
os_signpost_event_emit(
|
|
685
|
+
signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "cancellation",
|
|
686
|
+
"driver=%llu animation=%d target=%{public}d",
|
|
687
|
+
driverId, state.animation->animationId, useTarget);
|
|
688
|
+
#endif
|
|
689
|
+
if (useTarget) {
|
|
690
|
+
const Presentation target = state.latest;
|
|
691
|
+
cancelActive(driverId, state, true);
|
|
692
|
+
state.ownership = Ownership::Interactive;
|
|
693
|
+
return {true, target};
|
|
694
|
+
}
|
|
695
|
+
return {true, beginInteraction(driverId)};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
void destroyDriver(uint64_t driverId) {
|
|
699
|
+
if (!NSThread.isMainThread) {
|
|
700
|
+
// See setPresentation: never block an off-main caller.
|
|
701
|
+
RCTExecuteOnMainQueue(^{
|
|
702
|
+
destroyDriver(driverId);
|
|
703
|
+
});
|
|
704
|
+
return;
|
|
705
|
+
} auto iterator = registry().find(driverId);
|
|
706
|
+
if (iterator == registry().end()) return;
|
|
707
|
+
auto &state = iterator->second;
|
|
708
|
+
cancelActive(driverId, state, false);
|
|
709
|
+
if (state.views.empty()) {
|
|
710
|
+
registry().erase(iterator);
|
|
711
|
+
} else {
|
|
712
|
+
// Views can outlive the hook briefly (StrictMode effect replay, hosts in
|
|
713
|
+
// another subtree). Keep a tombstone so their registration stays intact;
|
|
714
|
+
// it is erased when the last view leaves and revived by the hook's
|
|
715
|
+
// authoritative take-ownership re-seed.
|
|
716
|
+
state.destroyed = true;
|
|
717
|
+
state.ownership = Ownership::Interactive;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
void viewAnimationDidStop(
|
|
722
|
+
uint64_t driverId,
|
|
723
|
+
int32_t animationId,
|
|
724
|
+
SmoothClipView *view,
|
|
725
|
+
bool finished) {
|
|
726
|
+
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
727
|
+
auto iterator = registry().find(driverId);
|
|
728
|
+
if (iterator == registry().end()) return;
|
|
729
|
+
auto &state = iterator->second;
|
|
730
|
+
if (!state.animation.has_value() ||
|
|
731
|
+
state.animation->animationId != animationId) return;
|
|
732
|
+
|
|
733
|
+
const ViewKey key = keyForView(view);
|
|
734
|
+
if (state.animation->participants.erase(key) == 0) return;
|
|
735
|
+
state.animation->finished = state.animation->finished && finished;
|
|
736
|
+
if (state.animation->participants.empty()) {
|
|
737
|
+
state.ownership = Ownership::Interactive;
|
|
738
|
+
emitCompletion(
|
|
739
|
+
driverId, state, animationId, state.animation->finished);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
size_t registeredViewCount(uint64_t driverId) {
|
|
744
|
+
auto iterator = registry().find(driverId);
|
|
745
|
+
return iterator == registry().end() ? 0 : iterator->second.views.size();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
bool hasActiveAnimation(uint64_t driverId) {
|
|
749
|
+
auto iterator = registry().find(driverId);
|
|
750
|
+
return iterator != registry().end() &&
|
|
751
|
+
iterator->second.animation.has_value();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
} // namespace smoothclip
|