@rafaelaugusto/jolt 0.1.0-rc.2 → 0.1.0-rc.3

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 (50) hide show
  1. package/README.md +4 -4
  2. package/dist/animate.d.ts.map +1 -1
  3. package/dist/animate.js +20 -5
  4. package/dist/animate.js.map +1 -1
  5. package/dist/driver/android.d.ts.map +1 -1
  6. package/dist/driver/android.js +15 -7
  7. package/dist/driver/android.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/interactive-navigation/index.d.ts +2 -0
  13. package/dist/interactive-navigation/index.d.ts.map +1 -1
  14. package/dist/interactive-navigation/index.js +1 -0
  15. package/dist/interactive-navigation/index.js.map +1 -1
  16. package/dist/interactive-navigation/modal.d.ts +34 -0
  17. package/dist/interactive-navigation/modal.d.ts.map +1 -0
  18. package/dist/interactive-navigation/modal.js +253 -0
  19. package/dist/interactive-navigation/modal.js.map +1 -0
  20. package/dist/scroll-header/geometry.d.ts +1 -1
  21. package/dist/scroll-header/geometry.d.ts.map +1 -1
  22. package/dist/scroll-header/geometry.js +7 -2
  23. package/dist/scroll-header/geometry.js.map +1 -1
  24. package/dist/scroll-header/index.d.ts.map +1 -1
  25. package/dist/scroll-header/index.js +13 -0
  26. package/dist/scroll-header/index.js.map +1 -1
  27. package/dist/shapes/index.d.ts +1 -1
  28. package/dist/shapes/index.d.ts.map +1 -1
  29. package/dist/shapes/index.js +23 -9
  30. package/dist/shapes/index.js.map +1 -1
  31. package/dist/types.d.ts +5 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/platforms/android/java/org/rafaelaugusto/jolt/KeyframeFactory.java +26 -0
  35. package/platforms/android/java/org/rafaelaugusto/jolt/ScrollHeader.java +3 -2
  36. package/platforms/android/proguard-rules.pro +1 -0
  37. package/platforms/ios/src/JoltInteractiveModal.h +16 -0
  38. package/platforms/ios/src/JoltInteractiveModal.m +178 -0
  39. package/platforms/ios/src/module.modulemap +1 -0
  40. package/src/android-plugin.d.ts +9 -0
  41. package/src/animate.ts +23 -5
  42. package/src/driver/android.ts +31 -14
  43. package/src/index.ts +1 -1
  44. package/src/interactive-navigation/index.ts +7 -0
  45. package/src/interactive-navigation/modal.ts +306 -0
  46. package/src/ios-plugin.d.ts +22 -0
  47. package/src/scroll-header/geometry.ts +7 -2
  48. package/src/scroll-header/index.ts +12 -0
  49. package/src/shapes/index.ts +20 -9
  50. package/src/types.ts +5 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafaelaugusto/jolt",
3
- "version": "0.1.0-rc.2",
3
+ "version": "0.1.0-rc.3",
4
4
  "description": "Native animations for NativeScript: presets, springs, keyframes and stagger in one call.",
