@rafaelaugusto/jolt 0.1.0-beta.15 → 0.1.0-beta.17

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.
Files changed (132) hide show
  1. package/README.md +9 -2
  2. package/dist/bottom-sheet/geometry.d.ts +18 -0
  3. package/dist/bottom-sheet/geometry.d.ts.map +1 -0
  4. package/dist/bottom-sheet/geometry.js +51 -0
  5. package/dist/bottom-sheet/geometry.js.map +1 -0
  6. package/dist/bottom-sheet/index.d.ts +56 -0
  7. package/dist/bottom-sheet/index.d.ts.map +1 -0
  8. package/dist/bottom-sheet/index.js +462 -0
  9. package/dist/bottom-sheet/index.js.map +1 -0
  10. package/dist/bottom-sheet/native.d.ts +11 -0
  11. package/dist/bottom-sheet/native.d.ts.map +1 -0
  12. package/dist/bottom-sheet/native.js +173 -0
  13. package/dist/bottom-sheet/native.js.map +1 -0
  14. package/dist/drag.js +1 -1
  15. package/dist/drag.js.map +1 -1
  16. package/dist/driver/android.d.ts.map +1 -1
  17. package/dist/driver/android.js +2 -0
  18. package/dist/driver/android.js.map +1 -1
  19. package/dist/driver/ios-scroll-delegate.d.ts +4 -0
  20. package/dist/driver/ios-scroll-delegate.d.ts.map +1 -0
  21. package/dist/driver/ios-scroll-delegate.js +26 -0
  22. package/dist/driver/ios-scroll-delegate.js.map +1 -0
  23. package/dist/driver/ios.d.ts.map +1 -1
  24. package/dist/driver/ios.js +2 -0
  25. package/dist/driver/ios.js.map +1 -1
  26. package/dist/gesture-native.d.ts +28 -0
  27. package/dist/gesture-native.d.ts.map +1 -0
  28. package/dist/gesture-native.js +61 -0
  29. package/dist/gesture-native.js.map +1 -0
  30. package/dist/gestures/index.d.ts +69 -0
  31. package/dist/gestures/index.d.ts.map +1 -0
  32. package/dist/gestures/index.js +262 -0
  33. package/dist/gestures/index.js.map +1 -0
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/live-spring-driver.d.ts +52 -0
  39. package/dist/live-spring-driver.d.ts.map +1 -0
  40. package/dist/live-spring-driver.js +104 -0
  41. package/dist/live-spring-driver.js.map +1 -0
  42. package/dist/mixer.d.ts +6 -0
  43. package/dist/mixer.d.ts.map +1 -1
  44. package/dist/mixer.js +41 -1
  45. package/dist/mixer.js.map +1 -1
  46. package/dist/motion-value.d.ts +34 -2
  47. package/dist/motion-value.d.ts.map +1 -1
  48. package/dist/motion-value.js +277 -10
  49. package/dist/motion-value.js.map +1 -1
  50. package/dist/native/index.d.ts +2 -0
  51. package/dist/native/index.d.ts.map +1 -1
  52. package/dist/native/index.js +1 -0
  53. package/dist/native/index.js.map +1 -1
  54. package/dist/native-property.d.ts +2 -0
  55. package/dist/native-property.d.ts.map +1 -1
  56. package/dist/native-property.js +12 -4
  57. package/dist/native-property.js.map +1 -1
  58. package/dist/reorder/handle.d.ts +6 -0
  59. package/dist/reorder/handle.d.ts.map +1 -0
  60. package/dist/reorder/handle.js +10 -0
  61. package/dist/reorder/handle.js.map +1 -0
  62. package/dist/reorder/index.d.ts +10 -0
  63. package/dist/reorder/index.d.ts.map +1 -0
  64. package/dist/reorder/index.js +233 -0
  65. package/dist/reorder/index.js.map +1 -0
  66. package/dist/reorder/list-view.d.ts +5 -0
  67. package/dist/reorder/list-view.d.ts.map +1 -0
  68. package/dist/reorder/list-view.js +238 -0
  69. package/dist/reorder/list-view.js.map +1 -0
  70. package/dist/reorder/model.d.ts +6 -0
  71. package/dist/reorder/model.d.ts.map +1 -0
  72. package/dist/reorder/model.js +30 -0
  73. package/dist/reorder/model.js.map +1 -0
  74. package/dist/reorder/surface.d.ts +6 -0
  75. package/dist/reorder/surface.d.ts.map +1 -0
  76. package/dist/reorder/surface.js +10 -0
  77. package/dist/reorder/surface.js.map +1 -0
  78. package/dist/reorder/types.d.ts +92 -0
  79. package/dist/reorder/types.d.ts.map +1 -0
  80. package/dist/reorder/types.js +2 -0
  81. package/dist/reorder/types.js.map +1 -0
  82. package/dist/scene-shader.d.ts +18 -0
  83. package/dist/scene-shader.d.ts.map +1 -0
  84. package/dist/scene-shader.js +76 -0
  85. package/dist/scene-shader.js.map +1 -0
  86. package/dist/value-observers.d.ts +3 -1
  87. package/dist/value-observers.d.ts.map +1 -1
  88. package/dist/value-observers.js +10 -2
  89. package/dist/value-observers.js.map +1 -1
  90. package/package.json +13 -1
  91. package/platforms/android/include.gradle +9 -0
  92. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +198 -0
  93. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSurface.java +53 -0
  94. package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +270 -0
  95. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderHandle.java +25 -0
  96. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSession.java +213 -0
  97. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSurface.java +27 -0
  98. package/platforms/android/java/org/rafaelaugusto/jolt/SpringState.java +39 -0
  99. package/platforms/android/proguard-rules.pro +5 -0
  100. package/platforms/ios/src/JoltGestureSession.h +18 -0
  101. package/platforms/ios/src/JoltGestureSession.m +222 -0
  102. package/platforms/ios/src/JoltLiveSpring.h +28 -0
  103. package/platforms/ios/src/JoltLiveSpring.m +236 -0
  104. package/platforms/ios/src/JoltReorderSession.h +15 -0
  105. package/platforms/ios/src/JoltReorderSession.m +205 -0
  106. package/platforms/ios/src/JoltSpringState.h +35 -0
  107. package/platforms/ios/src/module.modulemap +3 -0
  108. package/src/android-plugin.d.ts +93 -0
  109. package/src/bottom-sheet/geometry.ts +84 -0
  110. package/src/bottom-sheet/index.ts +536 -0
  111. package/src/bottom-sheet/native.ts +214 -0
  112. package/src/drag.ts +1 -1
  113. package/src/driver/android.ts +2 -0
  114. package/src/driver/ios-scroll-delegate.ts +23 -0
  115. package/src/driver/ios.ts +2 -0
  116. package/src/gesture-native.ts +139 -0
  117. package/src/gestures/index.ts +365 -0
  118. package/src/index.ts +3 -1
  119. package/src/ios-plugin.d.ts +90 -0
  120. package/src/live-spring-driver.ts +181 -0
  121. package/src/mixer.ts +43 -1
  122. package/src/motion-value.ts +318 -11
  123. package/src/native/index.ts +6 -0
  124. package/src/native-property.ts +17 -4
  125. package/src/reorder/handle.ts +9 -0
  126. package/src/reorder/index.ts +243 -0
  127. package/src/reorder/list-view.ts +262 -0
  128. package/src/reorder/model.ts +47 -0
  129. package/src/reorder/surface.ts +10 -0
  130. package/src/reorder/types.ts +87 -0
  131. package/src/scene-shader.ts +139 -0
  132. package/src/value-observers.ts +12 -2
