@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafaelaugusto/jolt",
3
- "version": "0.1.0-beta.15",
3
+ "version": "0.1.0-beta.17",
4
4
  "description": "Native animations for NativeScript: presets, springs, keyframes and stagger in one call.",
5
5
  "keywords": [
6
6
  "nativescript",
@@ -34,9 +34,21 @@
34
34
  "default": "./dist/vue/index.js"
35
35
  },
36
36
  "./package.json": "./package.json",
37
+ "./gestures": {
38
+ "types": "./dist/gestures/index.d.ts",
39
+ "default": "./dist/gestures/index.js"
40
+ },
37
41
  "./native": {
38
42
  "types": "./dist/native/index.d.ts",
39
43
  "default": "./dist/native/index.js"
44
+ },
45
+ "./bottom-sheet": {
46
+ "types": "./dist/bottom-sheet/index.d.ts",
47
+ "default": "./dist/bottom-sheet/index.js"
48
+ },
49
+ "./reorder": {
50
+ "types": "./dist/reorder/index.d.ts",
51
+ "default": "./dist/reorder/index.js"
40
52
  }
41
53
  },
42
54
  "files": [
@@ -1,3 +1,12 @@
1
+ // Compile against the host's widgets; the app already packages NativeScript core.
2
+ dependencies {
3
+ compileOnly 'androidx.core:core:1.13.0'
4
+ if (project.hasProperty('tempBuild')) {
5
+ def widgets = new File(project.ext.USER_PROJECT_ROOT, 'node_modules/@nativescript/core/platforms/android/widgets-release.aar')
6
+ compileOnly files(zipTree(widgets).matching { include 'classes.jar' })
7
+ }
8
+ }
9
+
1
10
  // Applied by the NativeScript app. Preserve fields used by the native color target.
2
11
  def joltDependency = nativescriptDependencies.find { it.name == '@rafaelaugusto/jolt' }
3
12
  if (joltDependency != null) {
@@ -0,0 +1,198 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.graphics.Rect;
4
+ import android.view.GestureDetector;
5
+ import android.view.MotionEvent;
6
+ import android.view.VelocityTracker;
7
+ import android.view.View;
8
+ import org.json.JSONObject;
9
+ import org.json.JSONException;
10
+
11
+ /** Native arbitration, pointer rebasing and NestedScrollView consumption. */
12
+ public final class GestureSession {
13
+ public interface Listener { void onState(int state, double velocityX, double velocityY); }
14
+ private final GestureSurface surface;
15
+ private final View scroll;
16
+ private final LiveSpring x, y, scale, rotation;
17
+ private final Listener listener;
18
+ private final GestureDetector taps;
19
+ private final double density, threshold, failOffset;
20
+ private final int axis;
21
+ private final boolean pinch, rotate, tapEnabled, doubleEnabled, contactEvents;
22
+ private double minX, maxX, minY, maxY, minScale, maxScale;
23
+ private double downX, downY, lastX, lastY, lastSpan, lastAngle, velocityX, velocityY;
24
+ private boolean enabled = true, disposed, active, rejected, fromScroll, contact, ending;
25
+ private VelocityTracker velocity;
26
+
27
+ public GestureSession(GestureSurface surface, View scroll, LiveSpring x, LiveSpring y, LiveSpring scale, LiveSpring rotation, String options, Listener listener) {
28
+ if (scroll != null && !(scroll instanceof androidx.core.view.NestedScrollingChild3))
29
+ throw new IllegalArgumentException("Scroll handoff requires NativeScript's NestedScrollView implementation");
30
+ this.surface = surface; this.scroll = scroll; this.x = x; this.y = y; this.scale = scale; this.rotation = rotation; this.listener = listener;
31
+ density = surface.getResources().getDisplayMetrics().density;
32
+ try {
33
+ JSONObject config = new JSONObject(options);
34
+ axis = config.getInt("axis"); threshold = config.getDouble("threshold"); failOffset = config.getDouble("failOffset");
35
+ pinch = config.getBoolean("pinch"); rotate = config.getBoolean("rotate");
36
+ tapEnabled = config.getBoolean("tap"); doubleEnabled = config.getBoolean("doubleTap");
37
+ contactEvents = config.optBoolean("contactEvents", false);
38
+ minX = config.getDouble("minX"); maxX = config.getDouble("maxX"); minY = config.getDouble("minY"); maxY = config.getDouble("maxY");
39
+ minScale = config.getDouble("minScale"); maxScale = config.getDouble("maxScale");
40
+ } catch (JSONException error) { throw new IllegalArgumentException(error); }
41
+ taps = new GestureDetector(surface.getContext(), new GestureDetector.SimpleOnGestureListener() {
42
+ @Override public boolean onDown(MotionEvent event) { return true; }
43
+ @Override public boolean onSingleTapUp(MotionEvent event) { if (tapEnabled && !doubleEnabled) emit(4); return true; }
44
+ @Override public boolean onSingleTapConfirmed(MotionEvent event) { if (tapEnabled) emit(4); return true; }
45
+ @Override public boolean onDoubleTapEvent(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_UP) emit(5); return true; }
46
+ });
47
+ if (!doubleEnabled) taps.setOnDoubleTapListener(null);
48
+ surface.setSession(this);
49
+ }
50
+ public static boolean supportsContactEvents() { return true; }
51
+ public boolean isActive() { return active; }
52
+ boolean intercepts() { return enabled && active && !fromScroll; }
53
+ boolean accepts(View target) { return enabled && !disposed && target == scroll; }
54
+ private static double clamp(double value, double min, double max) { return Math.max(min, Math.min(max, value)); }
55
+ private void emit(int state) { if (!disposed && listener != null) listener.onState(state, velocityX, velocityY); }
56
+ private void begin() {
57
+ if (active || !enabled || disposed) return;
58
+ active = true;
59
+ x.holdInput(x.getPosition(), 0); y.holdInput(y.getPosition(), 0);
60
+ scale.holdInput(scale.getPosition(), 0); rotation.holdInput(rotation.getPosition(), 0);
61
+ if (surface.getParent() != null) surface.getParent().requestDisallowInterceptTouchEvent(true);
62
+ emit(1);
63
+ }
64
+ void before(MotionEvent event) {
65
+ if (!enabled || disposed) return;
66
+ if (!surface.isEnabled()) { cancel(); return; }
67
+ int action = event.getActionMasked();
68
+ if (action == MotionEvent.ACTION_DOWN) {
69
+ cancel(); if (disposed || !enabled) return;
70
+ contact = true; rejected = false; velocityX = velocityY = 0;
71
+ velocity = VelocityTracker.obtain();
72
+ fromScroll = false;
73
+ if (scroll != null) {
74
+ Rect bounds = new Rect(); scroll.getDrawingRect(bounds); surface.offsetDescendantRectToMyCoords(scroll, bounds);
75
+ fromScroll = bounds.contains((int)event.getX(), (int)event.getY());
76
+ }
77
+ rebase(event, -1); downX = lastX; downY = lastY;
78
+ if (contactEvents) emit(6);
79
+ }
80
+ if (!contact) return;
81
+ if (velocity != null) {
82
+ MotionEvent raw = MotionEvent.obtain(event);
83
+ raw.offsetLocation(event.getRawX() - event.getX(), event.getRawY() - event.getY());
84
+ velocity.addMovement(raw); raw.recycle();
85
+ if (action == MotionEvent.ACTION_UP) {
86
+ velocity.computeCurrentVelocity(1000);
87
+ velocityX = (axis & 1) != 0 ? velocity.getXVelocity(event.getPointerId(0)) / density : 0;
88
+ velocityY = (axis & 2) != 0 ? velocity.getYVelocity(event.getPointerId(0)) / density : 0;
89
+ }
90
+ }
91
+ if (tapEnabled || doubleEnabled) taps.onTouchEvent(event);
92
+ if (disposed || !enabled) return;
93
+ if (action == MotionEvent.ACTION_POINTER_DOWN || action == MotionEvent.ACTION_POINTER_UP) {
94
+ rebase(event, action == MotionEvent.ACTION_POINTER_UP ? event.getActionIndex() : -1);
95
+ if (!active) { downX = lastX; downY = lastY; }
96
+ if (velocity != null) velocity.clear();
97
+ if (!fromScroll && (pinch || rotate) && event.getPointerCount() >= 2 && action == MotionEvent.ACTION_POINTER_DOWN) begin();
98
+ return;
99
+ }
100
+ if (action != MotionEvent.ACTION_MOVE) return;
101
+ double cx = 0, cy = 0;
102
+ for (int index = 0; index < event.getPointerCount(); index++) { cx += event.getX(index); cy += event.getY(index); }
103
+ cx /= event.getPointerCount() * density; cy /= event.getPointerCount() * density;
104
+ cx += (event.getRawX() - event.getX()) / density; cy += (event.getRawY() - event.getY()) / density;
105
+ if (velocity != null) {
106
+ velocity.computeCurrentVelocity(1000);
107
+ velocityX = (axis & 1) != 0 ? velocity.getXVelocity(event.getPointerId(0)) / density : 0;
108
+ velocityY = (axis & 2) != 0 ? velocity.getYVelocity(event.getPointerId(0)) / density : 0;
109
+ }
110
+ if (!active && !fromScroll && !rejected) {
111
+ double dx = Math.abs(cx - downX), dy = Math.abs(cy - downY);
112
+ if (axis == 1 && dy > failOffset || axis == 2 && dx > failOffset) rejected = true;
113
+ else if (axis != 0 && (axis == 1 ? dx : axis == 2 ? dy : Math.hypot(dx, dy)) >= threshold) begin();
114
+ }
115
+ if (active && !fromScroll && !disposed) {
116
+ if ((axis & 1) != 0) x.holdInput(clamp(x.getPosition() + cx - lastX, minX, maxX), velocityX);
117
+ if ((axis & 2) != 0) y.holdInput(clamp(y.getPosition() + cy - lastY, minY, maxY), velocityY);
118
+ if (event.getPointerCount() >= 2) {
119
+ double dx = event.getX(1) - event.getX(0), dy = event.getY(1) - event.getY(0);
120
+ double span = Math.hypot(dx, dy), angle = Math.atan2(dy, dx);
121
+ if (pinch && lastSpan > 0) scale.holdInput(clamp(scale.getPosition() * span / lastSpan, minScale, maxScale), 0);
122
+ if (rotate && lastSpan > 0) rotation.holdInput(rotation.getPosition() + Math.atan2(Math.sin(angle - lastAngle), Math.cos(angle - lastAngle)) * 180 / Math.PI, 0);
123
+ lastSpan = span; lastAngle = angle;
124
+ }
125
+ }
126
+ lastX = cx; lastY = cy;
127
+ }
128
+ private void rebase(MotionEvent event, int excluding) {
129
+ double cx = 0, cy = 0; int count = 0, a = -1, b = -1;
130
+ for (int index = 0; index < event.getPointerCount(); index++) if (index != excluding) {
131
+ cx += event.getX(index); cy += event.getY(index); count++;
132
+ if (a < 0) a = index; else if (b < 0) b = index;
133
+ }
134
+ if (count > 0) { lastX = (cx / count + event.getRawX() - event.getX()) / density; lastY = (cy / count + event.getRawY() - event.getY()) / density; }
135
+ lastSpan = b < 0 ? 0 : Math.hypot(event.getX(b) - event.getX(a), event.getY(b) - event.getY(a));
136
+ lastAngle = b < 0 ? 0 : Math.atan2(event.getY(b) - event.getY(a), event.getX(b) - event.getX(a));
137
+ }
138
+ void after(MotionEvent event) {
139
+ int action = event.getActionMasked();
140
+ if (action == MotionEvent.ACTION_UP) finish(false);
141
+ else if (action == MotionEvent.ACTION_CANCEL) finish(true);
142
+ }
143
+ int consume(int pixels, boolean atTop) {
144
+ if (!contact || pixels == 0 || pixels < 0 && !atTop) return 0;
145
+ double before = y.getPosition(), next = clamp(before - pixels / density, minY, maxY);
146
+ int consumed = (int)Math.round((before - next) * density);
147
+ if (consumed == 0) return 0;
148
+ begin();
149
+ if (disposed || !enabled || !active) return 0;
150
+ y.holdInput(before - consumed / density, velocityY);
151
+ return consumed;
152
+ }
153
+ boolean preFling(float vx, float vy) {
154
+ if (!active) return false;
155
+ velocityX = 0; velocityY = -vy / density;
156
+ return y.getPosition() > minY + .01 || vy < 0 && !scroll.canScrollVertically(-1);
157
+ }
158
+ private void finish(boolean cancelled) {
159
+ boolean hadContact = contact;
160
+ contact = false;
161
+ if (velocity != null) { velocity.recycle(); velocity = null; }
162
+ if (!active) { if (hadContact && contactEvents) emit(7); return; }
163
+ active = false;
164
+ x.releaseInput(x.getPosition(), 0); y.releaseInput(y.getPosition(), 0);
165
+ scale.releaseInput(scale.getPosition(), 0); rotation.releaseInput(rotation.getPosition(), 0);
166
+ if (surface.getParent() != null) surface.getParent().requestDisallowInterceptTouchEvent(false);
167
+ ending = !cancelled;
168
+ try { emit(cancelled ? 3 : 2); } finally { ending = false; }
169
+ if (hadContact && contactEvents) emit(7);
170
+ }
171
+ public void cancel() {
172
+ if (active) finish(true);
173
+ else {
174
+ finish(true);
175
+ x.releaseInput(x.getPosition(), 0); y.releaseInput(y.getPosition(), 0);
176
+ scale.releaseInput(scale.getPosition(), 0); rotation.releaseInput(rotation.getPosition(), 0);
177
+ }
178
+ }
179
+ public void setEnabled(boolean value) { enabled = value; if (!value) cancel(); }
180
+ public void settle(double tx, double ty, double ts, double tr, boolean animated) {
181
+ if (disposed) return;
182
+ double vx = ending ? velocityX : x.getVelocity(), vy = ending ? velocityY : y.getVelocity();
183
+ cancel();
184
+ double[] targets = {clamp(tx, minX, maxX), clamp(ty, minY, maxY), clamp(ts, minScale, maxScale), tr};
185
+ LiveSpring[] nodes = {x, y, scale, rotation};
186
+ for (int index = 0; index < nodes.length; index++) {
187
+ if (!animated) nodes[index].holdInput(targets[index], 0);
188
+ nodes[index].releaseInput(targets[index], animated ? index == 0 ? vx : index == 1 ? vy : 0 : 0);
189
+ }
190
+ }
191
+ public void updateBounds(double left, double right, double top, double bottom) { minX = left; maxX = right; minY = top; maxY = bottom; }
192
+ public void dispose() {
193
+ if (disposed) return;
194
+ cancel(); disposed = true; surface.setSession(null);
195
+ MotionEvent cancel = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
196
+ taps.onTouchEvent(cancel); cancel.recycle();
197
+ }
198
+ }
@@ -0,0 +1,53 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.content.Context;
4
+ import android.view.MotionEvent;
5
+ import android.view.View;
6
+ import androidx.core.view.NestedScrollingParent3;
7
+ import androidx.core.view.NestedScrollingParentHelper;
8
+ import androidx.core.view.ViewCompat;
9
+
10
+ /** A GridLayout dispatch boundary; it does not replace NativeScript's touch listener. */
11
+ public final class GestureSurface extends org.nativescript.widgets.GridLayout implements NestedScrollingParent3 {
12
+ private GestureSession session;
13
+ private final NestedScrollingParentHelper nested = new NestedScrollingParentHelper(this);
14
+ public GestureSurface(Context context) { super(context); }
15
+ public void setSession(GestureSession value) { session = value; }
16
+ @Override public boolean dispatchTouchEvent(MotionEvent event) {
17
+ GestureSession current = session;
18
+ if (current != null) current.before(event);
19
+ boolean handled = super.dispatchTouchEvent(event);
20
+ if (current != null) current.after(event);
21
+ return handled || current != null;
22
+ }
23
+ @Override public boolean onInterceptTouchEvent(MotionEvent event) {
24
+ return session != null && session.intercepts() || super.onInterceptTouchEvent(event);
25
+ }
26
+ @Override public boolean onTouchEvent(MotionEvent event) {
27
+ return session != null || super.onTouchEvent(event);
28
+ }
29
+ @Override public boolean onStartNestedScroll(View child, View target, int axes, int type) {
30
+ return session != null && type == ViewCompat.TYPE_TOUCH && (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && session.accepts(target);
31
+ }
32
+ @Override public void onNestedScrollAccepted(View child, View target, int axes, int type) { nested.onNestedScrollAccepted(child, target, axes, type); }
33
+ @Override public void onStopNestedScroll(View target, int type) { nested.onStopNestedScroll(target, type); }
34
+ @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
35
+ if (session != null && type == ViewCompat.TYPE_TOUCH && session.accepts(target)) consumed[1] += session.consume(dy, !target.canScrollVertically(-1));
36
+ }
37
+ @Override public void onNestedScroll(View target, int dx, int dy, int remainingX, int remainingY, int type, int[] consumed) {
38
+ if (session != null && type == ViewCompat.TYPE_TOUCH && session.accepts(target)) consumed[1] += session.consume(remainingY, true);
39
+ }
40
+ @Override public boolean onNestedPreFling(View target, float vx, float vy) { return session != null && session.accepts(target) && session.preFling(vx, vy); }
41
+ @Override public boolean onNestedFling(View target, float vx, float vy, boolean consumed) { return false; }
42
+ @Override public int getNestedScrollAxes() { return nested.getNestedScrollAxes(); }
43
+ @Override public boolean onStartNestedScroll(View child, View target, int axes) { return onStartNestedScroll(child, target, axes, ViewCompat.TYPE_TOUCH); }
44
+ @Override public void onNestedScrollAccepted(View child, View target, int axes) { onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH); }
45
+ @Override public void onStopNestedScroll(View target) { onStopNestedScroll(target, ViewCompat.TYPE_TOUCH); }
46
+ @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH); }
47
+ @Override public void onNestedScroll(View target, int dx, int dy, int rx, int ry) { onNestedScroll(target, dx, dy, rx, ry, ViewCompat.TYPE_TOUCH); }
48
+ @Override public void onNestedScroll(View target, int dx, int dy, int rx, int ry, int type) { onNestedScroll(target, dx, dy, rx, ry, type, new int[2]); }
49
+ @Override protected void onDetachedFromWindow() {
50
+ if (session != null) session.cancel();
51
+ super.onDetachedFromWindow();
52
+ }
53
+ }
@@ -0,0 +1,270 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.os.SystemClock;
4
+ import android.view.Choreographer;
5
+ import android.view.View;
6
+ import java.lang.ref.WeakReference;
7
+ import java.util.ArrayList;
8
+ import org.json.JSONArray;
9
+ import org.json.JSONException;
10
+ import org.json.JSONObject;
11
+
12
+ /** Persistent native follower graph. The frame callback never enters JavaScript. */
13
+ public final class LiveSpring {
14
+ private static final ArrayList<LiveSpring> nodes = new ArrayList<>();
15
+ private static boolean scheduled;
16
+ private static long previousFrame;
17
+ private static final Choreographer.FrameCallback clock = new Choreographer.FrameCallback() {
18
+ @Override public void doFrame(long nanos) {
19
+ scheduled = false;
20
+ double now = nanos / 1e6;
21
+ double remaining = previousFrame == 0 ? 1.0 / 120 : Math.min(.064, Math.max(0, (nanos - previousFrame) / 1e9));
22
+ previousFrame = nanos;
23
+ // Older nodes precede their followers. Substeps bound chain phase error at 60/120Hz.
24
+ while (remaining > 1e-9) {
25
+ double dt = Math.min(remaining, 1.0 / 240);
26
+ remaining -= dt;
27
+ for (int i = 0; i < nodes.size(); i++) nodes.get(i).step(now - remaining * 1000, dt);
28
+ }
29
+ boolean active = false;
30
+ for (int i = 0; i < nodes.size(); i++) {
31
+ LiveSpring node = nodes.get(i);
32
+ node.apply();
33
+ active |= node.moving || node.layersMoving(now);
34
+ }
35
+ if (active) schedule(); else previousFrame = 0;
36
+ }
37
+ };
38
+ private static void schedule() {
39
+ if (scheduled || nodes.isEmpty()) return;
40
+ scheduled = true;
41
+ Choreographer.getInstance().postFrameCallback(clock);
42
+ }
43
+
44
+ private final SpringState state;
45
+ private final double stiffness, damping, mass, restDelta, restSpeed;
46
+ private final ArrayList<Binding> bindings = new ArrayList<>();
47
+ private LiveSpring source;
48
+ private Mapping[] sourceMaps = new Mapping[0];
49
+ private SampledInput input;
50
+ private boolean direct = true;
51
+ private double targetValue;
52
+ private boolean moving, reduced, disposed, paused, inputHeld;
53
+
54
+ public LiveSpring(double initial, double stiffness, double damping, double mass,
55
+ double restDelta, double restSpeed, boolean reduced) {
56
+ state = new SpringState(initial, 0);
57
+ this.stiffness = stiffness; this.damping = damping; this.mass = mass;
58
+ this.restDelta = restDelta; this.restSpeed = restSpeed; this.reduced = reduced;
59
+ targetValue = initial;
60
+ nodes.add(this);
61
+ }
62
+ public double getPosition() { return state.position; }
63
+ public double getVelocity() { return state.velocity; }
64
+ public boolean isMoving() { return moving; }
65
+ public static int activeCount() { return scheduled ? 1 : 0; }
66
+ public static int nodeCount() { return nodes.size(); }
67
+
68
+ public void follow(LiveSpring parent, String mappings) {
69
+ if (disposed || parent.disposed || nodes.indexOf(parent) >= nodes.indexOf(this))
70
+ throw new IllegalArgumentException("A spring can only follow an existing live node");
71
+ sourceMaps = maps(mappings);
72
+ source = parent;
73
+ wake();
74
+ }
75
+ public void updateInput(String snapshot) {
76
+ if (disposed) return;
77
+ try { input = new SampledInput(new JSONObject(snapshot)); }
78
+ catch (JSONException error) { throw new IllegalArgumentException(error); }
79
+ direct = false;
80
+ wake();
81
+ }
82
+ public void setTarget(double value) {
83
+ if (disposed) return;
84
+ direct = true; targetValue = value; input = null;
85
+ wake();
86
+ }
87
+ public void holdInput(double value, double velocity) {
88
+ if (disposed) return;
89
+ inputHeld = true; direct = true; targetValue = value; input = null;
90
+ state.position = value; state.velocity = velocity; moving = true;
91
+ apply(); schedule();
92
+ }
93
+ public void releaseInput(double target, double velocity) {
94
+ if (disposed) return;
95
+ inputHeld = false; state.velocity = velocity; setTarget(target);
96
+ }
97
+ public void setReduced(boolean value) { reduced = value; wake(); }
98
+ public void setPaused(boolean value) {
99
+ paused = value;
100
+ if (value) moving = false; else wake();
101
+ }
102
+ private void wake() {
103
+ if (disposed || paused) return;
104
+ moving = true;
105
+ if (reduced) { step(SystemClock.uptimeMillis(), 0); apply(); }
106
+ schedule();
107
+ }
108
+ private void step(double now, double dt) {
109
+ if (disposed || paused || inputHeld) return;
110
+ double target = source == null ? (direct ? targetValue : input.value(now)) : map(source.state.position, sourceMaps);
111
+ boolean inputMoving = source == null ? !direct && input.running(now) : source.moving;
112
+ if (reduced) { state.position = target; state.velocity = 0; }
113
+ else state.advance(target, stiffness, damping, mass, dt);
114
+ boolean rest = Math.abs(state.position - target) <= restDelta && Math.abs(state.velocity) <= restSpeed;
115
+ if (rest && !inputMoving) { state.position = target; state.velocity = 0; }
116
+ moving = inputMoving || !rest;
117
+ }
118
+ public Binding bind(View view, String property, String mappings) {
119
+ if (disposed) throw new IllegalStateException("Spring disposed");
120
+ Binding binding = new Binding(view, property, maps(mappings));
121
+ bindings.add(binding);
122
+ binding.apply();
123
+ return binding;
124
+ }
125
+ private void apply() {
126
+ for (int i = bindings.size() - 1; i >= 0; i--) {
127
+ Binding binding = bindings.get(i);
128
+ if (binding.view.get() == null) bindings.remove(i); else binding.apply();
129
+ }
130
+ }
131
+ private boolean layersMoving(double now) {
132
+ for (int i = 0; i < bindings.size(); i++)
133
+ for (SampledInput layer : bindings.get(i).layers)
134
+ if (layer.running(now)) return true;
135
+ return false;
136
+ }
137
+ public static void sync(View view) {
138
+ for (int i = 0; i < nodes.size(); i++) {
139
+ LiveSpring node = nodes.get(i);
140
+ for (int j = 0; j < node.bindings.size(); j++) {
141
+ Binding binding = node.bindings.get(j);
142
+ if (binding.view.get() == view) binding.apply();
143
+ }
144
+ }
145
+ }
146
+ public void dispose() {
147
+ if (disposed) return;
148
+ disposed = true; moving = false;
149
+ bindings.clear();
150
+ nodes.remove(this);
151
+ // A retained child continues from its own state toward the parent's last value.
152
+ for (int i = 0; i < nodes.size(); i++) {
153
+ LiveSpring child = nodes.get(i);
154
+ if (child.source == this) {
155
+ child.source = null;
156
+ child.setTarget(map(state.position, child.sourceMaps));
157
+ }
158
+ }
159
+ source = null;
160
+ if (nodes.isEmpty() && scheduled) {
161
+ Choreographer.getInstance().removeFrameCallback(clock);
162
+ scheduled = false; previousFrame = 0;
163
+ }
164
+ }
165
+
166
+ public final class Binding {
167
+ private final WeakReference<View> view;
168
+ private final String property;
169
+ private final Mapping[] mappings;
170
+ private final float density;
171
+ private boolean stopped;
172
+ private SampledInput[] layers = new SampledInput[0];
173
+ Binding(View view, String property, Mapping[] mappings) {
174
+ this.view = new WeakReference<>(view); this.property = property; this.mappings = mappings;
175
+ density = view.getResources().getDisplayMetrics().density;
176
+ }
177
+ public double getValue() { return map(state.position, mappings); }
178
+ public void setLayers(String inputs) {
179
+ try {
180
+ JSONArray array = new JSONArray(inputs);
181
+ SampledInput[] next = new SampledInput[array.length()];
182
+ for (int i = 0; i < next.length; i++) next[i] = new SampledInput(array.getJSONObject(i));
183
+ layers = next;
184
+ } catch (JSONException error) { throw new IllegalArgumentException(error); }
185
+ apply(); schedule();
186
+ }
187
+ public void cancel() { stopped = true; bindings.remove(this); }
188
+ private void apply() {
189
+ View target = view.get();
190
+ if (stopped || target == null || !target.isAttachedToWindow()) return;
191
+ double composed = getValue();
192
+ boolean multiply = property.equals("opacity") || property.startsWith("scale");
193
+ double now = SystemClock.uptimeMillis();
194
+ for (SampledInput layer : layers) composed = multiply ? composed * layer.value(now) : composed + layer.value(now);
195
+ float value = (float)composed;
196
+ switch (property) {
197
+ case "x": target.setTranslationX(value * density); break;
198
+ case "y": target.setTranslationY(value * density); break;
199
+ case "scaleX": target.setScaleX(value); break;
200
+ case "scaleY": target.setScaleY(value); break;
201
+ case "rotate": target.setRotation(value); break;
202
+ case "rotateX": target.setRotationX(value); break;
203
+ case "rotateY": target.setRotationY(value); break;
204
+ case "opacity": target.setAlpha(Math.max(0, Math.min(1, value))); break;
205
+ default: throw new IllegalArgumentException("Unsupported live property " + property);
206
+ }
207
+ }
208
+ }
209
+ private static final class Mapping {
210
+ final double[] input, output;
211
+ final boolean clamp;
212
+ Mapping(JSONObject json) throws JSONException {
213
+ input = array(json.getJSONArray("input")); output = array(json.getJSONArray("output"));
214
+ clamp = json.getBoolean("clamp");
215
+ }
216
+ double value(double value) {
217
+ if (clamp && value <= input[0]) return output[0];
218
+ if (clamp && value >= input[input.length - 1]) return output[output.length - 1];
219
+ int index = 0;
220
+ while (index < input.length - 2 && value > input[index + 1]) index++;
221
+ double span = input[index + 1] - input[index];
222
+ double fraction = span > 0 ? (value - input[index]) / span : value < input[index] ? 0 : 1;
223
+ return output[index] + (output[index + 1] - output[index]) * fraction;
224
+ }
225
+ }
226
+ private static Mapping[] maps(String json) {
227
+ try {
228
+ JSONArray array = new JSONArray(json);
229
+ Mapping[] result = new Mapping[array.length()];
230
+ for (int i = 0; i < result.length; i++) result[i] = new Mapping(array.getJSONObject(i));
231
+ return result;
232
+ } catch (JSONException error) { throw new IllegalArgumentException(error); }
233
+ }
234
+ private static double map(double value, Mapping[] maps) {
235
+ for (Mapping mapping : maps) value = mapping.value(value);
236
+ return value;
237
+ }
238
+ private static double[] array(JSONArray input) throws JSONException {
239
+ double[] result = new double[input.length()];
240
+ for (int i = 0; i < result.length; i++) result[i] = input.getDouble(i);
241
+ return result;
242
+ }
243
+ private static final class SampledInput {
244
+ final double value, anchor, elapsed, duration, delay, repeat, end;
245
+ final double[] values, times;
246
+ final boolean reverse, playing;
247
+ SampledInput(JSONObject json) throws JSONException {
248
+ value = json.getDouble("value");
249
+ anchor = SystemClock.uptimeMillis(); elapsed = json.getDouble("elapsed");
250
+ playing = json.getBoolean("running");
251
+ duration = json.getDouble("duration"); delay = json.getDouble("delay");
252
+ repeat = json.getDouble("repeat"); end = json.getDouble("to");
253
+ reverse = json.getBoolean("reverse");
254
+ values = array(json.getJSONArray("values")); times = array(json.getJSONArray("times"));
255
+ }
256
+ boolean running(double now) { return playing && (repeat < 0 || now - anchor + elapsed < delay + duration * (repeat + 1)); }
257
+ double value(double now) {
258
+ if (!playing || values.length < 2) return value;
259
+ if (!running(now)) return end;
260
+ double time = Math.max(0, now - anchor + elapsed - delay);
261
+ double iteration = Math.floor(time / Math.max(1, duration));
262
+ double progress = (time % Math.max(1, duration)) / Math.max(1, duration);
263
+ if (reverse && iteration % 2 >= 1) progress = 1 - progress;
264
+ int lo = 0, hi = times.length - 1;
265
+ while (lo + 1 < hi) { int mid = (lo + hi) / 2; if (times[mid] <= progress) lo = mid; else hi = mid; }
266
+ double fraction = times[hi] > times[lo] ? (progress - times[lo]) / (times[hi] - times[lo]) : 1;
267
+ return values[lo] + (values[hi] - values[lo]) * Math.max(0, Math.min(1, fraction));
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,25 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.content.Context;
4
+ import android.view.KeyEvent;
5
+ import android.view.MotionEvent;
6
+ import android.widget.Button;
7
+
8
+ /** A focusable button with explicit keyboard move actions. */
9
+ public final class ReorderHandle extends Button {
10
+ public interface MoveListener { void move(int direction); }
11
+ private MoveListener move;
12
+ public ReorderHandle(Context context) { super(context); setFocusable(true); setFocusableInTouchMode(true); }
13
+ public void setMoveListener(MoveListener value) { move = value; }
14
+ @Override public boolean onTouchEvent(MotionEvent event) {
15
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) requestFocus();
16
+ return super.onTouchEvent(event);
17
+ }
18
+ @Override public boolean onKeyDown(int code, KeyEvent event) {
19
+ if (event.isAltPressed() && move != null && (code == KeyEvent.KEYCODE_DPAD_UP || code == KeyEvent.KEYCODE_DPAD_DOWN)) {
20
+ if (event.getRepeatCount() == 0) move.move(code == KeyEvent.KEYCODE_DPAD_UP ? -1 : 1);
21
+ return true;
22
+ }
23
+ return super.onKeyDown(code, event);
24
+ }
25
+ }