5
5
  "keywords": [
6
6
  "nativescript",
@@ -0,0 +1,26 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.animation.Keyframe;
4
+ import android.animation.ObjectAnimator;
5
+ import android.animation.PropertyValuesHolder;
6
+ import android.util.Property;
7
+ import android.view.View;
8
+
9
+ /** Builds a numeric track without creating a JavaScript proxy for every keyframe. */
10
+ public final class KeyframeFactory {
11
+ private KeyframeFactory() {}
12
+
13
+ public static ObjectAnimator create(View view, Property<View, Float> property,
14
+ double[] times, double[] values, double unit) {
15
+ if (times.length != values.length || values.length < 2) {
16
+ throw new IllegalArgumentException("A track needs matching times and at least two values");
17
+ }
18
+ Keyframe[] frames = new Keyframe[values.length];
19
+ for (int i = 0; i < values.length; i++) {
20
+ // Scale in double precision before the same float conversion as Keyframe.ofFloat.
21
+ frames[i] = Keyframe.ofFloat((float) times[i], (float) (values[i] * unit));
22
+ }
23
+ return ObjectAnimator.ofPropertyValuesHolder(view,
24
+ PropertyValuesHolder.ofKeyframe(property, frames));
25
+ }
26
+ }
@@ -68,12 +68,13 @@ public final class ScrollHeader implements ViewTreeObserver.OnScrollChangedListe
68
68
  public void scroll(double target,boolean animated) {
69
69
  if(disposed || !Double.isFinite(target))return;
70
70
  target=Math.max(0,target);
71
- if(animated){list.smoothScrollBy((int)Math.round((target-current())*density),300);return;}
72
71
  double divider=list.getDividerHeight()/density, stride=rowHeight+divider;
73
72
  int index=target<spacer+divider?0:1+(int)Math.floor((target-spacer-divider)/stride);
74
73
  index=Math.max(0,Math.min(list.getCount()-1,index));
75
74
  double start=index==0?0:spacer+divider+(index-1)*stride;
76
- list.setSelectionFromTop(index,(int)Math.round((start-target)*density));
75
+ int top=(int)Math.round((start-target)*density);
76
+ if(animated){list.smoothScrollToPositionFromTop(index,top,300);return;}
77
+ list.setSelectionFromTop(index,top);
77
78
  list.post(new Runnable(){public void run(){if(!disposed)update(true);}});
78
79
  }
79
80
  public void dispose() {
@@ -1,4 +1,5 @@
1
1
  -keep class org.rafaelaugusto.jolt.ColorTarget { *; }
2
+ -keep class org.rafaelaugusto.jolt.KeyframeFactory { *; }
2
3
  -keep class org.rafaelaugusto.jolt.CircleWorld { *; }
3
4
  -keep class org.rafaelaugusto.jolt.CircleWorld$* { *; }
4
5
  -keep class org.rafaelaugusto.jolt.MotionProbe { *; }
@@ -0,0 +1,16 @@
1
+ #import <UIKit/UIKit.h>
2
+ NS_ASSUME_NONNULL_BEGIN
3
+
4
+ /** Animator supplied through Core's modal transition delegate. No delegate replacement. */
5
+ @interface JoltInteractiveModal : NSObject <UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning, UIGestureRecognizerDelegate>
6
+ + (instancetype)attach:(UIViewController *)controller stiffness:(double)stiffness damping:(double)damping mass:(double)mass shouldBegin:(BOOL (^)(void))shouldBegin onEvent:(void (^)(NSInteger))onEvent;
7
+ + (NSUInteger)activeCount;
8
+ + (NSUInteger)recognizerCount;
9
+ @property(nonatomic, readonly) BOOL interactive;
10
+ @property(nonatomic, readonly) double progress;
11
+ @property(nonatomic, readonly) double velocity;
12
+ - (void)preparePresenting:(BOOL)presenting reduced:(BOOL)reduced timeScale:(double)timeScale;
13
+ - (void)cancel;
14
+ - (void)dispose;
15
+ @end
16
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,178 @@
1
+ #import "JoltInteractiveModal.h"
2
+ #import "JoltSpringState.h"
3
+
4
+ @interface JoltInteractiveModal () {
5
+ JoltSpringState _spring;
6
+ }
7
+ @property(nonatomic, weak) UIViewController *controller;
8
+ @property(nonatomic) UIPanGestureRecognizer *pan;
9
+ @property(nonatomic, copy) BOOL (^shouldBegin)(void);
10
+ @property(nonatomic, copy) void (^onEvent)(NSInteger);
11
+ @property(nonatomic) id<UIViewControllerContextTransitioning> context;
12
+ @property(nonatomic) UIView *moving;
13
+ @property(nonatomic) CGAffineTransform originalTransform;
14
+ @property(nonatomic) CGRect bounds;
15
+ @property(nonatomic) CADisplayLink *link;
16
+ @property(nonatomic) double stiffness, damping, mass, previous, started, target, timeScale;
17
+ @property(nonatomic) NSNumber *pendingEnd;
18
+ @property(nonatomic, readwrite) BOOL interactive;
19
+ @property(nonatomic) BOOL presenting, reduced, settling, committed, disposed;
20
+ @end
21
+
22
+ @implementation JoltInteractiveModal
23
+ static NSUInteger active = 0, recognizers = 0;
24
+ + (NSUInteger)activeCount { return active; }
25
+ + (NSUInteger)recognizerCount { return recognizers; }
26
+ + (instancetype)attach:(UIViewController *)controller stiffness:(double)stiffness damping:(double)damping mass:(double)mass shouldBegin:(BOOL (^)(void))shouldBegin onEvent:(void (^)(NSInteger))onEvent {
27
+ JoltInteractiveModal *owner = [self new];
28
+ owner.controller = controller;
29
+ owner.stiffness = stiffness; owner.damping = damping; owner.mass = mass;
30
+ owner.timeScale = 1;
31
+ owner.shouldBegin = shouldBegin; owner.onEvent = onEvent;
32
+ owner.pan = [[UIPanGestureRecognizer alloc] initWithTarget:owner action:@selector(pan:)];
33
+ owner.pan.maximumNumberOfTouches = 1;
34
+ owner.pan.delegate = owner;
35
+ [controller.view addGestureRecognizer:owner.pan]; recognizers++;
36
+ [[NSNotificationCenter defaultCenter] addObserver:owner selector:@selector(cancel) name:UIApplicationWillResignActiveNotification object:nil];
37
+ return owner;
38
+ }
39
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)recognizer shouldReceiveTouch:(UITouch *)touch {
40
+ // Scrolling content and native controls keep their own interaction. A modal
41
+ // can expose an ordinary, non-scrolling header as its dismissal surface.
42
+ for (UIView *view = touch.view; view; view = view.superview) {
43
+ if ([view isKindOfClass:UIControl.class] || [view isKindOfClass:UIScrollView.class]) return NO;
44
+ if (view == self.controller.view) break;
45
+ }
46
+ return YES;
47
+ }
48
+ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)recognizer {
49
+ UIViewController *controller = self.controller;
50
+ CGPoint velocity = [self.pan velocityInView:controller.view];
51
+ return !self.disposed && !self.context && controller.presentingViewController &&
52
+ !controller.presentedViewController && !controller.transitionCoordinator && !controller.modalInPresentation &&
53
+ velocity.y > fabs(velocity.x) && self.shouldBegin && self.shouldBegin();
54
+ }
55
+ - (void)preparePresenting:(BOOL)presenting reduced:(BOOL)reduced timeScale:(double)timeScale {
56
+ self.presenting = presenting; self.reduced = reduced; self.timeScale = timeScale;
57
+ }
58
+ - (void)pan:(UIPanGestureRecognizer *)pan {
59
+ if (pan.state == UIGestureRecognizerStateBegan) {
60
+ self.pendingEnd = nil; self.interactive = YES;
61
+ _spring = (JoltSpringState){0, 0};
62
+ if (self.onEvent) self.onEvent(2);
63
+ }
64
+ if (!self.interactive || self.settling) return;
65
+ double height = MAX(1, self.context ? self.bounds.size.height : self.controller.view.bounds.size.height);
66
+ double progress = MAX(0, MIN(1, [pan translationInView:self.controller.view].y / height));
67
+ double velocity = [pan velocityInView:self.controller.view].y / height;
68
+ _spring = (JoltSpringState){progress, velocity};
69
+ if (self.context) { [self render:progress]; [self.context updateInteractiveTransition:progress]; }
70
+ if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateFailed) {
71
+ BOOL finish = pan.state == UIGestureRecognizerStateEnded && progress + velocity * .16 > .5;
72
+ if (self.context) [self settle:finish velocity:velocity];
73
+ else self.pendingEnd = @(finish);
74
+ }
75
+ }
76
+ - (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)context { return self.reduced ? 0 : .55; }
77
+ - (BOOL)wantsInteractiveStart { return self.interactive; }
78
+ - (CGFloat)completionSpeed { return 1; }
79
+ - (UIViewAnimationCurve)completionCurve { return UIViewAnimationCurveLinear; }
80
+ - (double)progress { return _spring.position; }
81
+ - (double)velocity { return _spring.velocity; }
82
+ - (void)prepare:(id<UIViewControllerContextTransitioning>)context {
83
+ self.context = context; active++;
84
+ UIViewController *controller = [context viewControllerForKey:self.presenting ? UITransitionContextToViewControllerKey : UITransitionContextFromViewControllerKey];
85
+ self.moving = [context viewForKey:self.presenting ? UITransitionContextToViewKey : UITransitionContextFromViewKey] ?: controller.view;
86
+ self.originalTransform = self.moving.transform;
87
+ if (self.presenting) {
88
+ self.moving.frame = [context finalFrameForViewController:controller];
89
+ [context.containerView addSubview:self.moving];
90
+ }
91
+ [self.moving setNeedsLayout]; [self.moving layoutIfNeeded];
92
+ self.bounds = context.containerView.bounds;
93
+ _spring = (JoltSpringState){0, 0};
94
+ [self render:0];
95
+ }
96
+ - (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)context {
97
+ JoltSpringState held = _spring;
98
+ [self prepare:context]; _spring = held;
99
+ [self render:_spring.position]; [context updateInteractiveTransition:_spring.position];
100
+ [self startLink];
101
+ if (self.pendingEnd || self.disposed) {
102
+ [self settle:!self.disposed && self.pendingEnd.boolValue velocity:_spring.velocity];
103
+ self.pendingEnd = nil;
104
+ }
105
+ }
106
+ - (void)animateTransition:(id<UIViewControllerContextTransitioning>)context {
107
+ [self prepare:context];
108
+ if (self.onEvent) self.onEvent(0);
109
+ [self settle:YES velocity:0];
110
+ }
111
+ - (void)render:(double)progress {
112
+ double p = MAX(0, MIN(1, progress));
113
+ double translation = self.bounds.size.height * (self.presenting ? 1 - p : p);
114
+ [CATransaction begin]; [CATransaction setDisableActions:YES];
115
+ self.moving.transform = CGAffineTransformConcat(self.originalTransform, CGAffineTransformMakeTranslation(0, translation));
116
+ [CATransaction commit];
117
+ }
118
+ - (void)startLink {
119
+ if (self.link || !self.context) return;
120
+ self.previous = CACurrentMediaTime();
121
+ self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
122
+ if (@available(iOS 15.0, *)) {
123
+ float maximum = MIN(120, UIScreen.mainScreen.maximumFramesPerSecond);
124
+ self.link.preferredFrameRateRange = CAFrameRateRangeMake(MIN(80, maximum), maximum, maximum);
125
+ }
126
+ [self.link addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
127
+ }
128
+ - (void)settle:(BOOL)finish velocity:(double)velocity {
129
+ if (!self.context || self.settling) return;
130
+ self.committed = finish; self.settling = YES; self.target = finish ? 1 : 0;
131
+ _spring.velocity = velocity;
132
+ if (self.interactive) {
133
+ // Core must know cancellation BEFORE UIKit executes dismissal completion.
134
+ if (self.onEvent) self.onEvent(finish ? 3 : 4);
135
+ if (!self.context) return;
136
+ if (finish) [self.context finishInteractiveTransition]; else [self.context cancelInteractiveTransition];
137
+ }
138
+ self.started = CACurrentMediaTime(); self.previous = self.started;
139
+ if (self.reduced || self.disposed) { [self finish]; return; }
140
+ [self startLink];
141
+ }
142
+ - (void)tick:(CADisplayLink *)link {
143
+ if (!CGRectEqualToRect(self.bounds, self.context.containerView.bounds) || !self.context.containerView.window) { [self cancel]; return; }
144
+ if (!self.settling) return;
145
+ double now = CACurrentMediaTime(), seconds = MAX(0, now - self.previous); self.previous = now;
146
+ _spring = JoltSpringAdvance(_spring, self.target, self.stiffness*self.timeScale*self.timeScale, self.damping*self.timeScale, self.mass, seconds);
147
+ if (!isfinite(_spring.position) || !isfinite(_spring.velocity)) { [self finish]; return; }
148
+ [self render:_spring.position];
149
+ if ((fabs(_spring.position-self.target)<.0005 && fabs(_spring.velocity)<.005) || now-self.started>=3) [self finish];
150
+ }
151
+ - (void)finish {
152
+ if (!self.context) return;
153
+ [self.link invalidate]; self.link = nil;
154
+ BOOL completed = self.committed, presenting = self.presenting;
155
+ _spring = (JoltSpringState){completed ? 1 : 0, 0}; [self render:_spring.position];
156
+ self.moving.transform = self.originalTransform;
157
+ id<UIViewControllerContextTransitioning> context = self.context;
158
+ self.context = nil; self.moving = nil; self.settling = NO; self.interactive = NO; active--;
159
+ [context completeTransition:completed];
160
+ if (self.onEvent) self.onEvent(presenting ? 1 : completed ? 5 : 6);
161
+ }
162
+ - (void)cancel {
163
+ if (!self.context) { if (self.interactive) self.pendingEnd = @NO; return; }
164
+ if (!self.settling) [self settle:!self.interactive velocity:0];
165
+ [self finish];
166
+ }
167
+ - (void)dispose {
168
+ if (self.disposed) return;
169
+ self.disposed = YES;
170
+ [self cancel];
171
+ [self.pan.view removeGestureRecognizer:self.pan];
172
+ self.pan.delegate = nil;
173
+ if (self.pan) recognizers--;
174
+ self.pan = nil;
175
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
176
+ self.shouldBegin = nil; self.onEvent = nil;
177
+ }
178
+ @end
@@ -7,6 +7,7 @@ module JoltMotion {
7
7
  header "JoltPhotoZoom.h"
8
8
  header "JoltInteractiveAnimation.h"
9
9
  header "JoltInteractiveNavigation.h"
10
+ header "JoltInteractiveModal.h"
10
11
  header "JoltLiveSpring.h"
11
12
  header "JoltGestureSession.h"
12
13
  header "JoltReorderSession.h"
@@ -1,4 +1,13 @@
1
1
  declare namespace org.rafaelaugusto.jolt {
2
+ class KeyframeFactory {
3
+ static create(
4
+ view: android.view.View,
5
+ property: android.util.Property<android.view.View, java.lang.Float>,
6
+ times: readonly number[],
7
+ values: readonly number[],
8
+ unit: number,
9
+ ): android.animation.ObjectAnimator
10
+ }
2
11
  class ShapeLayer extends android.graphics.drawable.Drawable {
3
12
  constructor(options: string)
4
13
  static liveCount(): number
package/src/animate.ts CHANGED
@@ -308,12 +308,30 @@ export function group(animations: readonly Animation[]): Animation {
308
308
  }
309
309
 
310
310
  export function toView(target: Target): View {
311
- if (target instanceof View) return target
312
- const view =
313
- (target as { nativeView?: unknown } | null)?.nativeView ??
314
- (target as { nativeElement?: unknown } | null)?.nativeElement
311
+ const view = unwrap(target)
315
312
  if (view instanceof View) return view
316
- throw new TypeError(`Expected a NativeScript view, got ${String(target)}.`)
313
+ throw new TypeError(`Expected a NativeScript view, got ${describe(target)}.`)
314
+ }
315
+
316
+ function unwrap(target: unknown, nested = false): unknown {
317
+ if (target instanceof View || target === null || typeof target !== 'object')
318
+ return target
319
+ const holder = target as {
320
+ nativeView?: unknown
321
+ nativeElement?: unknown
322
+ $el?: unknown
323
+ }
324
+ if (!nested && '$el' in holder) return unwrap(holder.$el, true)
325
+ return holder.nativeView ?? holder.nativeElement
326
+ }
327
+
328
+ function describe(target: unknown) {
329
+ if (target === null || typeof target !== 'object')
330
+ return typeof target === 'function' ? 'a function' : String(target)
331
+ if (Array.isArray(target)) return 'an array'
332
+ if ('$el' in target) return 'a component instance without a root view'
333
+ const name = Object.getPrototypeOf(target)?.constructor?.name
334
+ return name && name !== 'Object' ? `a ${name}` : 'an object without a view'
317
335
  }
318
336
 
319
337
  export function toViews(target: Target | readonly Target[]): View[] {
@@ -130,20 +130,37 @@ export function createAndroidDriver(): Driver {
130
130
  )
131
131
  }
132
132
 
133
- const keyframes = Array.create(android.animation.Keyframe, track.values.length)
134
- track.values.forEach((value, i) => {
135
- keyframes[i] = android.animation.Keyframe.ofFloat(track.times[i]!, value * scale)
136
- })
137
- const holders = Array.create(android.animation.PropertyValuesHolder, 1)
138
- holders[0] = android.animation.PropertyValuesHolder.ofKeyframe(
139
- nativeProperties[track.property],
140
- keyframes,
141
- )
142
-
143
- const animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
144
- nativeView,
145
- holders,
146
- )
133
+ let animator: android.animation.ObjectAnimator
134
+ if (
135
+ typeof org !== 'undefined' &&
136
+ typeof org.rafaelaugusto?.jolt?.KeyframeFactory === 'function'
137
+ ) {
138
+ animator = org.rafaelaugusto.jolt.KeyframeFactory.create(
139
+ nativeView,
140
+ nativeProperties[track.property],
141
+ track.times,
142
+ track.values,
143
+ scale,
144
+ )
145
+ } else {
146
+ // Keep JS-only updates working with an app binary built before the factory.
147
+ const keyframes = Array.create(android.animation.Keyframe, track.values.length)
148
+ track.values.forEach((value, i) => {
149
+ keyframes[i] = android.animation.Keyframe.ofFloat(
150
+ track.times[i]!,
151
+ value * scale,
152
+ )
153
+ })
154
+ const holders = Array.create(android.animation.PropertyValuesHolder, 1)
155
+ holders[0] = android.animation.PropertyValuesHolder.ofKeyframe(
156
+ nativeProperties[track.property],
157
+ keyframes,
158
+ )
159
+ animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
160
+ nativeView,
161
+ holders,
162
+ )
163
+ }
147
164
  let restoreLayer: (() => void) | undefined
148
165
  const releaseLayer = () => {
149
166
  restoreLayer?.()
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { timeline } from './sequence.js'
2
2
  export type { TimelineAnimation, TimelineStep, PlayableAnimation } from './sequence.js'
3
3
  export { playbackProgress } from './motion-value.js'
4
- export { animate, decay, set, stop } from './animate.js'
4
+ export { animate, decay, set, stop, toView } from './animate.js'
5
5
  export { bind, motionValue, scrollValue, springValue } from './motion-value.js'
6
6
  export type {
7
7
  Bindings,
@@ -1,4 +1,11 @@
1
1
  import { ReturnScope } from '../shared-navigation/return-signal.js'
2
+ export { interactiveModal } from './modal.js'
3
+ export type {
4
+ InteractiveModal,
5
+ InteractiveModalEvent,
6
+ InteractiveModalOptions,
7
+ InteractiveModalState,
8
+ } from './modal.js'
2
9
  import {
3
10
  Application,
4
11
  Image,