@@ -0,0 +1,236 @@
1
+ #import "JoltLiveSpring.h"
2
+ #import "JoltSpringState.h"
3
+ #import <QuartzCore/QuartzCore.h>
4
+
5
+ static id decode(NSString *json) {
6
+ return [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
7
+ }
8
+ static double mapped(double value, NSArray *mappings) {
9
+ for (NSDictionary *mapping in mappings) {
10
+ NSArray *input = mapping[@"input"], *output = mapping[@"output"];
11
+ BOOL clamp = [mapping[@"clamp"] boolValue];
12
+ if (clamp && value <= [input.firstObject doubleValue]) { value = [output.firstObject doubleValue]; continue; }
13
+ if (clamp && value >= [input.lastObject doubleValue]) { value = [output.lastObject doubleValue]; continue; }
14
+ NSUInteger index = 0;
15
+ while (index < input.count - 2 && value > [input[index + 1] doubleValue]) index++;
16
+ double span = [input[index + 1] doubleValue] - [input[index] doubleValue];
17
+ double fraction = span > 0 ? (value - [input[index] doubleValue]) / span : value < [input[index] doubleValue] ? 0 : 1;
18
+ value = [output[index] doubleValue] + ([output[index + 1] doubleValue] - [output[index] doubleValue]) * fraction;
19
+ }
20
+ return value;
21
+ }
22
+ static BOOL inputRunning(NSDictionary *input, double anchor, double now) {
23
+ double repeat = [input[@"repeat"] doubleValue];
24
+ return [input[@"running"] boolValue] && (repeat < 0 || now - anchor + [input[@"elapsed"] doubleValue] <
25
+ [input[@"delay"] doubleValue] + [input[@"duration"] doubleValue] * (repeat + 1));
26
+ }
27
+ static double inputValue(NSDictionary *input, double anchor, double now) {
28
+ NSArray *values = input[@"values"], *times = input[@"times"];
29
+ if (![input[@"running"] boolValue] || values.count < 2) return [input[@"value"] doubleValue];
30
+ if (!inputRunning(input, anchor, now)) return [input[@"to"] doubleValue];
31
+ double time = MAX(0, now - anchor + [input[@"elapsed"] doubleValue] - [input[@"delay"] doubleValue]);
32
+ double duration = MAX(1, [input[@"duration"] doubleValue]);
33
+ double iteration = floor(time / duration), progress = fmod(time, duration) / duration;
34
+ if ([input[@"reverse"] boolValue] && fmod(iteration, 2) >= 1) progress = 1 - progress;
35
+ NSUInteger lo = 0, hi = times.count - 1;
36
+ while (lo + 1 < hi) { NSUInteger mid = (lo + hi) / 2; if ([times[mid] doubleValue] <= progress) lo = mid; else hi = mid; }
37
+ double start = [times[lo] doubleValue], end = [times[hi] doubleValue];
38
+ double fraction = end > start ? (progress - start) / (end - start) : 1;
39
+ return [values[lo] doubleValue] + ([values[hi] doubleValue] - [values[lo] doubleValue]) * MAX(0, MIN(1, fraction));
40
+ }
41
+
42
+ @interface JoltLiveSpring () {
43
+ JoltSpringState _state;
44
+ }
45
+ @property(nonatomic) double stiffness, damping, mass, restDelta, restSpeed;
46
+ @property(nonatomic, readwrite) BOOL moving;
47
+ @property(nonatomic) BOOL reduced, paused, disposed, direct, inputHeld;
48
+ @property(nonatomic) double targetValue;
49
+ @property(nonatomic, nullable) JoltLiveSpring *source;
50
+ @property(nonatomic) NSArray *sourceMaps;
51
+ @property(nonatomic) NSDictionary *input;
52
+ @property(nonatomic) double anchor;
53
+ @property(nonatomic) NSMutableArray<JoltLiveSpringBinding *> *bindings;
54
+ - (void)step:(double)now seconds:(double)seconds;
55
+ - (void)apply;
56
+ - (void)wake;
57
+ @end
58
+ @interface JoltLiveSpringBinding ()
59
+ @property(nonatomic, weak) UIView *view;
60
+ @property(nonatomic, weak) JoltLiveSpring *node;
61
+ @property(nonatomic) NSString *property;
62
+ @property(nonatomic) NSArray *mappings;
63
+ @property(nonatomic) BOOL stopped;
64
+ @property(nonatomic) NSArray *sampledLayers;
65
+ @property(nonatomic) double layerAnchor;
66
+ - (BOOL)running:(double)now;
67
+ - (void)apply;
68
+ @end
69
+
70
+ @implementation JoltLiveSpring
71
+ static NSMutableArray<JoltLiveSpring *> *nodes;
72
+ static CADisplayLink *displayLink;
73
+ static double previousFrame;
74
+ + (void)initialize {
75
+ if (self == [JoltLiveSpring class]) nodes = [NSMutableArray new];
76
+ }
77
+ + (void)schedule {
78
+ if (displayLink || !nodes.count) return;
79
+ displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
80
+ if (@available(iOS 15.0, *)) {
81
+ float maximum = MIN(120, UIScreen.mainScreen.maximumFramesPerSecond);
82
+ displayLink.preferredFrameRateRange = CAFrameRateRangeMake(MIN(80, maximum), maximum, maximum);
83
+ }
84
+ [displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
85
+ }
86
+ + (void)tick:(CADisplayLink *)link {
87
+ double now = link.timestamp * 1000;
88
+ double remaining = previousFrame == 0 ? 1.0 / 120 : MIN(.064, MAX(0, link.timestamp - previousFrame));
89
+ previousFrame = link.timestamp;
90
+ while (remaining > 1e-9) {
91
+ double dt = MIN(remaining, 1.0 / 240);
92
+ remaining -= dt;
93
+ for (JoltLiveSpring *node in nodes) [node step:now - remaining * 1000 seconds:dt];
94
+ }
95
+ BOOL active = NO;
96
+ [CATransaction begin]; [CATransaction setDisableActions:YES];
97
+ for (JoltLiveSpring *node in nodes) {
98
+ [node apply]; active |= node.moving;
99
+ for (JoltLiveSpringBinding *binding in node.bindings) active |= [binding running:now];
100
+ }
101
+ [CATransaction commit];
102
+ if (!active) { [displayLink invalidate]; displayLink = nil; previousFrame = 0; }
103
+ }
104
+ + (instancetype)create:(double)initial stiffness:(double)stiffness damping:(double)damping mass:(double)mass
105
+ restDelta:(double)restDelta restSpeed:(double)restSpeed reduced:(BOOL)reduced {
106
+ JoltLiveSpring *node = [self new];
107
+ node->_state = (JoltSpringState){initial, 0};
108
+ node.stiffness = stiffness; node.damping = damping; node.mass = mass;
109
+ node.restDelta = restDelta; node.restSpeed = restSpeed; node->_reduced = reduced;
110
+ node.direct = YES; node.targetValue = initial;
111
+ node.bindings = [NSMutableArray new]; node.sourceMaps = @[];
112
+ [nodes addObject:node];
113
+ return node;
114
+ }
115
+ - (double)position { return _state.position; }
116
+ - (double)velocity { return _state.velocity; }
117
+ + (NSUInteger)activeCount { return displayLink ? 1 : 0; }
118
+ + (NSUInteger)nodeCount { return nodes.count; }
119
+ - (void)follow:(JoltLiveSpring *)source mappings:(NSString *)mappings {
120
+ if (self.disposed || source.disposed || [nodes indexOfObject:source] >= [nodes indexOfObject:self])
121
+ [NSException raise:NSInvalidArgumentException format:@"A spring can only follow an existing live node"];
122
+ self.sourceMaps = decode(mappings); self.source = source; [self wake];
123
+ }
124
+ - (void)updateInput:(NSString *)snapshot {
125
+ if (self.disposed) return;
126
+ self.input = decode(snapshot); self.direct = NO; self.anchor = CACurrentMediaTime() * 1000; [self wake];
127
+ }
128
+ - (void)setTarget:(double)value {
129
+ if (self.disposed) return;
130
+ self.direct = YES; self.targetValue = value; self.input = nil; [self wake];
131
+ }
132
+ - (void)holdInput:(double)value velocity:(double)velocity {
133
+ if (self.disposed) return;
134
+ self.inputHeld = YES; self.direct = YES; self.targetValue = value; self.input = nil;
135
+ _state = (JoltSpringState){value, velocity}; self.moving = YES;
136
+ [CATransaction begin]; [CATransaction setDisableActions:YES]; [self apply]; [CATransaction commit];
137
+ [JoltLiveSpring schedule];
138
+ }
139
+ - (void)releaseInput:(double)target velocity:(double)velocity {
140
+ if (self.disposed) return;
141
+ self.inputHeld = NO; _state.velocity = velocity; [self setTarget:target];
142
+ }
143
+ - (void)setReduced:(BOOL)value { _reduced = value; [self wake]; }
144
+ - (void)setPaused:(BOOL)value {
145
+ _paused = value;
146
+ if (value) self.moving = NO; else [self wake];
147
+ }
148
+ - (void)wake {
149
+ if (self.disposed || self.paused) return;
150
+ self.moving = YES;
151
+ if (self.reduced) {
152
+ [self step:CACurrentMediaTime() * 1000 seconds:0];
153
+ [CATransaction begin]; [CATransaction setDisableActions:YES]; [self apply]; [CATransaction commit];
154
+ }
155
+ [JoltLiveSpring schedule];
156
+ }
157
+ - (void)step:(double)now seconds:(double)seconds {
158
+ if (self.disposed || self.paused || self.inputHeld) return;
159
+ double target = self.source ? mapped(self.source.position, self.sourceMaps) : (self.direct ? self.targetValue : inputValue(self.input, self.anchor, now));
160
+ BOOL inputMoving = self.source ? self.source.moving : (!self.direct && inputRunning(self.input, self.anchor, now));
161
+ if (self.reduced) _state = (JoltSpringState){target, 0};
162
+ else _state = JoltSpringAdvance(_state, target, self.stiffness, self.damping, self.mass, seconds);
163
+ BOOL rest = fabs(_state.position - target) <= self.restDelta && fabs(_state.velocity) <= self.restSpeed;
164
+ if (rest && !inputMoving) _state = (JoltSpringState){target, 0};
165
+ self.moving = inputMoving || !rest;
166
+ }
167
+ - (JoltLiveSpringBinding *)bind:(UIView *)view property:(NSString *)property mappings:(NSString *)mappings {
168
+ if (self.disposed) [NSException raise:NSInvalidArgumentException format:@"Spring disposed"];
169
+ JoltLiveSpringBinding *binding = [JoltLiveSpringBinding new];
170
+ binding.node = self; binding.view = view; binding.property = property; binding.mappings = decode(mappings);
171
+ [self.bindings addObject:binding];
172
+ [CATransaction begin]; [CATransaction setDisableActions:YES]; [binding apply]; [CATransaction commit];
173
+ return binding;
174
+ }
175
+ - (void)apply {
176
+ for (NSInteger i = self.bindings.count - 1; i >= 0; i--) {
177
+ JoltLiveSpringBinding *binding = self.bindings[i];
178
+ if (!binding.view) [self.bindings removeObjectAtIndex:i]; else [binding apply];
179
+ }
180
+ }
181
+ + (void)sync:(UIView *)view {
182
+ [CATransaction begin]; [CATransaction setDisableActions:YES];
183
+ for (JoltLiveSpring *node in nodes)
184
+ for (JoltLiveSpringBinding *binding in node.bindings)
185
+ if (binding.view == view) [binding apply];
186
+ [CATransaction commit];
187
+ }
188
+ - (void)dispose {
189
+ if (self.disposed) return;
190
+ self.disposed = YES; self.moving = NO; [self.bindings removeAllObjects]; [nodes removeObject:self];
191
+ for (JoltLiveSpring *child in nodes) if (child.source == self) {
192
+ child.source = nil; [child setTarget:mapped(self.position, child.sourceMaps)];
193
+ }
194
+ self.source = nil;
195
+ if (!nodes.count) { [displayLink invalidate]; displayLink = nil; previousFrame = 0; }
196
+ }
197
+ @end
198
+
199
+ @implementation JoltLiveSpringBinding
200
+ - (double)value { return mapped(self.node.position, self.mappings); }
201
+ - (void)setLayers:(NSString *)inputs {
202
+ self.sampledLayers = decode(inputs); self.layerAnchor = CACurrentMediaTime() * 1000;
203
+ [CATransaction begin]; [CATransaction setDisableActions:YES]; [self apply]; [CATransaction commit];
204
+ [JoltLiveSpring schedule];
205
+ }
206
+ - (BOOL)running:(double)now {
207
+ for (NSDictionary *layer in self.sampledLayers) if (inputRunning(layer, self.layerAnchor, now)) return YES;
208
+ return NO;
209
+ }
210
+ - (void)cancel { self.stopped = YES; [self.node.bindings removeObject:self]; }
211
+ - (void)apply {
212
+ UIView *view = self.view;
213
+ if (self.stopped || !view.window) return;
214
+ NSString *key = self.property;
215
+ double value = self.value;
216
+ BOOL multiply = [key isEqualToString:@"opacity"] || [key hasPrefix:@"scale"];
217
+ double now = CACurrentMediaTime() * 1000;
218
+ for (NSDictionary *layer in self.sampledLayers) {
219
+ double part = inputValue(layer, self.layerAnchor, now);
220
+ value = multiply ? value * part : value + part;
221
+ }
222
+ if ([key isEqualToString:@"opacity"]) { view.alpha = MAX(0, MIN(1, value)); return; }
223
+ if ([key isEqualToString:@"x"]) key = @"transform.translation.x";
224
+ else if ([key isEqualToString:@"y"]) key = @"transform.translation.y";
225
+ else if ([key isEqualToString:@"scaleX"]) key = @"transform.scale.x";
226
+ else if ([key isEqualToString:@"scaleY"]) key = @"transform.scale.y";
227
+ else if ([key isEqualToString:@"rotate"]) { key = @"transform.rotation.z"; value *= M_PI / 180; }
228
+ else if ([key isEqualToString:@"rotateX"]) { key = @"transform.rotation.x"; value *= M_PI / 180; }
229
+ else if ([key isEqualToString:@"rotateY"]) { key = @"transform.rotation.y"; value *= M_PI / 180; }
230
+ [view.layer setValue:@(value) forKeyPath:key];
231
+ if ([view respondsToSelector:NSSelectorFromString(@"outerShadowContainerLayer")]) {
232
+ CALayer *shadow = [view valueForKey:@"outerShadowContainerLayer"];
233
+ [shadow setValue:@(value) forKeyPath:key];
234
+ }
235
+ }
236
+ @end
@@ -0,0 +1,15 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ @interface JoltReorderHandle : UIButton
4
+ @property(nonatomic, copy) void (^onMove)(NSInteger direction);
5
+ @end
6
+
7
+ @interface JoltReorderSession : NSObject
8
+ + (instancetype)attach:(UITableView *)list options:(NSString *)options onState:(void (^)(NSInteger state, NSInteger index, NSInteger toIndex))onState;
9
+ - (void)registerHandle:(UIView *)view;
10
+ - (void)setEnabled:(BOOL)enabled;
11
+ - (void)setReducedMotion:(BOOL)reduced;
12
+ - (void)cancel;
13
+ - (void)dispose;
14
+ @property(nonatomic, readonly) BOOL active;
15
+ @end
@@ -0,0 +1,205 @@
1
+ #import "JoltReorderSession.h"
2
+
3
+ @implementation JoltReorderHandle
4
+ - (BOOL)canBecomeFirstResponder { return YES; }
5
+ - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
6
+ [self becomeFirstResponder]; return [super beginTrackingWithTouch:touch withEvent:event];
7
+ }
8
+ - (NSArray<UIKeyCommand *> *)keyCommands {
9
+ if (!self.onMove) return [super keyCommands];
10
+ UIKeyCommand *up = [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:UIKeyModifierAlternate action:@selector(moveUp:)];
11
+ UIKeyCommand *down = [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:UIKeyModifierAlternate action:@selector(moveDown:)];
12
+ if (@available(iOS 15.0, *)) { up.wantsPriorityOverSystemBehavior = YES; down.wantsPriorityOverSystemBehavior = YES; }
13
+ return @[up, down];
14
+ }
15
+ - (void)moveUp:(UIKeyCommand *)command { if (self.onMove) self.onMove(-1); }
16
+ - (void)moveDown:(UIKeyCommand *)command { if (self.onMove) self.onMove(1); }
17
+ @end
18
+
19
+ @interface JoltReorderCellState : NSObject
20
+ @property(nonatomic) CATransform3D transform;
21
+ @property(nonatomic) BOOL cellClips, contentClips;
22
+ @property(nonatomic) CGFloat alpha, offset;
23
+ @property(nonatomic) NSInteger index;
24
+ @end
25
+ @implementation JoltReorderCellState
26
+ @end
27
+
28
+ @interface JoltReorderSession () <UIGestureRecognizerDelegate>
29
+ @property(nonatomic, weak) UITableView *list;
30
+ @property(nonatomic) UILongPressGestureRecognizer *press;
31
+ @property(nonatomic) NSHashTable<UIView *> *handles;
32
+ @property(nonatomic) NSMapTable<UITableViewCell *, JoltReorderCellState *> *cells;
33
+ @property(nonatomic) UIView *overlay;
34
+ @property(nonatomic) CADisplayLink *clock;
35
+ @property(nonatomic, copy) void (^onState)(NSInteger, NSInteger, NSInteger);
36
+ @property(nonatomic) NSInteger from, target, count, generation;
37
+ @property(nonatomic) CGFloat edgeSize, edgeSpeed, height, grabOffset, pointerY, pointerX;
38
+ @property(nonatomic) CGSize viewport;
39
+ @property(nonatomic) double previousTime;
40
+ @property(nonatomic) BOOL enabled, disposed, handlesOnly, reduced, settling, scrollEnabled, cancelling;
41
+ @property(nonatomic, readwrite) BOOL active;
42
+ @end
43
+
44
+ @implementation JoltReorderSession
45
+ + (instancetype)attach:(UITableView *)list options:(NSString *)options onState:(void (^)(NSInteger, NSInteger, NSInteger))onState {
46
+ if (![list isKindOfClass:UITableView.class]) @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Reorder requires a UITableView" userInfo:nil];
47
+ NSDictionary *config = [NSJSONSerialization JSONObjectWithData:[options dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
48
+ JoltReorderSession *s = [self new]; s.list = list; s.onState = onState; s.enabled = YES;
49
+ s.edgeSize = [config[@"edgeSize"] doubleValue]; s.edgeSpeed = [config[@"edgeSpeed"] doubleValue];
50
+ s.reduced = [config[@"reducedMotion"] boolValue]; s.handlesOnly = [config[@"handles"] boolValue];
51
+ s.handles = [NSHashTable weakObjectsHashTable]; s.cells = [NSMapTable weakToStrongObjectsMapTable];
52
+ s.press = [[UILongPressGestureRecognizer alloc] initWithTarget:s action:@selector(pressed:)];
53
+ s.press.minimumPressDuration = [config[@"longPress"] doubleValue] / 1000.; s.press.numberOfTouchesRequired = 1;
54
+ s.press.allowableMovement = 10; s.press.delegate = s;
55
+ [list addGestureRecognizer:s.press];
56
+ return s;
57
+ }
58
+ - (void)registerHandle:(UIView *)view { if (!self.disposed && view) [self.handles addObject:view]; }
59
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)recognizer shouldReceiveTouch:(UITouch *)touch {
60
+ if (self.active) { [self cancel]; return NO; }
61
+ if (!self.enabled || self.disposed || self.active || !self.list.window) return NO;
62
+ if (!self.handlesOnly) return YES;
63
+ for (UIView *view = touch.view; view && view != self.list; view = view.superview)
64
+ if ([self.handles containsObject:view]) return YES;
65
+ return NO;
66
+ }
67
+ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)recognizer {
68
+ NSIndexPath *path = [self.list indexPathForRowAtPoint:[recognizer locationInView:self.list]];
69
+ return path && path.section == 0 && self.list.numberOfSections == 1 && !self.list.dragging && !self.list.decelerating;
70
+ }
71
+ - (void)emit:(NSInteger)state detail:(NSInteger)detail { if (!self.disposed && self.onState) self.onState(state, self.from, detail); }
72
+ - (void)pressed:(UILongPressGestureRecognizer *)press {
73
+ if (press.state == UIGestureRecognizerStateBegan) [self begin];
74
+ else if (press.state == UIGestureRecognizerStateChanged && self.active && !self.settling) {
75
+ CGPoint p = [press locationInView:self.list.window]; self.pointerY = p.y; self.pointerX = p.x; [self update];
76
+ } else if (press.state == UIGestureRecognizerStateEnded && self.active) [self drop];
77
+ else if (press.state == UIGestureRecognizerStateCancelled || press.state == UIGestureRecognizerStateFailed) [self cancel];
78
+ }
79
+ - (void)begin {
80
+ UITableView *list = self.list;
81
+ NSIndexPath *path = [list indexPathForRowAtPoint:[self.press locationInView:list]];
82
+ UITableViewCell *cell = path ? [list cellForRowAtIndexPath:path] : nil;
83
+ if (!cell || path.section != 0 || !list.window) return;
84
+ UIView *snapshot = [cell snapshotViewAfterScreenUpdates:NO];
85
+ if (!snapshot) return;
86
+ CGRect frame = [list convertRect:[list rectForRowAtIndexPath:path] toView:list.window];
87
+ self.overlay = snapshot; snapshot.frame = frame; snapshot.userInteractionEnabled = NO;
88
+ snapshot.accessibilityElementsHidden = YES;
89
+ snapshot.layer.shadowColor = UIColor.blackColor.CGColor; snapshot.layer.shadowRadius = 12;
90
+ snapshot.layer.shadowOpacity = .18; snapshot.layer.shadowOffset = CGSizeMake(0, 6);
91
+ [list.window addSubview:snapshot];
92
+ CGPoint p = [self.press locationInView:list.window];
93
+ self.pointerY = p.y; self.pointerX = p.x; self.grabOffset = p.y - CGRectGetMidY(frame);
94
+ self.height = frame.size.height; self.viewport = list.bounds.size;
95
+ self.count = [list numberOfRowsInSection:0]; self.from = self.target = path.row;
96
+ self.scrollEnabled = list.scrollEnabled; list.scrollEnabled = NO;
97
+ self.active = YES; self.settling = NO; self.previousTime = CACurrentMediaTime(); self.generation++;
98
+ [self emit:1 detail:self.from];
99
+ if (!self.active || self.disposed) return;
100
+ self.clock = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
101
+ [self.clock addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
102
+ [self update];
103
+ }
104
+ - (void)tick:(CADisplayLink *)clock {
105
+ if (!self.active || self.settling) return;
106
+ UITableView *list = self.list;
107
+ if (!list.window || !CGSizeEqualToSize(self.viewport, list.bounds.size) || self.count != [list numberOfRowsInSection:0]) {
108
+ [self finish:NO detail:1]; return;
109
+ }
110
+ double now = CACurrentMediaTime(), dt = MIN(.04, MAX(0, now - self.previousTime)); self.previousTime = now;
111
+ CGRect viewport = [list convertRect:list.bounds toView:list.window];
112
+ CGFloat inset = MIN(self.edgeSize, viewport.size.height / 3), distance = 0;
113
+ if (self.pointerY < CGRectGetMinY(viewport) + inset) distance = -MIN(1, (CGRectGetMinY(viewport) + inset - self.pointerY) / inset);
114
+ else if (self.pointerY > CGRectGetMaxY(viewport) - inset) distance = MIN(1, (self.pointerY - CGRectGetMaxY(viewport) + inset) / inset);
115
+ CGFloat top = -list.adjustedContentInset.top, bottom = MAX(top, list.contentSize.height - list.bounds.size.height + list.adjustedContentInset.bottom);
116
+ CGFloat next = MAX(top, MIN(bottom, list.contentOffset.y + distance * self.edgeSpeed * dt));
117
+ if (fabs(next - list.contentOffset.y) > .01) { [list setContentOffset:CGPointMake(list.contentOffset.x, next) animated:NO]; [list layoutIfNeeded]; }
118
+ [self update];
119
+ }
120
+ - (void)restore:(UITableViewCell *)cell state:(JoltReorderCellState *)state {
121
+ [cell.contentView.layer removeAnimationForKey:@"jolt.reorder"];
122
+ cell.contentView.layer.sublayerTransform = state.transform; cell.alpha = state.alpha; state.offset = 0;
123
+ cell.clipsToBounds = state.cellClips; cell.contentView.clipsToBounds = state.contentClips;
124
+ }
125
+ - (void)update {
126
+ if (!self.active || self.settling) return;
127
+ UITableView *list = self.list;
128
+ CGFloat center = self.pointerY - self.grabOffset;
129
+ CGPoint inList = [list convertPoint:CGPointMake(self.pointerX, center) fromView:list.window];
130
+ CGFloat draggedTop = inList.y - self.height / 2;
131
+ self.overlay.center = CGPointMake(self.overlay.center.x, center);
132
+ NSArray<NSIndexPath *> *paths = [[list indexPathsForVisibleRows] sortedArrayUsingSelector:@selector(compare:)];
133
+ NSInteger destination = self.from;
134
+ for (NSIndexPath *path in paths) {
135
+ if (path.section || path.row == self.from) continue;
136
+ CGRect frame = [list rectForRowAtIndexPath:path];
137
+ CGFloat midpoint = CGRectGetMidY(frame) - (path.row > self.from ? self.height : 0);
138
+ NSInteger insertion = path.row - (path.row > self.from ? 1 : 0);
139
+ destination = insertion;
140
+ if (draggedTop < midpoint) break;
141
+ destination = insertion + 1;
142
+ }
143
+ self.target = MAX(0, MIN(self.count - 1, destination));
144
+ NSSet *visible = [NSSet setWithArray:list.visibleCells];
145
+ for (UITableViewCell *cell in self.cells.keyEnumerator.allObjects)
146
+ if (![visible containsObject:cell]) { [self restore:cell state:[self.cells objectForKey:cell]]; [self.cells removeObjectForKey:cell]; }
147
+ for (NSIndexPath *path in paths) {
148
+ UITableViewCell *cell = [list cellForRowAtIndexPath:path]; if (!cell) continue;
149
+ JoltReorderCellState *state = [self.cells objectForKey:cell];
150
+ if (state && state.index != path.row) { [self restore:cell state:state]; [self.cells removeObjectForKey:cell]; state = nil; }
151
+ if (!state) { state = [JoltReorderCellState new]; state.transform = cell.contentView.layer.sublayerTransform; state.cellClips = cell.clipsToBounds; state.contentClips = cell.contentView.clipsToBounds; state.alpha = cell.alpha; state.index = path.row; [self.cells setObject:state forKey:cell]; }
152
+ cell.clipsToBounds = NO; cell.contentView.clipsToBounds = NO;
153
+ cell.alpha = path.row == self.from ? 0 : state.alpha;
154
+ CGFloat offset = path.row > self.from && path.row <= self.target ? -self.height : path.row < self.from && path.row >= self.target ? self.height : 0;
155
+ if (fabs(offset - state.offset) < .01) continue;
156
+ // UITableView owns cell/contentView frames. Move only the contents of
157
+ // the layer so height estimation and recycling cannot corrupt geometry.
158
+ CALayer *layer = cell.contentView.layer;
159
+ CATransform3D previous = ((CALayer *)layer.presentationLayer ?: layer).sublayerTransform;
160
+ state.offset = offset; layer.sublayerTransform = CATransform3DTranslate(state.transform, 0, offset, 0);
161
+ [layer removeAnimationForKey:@"jolt.reorder"];
162
+ if (!self.reduced) {
163
+ CASpringAnimation *spring = [CASpringAnimation animationWithKeyPath:@"sublayerTransform"];
164
+ spring.mass = 1; spring.stiffness = 360; spring.damping = 34;
165
+ spring.fromValue = [NSValue valueWithCATransform3D:previous]; spring.toValue = [NSValue valueWithCATransform3D:layer.sublayerTransform]; spring.duration = spring.settlingDuration;
166
+ [layer addAnimation:spring forKey:@"jolt.reorder"];
167
+ }
168
+ }
169
+ }
170
+ - (void)drop {
171
+ if (!self.active || self.settling) return;
172
+ self.settling = YES; [self.clock invalidate]; self.clock = nil;
173
+ NSInteger generation = self.generation;
174
+ CGRect row = [self.list rectForRowAtIndexPath:[NSIndexPath indexPathForRow:self.target inSection:0]];
175
+ CGFloat top = self.target > self.from ? CGRectGetMaxY(row) - self.height : row.origin.y;
176
+ CGPoint end = [self.list convertPoint:CGPointMake(row.origin.x + row.size.width / 2, top + self.height / 2) toView:self.list.window];
177
+ if (self.reduced) { [self finish:YES detail:0]; return; }
178
+ [UIView animateWithDuration:.18 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut animations:^{ self.overlay.center = end; } completion:^(BOOL finished) {
179
+ if (self.active && generation == self.generation) [self finish:YES detail:0];
180
+ }];
181
+ }
182
+ - (void)finish:(BOOL)commit detail:(NSInteger)detail {
183
+ if (!self.active) return;
184
+ self.active = NO; self.settling = NO; self.generation++;
185
+ [self.clock invalidate]; self.clock = nil;
186
+ [self.overlay.layer removeAllAnimations]; [self.overlay removeFromSuperview]; self.overlay = nil;
187
+ for (UITableViewCell *cell in self.cells.keyEnumerator.allObjects) [self restore:cell state:[self.cells objectForKey:cell]];
188
+ [self.cells removeAllObjects]; self.list.scrollEnabled = self.scrollEnabled;
189
+ [self emit:commit ? 2 : 3 detail:commit ? self.target : detail];
190
+ }
191
+ - (void)cancel {
192
+ if (self.cancelling) return;
193
+ self.cancelling = YES;
194
+ [self finish:NO detail:0];
195
+ if (self.press.enabled) { self.press.enabled = NO; self.press.enabled = YES; }
196
+ self.cancelling = NO;
197
+ }
198
+ - (void)setEnabled:(BOOL)value { _enabled = value; if (!value) [self cancel]; }
199
+ - (void)setReducedMotion:(BOOL)value { self.reduced = value; }
200
+ - (void)dispose {
201
+ if (self.disposed) return;
202
+ [self cancel]; self.disposed = YES; [self.list removeGestureRecognizer:self.press]; self.onState = nil;
203
+ [self.handles removeAllObjects];
204
+ }
205
+ @end
@@ -0,0 +1,35 @@
1
+ #ifndef JOLT_SPRING_STATE_H
2
+ #define JOLT_SPRING_STATE_H
3
+ #include <math.h>
4
+
5
+ /* Exact constant-target solution. Position and velocity survive every retarget. */
6
+ typedef struct { double position, velocity; } JoltSpringState;
7
+ static inline JoltSpringState JoltSpringAdvance(JoltSpringState state, double target,
8
+ double stiffness, double damping, double mass, double seconds) {
9
+ if (seconds <= 0) return state;
10
+ const double x = state.position - target;
11
+ const double w = sqrt(stiffness / mass);
12
+ const double z = damping / (2 * sqrt(stiffness * mass));
13
+ double next, speed;
14
+ if (fabs(z - 1) < 1e-6) {
15
+ const double b = state.velocity + w * x, e = exp(-w * seconds);
16
+ next = e * (x + b * seconds);
17
+ speed = e * (b - w * (x + b * seconds));
18
+ } else if (z < 1) {
19
+ const double a = z * w, d = w * sqrt(1 - z * z);
20
+ const double b = (state.velocity + a * x) / d;
21
+ const double c = cos(d * seconds), s = sin(d * seconds), e = exp(-a * seconds);
22
+ next = e * (x * c + b * s);
23
+ speed = e * ((-a * x + b * d) * c + (-a * b - x * d) * s);
24
+ } else {
25
+ const double q = sqrt(z * z - 1);
26
+ /* Rationalized slow root avoids catastrophic cancellation at high damping. */
27
+ const double r1 = -w / (z + q), r2 = -w * (z + q);
28
+ const double c1 = (state.velocity - r2 * x) / (r1 - r2), c2 = x - c1;
29
+ const double e1 = exp(r1 * seconds), e2 = exp(r2 * seconds);
30
+ next = c1 * e1 + c2 * e2;
31
+ speed = c1 * r1 * e1 + c2 * r2 * e2;
32
+ }
33
+ return (JoltSpringState){target + next, speed};
34
+ }
35
+ #endif
@@ -2,5 +2,8 @@ module JoltMotion {
2
2
  header "JoltImageEffect.h"
3
3
  header "JoltPhotoZoom.h"
4
4
  header "JoltInteractiveAnimation.h"
5
+ header "JoltLiveSpring.h"
6
+ header "JoltGestureSession.h"
7
+ header "JoltReorderSession.h"
5
8
  export *
6
9
  }
@@ -1,4 +1,97 @@
1
1
  declare namespace org.rafaelaugusto.jolt {
2
+ class ReorderSurface extends org.nativescript.widgets.GridLayout {
3
+ constructor(context: android.content.Context)
4
+ }
5
+ class ReorderHandle extends android.widget.Button {
6
+ constructor(context: android.content.Context)
7
+ setMoveListener(listener: ReorderHandle.MoveListener | null): void
8
+ }
9
+ namespace ReorderHandle {
10
+ class MoveListener {
11
+ constructor(implementation: { move(direction: number): void })
12
+ }
13
+ }
14
+ class ReorderSession {
15
+ constructor(
16
+ surface: ReorderSurface,
17
+ list: android.widget.ListView,
18
+ options: string,
19
+ listener: ReorderSession.Listener,
20
+ )
21
+ isActive(): boolean
22
+ registerHandle(view: android.view.View): void
23
+ setEnabled(enabled: boolean): void
24
+ setReducedMotion(reduced: boolean): void
25
+ cancel(): void
26
+ dispose(): void
27
+ }
28
+ namespace ReorderSession {
29
+ class Listener {
30
+ constructor(implementation: {
31
+ onState(state: number, index: number, toIndex: number): void
32
+ })
33
+ }
34
+ }
35
+ class GestureSurface extends org.nativescript.widgets.GridLayout {
36
+ constructor(context: android.content.Context)
37
+ }
38
+ class GestureSession {
39
+ static supportsContactEvents(): boolean
40
+ constructor(
41
+ surface: GestureSurface,
42
+ scroll: android.view.View | null,
43
+ x: LiveSpring,
44
+ y: LiveSpring,
45
+ scale: LiveSpring,
46
+ rotation: LiveSpring,
47
+ options: string,
48
+ listener: GestureSession.Listener,
49
+ )
50
+ isActive(): boolean
51
+ cancel(): void
52
+ setEnabled(value: boolean): void
53
+ settle(x: number, y: number, scale: number, rotation: number, animated: boolean): void
54
+ updateBounds(left: number, right: number, top: number, bottom: number): void
55
+ dispose(): void
56
+ }
57
+ namespace GestureSession {
58
+ class Listener {
59
+ constructor(implementation: {
60
+ onState(state: number, velocityX: number, velocityY: number): void
61
+ })
62
+ }
63
+ }
64
+ class LiveSpring {
65
+ constructor(
66
+ initial: number,
67
+ stiffness: number,
68
+ damping: number,
69
+ mass: number,
70
+ restDelta: number,
71
+ restSpeed: number,
72
+ reduced: boolean,
73
+ )
74
+ static activeCount(): number
75
+ static nodeCount(): number
76
+ static sync(view: android.view.View): void
77
+ getPosition(): number
78
+ getVelocity(): number
79
+ isMoving(): boolean
80
+ follow(source: LiveSpring, mappings: string): void
81
+ updateInput(snapshot: string): void
82
+ setTarget(value: number): void
83
+ setReduced(value: boolean): void
84
+ setPaused(value: boolean): void
85
+ bind(view: android.view.View, property: string, mappings: string): LiveSpring.Binding
86
+ dispose(): void
87
+ }
88
+ namespace LiveSpring {
89
+ class Binding {
90
+ getValue(): number
91
+ setLayers(inputs: string): void
92
+ cancel(): void
93
+ }
94
+ }
2
95
  class ParticleEmitter {
3
96
  constructor(
4
97
  target: android.view.View,