react-native-smooth-clip-view 0.4.6 → 0.5.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/CHANGELOG.md +55 -0
- package/README.md +47 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +22 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +37 -3
- package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -2
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +18 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropView.kt +59 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropViewManager.kt +63 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +15 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +316 -34
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +19 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +1 -1
- package/cpp/SmoothClipAnimationCurve.h +36 -11
- package/cpp/SmoothClipRegistry.h +11 -4
- package/ios/SmoothClipBackdropView.h +17 -0
- package/ios/SmoothClipBackdropView.mm +267 -0
- package/ios/SmoothClipRegistry.mm +192 -9
- package/ios/SmoothClipTurboModule.cpp +33 -11
- package/ios/SmoothClipView.mm +713 -22
- package/ios/SmoothClipViewRegistry.h +14 -0
- package/lib/module/SmoothClipBackdropView.js +40 -0
- package/lib/module/SmoothClipBackdropView.js.map +1 -0
- package/lib/module/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/lib/module/SmoothClipView.ios.js +17 -1
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +4 -0
- package/lib/module/geometry.js +44 -6
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groups.native.js +10 -3
- package/lib/module/groups.native.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/presentationCodec.js +6 -2
- package/lib/module/presentationCodec.js.map +1 -1
- package/lib/typescript/src/SmoothClipBackdropView.d.ts +155 -0
- package/lib/typescript/src/SmoothClipBackdropView.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts +8 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +15 -1
- package/lib/typescript/src/SmoothClipView.ios.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/geometry.d.ts +19 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groups.native.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +1 -1
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/SmoothClipBackdropView.tsx +53 -0
- package/src/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/src/SmoothClipView.android.tsx +4 -1
- package/src/SmoothClipView.ios.tsx +24 -1
- package/src/SmoothClipView.tsx +4 -1
- package/src/SmoothClipViewNativeComponent.ts +4 -0
- package/src/geometry.ts +59 -4
- package/src/groups.native.ts +9 -3
- package/src/index.ts +7 -1
- package/src/presentationCodec.ts +8 -2
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#import "SmoothClipBackdropView.h"
|
|
2
|
+
|
|
3
|
+
#import "SmoothClipViewRegistry.h"
|
|
4
|
+
|
|
5
|
+
#include "SmoothClipAnimationCurve.h"
|
|
6
|
+
|
|
7
|
+
#import <QuartzCore/QuartzCore.h>
|
|
8
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
9
|
+
#import <react/renderer/components/SmoothClipViewSpec/ComponentDescriptors.h>
|
|
10
|
+
#import <react/renderer/components/SmoothClipViewSpec/Props.h>
|
|
11
|
+
#import <react/renderer/components/SmoothClipViewSpec/RCTComponentViewHelpers.h>
|
|
12
|
+
|
|
13
|
+
#include <algorithm>
|
|
14
|
+
#include <cmath>
|
|
15
|
+
|
|
16
|
+
using namespace facebook::react;
|
|
17
|
+
|
|
18
|
+
static NSString *const kSmoothClipBackdropAnimationKey = @"smoothClip.backdrop";
|
|
19
|
+
|
|
20
|
+
@interface SmoothClipBackdropContentView : UIView
|
|
21
|
+
@end
|
|
22
|
+
|
|
23
|
+
@implementation SmoothClipBackdropContentView
|
|
24
|
+
|
|
25
|
+
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
|
|
26
|
+
// The registry drives the translation explicitly, by a model write or by
|
|
27
|
+
// the animations installed below; no implicit action per frame.
|
|
28
|
+
return (id<CAAction>)[NSNull null];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@end
|
|
32
|
+
|
|
33
|
+
@implementation SmoothClipBackdropView {
|
|
34
|
+
SmoothClipBackdropContentView *_content;
|
|
35
|
+
uint64_t _driverId;
|
|
36
|
+
CGPoint _translation;
|
|
37
|
+
BOOL _animating;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
41
|
+
return concreteComponentDescriptorProvider<
|
|
42
|
+
SmoothClipBackdropViewComponentDescriptor>();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
+ (BOOL)shouldBeRecycled {
|
|
46
|
+
return NO;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
50
|
+
if (self = [super initWithFrame:frame]) {
|
|
51
|
+
static const auto defaultProps =
|
|
52
|
+
std::make_shared<const SmoothClipBackdropViewProps>();
|
|
53
|
+
_props = defaultProps;
|
|
54
|
+
_content = [[SmoothClipBackdropContentView alloc] initWithFrame:self.bounds];
|
|
55
|
+
_content.autoresizesSubviews = NO;
|
|
56
|
+
[self addSubview:_content];
|
|
57
|
+
_driverId = 0;
|
|
58
|
+
_translation = CGPointZero;
|
|
59
|
+
_animating = NO;
|
|
60
|
+
}
|
|
61
|
+
return self;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)dealloc {
|
|
65
|
+
if (_driverId != 0) {
|
|
66
|
+
smoothclip::unregisterBackdropView(_driverId, self);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
71
|
+
index:(NSInteger)index {
|
|
72
|
+
NSAssert(childComponentView.superview == nil,
|
|
73
|
+
@"SmoothClipBackdropView attempted to mount an already-mounted child");
|
|
74
|
+
[_content insertSubview:childComponentView atIndex:index];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
78
|
+
index:(NSInteger)index {
|
|
79
|
+
NSAssert(childComponentView.superview == _content,
|
|
80
|
+
@"SmoothClipBackdropView attempted to unmount a child from another parent");
|
|
81
|
+
[childComponentView removeFromSuperview];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
85
|
+
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
|
|
86
|
+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
87
|
+
// Bounds and centre, never frame: the content layer carries the channel as
|
|
88
|
+
// a translation transform, and a frame write under a transform re-derives
|
|
89
|
+
// the position through the transform's inverse, folding the current
|
|
90
|
+
// translation into the position for good. Fabric sets props (and so the
|
|
91
|
+
// channel) before it lays the view out.
|
|
92
|
+
const CGRect bounds = self.bounds;
|
|
93
|
+
_content.bounds = bounds;
|
|
94
|
+
_content.center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
- (void)updateProps:(const Props::Shared &)props
|
|
98
|
+
oldProps:(const Props::Shared &)oldProps {
|
|
99
|
+
const auto &newProps =
|
|
100
|
+
*std::static_pointer_cast<const SmoothClipBackdropViewProps>(props);
|
|
101
|
+
[super updateProps:props oldProps:oldProps];
|
|
102
|
+
const uint64_t nextDriverId =
|
|
103
|
+
isfinite(newProps.driverId) && newProps.driverId > 0
|
|
104
|
+
? static_cast<uint64_t>(newProps.driverId)
|
|
105
|
+
: 0;
|
|
106
|
+
if (nextDriverId == _driverId) return;
|
|
107
|
+
if (_driverId != 0) {
|
|
108
|
+
smoothclip::unregisterBackdropView(_driverId, self);
|
|
109
|
+
}
|
|
110
|
+
_driverId = nextDriverId;
|
|
111
|
+
if (_driverId != 0) {
|
|
112
|
+
smoothclip::registerBackdropView(_driverId, self);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
- (void)prepareForRecycle {
|
|
117
|
+
if (_driverId != 0) {
|
|
118
|
+
smoothclip::unregisterBackdropView(_driverId, self);
|
|
119
|
+
_driverId = 0;
|
|
120
|
+
}
|
|
121
|
+
[self stopBackdropAnimation];
|
|
122
|
+
[self writeTranslation:CGPointZero];
|
|
123
|
+
[super prepareForRecycle];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - Registry hooks
|
|
127
|
+
|
|
128
|
+
- (void)writeTranslation:(CGPoint)translation {
|
|
129
|
+
_translation = translation;
|
|
130
|
+
_content.layer.transform =
|
|
131
|
+
CATransform3DMakeTranslation(translation.x, translation.y, 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
- (void)stopBackdropAnimation {
|
|
135
|
+
if (!_animating) return;
|
|
136
|
+
[_content.layer removeAnimationForKey:kSmoothClipBackdropAnimationKey];
|
|
137
|
+
_animating = NO;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
- (CGPoint)smoothClipBackdropCurrentTranslation {
|
|
141
|
+
CALayer *layer = _animating ? _content.layer.presentationLayer : nil;
|
|
142
|
+
if (layer == nil) return _translation;
|
|
143
|
+
const CATransform3D transform = layer.transform;
|
|
144
|
+
return CGPointMake(transform.m41, transform.m42);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
- (void)smoothClipApplyBackdrop:(CGPoint)translation {
|
|
148
|
+
[self stopBackdropAnimation];
|
|
149
|
+
[self writeTranslation:translation];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
- (void)installBackdropGroup:(CAAnimationGroup *)group
|
|
153
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime {
|
|
154
|
+
if (sharedBeginTime <= 0) sharedBeginTime = CACurrentMediaTime();
|
|
155
|
+
// The clip's epoch, translated into this layer's clock: a stamped epoch
|
|
156
|
+
// may sit up to one frame ahead of the commit, and until it arrives the
|
|
157
|
+
// run presents its first frame rather than the target the model holds.
|
|
158
|
+
group.beginTime = [_content.layer convertTime:sharedBeginTime fromLayer:nil];
|
|
159
|
+
group.fillMode = kCAFillModeBackwards;
|
|
160
|
+
[_content.layer addAnimation:group forKey:kSmoothClipBackdropAnimationKey];
|
|
161
|
+
_animating = YES;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
- (BOOL)smoothClipAnimateBackdropTo:(CGPoint)target
|
|
165
|
+
timing:(smoothclip::TimingAnimation)timing
|
|
166
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime {
|
|
167
|
+
const CGPoint from = [self smoothClipBackdropCurrentTranslation];
|
|
168
|
+
[self stopBackdropAnimation];
|
|
169
|
+
[self writeTranslation:target];
|
|
170
|
+
if (self.window == nil) return NO;
|
|
171
|
+
CAMediaTimingFunction *function = [CAMediaTimingFunction
|
|
172
|
+
functionWithControlPoints:timing.controlPoint1X
|
|
173
|
+
:timing.controlPoint1Y
|
|
174
|
+
:timing.controlPoint2X
|
|
175
|
+
:timing.controlPoint2Y];
|
|
176
|
+
CABasicAnimation *x =
|
|
177
|
+
[CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
|
|
178
|
+
x.fromValue = @(from.x);
|
|
179
|
+
x.toValue = @(target.x);
|
|
180
|
+
x.timingFunction = function;
|
|
181
|
+
CABasicAnimation *y =
|
|
182
|
+
[CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
|
|
183
|
+
y.fromValue = @(from.y);
|
|
184
|
+
y.toValue = @(target.y);
|
|
185
|
+
y.timingFunction = function;
|
|
186
|
+
CAAnimationGroup *group = [CAAnimationGroup animation];
|
|
187
|
+
group.animations = @[ x, y ];
|
|
188
|
+
group.duration = std::max(0.0, timing.durationMs) / 1000.0;
|
|
189
|
+
[self installBackdropGroup:group sharedBeginTime:sharedBeginTime];
|
|
190
|
+
return YES;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
- (BOOL)smoothClipAnimateBackdropTo:(CGPoint)target
|
|
194
|
+
spring:(smoothclip::SpringAnimation)spring
|
|
195
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime {
|
|
196
|
+
const CGPoint from = [self smoothClipBackdropCurrentTranslation];
|
|
197
|
+
[self stopBackdropAnimation];
|
|
198
|
+
[self writeTranslation:target];
|
|
199
|
+
if (self.window == nil) return NO;
|
|
200
|
+
// The same normalized trajectory and settling rule as the clip host, so
|
|
201
|
+
// both layers stop in the same frame.
|
|
202
|
+
const double duration =
|
|
203
|
+
smoothclip::springSettleDuration(spring, spring.initialVelocity);
|
|
204
|
+
const auto animation = [&](NSString *keyPath, double fromValue, double toValue) {
|
|
205
|
+
CASpringAnimation *result = [CASpringAnimation animationWithKeyPath:keyPath];
|
|
206
|
+
result.fromValue = @(fromValue);
|
|
207
|
+
result.toValue = @(toValue);
|
|
208
|
+
result.mass = spring.mass;
|
|
209
|
+
result.stiffness = spring.stiffness;
|
|
210
|
+
result.damping = spring.damping;
|
|
211
|
+
result.initialVelocity = spring.initialVelocity;
|
|
212
|
+
result.duration = duration;
|
|
213
|
+
return result;
|
|
214
|
+
};
|
|
215
|
+
CAAnimationGroup *group = [CAAnimationGroup animation];
|
|
216
|
+
group.animations = @[
|
|
217
|
+
animation(@"transform.translation.x", from.x, target.x),
|
|
218
|
+
animation(@"transform.translation.y", from.y, target.y),
|
|
219
|
+
];
|
|
220
|
+
group.duration = duration;
|
|
221
|
+
[self installBackdropGroup:group sharedBeginTime:sharedBeginTime];
|
|
222
|
+
return YES;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
- (void)smoothClipCancelBackdropUsingTarget:(BOOL)useTarget {
|
|
226
|
+
if (!_animating) return;
|
|
227
|
+
const CGPoint visible = [self smoothClipBackdropCurrentTranslation];
|
|
228
|
+
[self stopBackdropAnimation];
|
|
229
|
+
if (!useTarget) [self writeTranslation:visible];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
- (BOOL)smoothClipPauseBackdropAtMediaTime:(CFTimeInterval)mediaTime {
|
|
233
|
+
CAAnimation *installed =
|
|
234
|
+
[_content.layer animationForKey:kSmoothClipBackdropAnimationKey];
|
|
235
|
+
if (!_animating || installed == nil) return NO;
|
|
236
|
+
CAAnimation *paused = [installed copy];
|
|
237
|
+
const CFTimeInterval localNow =
|
|
238
|
+
[_content.layer convertTime:mediaTime fromLayer:nil];
|
|
239
|
+
paused.speed = 0;
|
|
240
|
+
paused.timeOffset = std::clamp(
|
|
241
|
+
localNow - installed.beginTime, (CFTimeInterval)0, installed.duration);
|
|
242
|
+
paused.beginTime = 0;
|
|
243
|
+
[_content.layer removeAnimationForKey:kSmoothClipBackdropAnimationKey];
|
|
244
|
+
[_content.layer addAnimation:paused forKey:kSmoothClipBackdropAnimationKey];
|
|
245
|
+
return YES;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
- (BOOL)smoothClipResumeBackdropAtMediaTime:(CFTimeInterval)mediaTime
|
|
249
|
+
catchUpBy:(CFTimeInterval)catchUp {
|
|
250
|
+
CAAnimation *installed =
|
|
251
|
+
[_content.layer animationForKey:kSmoothClipBackdropAnimationKey];
|
|
252
|
+
if (!_animating || installed == nil) return NO;
|
|
253
|
+
CAAnimation *resumed = [installed copy];
|
|
254
|
+
const CFTimeInterval localNow =
|
|
255
|
+
[_content.layer convertTime:mediaTime fromLayer:nil];
|
|
256
|
+
const CFTimeInterval resumedTime = std::min(
|
|
257
|
+
installed.duration,
|
|
258
|
+
std::max((CFTimeInterval)0, installed.timeOffset + catchUp));
|
|
259
|
+
resumed.speed = 1;
|
|
260
|
+
resumed.timeOffset = 0;
|
|
261
|
+
resumed.beginTime = localNow - resumedTime;
|
|
262
|
+
[_content.layer removeAnimationForKey:kSmoothClipBackdropAnimationKey];
|
|
263
|
+
[_content.layer addAnimation:resumed forKey:kSmoothClipBackdropAnimationKey];
|
|
264
|
+
return YES;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#import "SmoothClipViewRegistry.h"
|
|
2
2
|
|
|
3
|
+
#import "SmoothClipBackdropView.h"
|
|
3
4
|
#import "SmoothClipView.h"
|
|
4
5
|
|
|
5
6
|
#import <React/RCTUtils.h>
|
|
@@ -57,6 +58,21 @@
|
|
|
57
58
|
- (void)smoothClipClearVelocitySamples;
|
|
58
59
|
@end
|
|
59
60
|
|
|
61
|
+
@interface SmoothClipBackdropView (Registry)
|
|
62
|
+
- (CGPoint)smoothClipBackdropCurrentTranslation;
|
|
63
|
+
- (void)smoothClipApplyBackdrop:(CGPoint)translation;
|
|
64
|
+
- (BOOL)smoothClipAnimateBackdropTo:(CGPoint)target
|
|
65
|
+
timing:(smoothclip::TimingAnimation)timing
|
|
66
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime;
|
|
67
|
+
- (BOOL)smoothClipAnimateBackdropTo:(CGPoint)target
|
|
68
|
+
spring:(smoothclip::SpringAnimation)spring
|
|
69
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime;
|
|
70
|
+
- (void)smoothClipCancelBackdropUsingTarget:(BOOL)useTarget;
|
|
71
|
+
- (BOOL)smoothClipPauseBackdropAtMediaTime:(CFTimeInterval)mediaTime;
|
|
72
|
+
- (BOOL)smoothClipResumeBackdropAtMediaTime:(CFTimeInterval)mediaTime
|
|
73
|
+
catchUpBy:(CFTimeInterval)catchUp;
|
|
74
|
+
@end
|
|
75
|
+
|
|
60
76
|
namespace smoothclip {
|
|
61
77
|
namespace {
|
|
62
78
|
|
|
@@ -91,6 +107,9 @@ struct DriverState {
|
|
|
91
107
|
bool hasLatest = false;
|
|
92
108
|
Ownership ownership = Ownership::Interactive;
|
|
93
109
|
std::vector<ViewKey> views;
|
|
110
|
+
// Backdrop views bound to the driver: translated by the presentation's
|
|
111
|
+
// backdrop channel on every write and animated on every run's epoch.
|
|
112
|
+
std::vector<ViewKey> backdrops;
|
|
94
113
|
std::optional<ActiveAnimation> animation;
|
|
95
114
|
// Set by destroyDriver while views are still registered (StrictMode effect
|
|
96
115
|
// replay, hosts mounted in another subtree). The entry is erased when the
|
|
@@ -116,8 +135,19 @@ struct GroupState {
|
|
|
116
135
|
std::unordered_set<uint64_t> remainingDriverIds;
|
|
117
136
|
bool finished = true;
|
|
118
137
|
bool mutating = false;
|
|
138
|
+
// Media time the caller's UI frame was stamped with; 0 when none was given.
|
|
139
|
+
CFTimeInterval beginTimeHint = 0;
|
|
119
140
|
};
|
|
120
141
|
|
|
142
|
+
// A frame stamp sits at most one frame ahead of the commit that carries it.
|
|
143
|
+
// Anything further ahead is not a frame stamp and must not hold the run.
|
|
144
|
+
// Behind the commit it is trusted only within the same window the Android
|
|
145
|
+
// frame loop uses (kStartStampSanityWindowS): a group held pending longer than
|
|
146
|
+
// that (a host that could not display, the app inactive) or a stamp from a
|
|
147
|
+
// rescaled clock (Slow Animations) would otherwise begin fully elapsed and
|
|
148
|
+
// snap to its target.
|
|
149
|
+
constexpr CFTimeInterval kMaxBeginTimeLeadS = 0.25;
|
|
150
|
+
|
|
121
151
|
std::unordered_map<uint64_t, DriverState> ®istry() {
|
|
122
152
|
static std::unordered_map<uint64_t, DriverState> value;
|
|
123
153
|
return value;
|
|
@@ -199,6 +229,7 @@ void tryStartGroup(int32_t groupId);
|
|
|
199
229
|
void finishGroupForHostLoss(int32_t groupId);
|
|
200
230
|
DriverSnapshot snapshotForDriver(uint64_t driverId);
|
|
201
231
|
SmoothClipView *viewForKey(ViewKey key);
|
|
232
|
+
SmoothClipBackdropView *backdropForKey(ViewKey key);
|
|
202
233
|
|
|
203
234
|
bool animationNeedsForegroundReconciliation(const DriverState &state) {
|
|
204
235
|
if (!state.animation.has_value()) return false;
|
|
@@ -248,6 +279,9 @@ bool pauseSpringForInactivity(
|
|
|
248
279
|
smoothClipPauseSpringAnimation:animation.animationId
|
|
249
280
|
atMediaTime:mediaTime] && paused;
|
|
250
281
|
}
|
|
282
|
+
for (const ViewKey key : state.backdrops) {
|
|
283
|
+
[backdropForKey(key) smoothClipPauseBackdropAtMediaTime:mediaTime];
|
|
284
|
+
}
|
|
251
285
|
if (!paused) animation.needsForegroundReconciliation = true;
|
|
252
286
|
return paused;
|
|
253
287
|
}
|
|
@@ -268,6 +302,10 @@ bool resumeSpringAfterInactivity(
|
|
|
268
302
|
atMediaTime:mediaTime
|
|
269
303
|
catchUpBy:catchUp] && resumed;
|
|
270
304
|
}
|
|
305
|
+
for (const ViewKey key : state.backdrops) {
|
|
306
|
+
[backdropForKey(key) smoothClipResumeBackdropAtMediaTime:mediaTime
|
|
307
|
+
catchUpBy:catchUp];
|
|
308
|
+
}
|
|
271
309
|
if (!resumed) animation.needsForegroundReconciliation = true;
|
|
272
310
|
return resumed;
|
|
273
311
|
}
|
|
@@ -328,6 +366,83 @@ SmoothClipView *viewForKey(ViewKey key) {
|
|
|
328
366
|
return (__bridge SmoothClipView *)(reinterpret_cast<void *>(key));
|
|
329
367
|
}
|
|
330
368
|
|
|
369
|
+
ViewKey keyForBackdrop(SmoothClipBackdropView *view) {
|
|
370
|
+
return reinterpret_cast<ViewKey>((__bridge void *)view);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
SmoothClipBackdropView *backdropForKey(ViewKey key) {
|
|
374
|
+
return (__bridge SmoothClipBackdropView *)(reinterpret_cast<void *>(key));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
CGPoint backdropTranslation(const Presentation &presentation) {
|
|
378
|
+
return CGPointMake(
|
|
379
|
+
presentation.backdropTranslateX, presentation.backdropTranslateY);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// The clip host knows nothing of the backdrop channel, so a presentation
|
|
383
|
+
// read back from it carries none. A bound backdrop's visible translation is
|
|
384
|
+
// the truth; without one, the driver's model (a pending run's start, else the
|
|
385
|
+
// latest target).
|
|
386
|
+
void overlayBackdrop(const DriverState &state, Presentation &presentation) {
|
|
387
|
+
if (!state.backdrops.empty()) {
|
|
388
|
+
const CGPoint translation = [backdropForKey(state.backdrops.front())
|
|
389
|
+
smoothClipBackdropCurrentTranslation];
|
|
390
|
+
presentation.backdropTranslateX = translation.x;
|
|
391
|
+
presentation.backdropTranslateY = translation.y;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const Presentation &model =
|
|
395
|
+
state.animation.has_value() && !state.animation->started
|
|
396
|
+
? state.animation->start
|
|
397
|
+
: state.latest;
|
|
398
|
+
presentation.backdropTranslateX = model.backdropTranslateX;
|
|
399
|
+
presentation.backdropTranslateY = model.backdropTranslateY;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
void applyBackdrops(const DriverState &state, const Presentation &presentation) {
|
|
403
|
+
const CGPoint translation = backdropTranslation(presentation);
|
|
404
|
+
for (const ViewKey key : state.backdrops) {
|
|
405
|
+
[backdropForKey(key) smoothClipApplyBackdrop:translation];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
void cancelBackdrops(const DriverState &state, bool useTarget) {
|
|
410
|
+
for (const ViewKey key : state.backdrops) {
|
|
411
|
+
[backdropForKey(key) smoothClipCancelBackdropUsingTarget:useTarget];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Freezes every bound backdrop where it is and reads the channel back into
|
|
416
|
+
// the frozen presentation.
|
|
417
|
+
void freezeBackdropsInto(const DriverState &state, Presentation &presentation) {
|
|
418
|
+
cancelBackdrops(state, false);
|
|
419
|
+
overlayBackdrop(state, presentation);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Runs the pending animation's backdrop channel on every bound backdrop, on
|
|
423
|
+
// the same epoch as the clip's group. An inherited spring velocity was
|
|
424
|
+
// resolved once by the registry before dispatch (resolvedSpringVelocity), so
|
|
425
|
+
// `active.spring` carries the same trajectory the clip host runs.
|
|
426
|
+
void installBackdropAnimations(
|
|
427
|
+
const DriverState &state,
|
|
428
|
+
CFTimeInterval sharedBeginTime) {
|
|
429
|
+
if (!state.animation.has_value() || state.backdrops.empty()) return;
|
|
430
|
+
const ActiveAnimation &active = *state.animation;
|
|
431
|
+
const CGPoint target = backdropTranslation(state.latest);
|
|
432
|
+
for (const ViewKey key : state.backdrops) {
|
|
433
|
+
SmoothClipBackdropView *backdrop = backdropForKey(key);
|
|
434
|
+
if (active.kind == AnimationKind::Timing) {
|
|
435
|
+
[backdrop smoothClipAnimateBackdropTo:target
|
|
436
|
+
timing:active.timing
|
|
437
|
+
sharedBeginTime:sharedBeginTime];
|
|
438
|
+
} else {
|
|
439
|
+
[backdrop smoothClipAnimateBackdropTo:target
|
|
440
|
+
spring:active.spring
|
|
441
|
+
sharedBeginTime:sharedBeginTime];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
331
446
|
bool shouldReduceMotion(int32_t setting) {
|
|
332
447
|
return setting == 1 ||
|
|
333
448
|
(setting == 0 && UIAccessibilityIsReduceMotionEnabled());
|
|
@@ -468,6 +583,7 @@ Presentation canonicalFrozenPresentation(
|
|
|
468
583
|
if (!hasCanonical && hasParticipantFallback) {
|
|
469
584
|
canonical = participantFallback;
|
|
470
585
|
}
|
|
586
|
+
freezeBackdropsInto(state, canonical);
|
|
471
587
|
return canonical;
|
|
472
588
|
}
|
|
473
589
|
|
|
@@ -478,7 +594,9 @@ Presentation canonicalVisiblePresentation(const DriverState &state) {
|
|
|
478
594
|
for (const ViewKey key : state.views) {
|
|
479
595
|
SmoothClipView *candidate = viewForKey(key);
|
|
480
596
|
if ([candidate smoothClipIsJoinable]) {
|
|
481
|
-
|
|
597
|
+
Presentation visible = [candidate smoothClipCurrentPresentation];
|
|
598
|
+
overlayBackdrop(state, visible);
|
|
599
|
+
return visible;
|
|
482
600
|
}
|
|
483
601
|
}
|
|
484
602
|
return state.latest;
|
|
@@ -554,11 +672,13 @@ std::vector<DriverSnapshot> cancelGroupInternal(
|
|
|
554
672
|
for (const ViewKey key : state.views) {
|
|
555
673
|
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
556
674
|
}
|
|
675
|
+
cancelBackdrops(state, true);
|
|
557
676
|
if (wasPending) {
|
|
558
677
|
for (const ViewKey key : state.views) {
|
|
559
678
|
[viewForKey(key) smoothClipApplyPresentation:resolved
|
|
560
679
|
recordVelocitySample:NO];
|
|
561
680
|
}
|
|
681
|
+
applyBackdrops(state, resolved);
|
|
562
682
|
}
|
|
563
683
|
} else {
|
|
564
684
|
resolved = canonicalFrozenPresentation(state);
|
|
@@ -614,6 +734,7 @@ void cancelActive(
|
|
|
614
734
|
for (const ViewKey key : state.views) {
|
|
615
735
|
[viewForKey(key) smoothClipCancelAnimationUsingTarget:useTarget];
|
|
616
736
|
}
|
|
737
|
+
cancelBackdrops(state, useTarget);
|
|
617
738
|
emitCompletion(driverId, state, animationId, false);
|
|
618
739
|
}
|
|
619
740
|
|
|
@@ -628,6 +749,7 @@ void finishStandaloneAtTarget(uint64_t driverId, DriverState &state) {
|
|
|
628
749
|
for (const ViewKey key : state.views) {
|
|
629
750
|
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
630
751
|
}
|
|
752
|
+
cancelBackdrops(state, true);
|
|
631
753
|
state.ownership = Ownership::Interactive;
|
|
632
754
|
emitCompletion(driverId, state, animationId, true);
|
|
633
755
|
}
|
|
@@ -646,6 +768,7 @@ void finishIfNoInstalledParticipants(uint64_t driverId, DriverState &state) {
|
|
|
646
768
|
for (const ViewKey key : state.views) {
|
|
647
769
|
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
648
770
|
}
|
|
771
|
+
cancelBackdrops(state, true);
|
|
649
772
|
state.ownership = Ownership::Interactive;
|
|
650
773
|
emitCompletion(driverId, state, animationId, finished);
|
|
651
774
|
}
|
|
@@ -662,6 +785,7 @@ void applyPresentation(
|
|
|
662
785
|
[viewForKey(key) smoothClipApplyPresentation:presentation
|
|
663
786
|
recordVelocitySample:recordVelocitySample];
|
|
664
787
|
}
|
|
788
|
+
applyBackdrops(state, presentation);
|
|
665
789
|
}
|
|
666
790
|
|
|
667
791
|
void prepareAnimation(
|
|
@@ -753,6 +877,7 @@ void startPendingAnimation(DriverState &state) {
|
|
|
753
877
|
[participant smoothClipApplyPresentation:start recordVelocitySample:NO];
|
|
754
878
|
installPendingAnimation(state, participant, key, start);
|
|
755
879
|
}
|
|
880
|
+
installBackdropAnimations(state, 0);
|
|
756
881
|
}
|
|
757
882
|
|
|
758
883
|
void tryStartGroup(int32_t groupId) {
|
|
@@ -773,7 +898,18 @@ void tryStartGroup(int32_t groupId) {
|
|
|
773
898
|
}
|
|
774
899
|
}
|
|
775
900
|
|
|
776
|
-
|
|
901
|
+
// Anchor the run to the UI frame that started it when JS stamped one
|
|
902
|
+
// (Reanimated's target vsync): a `withTiming` begun in that same frame and
|
|
903
|
+
// this Core Animation run then trace one epoch, instead of the run leading
|
|
904
|
+
// by the rest of the frame. A recent stale stamp (a run held pending until
|
|
905
|
+
// its host could display) starts partway through, as that JS clock did; one
|
|
906
|
+
// outside the sanity window starts now.
|
|
907
|
+
const CFTimeInterval now = CACurrentMediaTime();
|
|
908
|
+
const CFTimeInterval hint = groupIterator->second.beginTimeHint;
|
|
909
|
+
const CFTimeInterval sharedBeginTime =
|
|
910
|
+
hint > 0 && hint <= now + kMaxBeginTimeLeadS &&
|
|
911
|
+
hint >= now - kStartStampSanityWindowS
|
|
912
|
+
? hint : now;
|
|
777
913
|
[CATransaction begin];
|
|
778
914
|
[CATransaction setDisableActions:YES];
|
|
779
915
|
for (const uint64_t driverId : driverIds) {
|
|
@@ -788,6 +924,7 @@ void tryStartGroup(int32_t groupId) {
|
|
|
788
924
|
installPendingAnimation(
|
|
789
925
|
state, participant, key, start, sharedBeginTime);
|
|
790
926
|
}
|
|
927
|
+
installBackdropAnimations(state, sharedBeginTime);
|
|
791
928
|
}
|
|
792
929
|
[CATransaction commit];
|
|
793
930
|
}
|
|
@@ -947,6 +1084,7 @@ void registerView(
|
|
|
947
1084
|
state.views.push_back(key);
|
|
948
1085
|
}
|
|
949
1086
|
[view smoothClipApplyPresentation:visible recordVelocitySample:NO];
|
|
1087
|
+
applyBackdrops(state, visible);
|
|
950
1088
|
if (startsPendingRun) {
|
|
951
1089
|
startPendingAnimation(state);
|
|
952
1090
|
}
|
|
@@ -1002,7 +1140,45 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
1002
1140
|
state.views.erase(
|
|
1003
1141
|
std::remove(state.views.begin(), state.views.end(), key),
|
|
1004
1142
|
state.views.end());
|
|
1005
|
-
if (state.destroyed && state.views.empty()
|
|
1143
|
+
if (state.destroyed && state.views.empty() && state.backdrops.empty()) {
|
|
1144
|
+
registry().erase(iterator);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
void registerBackdropView(uint64_t driverId, SmoothClipBackdropView *view) {
|
|
1149
|
+
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
1150
|
+
auto &state = registry()[driverId];
|
|
1151
|
+
const ViewKey key = keyForBackdrop(view);
|
|
1152
|
+
if (std::find(state.backdrops.begin(), state.backdrops.end(), key) !=
|
|
1153
|
+
state.backdrops.end()) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
// Adopt the driver's current value: a pending run's start, an already
|
|
1157
|
+
// bound backdrop's visible value mid-run, else the model. A backdrop bound
|
|
1158
|
+
// while a run is in flight follows from the next run.
|
|
1159
|
+
Presentation visible =
|
|
1160
|
+
state.animation.has_value() && !state.animation->started
|
|
1161
|
+
? state.animation->start
|
|
1162
|
+
: state.latest;
|
|
1163
|
+
if (!state.backdrops.empty()) overlayBackdrop(state, visible);
|
|
1164
|
+
state.backdrops.push_back(key);
|
|
1165
|
+
if (state.hasLatest) {
|
|
1166
|
+
[view smoothClipApplyBackdrop:backdropTranslation(visible)];
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
void unregisterBackdropView(uint64_t driverId, SmoothClipBackdropView *view) {
|
|
1171
|
+
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
1172
|
+
auto iterator = registry().find(driverId);
|
|
1173
|
+
if (iterator == registry().end()) return;
|
|
1174
|
+
DriverState &state = iterator->second;
|
|
1175
|
+
const ViewKey key = keyForBackdrop(view);
|
|
1176
|
+
state.backdrops.erase(
|
|
1177
|
+
std::remove(state.backdrops.begin(), state.backdrops.end(), key),
|
|
1178
|
+
state.backdrops.end());
|
|
1179
|
+
if (state.destroyed && state.views.empty() && state.backdrops.empty()) {
|
|
1180
|
+
registry().erase(iterator);
|
|
1181
|
+
}
|
|
1006
1182
|
}
|
|
1007
1183
|
|
|
1008
1184
|
void setPresentation(
|
|
@@ -1325,7 +1501,8 @@ int32_t createGroupAnimation(
|
|
|
1325
1501
|
SpringAnimation spring,
|
|
1326
1502
|
double durationMs,
|
|
1327
1503
|
int32_t reduceMotion,
|
|
1328
|
-
int32_t completionTag
|
|
1504
|
+
int32_t completionTag,
|
|
1505
|
+
double startedAtHintS) {
|
|
1329
1506
|
std::vector<Presentation> resolvedStarts;
|
|
1330
1507
|
bool reduced = false;
|
|
1331
1508
|
if (!NSThread.isMainThread || controllerId == 0 ||
|
|
@@ -1377,6 +1554,9 @@ int32_t createGroupAnimation(
|
|
|
1377
1554
|
driverIds,
|
|
1378
1555
|
std::unordered_set<uint64_t>(driverIds.begin(), driverIds.end()),
|
|
1379
1556
|
};
|
|
1557
|
+
if (std::isfinite(startedAtHintS) && startedAtHintS > 0) {
|
|
1558
|
+
group.beginTimeHint = startedAtHintS;
|
|
1559
|
+
}
|
|
1380
1560
|
groupRegistry().emplace(groupId, std::move(group));
|
|
1381
1561
|
|
|
1382
1562
|
for (std::size_t entryIndex = 0; entryIndex < entries.size(); entryIndex += 1) {
|
|
@@ -1420,7 +1600,6 @@ int32_t animateTimingGroup(
|
|
|
1420
1600
|
TimingAnimation animation,
|
|
1421
1601
|
int32_t completionTag,
|
|
1422
1602
|
double startedAtHintS) {
|
|
1423
|
-
(void)startedAtHintS;
|
|
1424
1603
|
return createGroupAnimation(
|
|
1425
1604
|
controllerId,
|
|
1426
1605
|
std::move(entries),
|
|
@@ -1429,7 +1608,8 @@ int32_t animateTimingGroup(
|
|
|
1429
1608
|
{},
|
|
1430
1609
|
animation.durationMs,
|
|
1431
1610
|
animation.reduceMotion,
|
|
1432
|
-
completionTag
|
|
1611
|
+
completionTag,
|
|
1612
|
+
startedAtHintS);
|
|
1433
1613
|
}
|
|
1434
1614
|
|
|
1435
1615
|
int32_t animateSpringGroup(
|
|
@@ -1438,7 +1618,6 @@ int32_t animateSpringGroup(
|
|
|
1438
1618
|
SpringAnimation animation,
|
|
1439
1619
|
int32_t completionTag,
|
|
1440
1620
|
double startedAtHintS) {
|
|
1441
|
-
(void)startedAtHintS;
|
|
1442
1621
|
return createGroupAnimation(
|
|
1443
1622
|
controllerId,
|
|
1444
1623
|
std::move(entries),
|
|
@@ -1447,7 +1626,8 @@ int32_t animateSpringGroup(
|
|
|
1447
1626
|
animation,
|
|
1448
1627
|
0,
|
|
1449
1628
|
animation.reduceMotion,
|
|
1450
|
-
completionTag
|
|
1629
|
+
completionTag,
|
|
1630
|
+
startedAtHintS);
|
|
1451
1631
|
}
|
|
1452
1632
|
|
|
1453
1633
|
std::vector<DriverSnapshot> cancelAnimationGroup(
|
|
@@ -1544,6 +1724,7 @@ int32_t animateTiming(
|
|
|
1544
1724
|
state.animation->participants.insert(key);
|
|
1545
1725
|
}
|
|
1546
1726
|
}
|
|
1727
|
+
installBackdropAnimations(state, 0);
|
|
1547
1728
|
return animationId;
|
|
1548
1729
|
}
|
|
1549
1730
|
|
|
@@ -1633,6 +1814,7 @@ int32_t animateSpring(
|
|
|
1633
1814
|
state.animation->participants.insert(key);
|
|
1634
1815
|
}
|
|
1635
1816
|
}
|
|
1817
|
+
installBackdropAnimations(state, 0);
|
|
1636
1818
|
return animationId;
|
|
1637
1819
|
}
|
|
1638
1820
|
|
|
@@ -1709,6 +1891,7 @@ CancelResult cancelAnimation(
|
|
|
1709
1891
|
[viewForKey(key) smoothClipApplyPresentation:target
|
|
1710
1892
|
recordVelocitySample:NO];
|
|
1711
1893
|
}
|
|
1894
|
+
applyBackdrops(state, target);
|
|
1712
1895
|
}
|
|
1713
1896
|
state.ownership = Ownership::Interactive;
|
|
1714
1897
|
return {true, target};
|
|
@@ -1733,7 +1916,7 @@ void destroyDriver(uint64_t driverId) {
|
|
|
1733
1916
|
for (const ViewKey key : state.views) {
|
|
1734
1917
|
[viewForKey(key) smoothClipClearVelocitySamples];
|
|
1735
1918
|
}
|
|
1736
|
-
if (state.views.empty()) {
|
|
1919
|
+
if (state.views.empty() && state.backdrops.empty()) {
|
|
1737
1920
|
registry().erase(iterator);
|
|
1738
1921
|
} else {
|
|
1739
1922
|
// Views can outlive the hook briefly (StrictMode effect replay, hosts in
|