@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.
- package/README.md +9 -2
- package/dist/bottom-sheet/geometry.d.ts +18 -0
- package/dist/bottom-sheet/geometry.d.ts.map +1 -0
- package/dist/bottom-sheet/geometry.js +51 -0
- package/dist/bottom-sheet/geometry.js.map +1 -0
- package/dist/bottom-sheet/index.d.ts +56 -0
- package/dist/bottom-sheet/index.d.ts.map +1 -0
- package/dist/bottom-sheet/index.js +462 -0
- package/dist/bottom-sheet/index.js.map +1 -0
- package/dist/bottom-sheet/native.d.ts +11 -0
- package/dist/bottom-sheet/native.d.ts.map +1 -0
- package/dist/bottom-sheet/native.js +173 -0
- package/dist/bottom-sheet/native.js.map +1 -0
- package/dist/drag.js +1 -1
- package/dist/drag.js.map +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +2 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/ios-scroll-delegate.d.ts +4 -0
- package/dist/driver/ios-scroll-delegate.d.ts.map +1 -0
- package/dist/driver/ios-scroll-delegate.js +26 -0
- package/dist/driver/ios-scroll-delegate.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +2 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/gesture-native.d.ts +28 -0
- package/dist/gesture-native.d.ts.map +1 -0
- package/dist/gesture-native.js +61 -0
- package/dist/gesture-native.js.map +1 -0
- package/dist/gestures/index.d.ts +69 -0
- package/dist/gestures/index.d.ts.map +1 -0
- package/dist/gestures/index.js +262 -0
- package/dist/gestures/index.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/live-spring-driver.d.ts +52 -0
- package/dist/live-spring-driver.d.ts.map +1 -0
- package/dist/live-spring-driver.js +104 -0
- package/dist/live-spring-driver.js.map +1 -0
- package/dist/mixer.d.ts +6 -0
- package/dist/mixer.d.ts.map +1 -1
- package/dist/mixer.js +41 -1
- package/dist/mixer.js.map +1 -1
- package/dist/motion-value.d.ts +34 -2
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +277 -10
- package/dist/motion-value.js.map +1 -1
- package/dist/native/index.d.ts +2 -0
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +1 -0
- package/dist/native/index.js.map +1 -1
- package/dist/native-property.d.ts +2 -0
- package/dist/native-property.d.ts.map +1 -1
- package/dist/native-property.js +12 -4
- package/dist/native-property.js.map +1 -1
- package/dist/reorder/handle.d.ts +6 -0
- package/dist/reorder/handle.d.ts.map +1 -0
- package/dist/reorder/handle.js +10 -0
- package/dist/reorder/handle.js.map +1 -0
- package/dist/reorder/index.d.ts +10 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +233 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/list-view.d.ts +5 -0
- package/dist/reorder/list-view.d.ts.map +1 -0
- package/dist/reorder/list-view.js +238 -0
- package/dist/reorder/list-view.js.map +1 -0
- package/dist/reorder/model.d.ts +6 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +30 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/reorder/surface.d.ts +6 -0
- package/dist/reorder/surface.d.ts.map +1 -0
- package/dist/reorder/surface.js +10 -0
- package/dist/reorder/surface.js.map +1 -0
- package/dist/reorder/types.d.ts +92 -0
- package/dist/reorder/types.d.ts.map +1 -0
- package/dist/reorder/types.js +2 -0
- package/dist/reorder/types.js.map +1 -0
- package/dist/scene-shader.d.ts +18 -0
- package/dist/scene-shader.d.ts.map +1 -0
- package/dist/scene-shader.js +76 -0
- package/dist/scene-shader.js.map +1 -0
- package/dist/value-observers.d.ts +3 -1
- package/dist/value-observers.d.ts.map +1 -1
- package/dist/value-observers.js +10 -2
- package/dist/value-observers.js.map +1 -1
- package/package.json +13 -1
- package/platforms/android/include.gradle +9 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +198 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/GestureSurface.java +53 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +270 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ReorderHandle.java +25 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSession.java +213 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSurface.java +27 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/SpringState.java +39 -0
- package/platforms/android/proguard-rules.pro +5 -0
- package/platforms/ios/src/JoltGestureSession.h +18 -0
- package/platforms/ios/src/JoltGestureSession.m +222 -0
- package/platforms/ios/src/JoltLiveSpring.h +28 -0
- package/platforms/ios/src/JoltLiveSpring.m +236 -0
- package/platforms/ios/src/JoltReorderSession.h +15 -0
- package/platforms/ios/src/JoltReorderSession.m +205 -0
- package/platforms/ios/src/JoltSpringState.h +35 -0
- package/platforms/ios/src/module.modulemap +3 -0
- package/src/android-plugin.d.ts +93 -0
- package/src/bottom-sheet/geometry.ts +84 -0
- package/src/bottom-sheet/index.ts +536 -0
- package/src/bottom-sheet/native.ts +214 -0
- package/src/drag.ts +1 -1
- package/src/driver/android.ts +2 -0
- package/src/driver/ios-scroll-delegate.ts +23 -0
- package/src/driver/ios.ts +2 -0
- package/src/gesture-native.ts +139 -0
- package/src/gestures/index.ts +365 -0
- package/src/index.ts +3 -1
- package/src/ios-plugin.d.ts +90 -0
- package/src/live-spring-driver.ts +181 -0
- package/src/mixer.ts +43 -1
- package/src/motion-value.ts +318 -11
- package/src/native/index.ts +6 -0
- package/src/native-property.ts +17 -4
- package/src/reorder/handle.ts +9 -0
- package/src/reorder/index.ts +243 -0
- package/src/reorder/list-view.ts +262 -0
- package/src/reorder/model.ts +47 -0
- package/src/reorder/surface.ts +10 -0
- package/src/reorder/types.ts +87 -0
- package/src/scene-shader.ts +139 -0
- package/src/value-observers.ts +12 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.animation.Animator;
|
|
4
|
+
import android.animation.AnimatorListenerAdapter;
|
|
5
|
+
import android.animation.ValueAnimator;
|
|
6
|
+
import android.graphics.Bitmap;
|
|
7
|
+
import android.graphics.Canvas;
|
|
8
|
+
import android.graphics.Rect;
|
|
9
|
+
import android.view.Choreographer;
|
|
10
|
+
import android.view.MotionEvent;
|
|
11
|
+
import android.view.View;
|
|
12
|
+
import android.view.ViewConfiguration;
|
|
13
|
+
import android.view.ViewGroup;
|
|
14
|
+
import android.widget.ImageView;
|
|
15
|
+
import android.widget.ListView;
|
|
16
|
+
import org.json.JSONObject;
|
|
17
|
+
import java.util.ArrayList;
|
|
18
|
+
import java.util.Collections;
|
|
19
|
+
import java.util.Set;
|
|
20
|
+
import java.util.WeakHashMap;
|
|
21
|
+
|
|
22
|
+
/** Core ListView input, snapshot, recycling and edge autoscroll stay on the UI thread. */
|
|
23
|
+
public final class ReorderSession implements View.OnTouchListener, Choreographer.FrameCallback, View.OnLayoutChangeListener, View.OnAttachStateChangeListener {
|
|
24
|
+
public interface Listener { void onState(int state, int index, int toIndex); }
|
|
25
|
+
private static final class Cell {
|
|
26
|
+
final float translation, alpha;
|
|
27
|
+
final int index;
|
|
28
|
+
final SpringState spring = new SpringState(0, 0);
|
|
29
|
+
Cell(View view, int index) { this.translation = view.getTranslationY(); this.alpha = view.getAlpha(); this.index = index; }
|
|
30
|
+
}
|
|
31
|
+
final ListView list;
|
|
32
|
+
private final ReorderSurface surface;
|
|
33
|
+
private final Listener listener;
|
|
34
|
+
private final WeakHashMap<View, Cell> cells = new WeakHashMap<>();
|
|
35
|
+
private final Set<View> handles = Collections.newSetFromMap(new WeakHashMap<View, Boolean>());
|
|
36
|
+
private final float density, edgeSize, edgeSpeed, slop;
|
|
37
|
+
private final long longPress;
|
|
38
|
+
private final boolean handlesOnly;
|
|
39
|
+
private final Runnable activate = () -> begin();
|
|
40
|
+
private boolean enabled = true, disposed, active, settling, pending, reduced;
|
|
41
|
+
private int from, target, count, generation, width, height;
|
|
42
|
+
private float downX, downY, pointerX, pointerY, grabOffset, rowHeight, remainder;
|
|
43
|
+
private long previousTime;
|
|
44
|
+
private ImageView overlay;
|
|
45
|
+
private Bitmap bitmap;
|
|
46
|
+
private ValueAnimator drop;
|
|
47
|
+
|
|
48
|
+
public ReorderSession(ReorderSurface surface, ListView list, String options, Listener listener) {
|
|
49
|
+
this.surface = surface; this.list = list; this.listener = listener;
|
|
50
|
+
density = list.getResources().getDisplayMetrics().density;
|
|
51
|
+
slop = ViewConfiguration.get(list.getContext()).getScaledTouchSlop();
|
|
52
|
+
try {
|
|
53
|
+
JSONObject config = new JSONObject(options);
|
|
54
|
+
longPress = config.getLong("longPress"); edgeSize = (float)config.getDouble("edgeSize") * density;
|
|
55
|
+
edgeSpeed = (float)config.getDouble("edgeSpeed") * density;
|
|
56
|
+
reduced = config.getBoolean("reducedMotion"); handlesOnly = config.getBoolean("handles");
|
|
57
|
+
} catch (Exception error) { throw new IllegalArgumentException(error); }
|
|
58
|
+
surface.setSession(this); list.addOnLayoutChangeListener(this); list.addOnAttachStateChangeListener(this);
|
|
59
|
+
}
|
|
60
|
+
public boolean isActive() { return active; }
|
|
61
|
+
public void registerHandle(View view) {
|
|
62
|
+
if (!disposed && view != null) {
|
|
63
|
+
handles.add(view);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
private boolean inHandle(float x, float y) {
|
|
67
|
+
if (!handlesOnly) return true;
|
|
68
|
+
Rect rect = new Rect();
|
|
69
|
+
for (View handle : handles) {
|
|
70
|
+
if (handle == null || !handle.isShown()) continue;
|
|
71
|
+
android.view.ViewParent parent = handle.getParent();
|
|
72
|
+
while (parent != null && parent != list) parent = parent.getParent();
|
|
73
|
+
if (parent != list) continue;
|
|
74
|
+
handle.getDrawingRect(rect); list.offsetDescendantRectToMyCoords(handle, rect);
|
|
75
|
+
if (rect.contains((int)x, (int)y)) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
@Override public boolean onTouch(View view, MotionEvent event) {
|
|
80
|
+
if (disposed) return false;
|
|
81
|
+
int action = event.getActionMasked();
|
|
82
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
83
|
+
cancel();
|
|
84
|
+
downX = pointerX = event.getX(); downY = pointerY = event.getY();
|
|
85
|
+
if (enabled && list.isEnabled() && inHandle(downX, downY) && list.pointToPosition((int)downX, (int)downY) >= 0) {
|
|
86
|
+
pending = true; list.postDelayed(activate, longPress);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (event.getPointerCount() > 1) { boolean handled = active; cancel(); return handled; }
|
|
90
|
+
if (action == MotionEvent.ACTION_MOVE) {
|
|
91
|
+
pointerX = event.getX(); pointerY = event.getY();
|
|
92
|
+
if (pending && Math.hypot(pointerX - downX, pointerY - downY) > slop) clearPending();
|
|
93
|
+
if (active && !settling) update(0);
|
|
94
|
+
}
|
|
95
|
+
boolean owned = active;
|
|
96
|
+
if (action == MotionEvent.ACTION_UP) { clearPending(); if (active && !settling) drop(); }
|
|
97
|
+
else if (action == MotionEvent.ACTION_CANCEL) { clearPending(); if (active) finish(false, 0); }
|
|
98
|
+
if (owned) return true;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
private void clearPending() { pending = false; list.removeCallbacks(activate); }
|
|
102
|
+
private void emit(int state, int detail) { if (!disposed && listener != null) listener.onState(state, from, detail); }
|
|
103
|
+
private void begin() {
|
|
104
|
+
if (!pending || disposed || !enabled || !list.isAttachedToWindow()) return;
|
|
105
|
+
clearPending();
|
|
106
|
+
int index = list.pointToPosition((int)pointerX, (int)pointerY);
|
|
107
|
+
View cell = list.getChildAt(index - list.getFirstVisiblePosition());
|
|
108
|
+
if (cell == null || index < 0 || cell.getWidth() <= 0 || cell.getHeight() <= 0 || (long)cell.getWidth() * cell.getHeight() > 8000000) return;
|
|
109
|
+
bitmap = Bitmap.createBitmap(cell.getWidth(), cell.getHeight(), Bitmap.Config.ARGB_8888);
|
|
110
|
+
cell.draw(new Canvas(bitmap));
|
|
111
|
+
overlay = new ImageView(list.getContext()); overlay.setImageBitmap(bitmap);
|
|
112
|
+
overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
113
|
+
overlay.layout(cell.getLeft(), 0, cell.getRight(), cell.getHeight());
|
|
114
|
+
overlay.setTranslationY(cell.getTop()); overlay.setElevation(12 * density);
|
|
115
|
+
list.getOverlay().add(overlay);
|
|
116
|
+
rowHeight = cell.getHeight() + list.getDividerHeight(); grabOffset = pointerY - cell.getTop();
|
|
117
|
+
width = list.getWidth(); height = list.getHeight(); count = list.getCount();
|
|
118
|
+
from = target = index; active = true; settling = false; remainder = 0; generation++;
|
|
119
|
+
// Stop the ListView's pending click/scroll, retaining this pointer stream.
|
|
120
|
+
MotionEvent cancel = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, pointerX, pointerY, 0);
|
|
121
|
+
list.onTouchEvent(cancel); cancel.recycle();
|
|
122
|
+
if (surface.getParent() != null) surface.getParent().requestDisallowInterceptTouchEvent(true);
|
|
123
|
+
previousTime = System.nanoTime();
|
|
124
|
+
emit(1, from);
|
|
125
|
+
if (!active || disposed) return;
|
|
126
|
+
update(0); Choreographer.getInstance().postFrameCallback(this);
|
|
127
|
+
}
|
|
128
|
+
@Override public void doFrame(long time) {
|
|
129
|
+
if (!active || settling || disposed) return;
|
|
130
|
+
if (!list.isAttachedToWindow() || count != list.getCount() || width != list.getWidth() || height != list.getHeight()) { finish(false, 1); return; }
|
|
131
|
+
double dt = Math.min(.04, Math.max(0, (time - previousTime) / 1e9)); previousTime = time;
|
|
132
|
+
float edge = Math.min(edgeSize, list.getHeight() / 3f), direction = 0;
|
|
133
|
+
if (pointerY < list.getPaddingTop() + edge) direction = -Math.min(1, (list.getPaddingTop() + edge - pointerY) / edge);
|
|
134
|
+
else if (pointerY > list.getHeight() - list.getPaddingBottom() - edge) direction = Math.min(1, (pointerY - list.getHeight() + list.getPaddingBottom() + edge) / edge);
|
|
135
|
+
float distance = (float)(direction * edgeSpeed * dt) + remainder;
|
|
136
|
+
int pixels = (int)distance; remainder = distance - pixels;
|
|
137
|
+
if (pixels != 0) list.scrollListBy(pixels);
|
|
138
|
+
update(dt);
|
|
139
|
+
if (active) Choreographer.getInstance().postFrameCallback(this);
|
|
140
|
+
}
|
|
141
|
+
private void restore(View view, Cell state) { view.setTranslationY(state.translation); view.setAlpha(state.alpha); }
|
|
142
|
+
private void update(double dt) {
|
|
143
|
+
if (!active || settling) return;
|
|
144
|
+
float top = pointerY - grabOffset;
|
|
145
|
+
overlay.setTranslationY(top);
|
|
146
|
+
int first = list.getFirstVisiblePosition(), visible = list.getChildCount(), destination = from;
|
|
147
|
+
for (int offset = 0; offset < visible; offset++) {
|
|
148
|
+
int index = first + offset; if (index == from) continue;
|
|
149
|
+
View view = list.getChildAt(offset);
|
|
150
|
+
float midpoint = view.getTop() + view.getHeight() / 2f - (index > from ? rowHeight : 0);
|
|
151
|
+
int insertion = index - (index > from ? 1 : 0);
|
|
152
|
+
destination = insertion;
|
|
153
|
+
if (top < midpoint) break;
|
|
154
|
+
destination = insertion + 1;
|
|
155
|
+
}
|
|
156
|
+
target = Math.max(0, Math.min(count - 1, destination));
|
|
157
|
+
for (View view : new ArrayList<>(cells.keySet())) {
|
|
158
|
+
int index = list.getPositionForView(view);
|
|
159
|
+
Cell state = cells.get(view);
|
|
160
|
+
if (index < first || index >= first + visible || index != state.index) { restore(view, state); cells.remove(view); }
|
|
161
|
+
}
|
|
162
|
+
for (int offset = 0; offset < visible; offset++) {
|
|
163
|
+
int index = first + offset; View view = list.getChildAt(offset);
|
|
164
|
+
Cell state = cells.get(view);
|
|
165
|
+
if (state == null) { state = new Cell(view, index); cells.put(view, state); }
|
|
166
|
+
float displacement = index > from && index <= target ? -rowHeight : index < from && index >= target ? rowHeight : 0;
|
|
167
|
+
if (reduced) { state.spring.position = displacement; state.spring.velocity = 0; }
|
|
168
|
+
else state.spring.advance(displacement, 360, 34, 1, dt);
|
|
169
|
+
view.setTranslationY(state.translation + (float)state.spring.position);
|
|
170
|
+
view.setAlpha(index == from ? 0 : state.alpha);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
private void drop() {
|
|
174
|
+
if (!active || settling) return;
|
|
175
|
+
settling = true; Choreographer.getInstance().removeFrameCallback(this);
|
|
176
|
+
View destination = list.getChildAt(target - list.getFirstVisiblePosition());
|
|
177
|
+
float end = destination == null ? overlay.getTranslationY() : target > from ? destination.getBottom() - bitmap.getHeight() : destination.getTop();
|
|
178
|
+
if (reduced) { finish(true, 0); return; }
|
|
179
|
+
final int ticket = generation;
|
|
180
|
+
drop = ValueAnimator.ofFloat(overlay.getTranslationY(), end); drop.setDuration(180);
|
|
181
|
+
drop.addUpdateListener(animation -> { if (overlay != null) overlay.setTranslationY((float)animation.getAnimatedValue()); });
|
|
182
|
+
drop.addListener(new AnimatorListenerAdapter() {
|
|
183
|
+
@Override public void onAnimationEnd(Animator animation) { if (active && generation == ticket) finish(true, 0); }
|
|
184
|
+
});
|
|
185
|
+
drop.start();
|
|
186
|
+
}
|
|
187
|
+
private void finish(boolean commit, int detail) {
|
|
188
|
+
if (!active) return;
|
|
189
|
+
active = false; settling = false; generation++;
|
|
190
|
+
Choreographer.getInstance().removeFrameCallback(this);
|
|
191
|
+
if (drop != null) { ValueAnimator previous = drop; drop = null; previous.cancel(); }
|
|
192
|
+
if (overlay != null) { list.getOverlay().remove(overlay); overlay.setImageDrawable(null); overlay = null; }
|
|
193
|
+
if (bitmap != null) { bitmap.recycle(); bitmap = null; }
|
|
194
|
+
for (View view : new ArrayList<>(cells.keySet())) restore(view, cells.get(view));
|
|
195
|
+
cells.clear();
|
|
196
|
+
if (surface.getParent() != null) surface.getParent().requestDisallowInterceptTouchEvent(false);
|
|
197
|
+
emit(commit ? 2 : 3, commit ? target : detail);
|
|
198
|
+
}
|
|
199
|
+
public void cancel() { clearPending(); finish(false, 0); }
|
|
200
|
+
public void setEnabled(boolean value) { enabled = value; if (!value) cancel(); }
|
|
201
|
+
public void setReducedMotion(boolean value) { reduced = value; }
|
|
202
|
+
@Override public void onLayoutChange(View view, int l, int t, int r, int b, int ol, int ot, int or, int ob) {
|
|
203
|
+
if (active && (r - l != width || b - t != height)) finish(false, 1);
|
|
204
|
+
}
|
|
205
|
+
@Override public void onViewDetachedFromWindow(View view) { cancel(); }
|
|
206
|
+
@Override public void onViewAttachedToWindow(View view) {}
|
|
207
|
+
public void dispose() {
|
|
208
|
+
if (disposed) return;
|
|
209
|
+
cancel(); disposed = true; surface.setSession(null);
|
|
210
|
+
list.removeOnLayoutChangeListener(this); list.removeOnAttachStateChangeListener(this);
|
|
211
|
+
handles.clear();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Rect;
|
|
5
|
+
import android.view.MotionEvent;
|
|
6
|
+
|
|
7
|
+
/** Capture input above recycled cells without replacing any Core touch listener. */
|
|
8
|
+
public final class ReorderSurface extends org.nativescript.widgets.GridLayout {
|
|
9
|
+
private ReorderSession session;
|
|
10
|
+
public ReorderSurface(Context context) { super(context); }
|
|
11
|
+
void setSession(ReorderSession value) { session = value; }
|
|
12
|
+
@Override public boolean dispatchTouchEvent(MotionEvent event) {
|
|
13
|
+
ReorderSession current = session;
|
|
14
|
+
if (current != null) {
|
|
15
|
+
Rect rect = new Rect(); current.list.getDrawingRect(rect);
|
|
16
|
+
offsetDescendantRectToMyCoords(current.list, rect);
|
|
17
|
+
MotionEvent local = MotionEvent.obtain(event); local.offsetLocation(-rect.left, -rect.top);
|
|
18
|
+
current.onTouch(current.list, local); local.recycle();
|
|
19
|
+
}
|
|
20
|
+
return super.dispatchTouchEvent(event) || current != null;
|
|
21
|
+
}
|
|
22
|
+
@Override public boolean onInterceptTouchEvent(MotionEvent event) {
|
|
23
|
+
return session != null && session.isActive() || super.onInterceptTouchEvent(event);
|
|
24
|
+
}
|
|
25
|
+
@Override public boolean onTouchEvent(MotionEvent event) { return session != null || super.onTouchEvent(event); }
|
|
26
|
+
@Override protected void onDetachedFromWindow() { if (session != null) session.cancel(); super.onDetachedFromWindow(); }
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
/** Exact constant-target solution, matching JoltSpringState.h. No per-step allocation. */
|
|
4
|
+
public final class SpringState {
|
|
5
|
+
public double position, velocity;
|
|
6
|
+
|
|
7
|
+
public SpringState(double position, double velocity) {
|
|
8
|
+
this.position = position;
|
|
9
|
+
this.velocity = velocity;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public void advance(double target, double stiffness, double damping, double mass, double seconds) {
|
|
13
|
+
if (seconds <= 0) return;
|
|
14
|
+
double x = position - target;
|
|
15
|
+
double w = Math.sqrt(stiffness / mass);
|
|
16
|
+
double z = damping / (2 * Math.sqrt(stiffness * mass));
|
|
17
|
+
double next, speed;
|
|
18
|
+
if (Math.abs(z - 1) < 1e-6) {
|
|
19
|
+
double b = velocity + w * x, e = Math.exp(-w * seconds);
|
|
20
|
+
next = e * (x + b * seconds);
|
|
21
|
+
speed = e * (b - w * (x + b * seconds));
|
|
22
|
+
} else if (z < 1) {
|
|
23
|
+
double a = z * w, d = w * Math.sqrt(1 - z * z);
|
|
24
|
+
double b = (velocity + a * x) / d;
|
|
25
|
+
double c = Math.cos(d * seconds), s = Math.sin(d * seconds), e = Math.exp(-a * seconds);
|
|
26
|
+
next = e * (x * c + b * s);
|
|
27
|
+
speed = e * ((-a * x + b * d) * c + (-a * b - x * d) * s);
|
|
28
|
+
} else {
|
|
29
|
+
double q = Math.sqrt(z * z - 1);
|
|
30
|
+
double r1 = -w / (z + q), r2 = -w * (z + q);
|
|
31
|
+
double c1 = (velocity - r2 * x) / (r1 - r2), c2 = x - c1;
|
|
32
|
+
double e1 = Math.exp(r1 * seconds), e2 = Math.exp(r2 * seconds);
|
|
33
|
+
next = c1 * e1 + c2 * e2;
|
|
34
|
+
speed = c1 * r1 * e1 + c2 * r2 * e2;
|
|
35
|
+
}
|
|
36
|
+
position = target + next;
|
|
37
|
+
velocity = speed;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -21,3 +21,8 @@
|
|
|
21
21
|
-keep class org.rafaelaugusto.jolt.GradientLayer$* { *; }
|
|
22
22
|
-keep class org.rafaelaugusto.jolt.ParticleEmitter { *; }
|
|
23
23
|
-keep class org.rafaelaugusto.jolt.ParticleEmitter$* { *; }
|
|
24
|
+
-keep class org.rafaelaugusto.jolt.ReorderSession { *; }
|
|
25
|
+
-keep class org.rafaelaugusto.jolt.ReorderSession$* { *; }
|
|
26
|
+
-keep class org.rafaelaugusto.jolt.ReorderHandle { *; }
|
|
27
|
+
-keep class org.rafaelaugusto.jolt.ReorderHandle$* { *; }
|
|
28
|
+
-keep class org.rafaelaugusto.jolt.ReorderSurface { *; }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
#import "JoltLiveSpring.h"
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
@interface JoltGestureSession : NSObject
|
|
6
|
+
@property(nonatomic, readonly) BOOL active;
|
|
7
|
+
+ (BOOL)supportsContactEvents;
|
|
8
|
+
+ (instancetype)attach:(UIView *)surface scroll:(nullable UIScrollView *)scroll
|
|
9
|
+
x:(JoltLiveSpring *)x y:(JoltLiveSpring *)y scale:(JoltLiveSpring *)scale rotation:(JoltLiveSpring *)rotation
|
|
10
|
+
options:(NSString *)options onState:(void (^)(NSInteger, double, double))onState;
|
|
11
|
+
- (void)cancel;
|
|
12
|
+
- (void)refreshScrollDelegate;
|
|
13
|
+
- (void)setEnabled:(BOOL)enabled;
|
|
14
|
+
- (void)settleX:(double)x y:(double)y scale:(double)scale rotation:(double)rotation animated:(BOOL)animated;
|
|
15
|
+
- (void)updateLeft:(double)left right:(double)right top:(double)top bottom:(double)bottom;
|
|
16
|
+
- (void)dispose;
|
|
17
|
+
@end
|
|
18
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#import "JoltGestureSession.h"
|
|
2
|
+
#import <UIKit/UIGestureRecognizerSubclass.h>
|
|
3
|
+
|
|
4
|
+
static double clamp(double value, double minimum, double maximum) { return MAX(minimum, MIN(maximum, value)); }
|
|
5
|
+
@class JoltGestureSession;
|
|
6
|
+
|
|
7
|
+
/* Centroid tracking in window coordinates. Pointer membership changes rebase without movement. */
|
|
8
|
+
@interface JoltAxisPan : UIGestureRecognizer
|
|
9
|
+
@property(nonatomic, weak) JoltGestureSession *owner;
|
|
10
|
+
@property(nonatomic) NSMutableSet<UITouch *> *contacts;
|
|
11
|
+
@property(nonatomic) CGPoint previous, origin, delta, velocity;
|
|
12
|
+
@property(nonatomic) double previousTime, threshold, failOffset;
|
|
13
|
+
@property(nonatomic) NSInteger axis;
|
|
14
|
+
- (void)rebase;
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
@interface JoltGestureSession () <UIGestureRecognizerDelegate, UIScrollViewDelegate>
|
|
18
|
+
@property(nonatomic, weak) UIView *surface;
|
|
19
|
+
@property(nonatomic, weak) UIScrollView *scroll;
|
|
20
|
+
@property(nonatomic) id<UIScrollViewDelegate> previousDelegate;
|
|
21
|
+
@property(nonatomic) JoltAxisPan *pan;
|
|
22
|
+
@property(nonatomic) UIPinchGestureRecognizer *pinch;
|
|
23
|
+
@property(nonatomic) UIRotationGestureRecognizer *rotate;
|
|
24
|
+
@property(nonatomic) UITapGestureRecognizer *tap, *doubleTap;
|
|
25
|
+
@property(nonatomic) JoltLiveSpring *x, *y, *scale, *rotation;
|
|
26
|
+
@property(nonatomic) double minX, maxX, minY, maxY, minScale, maxScale, velocityX, velocityY, scrollPosition;
|
|
27
|
+
@property(nonatomic, readwrite) BOOL active;
|
|
28
|
+
@property(nonatomic) BOOL enabled, disposed, contactEvents, contact, fromScroll, adjusting, ending;
|
|
29
|
+
@property(nonatomic, copy) void (^onState)(NSInteger, double, double);
|
|
30
|
+
- (void)touchStarted:(UITouch *)touch;
|
|
31
|
+
- (void)finish:(BOOL)cancelled;
|
|
32
|
+
- (void)applyScroll;
|
|
33
|
+
@end
|
|
34
|
+
|
|
35
|
+
@implementation JoltAxisPan
|
|
36
|
+
- (instancetype)initWithTarget:(id)target action:(SEL)action {
|
|
37
|
+
if ((self = [super initWithTarget:target action:action])) self.contacts = [NSMutableSet new];
|
|
38
|
+
return self;
|
|
39
|
+
}
|
|
40
|
+
- (CGPoint)centroid {
|
|
41
|
+
CGPoint point = CGPointZero;
|
|
42
|
+
for (UITouch *touch in self.contacts) { CGPoint p = [touch locationInView:nil]; point.x += p.x; point.y += p.y; }
|
|
43
|
+
if (self.contacts.count) { point.x /= self.contacts.count; point.y /= self.contacts.count; }
|
|
44
|
+
return point;
|
|
45
|
+
}
|
|
46
|
+
- (void)rebase { self.previous = [self centroid]; self.previousTime = CACurrentMediaTime(); self.delta = CGPointZero; self.velocity = CGPointZero; }
|
|
47
|
+
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
48
|
+
BOOL first = !self.contacts.count;
|
|
49
|
+
[self.contacts unionSet:touches]; [self rebase];
|
|
50
|
+
if (self.state == UIGestureRecognizerStatePossible) self.origin = self.previous;
|
|
51
|
+
if (first) { self.origin = self.previous; [self.owner touchStarted:touches.anyObject]; }
|
|
52
|
+
}
|
|
53
|
+
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
54
|
+
CGPoint point = [self centroid]; double now = CACurrentMediaTime(), dt = MAX(.001, now - self.previousTime);
|
|
55
|
+
self.delta = CGPointMake(point.x - self.previous.x, point.y - self.previous.y);
|
|
56
|
+
self.velocity = CGPointMake(self.delta.x / dt, self.delta.y / dt);
|
|
57
|
+
self.previous = point; self.previousTime = now;
|
|
58
|
+
if (self.state == UIGestureRecognizerStatePossible) {
|
|
59
|
+
double dx = fabs(point.x - self.origin.x), dy = fabs(point.y - self.origin.y);
|
|
60
|
+
if ((self.axis == 1 && dy > self.failOffset) || (self.axis == 2 && dx > self.failOffset)) { self.state = UIGestureRecognizerStateFailed; return; }
|
|
61
|
+
double distance = self.axis == 1 ? dx : self.axis == 2 ? dy : hypot(dx, dy);
|
|
62
|
+
if (self.axis && distance >= self.threshold) self.state = UIGestureRecognizerStateBegan;
|
|
63
|
+
} else if (self.state == UIGestureRecognizerStateBegan || self.state == UIGestureRecognizerStateChanged) self.state = UIGestureRecognizerStateChanged;
|
|
64
|
+
}
|
|
65
|
+
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
66
|
+
[self.contacts minusSet:touches];
|
|
67
|
+
if (self.contacts.count) [self rebase];
|
|
68
|
+
else {
|
|
69
|
+
if (CACurrentMediaTime() - self.previousTime > .08) self.velocity = CGPointZero;
|
|
70
|
+
self.owner.velocityX = (self.axis & 1) ? self.velocity.x : 0; self.owner.velocityY = (self.axis & 2) ? self.velocity.y : 0;
|
|
71
|
+
self.state = self.state == UIGestureRecognizerStatePossible ? UIGestureRecognizerStateFailed : UIGestureRecognizerStateEnded;
|
|
72
|
+
[self.owner finish:NO];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.state = UIGestureRecognizerStateCancelled; [self.owner finish:YES]; }
|
|
76
|
+
- (void)reset { [super reset]; [self.contacts removeAllObjects]; self.delta = CGPointZero; self.velocity = CGPointZero; }
|
|
77
|
+
@end
|
|
78
|
+
|
|
79
|
+
@implementation JoltGestureSession
|
|
80
|
+
+ (BOOL)supportsContactEvents { return YES; }
|
|
81
|
+
+ (instancetype)attach:(UIView *)surface scroll:(UIScrollView *)scroll x:(JoltLiveSpring *)x y:(JoltLiveSpring *)y scale:(JoltLiveSpring *)scale rotation:(JoltLiveSpring *)rotation options:(NSString *)options onState:(void (^)(NSInteger,double,double))onState {
|
|
82
|
+
NSDictionary *config = [NSJSONSerialization JSONObjectWithData:[options dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
|
|
83
|
+
JoltGestureSession *s = [self new]; s.surface = surface; s.scroll = scroll; s.x = x; s.y = y; s.scale = scale; s.rotation = rotation;
|
|
84
|
+
s.minX = [config[@"minX"] doubleValue]; s.maxX = [config[@"maxX"] doubleValue]; s.minY = [config[@"minY"] doubleValue]; s.maxY = [config[@"maxY"] doubleValue];
|
|
85
|
+
s.minScale = [config[@"minScale"] doubleValue]; s.maxScale = [config[@"maxScale"] doubleValue]; s.onState = onState; s.enabled = YES;
|
|
86
|
+
s.pan = [[JoltAxisPan alloc] initWithTarget:s action:@selector(panned:)]; s.pan.owner = s;
|
|
87
|
+
s.pan.axis = [config[@"axis"] integerValue]; s.pan.threshold = [config[@"threshold"] doubleValue]; s.pan.failOffset = [config[@"failOffset"] doubleValue];
|
|
88
|
+
s.pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:s action:@selector(pinched:)];
|
|
89
|
+
s.rotate = [[UIRotationGestureRecognizer alloc] initWithTarget:s action:@selector(rotated:)];
|
|
90
|
+
s.tap = [[UITapGestureRecognizer alloc] initWithTarget:s action:@selector(tapped:)];
|
|
91
|
+
s.doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:s action:@selector(tapped:)]; s.doubleTap.numberOfTapsRequired = 2;
|
|
92
|
+
[s.tap requireGestureRecognizerToFail:s.doubleTap];
|
|
93
|
+
for (UIGestureRecognizer *g in @[s.pan, s.pinch, s.rotate, s.tap, s.doubleTap]) { g.delegate = s; [surface addGestureRecognizer:g]; }
|
|
94
|
+
s.pinch.enabled = [config[@"pinch"] boolValue]; s.rotate.enabled = [config[@"rotate"] boolValue];
|
|
95
|
+
s.contactEvents = [config[@"contactEvents"] boolValue];
|
|
96
|
+
s.tap.enabled = [config[@"tap"] boolValue]; s.doubleTap.enabled = [config[@"doubleTap"] boolValue];
|
|
97
|
+
// NativeScript's scroll delegate is forwarded, never replaced with a JS per-frame bridge.
|
|
98
|
+
if (scroll) { s.previousDelegate = scroll.delegate; scroll.delegate = s; }
|
|
99
|
+
return s;
|
|
100
|
+
}
|
|
101
|
+
- (BOOL)respondsToSelector:(SEL)selector { return [super respondsToSelector:selector] || [self.previousDelegate respondsToSelector:selector]; }
|
|
102
|
+
- (void)refreshScrollDelegate {
|
|
103
|
+
if (!self.disposed && self.scroll && self.scroll.delegate != self) {
|
|
104
|
+
self.previousDelegate = self.scroll.delegate; self.scroll.delegate = self;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
- (id)forwardingTargetForSelector:(SEL)selector { return [self.previousDelegate respondsToSelector:selector] ? self.previousDelegate : [super forwardingTargetForSelector:selector]; }
|
|
108
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)first shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)second {
|
|
109
|
+
if ([first isKindOfClass:UITapGestureRecognizer.class] || [second isKindOfClass:UITapGestureRecognizer.class]) return NO;
|
|
110
|
+
return YES;
|
|
111
|
+
}
|
|
112
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gesture shouldReceiveTouch:(UITouch *)touch {
|
|
113
|
+
return self.enabled && !self.disposed;
|
|
114
|
+
}
|
|
115
|
+
- (void)touchStarted:(UITouch *)touch {
|
|
116
|
+
if (self.disposed || !self.enabled) return;
|
|
117
|
+
self.contact = YES; self.fromScroll = self.scroll && [touch.view isDescendantOfView:self.scroll];
|
|
118
|
+
self.scrollPosition = MAX(-self.scroll.adjustedContentInset.top, self.scroll.contentOffset.y);
|
|
119
|
+
self.velocityX = self.velocityY = 0;
|
|
120
|
+
for (JoltLiveSpring *node in @[self.x, self.y, self.scale, self.rotation]) [node releaseInput:node.position velocity:0];
|
|
121
|
+
if (self.contactEvents) [self emit:6];
|
|
122
|
+
}
|
|
123
|
+
- (void)emit:(NSInteger)state { if (!self.disposed && self.onState) self.onState(state, self.velocityX, self.velocityY); }
|
|
124
|
+
- (void)begin {
|
|
125
|
+
if (self.active || self.disposed || !self.enabled) return;
|
|
126
|
+
self.active = YES;
|
|
127
|
+
for (JoltLiveSpring *node in @[self.x, self.y, self.scale, self.rotation]) [node holdInput:node.position velocity:0];
|
|
128
|
+
[self emit:1];
|
|
129
|
+
}
|
|
130
|
+
- (void)panned:(JoltAxisPan *)pan {
|
|
131
|
+
if (self.disposed || !self.enabled) return;
|
|
132
|
+
if (pan.state == UIGestureRecognizerStateBegan || pan.state == UIGestureRecognizerStateChanged) {
|
|
133
|
+
[self begin]; if (self.disposed || !self.enabled || !self.active) return;
|
|
134
|
+
self.velocityX = (pan.axis & 1) ? pan.velocity.x : 0; self.velocityY = (pan.axis & 2) ? pan.velocity.y : 0;
|
|
135
|
+
double dx = pan.delta.x, dy = pan.delta.y;
|
|
136
|
+
if (self.fromScroll) {
|
|
137
|
+
double oldY = self.y.position, nextY = oldY, top = -self.scroll.adjustedContentInset.top;
|
|
138
|
+
if (dy < 0) { nextY = MAX(self.minY, oldY + dy); dy -= nextY - oldY; }
|
|
139
|
+
else {
|
|
140
|
+
double consumed = MIN(dy, MAX(0, self.scrollPosition - top));
|
|
141
|
+
self.scrollPosition -= consumed; dy -= consumed;
|
|
142
|
+
nextY = MIN(self.maxY, oldY + dy); dy -= nextY - oldY;
|
|
143
|
+
}
|
|
144
|
+
double bottom = MAX(top, self.scroll.contentSize.height - self.scroll.bounds.size.height + self.scroll.adjustedContentInset.bottom);
|
|
145
|
+
self.scrollPosition = clamp(self.scrollPosition - dy, top, bottom);
|
|
146
|
+
[self.y holdInput:nextY velocity:self.velocityY]; [self applyScroll];
|
|
147
|
+
} else {
|
|
148
|
+
if (pan.axis & 1) [self.x holdInput:clamp(self.x.position + dx, self.minX, self.maxX) velocity:self.velocityX];
|
|
149
|
+
if (pan.axis & 2) [self.y holdInput:clamp(self.y.position + dy, self.minY, self.maxY) velocity:self.velocityY];
|
|
150
|
+
}
|
|
151
|
+
} else if (pan.state == UIGestureRecognizerStateEnded) [self finish:NO];
|
|
152
|
+
else if (pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateFailed) [self finish:YES];
|
|
153
|
+
}
|
|
154
|
+
- (void)pinched:(UIPinchGestureRecognizer *)gesture {
|
|
155
|
+
if (self.fromScroll || self.disposed || !self.enabled) return;
|
|
156
|
+
if (gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged) {
|
|
157
|
+
[self begin]; if (self.disposed || !self.active) return;
|
|
158
|
+
[self.scale holdInput:clamp(self.scale.position * gesture.scale, self.minScale, self.maxScale) velocity:0]; gesture.scale = 1;
|
|
159
|
+
} else if (!self.pan.contacts.count) [self finish:gesture.state == UIGestureRecognizerStateCancelled];
|
|
160
|
+
}
|
|
161
|
+
- (void)rotated:(UIRotationGestureRecognizer *)gesture {
|
|
162
|
+
if (self.fromScroll || self.disposed || !self.enabled) return;
|
|
163
|
+
if (gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged) {
|
|
164
|
+
[self begin]; if (self.disposed || !self.active) return;
|
|
165
|
+
[self.rotation holdInput:self.rotation.position + gesture.rotation * 180 / M_PI velocity:0]; gesture.rotation = 0;
|
|
166
|
+
} else if (!self.pan.contacts.count) [self finish:gesture.state == UIGestureRecognizerStateCancelled];
|
|
167
|
+
}
|
|
168
|
+
- (void)tapped:(UITapGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateRecognized) [self emit:gesture == self.doubleTap ? 5 : 4]; }
|
|
169
|
+
- (void)applyScroll {
|
|
170
|
+
if (!self.scroll || self.adjusting) return;
|
|
171
|
+
self.adjusting = YES;
|
|
172
|
+
[self.scroll setContentOffset:CGPointMake(self.scroll.contentOffset.x, self.scrollPosition) animated:NO];
|
|
173
|
+
self.adjusting = NO;
|
|
174
|
+
}
|
|
175
|
+
- (void)scrollViewDidScroll:(UIScrollView *)scroll {
|
|
176
|
+
if (self.contact && self.fromScroll && !self.adjusting) [self applyScroll];
|
|
177
|
+
if (!self.adjusting && [self.previousDelegate respondsToSelector:_cmd]) [self.previousDelegate scrollViewDidScroll:scroll];
|
|
178
|
+
}
|
|
179
|
+
- (void)scrollViewWillEndDragging:(UIScrollView *)scroll withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)target {
|
|
180
|
+
if (self.y.position > self.minY + .01) target->y = scroll.contentOffset.y;
|
|
181
|
+
if ([self.previousDelegate respondsToSelector:_cmd]) [self.previousDelegate scrollViewWillEndDragging:scroll withVelocity:velocity targetContentOffset:target];
|
|
182
|
+
}
|
|
183
|
+
- (void)finish:(BOOL)cancelled {
|
|
184
|
+
BOOL hadContact = self.contact;
|
|
185
|
+
self.contact = NO;
|
|
186
|
+
if (!self.active) { if (hadContact && self.contactEvents) [self emit:7]; return; }
|
|
187
|
+
self.active = NO;
|
|
188
|
+
for (JoltLiveSpring *node in @[self.x, self.y, self.scale, self.rotation]) [node releaseInput:node.position velocity:0];
|
|
189
|
+
self.ending = !cancelled;
|
|
190
|
+
@try { [self emit:cancelled ? 3 : 2]; } @finally { self.ending = NO; }
|
|
191
|
+
if (hadContact && self.contactEvents) [self emit:7];
|
|
192
|
+
}
|
|
193
|
+
- (void)cancel {
|
|
194
|
+
if (self.active) [self finish:YES];
|
|
195
|
+
else {
|
|
196
|
+
[self finish:YES];
|
|
197
|
+
for (JoltLiveSpring *node in @[self.x, self.y, self.scale, self.rotation]) [node releaseInput:node.position velocity:0];
|
|
198
|
+
}
|
|
199
|
+
for (UIGestureRecognizer *g in @[self.pan, self.pinch, self.rotate, self.tap, self.doubleTap]) if (g.enabled) { g.enabled = NO; g.enabled = YES; }
|
|
200
|
+
}
|
|
201
|
+
- (void)setEnabled:(BOOL)value { _enabled = value; if (!value) [self cancel]; }
|
|
202
|
+
- (void)settleX:(double)x y:(double)y scale:(double)scale rotation:(double)rotation animated:(BOOL)animated {
|
|
203
|
+
if (self.disposed) return;
|
|
204
|
+
double vx = self.ending ? self.velocityX : self.x.velocity, vy = self.ending ? self.velocityY : self.y.velocity;
|
|
205
|
+
[self cancel];
|
|
206
|
+
double targets[] = {clamp(x, self.minX, self.maxX), clamp(y, self.minY, self.maxY), clamp(scale, self.minScale, self.maxScale), rotation};
|
|
207
|
+
NSArray *nodes = @[self.x, self.y, self.scale, self.rotation];
|
|
208
|
+
for (NSUInteger i = 0; i < nodes.count; i++) {
|
|
209
|
+
JoltLiveSpring *node = nodes[i];
|
|
210
|
+
if (!animated) [node holdInput:targets[i] velocity:0];
|
|
211
|
+
[node releaseInput:targets[i] velocity:animated ? i == 0 ? vx : i == 1 ? vy : 0 : 0];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
- (void)updateLeft:(double)left right:(double)right top:(double)top bottom:(double)bottom { self.minX = left; self.maxX = right; self.minY = top; self.maxY = bottom; }
|
|
215
|
+
- (void)dispose {
|
|
216
|
+
if (self.disposed) return;
|
|
217
|
+
[self cancel]; self.disposed = YES;
|
|
218
|
+
for (UIGestureRecognizer *g in @[self.pan, self.pinch, self.rotate, self.tap, self.doubleTap]) [self.surface removeGestureRecognizer:g];
|
|
219
|
+
if (self.scroll.delegate == self) self.scroll.delegate = self.previousDelegate;
|
|
220
|
+
self.previousDelegate = nil; self.onState = nil;
|
|
221
|
+
}
|
|
222
|
+
@end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
3
|
+
@class JoltLiveSpringBinding;
|
|
4
|
+
@interface JoltLiveSpring : NSObject
|
|
5
|
+
+ (instancetype)create:(double)initial stiffness:(double)stiffness damping:(double)damping mass:(double)mass
|
|
6
|
+
restDelta:(double)restDelta restSpeed:(double)restSpeed reduced:(BOOL)reduced;
|
|
7
|
+
@property(nonatomic, readonly) double position;
|
|
8
|
+
@property(nonatomic, readonly) double velocity;
|
|
9
|
+
@property(nonatomic, readonly) BOOL moving;
|
|
10
|
+
+ (NSUInteger)activeCount;
|
|
11
|
+
+ (NSUInteger)nodeCount;
|
|
12
|
+
+ (void)sync:(UIView *)view;
|
|
13
|
+
- (void)follow:(JoltLiveSpring *)source mappings:(NSString *)mappings;
|
|
14
|
+
- (void)updateInput:(NSString *)snapshot;
|
|
15
|
+
- (void)setTarget:(double)value;
|
|
16
|
+
- (void)holdInput:(double)value velocity:(double)velocity;
|
|
17
|
+
- (void)releaseInput:(double)target velocity:(double)velocity;
|
|
18
|
+
- (void)setReduced:(BOOL)value;
|
|
19
|
+
- (void)setPaused:(BOOL)value;
|
|
20
|
+
- (JoltLiveSpringBinding *)bind:(UIView *)view property:(NSString *)property mappings:(NSString *)mappings;
|
|
21
|
+
- (void)dispose;
|
|
22
|
+
@end
|
|
23
|
+
@interface JoltLiveSpringBinding : NSObject
|
|
24
|
+
@property(nonatomic, readonly) double value;
|
|
25
|
+
- (void)setLayers:(NSString *)inputs;
|
|
26
|
+
- (void)cancel;
|
|
27
|
+
@end
|
|
28
|
+
NS_ASSUME_NONNULL_END
|