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