@rafaelaugusto/jolt 0.1.0-rc.1 → 0.1.0-rc.2
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 +39 -11
- package/dist/interactive-navigation/index.d.ts.map +1 -1
- package/dist/interactive-navigation/index.js +39 -3
- package/dist/interactive-navigation/index.js.map +1 -1
- package/dist/mixer.d.ts.map +1 -1
- package/dist/mixer.js +9 -1
- package/dist/mixer.js.map +1 -1
- package/dist/physics/index.d.ts +23 -0
- package/dist/physics/index.d.ts.map +1 -1
- package/dist/physics/index.js +10 -0
- package/dist/physics/index.js.map +1 -1
- package/dist/physics/native.d.ts +1 -0
- package/dist/physics/native.d.ts.map +1 -1
- package/dist/physics/native.js +6 -0
- package/dist/physics/native.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +11 -2
- package/dist/press.js.map +1 -1
- package/dist/scroll-header/index.d.ts.map +1 -1
- package/dist/scroll-header/index.js +36 -10
- package/dist/scroll-header/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/CircleMath.java +18 -11
- package/platforms/android/java/org/rafaelaugusto/jolt/CircleWorld.java +22 -11
- package/platforms/ios/src/JoltCircleMath.h +19 -11
- package/platforms/ios/src/JoltCircleWorld.h +1 -0
- package/platforms/ios/src/JoltCircleWorld.m +32 -17
- package/platforms/ios/src/JoltScrollHeader.m +8 -0
- package/src/android-plugin.d.ts +1 -0
- package/src/interactive-navigation/index.ts +32 -3
- package/src/ios-plugin.d.ts +1 -0
- package/src/mixer.ts +7 -1
- package/src/physics/index.ts +33 -0
- package/src/physics/native.ts +11 -0
- package/src/press.ts +10 -2
- package/src/scroll-header/index.ts +34 -8
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
double initialX[16],initialY[16],initialVX[16],initialVY[16],originalX[16],originalY[16];
|
|
10
10
|
}
|
|
11
11
|
@property(nonatomic) UIView *container;
|
|
12
|
-
@property(nonatomic) NSMutableArray *views,*pans,*guides;
|
|
12
|
+
@property(nonatomic) NSMutableArray *views,*pans,*guides,*pointerGuides;
|
|
13
13
|
@property(nonatomic) CADisplayLink *link;
|
|
14
14
|
@property(nonatomic) BOOL enabled,disposed,background,fits;
|
|
15
15
|
@property(nonatomic) double previous,accumulator,processingMs,droppedMs,offsetX,offsetY;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- (void)refresh;
|
|
19
19
|
- (void)apply;
|
|
20
20
|
- (void)cancelDrag;
|
|
21
|
+
- (id)makeGuide:(id)line;
|
|
21
22
|
- (void)configureAttachment:(id)value index:(int)index;
|
|
22
23
|
@end
|
|
23
24
|
|
|
@@ -30,7 +31,7 @@ static NSMutableSet<JoltCircleWorld *> *worlds;
|
|
|
30
31
|
NSDictionary *config=[NSJSONSerialization JSONObjectWithData:[options dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
|
|
31
32
|
NSArray *bodies=config[@"bodies"];
|
|
32
33
|
if(bodies.count<1||bodies.count>16)[NSException raise:NSInvalidArgumentException format:@"Physics needs 1–16 circles"];
|
|
33
|
-
JoltCircleWorld *w=[self new];w.container=container;w.views=[NSMutableArray new];w.pans=[NSMutableArray new];w.guides=[NSMutableArray new];w.dragging=-1;
|
|
34
|
+
JoltCircleWorld *w=[self new];w.container=container;w.views=[NSMutableArray new];w.pans=[NSMutableArray new];w.guides=[NSMutableArray new];w.pointerGuides=[NSMutableArray new];w.dragging=-1;
|
|
34
35
|
w->state.count=(int)bodies.count;w->state.width=[config[@"width"] doubleValue];w->state.height=[config[@"height"] doubleValue];
|
|
35
36
|
w->state.gx=[config[@"gravity"][@"x"] doubleValue];w->state.gy=[config[@"gravity"][@"y"] doubleValue];
|
|
36
37
|
w->state.restitution=[config[@"restitution"] doubleValue];w->state.damping=[config[@"damping"] doubleValue];
|
|
@@ -39,8 +40,13 @@ static NSMutableSet<JoltCircleWorld *> *worlds;
|
|
|
39
40
|
b->x=[v[@"x"] doubleValue];b->y=[v[@"y"] doubleValue];b->r=[v[@"radius"] doubleValue];b->m=[v[@"mass"] doubleValue];
|
|
40
41
|
b->vx=[v[@"vx"] doubleValue];b->vy=[v[@"vy"] doubleValue];
|
|
41
42
|
w->initialX[i]=b->x/w->state.width;w->initialY[i]=b->y/w->state.height;w->initialVX[i]=b->vx;w->initialVY[i]=b->vy;
|
|
42
|
-
[w.views addObject:NSNull.null];[w.pans addObject:NSNull.null];[w.guides addObject:NSNull.null];
|
|
43
|
+
[w.views addObject:NSNull.null];[w.pans addObject:NSNull.null];[w.guides addObject:NSNull.null];[w.pointerGuides addObject:NSNull.null];
|
|
43
44
|
[w configureAttachment:v[@"attachment"] index:i];
|
|
45
|
+
id drag=v[@"drag"];
|
|
46
|
+
if([drag isKindOfClass:NSDictionary.class]){
|
|
47
|
+
b->drag.enabled=true;b->drag.length=[drag[@"length"] doubleValue];b->drag.stiffness=[drag[@"stiffness"] doubleValue];b->drag.damping=[drag[@"damping"] doubleValue];
|
|
48
|
+
w.pointerGuides[i]=[w makeGuide:drag[@"line"]];
|
|
49
|
+
}
|
|
44
50
|
id snap=v[@"snap"];
|
|
45
51
|
if([snap isKindOfClass:NSDictionary.class]){
|
|
46
52
|
NSArray *points=snap[@"points"];if(points.count>JOLT_CIRCLE_SNAP_LIMIT)[NSException raise:NSInvalidArgumentException format:@"Too many snap points"];
|
|
@@ -60,13 +66,17 @@ static NSMutableSet<JoltCircleWorld *> *worlds;
|
|
|
60
66
|
else JoltCircleAttach(&state,index,false,0,0,0,180,24);
|
|
61
67
|
if(self.guides[index]!=NSNull.null){[(CALayer *)self.guides[index] removeFromSuperlayer];self.guides[index]=NSNull.null;}
|
|
62
68
|
id line=active?value[@"line"]:nil;
|
|
63
|
-
|
|
64
|
-
CAShapeLayer *guide=[CAShapeLayer layer];unsigned int color=[line[@"color"] unsignedIntValue];
|
|
65
|
-
guide.strokeColor=[UIColor colorWithRed:((color>>16)&255)/255.0 green:((color>>8)&255)/255.0 blue:(color&255)/255.0 alpha:1].CGColor;
|
|
66
|
-
guide.fillColor=nil;guide.contentsScale=UIScreen.mainScreen.scale;guide.lineWidth=[line[@"width"] doubleValue];guide.lineCap=kCALineCapRound;
|
|
67
|
-
[self.container.layer addSublayer:guide];self.guides[index]=guide;
|
|
68
|
-
}
|
|
69
|
+
self.guides[index]=[self makeGuide:line];
|
|
69
70
|
}
|
|
71
|
+
- (id)makeGuide:(id)line {
|
|
72
|
+
if(![line isKindOfClass:NSDictionary.class])return NSNull.null;
|
|
73
|
+
CAShapeLayer *guide=[CAShapeLayer layer];unsigned int color=[line[@"color"] unsignedIntValue];
|
|
74
|
+
guide.strokeColor=[UIColor colorWithRed:((color>>16)&255)/255.0 green:((color>>8)&255)/255.0 blue:(color&255)/255.0 alpha:1].CGColor;
|
|
75
|
+
guide.fillColor=nil;guide.contentsScale=UIScreen.mainScreen.scale;guide.lineWidth=[line[@"width"] doubleValue];guide.lineCap=kCALineCapRound;
|
|
76
|
+
[self.container.layer addSublayer:guide];return guide;
|
|
77
|
+
}
|
|
78
|
+
- (BOOL)supportsPointerSpring {return YES;}
|
|
79
|
+
|
|
70
80
|
- (void)attach:(NSInteger)index options:(NSString *)options {
|
|
71
81
|
if(self.disposed||index<0||index>=state.count)return;
|
|
72
82
|
id value=[NSJSONSerialization JSONObjectWithData:[options dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingFragmentsAllowed error:nil];
|
|
@@ -110,13 +120,17 @@ static NSMutableSet<JoltCircleWorld *> *worlds;
|
|
|
110
120
|
[v.layer setValue:@(state.bodies[i].x-v.layer.position.x) forKeyPath:@"transform.translation.x"];
|
|
111
121
|
[v.layer setValue:@(state.bodies[i].y-v.layer.position.y) forKeyPath:@"transform.translation.y"];
|
|
112
122
|
}
|
|
113
|
-
for(int i=0;i<state.count;i++)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
for(int i=0;i<state.count;i++)for(int kind=0;kind<2;kind++){
|
|
124
|
+
id candidate=(kind?self.pointerGuides:self.guides)[i];if(candidate==NSNull.null)continue;
|
|
125
|
+
JoltCircle *b=&state.bodies[i];CAShapeLayer *guide=candidate;
|
|
126
|
+
BOOL pointer=b->held&&b->drag.enabled;
|
|
127
|
+
guide.hidden=pointer!=(kind==1);if(guide.hidden)continue;
|
|
128
|
+
double x=pointer?b->tx:b->snap.active?JoltCircleSnapX(&state,b,b->snap.index):b->attachment.u*state.width;
|
|
129
|
+
double y=pointer?b->ty:b->snap.active?JoltCircleSnapY(&state,b,b->snap.index):b->attachment.v*state.height;
|
|
117
130
|
double dx=b->x-x,dy=b->y-y,d=hypot(dx,dy);
|
|
118
131
|
UIBezierPath *path=[UIBezierPath bezierPath];
|
|
119
|
-
if(d>b->r){[path moveToPoint:CGPointMake(x,y)];[path addLineToPoint:CGPointMake(b->x-dx/d*b->r,b->y-dy/d*b->r)];
|
|
132
|
+
if(d>b->r){[path moveToPoint:CGPointMake(x,y)];[path addLineToPoint:CGPointMake(b->x-dx/d*b->r,b->y-dy/d*b->r)];}
|
|
133
|
+
if(pointer||d>b->r)[path appendPath:[UIBezierPath bezierPathWithOvalInRect:CGRectMake(x-2.5,y-2.5,5,5)]];
|
|
120
134
|
guide.path=path.CGPath;
|
|
121
135
|
}
|
|
122
136
|
[CATransaction commit];
|
|
@@ -176,14 +190,15 @@ static NSMutableSet<JoltCircleWorld *> *worlds;
|
|
|
176
190
|
- (NSString *)snapshot {
|
|
177
191
|
NSMutableArray *bodies=[NSMutableArray new];
|
|
178
192
|
NSUInteger guideCount=0;
|
|
179
|
-
for(
|
|
193
|
+
for(NSArray *list in @[self.guides,self.pointerGuides])for(id guide in list)if(guide!=NSNull.null)guideCount++;
|
|
180
194
|
for(int i=0;i<state.count;i++){
|
|
181
195
|
JoltCircle *b=&state.bodies[i];
|
|
182
196
|
id attachment=b->attachment.enabled?@{@"anchor":@{@"x":@(b->attachment.u*state.width),@"y":@(b->attachment.v*state.height)},@"length":@(b->attachment.length),@"stiffness":@(b->attachment.stiffness),@"damping":@(b->attachment.damping)}:NSNull.null;
|
|
183
197
|
id snap=NSNull.null;
|
|
184
198
|
if(b->snap.active){double x=JoltCircleSnapX(&state,b,b->snap.index),y=JoltCircleSnapY(&state,b,b->snap.index);snap=@{@"index":@(b->snap.index),@"point":@{@"x":@(x),@"y":@(y)},@"settled":[NSNumber numberWithBool:hypot(b->x-x,b->y-y)<=.25&&hypot(b->vx,b->vy)<=4&&!b->held]};}
|
|
199
|
+
id pointer=b->held&&b->drag.enabled?@{@"anchor":@{@"x":@(b->tx),@"y":@(b->ty)},@"length":@(b->drag.length),@"stiffness":@(b->drag.stiffness),@"damping":@(b->drag.damping)}:NSNull.null;
|
|
185
200
|
id release=b->released?@{@"x":@(b->releaseX),@"y":@(b->releaseY),@"vx":@(b->releaseVX),@"vy":@(b->releaseVY)}:NSNull.null;
|
|
186
|
-
[bodies addObject:@{@"x":@(b->x),@"y":@(b->y),@"vx":@(b->vx),@"vy":@(b->vy),@"radius":@(b->r),@"held":@(b->held),@"attachment":attachment,@"snap":snap,@"release":release}];
|
|
201
|
+
[bodies addObject:@{@"x":@(b->x),@"y":@(b->y),@"vx":@(b->vx),@"vy":@(b->vy),@"radius":@(b->r),@"held":@(b->held),@"attachment":attachment,@"pointer":pointer,@"snap":snap,@"release":release}];
|
|
187
202
|
}
|
|
188
203
|
NSString *status=self.disposed?@"disposed":!self.enabled?@"paused":self.background||!self.container.window?@"detached":!self.fits?@"invalid-bounds":state.sleeping?@"sleeping":@"running";
|
|
189
204
|
NSDictionary *data=@{@"status":status,@"guides":@(guideCount),@"bodies":bodies,@"frames":@(self.frames),@"steps":@(state.steps),@"contacts":@(state.contacts),@"dragStarts":@(self.dragStarts),@"dragMoves":@(self.dragMoves),@"dragEnds":@(self.dragEnds),@"dragCancels":@(self.dragCancels),@"processingMs":@(self.processingMs),@"droppedMs":@(self.droppedMs),@"clock":[NSNumber numberWithBool:self.link!=nil]};
|
|
@@ -197,7 +212,7 @@ static NSMutableSet<JoltCircleWorld *> *worlds;
|
|
|
197
212
|
UIView *v=self.views[i];v.userInteractionEnabled=originalInteraction[i];if(self.pans[i]!=NSNull.null)[v removeGestureRecognizer:self.pans[i]];
|
|
198
213
|
[v.layer setValue:@(originalX[i]) forKeyPath:@"transform.translation.x"];[v.layer setValue:@(originalY[i]) forKeyPath:@"transform.translation.y"];
|
|
199
214
|
}
|
|
200
|
-
for(
|
|
215
|
+
for(NSMutableArray *list in @[self.guides,self.pointerGuides]){for(id guide in list)if(guide!=NSNull.null)[guide removeFromSuperlayer];[list removeAllObjects];}
|
|
201
216
|
[CATransaction commit];[self.views removeAllObjects];[self.pans removeAllObjects];self.container=nil;[worlds removeObject:self];
|
|
202
217
|
}
|
|
203
218
|
@end
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
@property(nonatomic,copy) void (^onState)(void);
|
|
22
22
|
@property(nonatomic) BOOL observing, disposed, titleHidden, toolbarTitleHidden, toolbarTouch;
|
|
23
23
|
@property(nonatomic) double handoff, minimumOffset, maximumOffset, range, spacer, rowHeight, offset;
|
|
24
|
+
@property(nonatomic) CGFloat estimatedSectionHeaderHeight, estimatedSectionFooterHeight;
|
|
24
25
|
@property(nonatomic) NSInteger phase, updates, callbacks;
|
|
25
26
|
@end
|
|
26
27
|
static NSInteger JoltScrollHeaderActive = 0;
|
|
@@ -32,6 +33,11 @@ static void *JoltScrollHeaderContext = &JoltScrollHeaderContext;
|
|
|
32
33
|
JoltScrollHeader *owner=[self new];owner.list=list;owner.node=node;owner.title=title;owner.toolbarTitle=toolbarTitle;owner.toolbar=toolbar;owner.diagnostic=diagnostic;owner.onState=onState;
|
|
33
34
|
owner.titleHidden=title.accessibilityElementsHidden;owner.toolbarTitleHidden=toolbarTitle.accessibilityElementsHidden;owner.toolbarTouch=toolbar.userInteractionEnabled;owner.phase=-1;owner.offset=NAN;
|
|
34
35
|
owner.proxy=[JoltScrollHeaderDelegate new];owner.proxy.forwarded=list.delegate;
|
|
36
|
+
// Flat lists have no section headers/footers. UIKit's automatic estimates
|
|
37
|
+
// otherwise adjust the scroll anchor during width/safe-area changes.
|
|
38
|
+
owner.estimatedSectionHeaderHeight=list.estimatedSectionHeaderHeight;
|
|
39
|
+
owner.estimatedSectionFooterHeight=list.estimatedSectionFooterHeight;
|
|
40
|
+
list.estimatedSectionHeaderHeight=0;list.estimatedSectionFooterHeight=0;
|
|
35
41
|
[owner configure:options];list.delegate=owner.proxy;[list reloadData];[list layoutIfNeeded];[owner setEnabled:YES];return owner;
|
|
36
42
|
}
|
|
37
43
|
- (void)configure:(NSString *)options {
|
|
@@ -88,6 +94,8 @@ static void *JoltScrollHeaderContext = &JoltScrollHeaderContext;
|
|
|
88
94
|
if(self.disposed)return;[self setEnabled:NO];self.disposed=YES;
|
|
89
95
|
self.title.accessibilityElementsHidden=self.titleHidden;self.toolbarTitle.accessibilityElementsHidden=self.toolbarTitleHidden;self.toolbar.userInteractionEnabled=self.toolbarTouch;
|
|
90
96
|
if(self.list.delegate==self.proxy)self.list.delegate=self.proxy.forwarded;
|
|
97
|
+
if(self.list.estimatedSectionHeaderHeight==0)self.list.estimatedSectionHeaderHeight=self.estimatedSectionHeaderHeight;
|
|
98
|
+
if(self.list.estimatedSectionFooterHeight==0)self.list.estimatedSectionFooterHeight=self.estimatedSectionFooterHeight;
|
|
91
99
|
self.proxy=nil;self.node=nil;self.onState=nil;
|
|
92
100
|
}
|
|
93
101
|
@end
|
package/src/android-plugin.d.ts
CHANGED
|
@@ -100,6 +100,8 @@ export function interactiveNavigation(
|
|
|
100
100
|
let detachTarget: (() => void) | undefined
|
|
101
101
|
let resolvedReturn: import('@nativescript/core').View | undefined
|
|
102
102
|
let returning = false
|
|
103
|
+
let suspended = false
|
|
104
|
+
let resumeTimer: ReturnType<typeof setTimeout> | undefined
|
|
103
105
|
const originalPage = frame.currentPage
|
|
104
106
|
function prepareShared() {
|
|
105
107
|
if (!native || !options.shared) return
|
|
@@ -210,7 +212,17 @@ export function interactiveNavigation(
|
|
|
210
212
|
targetScope?.abort()
|
|
211
213
|
native?.cancel()
|
|
212
214
|
}
|
|
213
|
-
const
|
|
215
|
+
const suspend = () => {
|
|
216
|
+
suspended = true
|
|
217
|
+
if (resumeTimer !== undefined) clearTimeout(resumeTimer)
|
|
218
|
+
resumeTimer = undefined
|
|
219
|
+
cancel()
|
|
220
|
+
}
|
|
221
|
+
const unload = () => {
|
|
222
|
+
// Core unloads the root view in the background, without removing its routes.
|
|
223
|
+
if (suspended) cancel()
|
|
224
|
+
else control.dispose()
|
|
225
|
+
}
|
|
214
226
|
const shown = () => native?.sync()
|
|
215
227
|
const swipeChanged = () => {
|
|
216
228
|
void Promise.resolve().then(shown)
|
|
@@ -226,6 +238,17 @@ export function interactiveNavigation(
|
|
|
226
238
|
void Promise.resolve().then(() => control.dispose())
|
|
227
239
|
else shown()
|
|
228
240
|
}
|
|
241
|
+
const resume = () => {
|
|
242
|
+
suspended = false
|
|
243
|
+
// Core raises resume before reloading the root. A macrotask lets that load
|
|
244
|
+
// finish, including when native callbacks drain JS microtasks in between.
|
|
245
|
+
if (resumeTimer !== undefined) clearTimeout(resumeTimer)
|
|
246
|
+
resumeTimer = setTimeout(() => {
|
|
247
|
+
resumeTimer = undefined
|
|
248
|
+
if (frame.isLoaded) arrived()
|
|
249
|
+
else control.dispose()
|
|
250
|
+
}, 0)
|
|
251
|
+
}
|
|
229
252
|
const entering = (args: EventData) => {
|
|
230
253
|
const event = args as NavigationData
|
|
231
254
|
if (!page && !event.isBack) candidate = event.entry?.resolvedPage
|
|
@@ -268,6 +291,8 @@ export function interactiveNavigation(
|
|
|
268
291
|
try {
|
|
269
292
|
return (
|
|
270
293
|
state === 'idle' &&
|
|
294
|
+
!suspended &&
|
|
295
|
+
frame.isLoaded &&
|
|
271
296
|
!!start &&
|
|
272
297
|
!reducedMotion() &&
|
|
273
298
|
!!page &&
|
|
@@ -365,11 +390,14 @@ export function interactiveNavigation(
|
|
|
365
390
|
native = undefined
|
|
366
391
|
emit('disposed')
|
|
367
392
|
releaseMotion?.()
|
|
393
|
+
if (resumeTimer !== undefined) clearTimeout(resumeTimer)
|
|
394
|
+
resumeTimer = undefined
|
|
368
395
|
frame.off('unloaded', unload)
|
|
369
396
|
frame.off('navigatingTo', entering)
|
|
370
397
|
frame.off('navigatedTo', arrived)
|
|
371
398
|
frame.off('viewControllerShown', shown)
|
|
372
|
-
Application.off(Application.suspendEvent,
|
|
399
|
+
Application.off(Application.suspendEvent, suspend)
|
|
400
|
+
Application.off(Application.resumeEvent, resume)
|
|
373
401
|
if (transition?.instance) SharedTransition.finishState(transition.instance.id)
|
|
374
402
|
page?.off('enableSwipeBackNavigationChange', swipeChanged)
|
|
375
403
|
start = undefined
|
|
@@ -389,7 +417,8 @@ export function interactiveNavigation(
|
|
|
389
417
|
frame.on('navigatedTo', arrived)
|
|
390
418
|
frame.on('viewControllerShown', shown)
|
|
391
419
|
frame.on('unloaded', unload)
|
|
392
|
-
Application.on(Application.suspendEvent,
|
|
420
|
+
Application.on(Application.suspendEvent, suspend)
|
|
421
|
+
Application.on(Application.resumeEvent, resume)
|
|
393
422
|
releaseMotion = onReducedMotion((value) => {
|
|
394
423
|
if (value) cancel()
|
|
395
424
|
})
|
package/src/ios-plugin.d.ts
CHANGED
|
@@ -355,6 +355,7 @@ declare class JoltCircleWorld extends NSObject {
|
|
|
355
355
|
attachOptions(index: number, options: string): void
|
|
356
356
|
snapToTarget(index: number, target: number): void
|
|
357
357
|
reset(): void
|
|
358
|
+
supportsPointerSpring(): boolean
|
|
358
359
|
snapshot(): string
|
|
359
360
|
dispose(): void
|
|
360
361
|
}
|
package/src/mixer.ts
CHANGED
|
@@ -279,11 +279,17 @@ export class Mix {
|
|
|
279
279
|
velocityAt: (elapsed) => (at(elapsed + 0.5) - at(elapsed - 0.5)) * 1000,
|
|
280
280
|
}
|
|
281
281
|
writeValue(this.view, this.property, track.to)
|
|
282
|
-
|
|
282
|
+
const native = driver.play(this.view, track, now, (finished = true) => {
|
|
283
283
|
if (instance !== this.generation || !finished) return
|
|
284
284
|
this.native = undefined
|
|
285
285
|
this.compose()
|
|
286
286
|
})
|
|
287
|
+
if (instance !== this.generation) return
|
|
288
|
+
this.native = native
|
|
289
|
+
// Building native keyframes can take time. Join the current composition
|
|
290
|
+
// phase after construction, including when a rolling window is replaced.
|
|
291
|
+
const elapsed = driver.now() - now
|
|
292
|
+
if (elapsed > 1) driver.control?.(this.view, native, elapsed, false)
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
private dissolve() {
|
package/src/physics/index.ts
CHANGED
|
@@ -29,6 +29,17 @@ export interface PhysicsSnap {
|
|
|
29
29
|
/** Seconds of release-velocity projection for choosing a point, 0–.3; default .12. */
|
|
30
30
|
projection?: number
|
|
31
31
|
}
|
|
32
|
+
export interface PhysicsSpringDrag {
|
|
33
|
+
type: 'spring'
|
|
34
|
+
/** Temporary pointer spring rest length in DIPs, 0–10000; default 0. */
|
|
35
|
+
length?: number
|
|
36
|
+
/** Spring force per DIP, 1–1000; default 180. */
|
|
37
|
+
stiffness?: number
|
|
38
|
+
/** Viscous damping, 0–120; default 24. */
|
|
39
|
+
damping?: number
|
|
40
|
+
/** Optional native tether, visible only during the gesture. */
|
|
41
|
+
line?: { color: string; width?: number }
|
|
42
|
+
}
|
|
32
43
|
export interface CircleBody {
|
|
33
44
|
id: string
|
|
34
45
|
/** A dedicated, square direct child of the container, sized to radius * 2. */
|
|
@@ -42,6 +53,8 @@ export interface CircleBody {
|
|
|
42
53
|
velocity?: PhysicsVector
|
|
43
54
|
attachment?: PhysicsAttachment
|
|
44
55
|
snap?: PhysicsSnap
|
|
56
|
+
/** Direct dragging by default. A spring drag keeps the body dynamic during contact. */
|
|
57
|
+
drag?: 'direct' | PhysicsSpringDrag
|
|
45
58
|
}
|
|
46
59
|
export interface PhysicsOptions {
|
|
47
60
|
/** DIPs/s². Defaults to { x: 0, y: 750 }. */
|
|
@@ -63,6 +76,13 @@ export interface PhysicsSnapshot {
|
|
|
63
76
|
vy: number
|
|
64
77
|
radius: number
|
|
65
78
|
held: boolean
|
|
79
|
+
/** Temporary center target: pointer plus the initial center-to-contact offset, in local DIPs. */
|
|
80
|
+
pointer: {
|
|
81
|
+
anchor: PhysicsVector
|
|
82
|
+
length: number
|
|
83
|
+
stiffness: number
|
|
84
|
+
damping: number
|
|
85
|
+
} | null
|
|
66
86
|
/** Last native release, before snap selection or subsequent integration. */
|
|
67
87
|
release: { x: number; y: number; vx: number; vy: number } | null
|
|
68
88
|
attachment: {
|
|
@@ -183,6 +203,18 @@ function prepareSnap(
|
|
|
183
203
|
projection: number(value.projection ?? 0.12, 'snap.projection', 0, 0.3),
|
|
184
204
|
}
|
|
185
205
|
}
|
|
206
|
+
function prepareDrag(value: CircleBody['drag'], width: number, height: number) {
|
|
207
|
+
if (value === undefined || value === 'direct') return null
|
|
208
|
+
if (!value || typeof value !== 'object' || value.type !== 'spring')
|
|
209
|
+
throw new TypeError('Expected direct dragging or a spring drag configuration.')
|
|
210
|
+
// Share the validated spring ranges and line style; the pointer supplies the anchor natively.
|
|
211
|
+
const {
|
|
212
|
+
x: _x,
|
|
213
|
+
y: _y,
|
|
214
|
+
...spring
|
|
215
|
+
} = prepareAttachment({ ...value, anchor: { x: 0, y: 0 } }, width, height)!
|
|
216
|
+
return spring
|
|
217
|
+
}
|
|
186
218
|
function untransformed(view: View) {
|
|
187
219
|
return (
|
|
188
220
|
(view.translateX ?? 0) === 0 &&
|
|
@@ -264,6 +296,7 @@ export function physicsWorld(
|
|
|
264
296
|
vy: velocity.y,
|
|
265
297
|
attachment: prepareAttachment(body.attachment ?? null, width, height),
|
|
266
298
|
snap: prepareSnap(body.snap, width, height, radius),
|
|
299
|
+
drag: prepareDrag(body.drag, width, height),
|
|
267
300
|
}
|
|
268
301
|
})
|
|
269
302
|
if (!host.isLoaded || !host.nativeViewProtected)
|
package/src/physics/native.ts
CHANGED
|
@@ -34,6 +34,7 @@ interface Configuration {
|
|
|
34
34
|
vx: number
|
|
35
35
|
vy: number
|
|
36
36
|
attachment: NativeAttachment | null
|
|
37
|
+
drag: Omit<NativeAttachment, 'x' | 'y'> | null
|
|
37
38
|
snap: {
|
|
38
39
|
points: PhysicsVector[]
|
|
39
40
|
stiffness: number
|
|
@@ -64,6 +65,11 @@ export const createCircleDriver: CircleFactory = (container, views, options) =>
|
|
|
64
65
|
try {
|
|
65
66
|
if (typeof native.attachOptions !== 'function')
|
|
66
67
|
throw new Error('Rebuild the native app to use physics attachments.')
|
|
68
|
+
if (
|
|
69
|
+
options.bodies.some((b) => b.drag) &&
|
|
70
|
+
typeof native.supportsPointerSpring !== 'function'
|
|
71
|
+
)
|
|
72
|
+
throw new Error('Rebuild the native app to use spring dragging.')
|
|
67
73
|
views.forEach((v, i) => native.connectIndex(v.nativeViewProtected, i))
|
|
68
74
|
} catch (e) {
|
|
69
75
|
native.dispose()
|
|
@@ -92,6 +98,11 @@ export const createCircleDriver: CircleFactory = (container, views, options) =>
|
|
|
92
98
|
try {
|
|
93
99
|
if (typeof native.attach !== 'function')
|
|
94
100
|
throw new Error('Rebuild the native app to use physics attachments.')
|
|
101
|
+
if (
|
|
102
|
+
options.bodies.some((b) => b.drag) &&
|
|
103
|
+
typeof native.supportsPointerSpring !== 'function'
|
|
104
|
+
)
|
|
105
|
+
throw new Error('Rebuild the native app to use spring dragging.')
|
|
95
106
|
views.forEach((v, i) => native.connect(v.nativeViewProtected, i))
|
|
96
107
|
} catch (e) {
|
|
97
108
|
native.dispose()
|
package/src/press.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Color, type EventData, type TouchGestureEventData } from '@nativescript/core'
|
|
2
2
|
import { isColorProperty } from './color.js'
|
|
3
|
-
import { animate, startingValues, toView } from './animate.js'
|
|
3
|
+
import { animate, startingValues, stop, toView } from './animate.js'
|
|
4
|
+
import { identity } from './mixer.js'
|
|
4
5
|
import type { PlaybackAnimation } from './playback.js'
|
|
5
6
|
import { readValue } from './properties.js'
|
|
6
7
|
import type { SpringName, Transition } from './transition.js'
|
|
@@ -25,6 +26,7 @@ export function press(
|
|
|
25
26
|
): () => void {
|
|
26
27
|
const view = toView(target)
|
|
27
28
|
const properties = [...startingValues(pressed, options).keys()]
|
|
29
|
+
const layer = typeof options === 'object' ? options.layer : undefined
|
|
28
30
|
let rest: Keyframes | undefined
|
|
29
31
|
let active: PlaybackAnimation | undefined
|
|
30
32
|
|
|
@@ -34,7 +36,11 @@ export function press(
|
|
|
34
36
|
rest = Object.fromEntries(
|
|
35
37
|
properties.map((p) => [
|
|
36
38
|
p,
|
|
37
|
-
|
|
39
|
+
layer !== undefined
|
|
40
|
+
? identity(p)
|
|
41
|
+
: isColorProperty(p)
|
|
42
|
+
? new Color(readValue(view, p))
|
|
43
|
+
: readValue(view, p),
|
|
38
44
|
]),
|
|
39
45
|
)
|
|
40
46
|
active = animate(view, pressed, options ?? pressIn)
|
|
@@ -52,6 +58,8 @@ export function press(
|
|
|
52
58
|
|
|
53
59
|
const cancel = () => {
|
|
54
60
|
active?.stop()
|
|
61
|
+
// A completed press still holds its layer contribution until it is removed.
|
|
62
|
+
if (layer !== undefined) stop(view, properties, { layer })
|
|
55
63
|
active = undefined
|
|
56
64
|
rest = undefined
|
|
57
65
|
}
|
|
@@ -152,17 +152,20 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
152
152
|
native: ScrollHeaderDriver | undefined,
|
|
153
153
|
value: SpringValue | undefined
|
|
154
154
|
let ended = false,
|
|
155
|
+
ready = false,
|
|
155
156
|
held = false,
|
|
156
157
|
background = false,
|
|
157
158
|
queued = false,
|
|
159
|
+
invalidated = false,
|
|
158
160
|
revision = 0
|
|
159
161
|
let measuring: Promise<void> | undefined,
|
|
162
|
+
restoringOffset: number | undefined,
|
|
160
163
|
operation = false
|
|
161
164
|
let detachBindings: (() => void)[] = [],
|
|
162
165
|
pending = new Set<() => void>(),
|
|
163
166
|
last = initialState()
|
|
164
167
|
const notify = () => {
|
|
165
|
-
if (ended) return
|
|
168
|
+
if (ended || !ready) return
|
|
166
169
|
last = native?.snapshot() ?? last
|
|
167
170
|
try {
|
|
168
171
|
options.onState?.(last)
|
|
@@ -304,16 +307,18 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
304
307
|
options.titleFade,
|
|
305
308
|
options.toolbarFade,
|
|
306
309
|
)
|
|
307
|
-
if (geometry && JSON.stringify(next) === JSON.stringify(geometry) && native)
|
|
310
|
+
if (ready && geometry && JSON.stringify(next) === JSON.stringify(geometry) && native)
|
|
311
|
+
return
|
|
308
312
|
const before = geometry,
|
|
309
313
|
offset = native?.snapshot().offset ?? (last.updates ? last.offset : undefined)
|
|
310
|
-
|
|
314
|
+
ready = false
|
|
311
315
|
native?.enabled(false)
|
|
312
316
|
try {
|
|
313
317
|
native?.configure(next, reducedMotion())
|
|
314
318
|
options.setSpacerHeight(next.spacer)
|
|
315
319
|
await layout(list)
|
|
316
320
|
if (ended || generation !== revision) return
|
|
321
|
+
geometry = next
|
|
317
322
|
value ??= springValue(motionValue(0), { duration: 0 })
|
|
318
323
|
maps()
|
|
319
324
|
if (!native)
|
|
@@ -327,20 +332,32 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
327
332
|
void Promise.resolve().then(notify)
|
|
328
333
|
},
|
|
329
334
|
)
|
|
330
|
-
if (offset !== undefined)
|
|
331
|
-
|
|
335
|
+
if (offset !== undefined) {
|
|
336
|
+
restoringOffset = before ? resizedOffset(offset, before, next) : offset
|
|
337
|
+
native.scroll(restoringOffset, false)
|
|
338
|
+
await layout(list)
|
|
339
|
+
if (ended || generation !== revision) return
|
|
340
|
+
}
|
|
332
341
|
native.enabled(!held && !background)
|
|
342
|
+
ready = true
|
|
333
343
|
notify()
|
|
334
344
|
} finally {
|
|
345
|
+
restoringOffset = undefined
|
|
335
346
|
native?.enabled(!held && !background)
|
|
336
347
|
}
|
|
337
348
|
}
|
|
338
349
|
const queueMeasure = () => {
|
|
339
|
-
if (ended
|
|
350
|
+
if (ended) return
|
|
351
|
+
if (measuring || operation) {
|
|
352
|
+
invalidated = true
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
if (queued) return
|
|
340
356
|
queued = true
|
|
341
357
|
void Promise.resolve()
|
|
342
358
|
.then(() => {
|
|
343
359
|
queued = false
|
|
360
|
+
invalidated = false
|
|
344
361
|
return control.remeasure()
|
|
345
362
|
})
|
|
346
363
|
.catch((error) => {
|
|
@@ -350,8 +367,10 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
350
367
|
}
|
|
351
368
|
const detach = () => {
|
|
352
369
|
revision++
|
|
370
|
+
ready = false
|
|
353
371
|
for (const cancel of [...pending]) cancel()
|
|
354
372
|
last = native?.snapshot() ?? last
|
|
373
|
+
if (restoringOffset !== undefined) last = { ...last, offset: restoringOffset }
|
|
355
374
|
native?.dispose()
|
|
356
375
|
native = undefined
|
|
357
376
|
detachBindings.splice(0).forEach((off) => off())
|
|
@@ -373,7 +392,7 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
373
392
|
})
|
|
374
393
|
const control: ScrollHeader = {
|
|
375
394
|
get ready() {
|
|
376
|
-
return
|
|
395
|
+
return ready && !ended
|
|
377
396
|
},
|
|
378
397
|
get disposed() {
|
|
379
398
|
return ended
|
|
@@ -385,9 +404,16 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
385
404
|
return geometry
|
|
386
405
|
},
|
|
387
406
|
remeasure() {
|
|
407
|
+
invalidated = true
|
|
388
408
|
if (measuring) return measuring
|
|
389
|
-
measuring =
|
|
409
|
+
measuring = (async () => {
|
|
410
|
+
do {
|
|
411
|
+
invalidated = false
|
|
412
|
+
await measure()
|
|
413
|
+
} while (invalidated && !ended && list.isLoaded && !operation)
|
|
414
|
+
})().finally(() => {
|
|
390
415
|
measuring = undefined
|
|
416
|
+
if (invalidated) queueMeasure()
|
|
391
417
|
})
|
|
392
418
|
return measuring
|
|
393
419
|
},
|