@rafaelaugusto/jolt 0.1.0-beta.4 → 0.1.0-beta.6

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 (201) hide show
  1. package/README.md +134 -4
  2. package/dist/animate.d.ts +7 -4
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +58 -13
  5. package/dist/animate.js.map +1 -1
  6. package/dist/config.d.ts +22 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +34 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/decoration.d.ts +18 -0
  11. package/dist/decoration.d.ts.map +1 -0
  12. package/dist/decoration.js +155 -0
  13. package/dist/decoration.js.map +1 -0
  14. package/dist/driver/android-color.d.ts +3 -0
  15. package/dist/driver/android-color.d.ts.map +1 -1
  16. package/dist/driver/android-color.js +20 -18
  17. package/dist/driver/android-color.js.map +1 -1
  18. package/dist/driver/android-decoration.d.ts +22 -0
  19. package/dist/driver/android-decoration.d.ts.map +1 -0
  20. package/dist/driver/android-decoration.js +139 -0
  21. package/dist/driver/android-decoration.js.map +1 -0
  22. package/dist/driver/android-playback.d.ts +13 -0
  23. package/dist/driver/android-playback.d.ts.map +1 -0
  24. package/dist/driver/android-playback.js +141 -0
  25. package/dist/driver/android-playback.js.map +1 -0
  26. package/dist/driver/android.d.ts.map +1 -1
  27. package/dist/driver/android.js +30 -36
  28. package/dist/driver/android.js.map +1 -1
  29. package/dist/driver/index.d.ts +9 -0
  30. package/dist/driver/index.d.ts.map +1 -1
  31. package/dist/driver/index.js.map +1 -1
  32. package/dist/driver/ios-color.d.ts +2 -0
  33. package/dist/driver/ios-color.d.ts.map +1 -1
  34. package/dist/driver/ios-color.js +19 -8
  35. package/dist/driver/ios-color.js.map +1 -1
  36. package/dist/driver/ios-decoration.d.ts +17 -0
  37. package/dist/driver/ios-decoration.d.ts.map +1 -0
  38. package/dist/driver/ios-decoration.js +271 -0
  39. package/dist/driver/ios-decoration.js.map +1 -0
  40. package/dist/driver/ios-playback.d.ts +10 -0
  41. package/dist/driver/ios-playback.d.ts.map +1 -0
  42. package/dist/driver/ios-playback.js +47 -0
  43. package/dist/driver/ios-playback.js.map +1 -0
  44. package/dist/driver/ios-shadow.d.ts +6 -0
  45. package/dist/driver/ios-shadow.d.ts.map +1 -0
  46. package/dist/driver/ios-shadow.js +52 -0
  47. package/dist/driver/ios-shadow.js.map +1 -0
  48. package/dist/driver/ios.d.ts.map +1 -1
  49. package/dist/driver/ios.js +76 -40
  50. package/dist/driver/ios.js.map +1 -1
  51. package/dist/engine.d.ts +64 -3
  52. package/dist/engine.d.ts.map +1 -1
  53. package/dist/engine.js +243 -19
  54. package/dist/engine.js.map +1 -1
  55. package/dist/haptics.d.ts +4 -1
  56. package/dist/haptics.d.ts.map +1 -1
  57. package/dist/haptics.js +27 -3
  58. package/dist/haptics.js.map +1 -1
  59. package/dist/index.d.ts +12 -1
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +5 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/items.d.ts +20 -0
  64. package/dist/items.d.ts.map +1 -0
  65. package/dist/items.js +109 -0
  66. package/dist/items.js.map +1 -0
  67. package/dist/keyboard.d.ts +17 -0
  68. package/dist/keyboard.d.ts.map +1 -0
  69. package/dist/keyboard.js +116 -0
  70. package/dist/keyboard.js.map +1 -0
  71. package/dist/label.d.ts +7 -0
  72. package/dist/label.d.ts.map +1 -0
  73. package/dist/label.js +60 -0
  74. package/dist/label.js.map +1 -0
  75. package/dist/liquid.d.ts.map +1 -1
  76. package/dist/liquid.js +2 -1
  77. package/dist/liquid.js.map +1 -1
  78. package/dist/loader.d.ts.map +1 -1
  79. package/dist/loader.js +2 -1
  80. package/dist/loader.js.map +1 -1
  81. package/dist/particles.d.ts.map +1 -1
  82. package/dist/particles.js +4 -3
  83. package/dist/particles.js.map +1 -1
  84. package/dist/path.d.ts +12 -0
  85. package/dist/path.d.ts.map +1 -0
  86. package/dist/path.js +90 -0
  87. package/dist/path.js.map +1 -0
  88. package/dist/playback-group.d.ts +6 -0
  89. package/dist/playback-group.d.ts.map +1 -0
  90. package/dist/playback-group.js +141 -0
  91. package/dist/playback-group.js.map +1 -0
  92. package/dist/playback.d.ts +25 -0
  93. package/dist/playback.d.ts.map +1 -0
  94. package/dist/playback.js +35 -0
  95. package/dist/playback.js.map +1 -0
  96. package/dist/press.d.ts.map +1 -1
  97. package/dist/press.js +3 -3
  98. package/dist/press.js.map +1 -1
  99. package/dist/properties.d.ts +2 -1
  100. package/dist/properties.d.ts.map +1 -1
  101. package/dist/properties.js +14 -1
  102. package/dist/properties.js.map +1 -1
  103. package/dist/reveal.d.ts.map +1 -1
  104. package/dist/reveal.js +3 -2
  105. package/dist/reveal.js.map +1 -1
  106. package/dist/roll-diff.d.ts +22 -0
  107. package/dist/roll-diff.d.ts.map +1 -0
  108. package/dist/roll-diff.js +36 -0
  109. package/dist/roll-diff.js.map +1 -0
  110. package/dist/roll.d.ts +29 -0
  111. package/dist/roll.d.ts.map +1 -0
  112. package/dist/roll.js +442 -0
  113. package/dist/roll.js.map +1 -0
  114. package/dist/scroll-linked.d.ts +10 -0
  115. package/dist/scroll-linked.d.ts.map +1 -0
  116. package/dist/scroll-linked.js +58 -0
  117. package/dist/scroll-linked.js.map +1 -0
  118. package/dist/sequence.d.ts +10 -5
  119. package/dist/sequence.d.ts.map +1 -1
  120. package/dist/sequence.js +208 -14
  121. package/dist/sequence.js.map +1 -1
  122. package/dist/shimmer.d.ts.map +1 -1
  123. package/dist/shimmer.js +2 -1
  124. package/dist/shimmer.js.map +1 -1
  125. package/dist/svg-path.d.ts +14 -0
  126. package/dist/svg-path.d.ts.map +1 -0
  127. package/dist/svg-path.js +281 -0
  128. package/dist/svg-path.js.map +1 -0
  129. package/dist/symbol-effect.d.ts.map +1 -1
  130. package/dist/symbol-effect.js +4 -3
  131. package/dist/symbol-effect.js.map +1 -1
  132. package/dist/text-layout.d.ts +8 -0
  133. package/dist/text-layout.d.ts.map +1 -1
  134. package/dist/text-layout.js +8 -2
  135. package/dist/text-layout.js.map +1 -1
  136. package/dist/text.d.ts +1 -1
  137. package/dist/text.d.ts.map +1 -1
  138. package/dist/text.js +60 -64
  139. package/dist/text.js.map +1 -1
  140. package/dist/track.d.ts +2 -0
  141. package/dist/track.d.ts.map +1 -1
  142. package/dist/track.js +72 -16
  143. package/dist/track.js.map +1 -1
  144. package/dist/transition.d.ts.map +1 -1
  145. package/dist/transition.js +4 -1
  146. package/dist/transition.js.map +1 -1
  147. package/dist/types.d.ts +22 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/dist/vue/index.d.ts +89 -0
  150. package/dist/vue/index.d.ts.map +1 -0
  151. package/dist/vue/index.js +134 -0
  152. package/dist/vue/index.js.map +1 -0
  153. package/package.json +15 -3
  154. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +2 -2
  155. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  156. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  157. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  158. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  159. package/platforms/android/proguard-rules.pro +7 -0
  160. package/src/android-plugin.d.ts +64 -0
  161. package/src/animate.ts +96 -33
  162. package/src/config.ts +46 -0
  163. package/src/decoration.ts +180 -0
  164. package/src/driver/android-color.ts +22 -20
  165. package/src/driver/android-decoration.ts +182 -0
  166. package/src/driver/android-playback.ts +157 -0
  167. package/src/driver/android.ts +57 -43
  168. package/src/driver/index.ts +15 -0
  169. package/src/driver/ios-color.ts +19 -10
  170. package/src/driver/ios-decoration.ts +332 -0
  171. package/src/driver/ios-playback.ts +51 -0
  172. package/src/driver/ios-shadow.ts +50 -0
  173. package/src/driver/ios.ts +87 -46
  174. package/src/engine.ts +267 -22
  175. package/src/haptics.ts +25 -2
  176. package/src/index.ts +12 -1
  177. package/src/items.ts +140 -0
  178. package/src/keyboard.ts +132 -0
  179. package/src/label.ts +76 -0
  180. package/src/liquid.ts +2 -1
  181. package/src/loader.ts +2 -1
  182. package/src/particles.ts +4 -4
  183. package/src/path.ts +124 -0
  184. package/src/playback-group.ts +143 -0
  185. package/src/playback.ts +62 -0
  186. package/src/press.ts +3 -5
  187. package/src/properties.ts +18 -2
  188. package/src/reveal.ts +3 -3
  189. package/src/roll-diff.ts +57 -0
  190. package/src/roll.ts +515 -0
  191. package/src/scroll-linked.ts +73 -0
  192. package/src/sequence.ts +204 -14
  193. package/src/shimmer.ts +2 -1
  194. package/src/svg-path.ts +355 -0
  195. package/src/symbol-effect.ts +4 -3
  196. package/src/text-layout.ts +14 -2
  197. package/src/text.ts +62 -81
  198. package/src/track.ts +98 -25
  199. package/src/transition.ts +4 -1
  200. package/src/types.ts +23 -1
  201. package/src/vue/index.ts +189 -0
@@ -0,0 +1,190 @@
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.Canvas;
7
+ import android.graphics.Paint;
8
+ import android.text.SpannableStringBuilder;
9
+ import android.text.Spanned;
10
+ import android.text.method.TransformationMethod;
11
+ import android.text.style.ReplacementSpan;
12
+ import android.view.animation.LinearInterpolator;
13
+ import android.view.animation.PathInterpolator;
14
+ import android.widget.TextView;
15
+
16
+ /** Rolls the changed positions of a TextView with one native animator. */
17
+ public final class NumberRoll {
18
+ public interface Callback {
19
+ void onEnd(boolean finished);
20
+ }
21
+
22
+ private static final int MAX_POSITIONS = 256;
23
+ private final TextView view;
24
+ private final CharSequence original;
25
+ private final CharSequence shown;
26
+ private final TransformationMethod method;
27
+ private final DigitSpan[] spans;
28
+ private final int[] starts;
29
+ private final int[] ends;
30
+ private final float[] times;
31
+ private final float[] values;
32
+ private final long duration;
33
+ private final long delay;
34
+ private final long stagger;
35
+ private final long fade;
36
+ private final boolean tabular;
37
+ private final Callback callback;
38
+ private final PathInterpolator ease = new PathInterpolator(0f, 0f, 0.58f, 1f);
39
+ private ValueAnimator animator;
40
+ private CharSequence current;
41
+ private boolean removedMethod;
42
+ private boolean ended;
43
+
44
+ // Positions are ordered from the end of the text; each one starts `stagger` after the previous.
45
+ public NumberRoll(TextView view, int[] starts, int[] ends, String[] previous, int direction,
46
+ float[] times, float[] values, long duration, long delay, long stagger, float shift,
47
+ long fade, boolean tabular, Callback callback) {
48
+ this.view = view;
49
+ this.original = view.getText();
50
+ // NativeScript installs a TransformationMethod that rebuilds the displayed text from its
51
+ // own model, discarding spans set from here. Work on what is displayed and remove it while rolling.
52
+ this.method = view.getTransformationMethod();
53
+ CharSequence displayed = method != null && original != null ? method.getTransformation(original, view) : original;
54
+ this.shown = displayed != null ? displayed : "";
55
+ this.starts = starts;
56
+ this.ends = ends;
57
+ this.times = times;
58
+ this.values = values;
59
+ this.duration = duration;
60
+ this.delay = delay;
61
+ this.stagger = stagger;
62
+ this.fade = fade;
63
+ this.tabular = tabular;
64
+ this.callback = callback;
65
+ spans = new DigitSpan[starts.length];
66
+ for (int i = 0; i < starts.length; i++) {
67
+ String old = i < previous.length && previous[i] != null ? previous[i] : "";
68
+ spans[i] = new DigitSpan(old, direction, shift, values.length > 0 ? values[0] : shift);
69
+ }
70
+ }
71
+
72
+ public boolean isSupported() {
73
+ if (original == null || starts.length == 0 || starts.length > MAX_POSITIONS) return false;
74
+ if (ends.length != starts.length || times.length < 2 || times.length != values.length) return false;
75
+ int length = shown.length();
76
+ for (int i = 0; i < starts.length; i++) {
77
+ if (starts[i] < 0 || ends[i] > length || starts[i] >= ends[i]) return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ public void start() {
83
+ if (ended || animator != null) return;
84
+ if (tabular) view.setFontFeatureSettings("tnum");
85
+ SpannableStringBuilder spannable = new SpannableStringBuilder(shown);
86
+ for (int i = 0; i < spans.length; i++) {
87
+ spannable.setSpan(spans[i], starts[i], ends[i], Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
88
+ }
89
+ if (method != null) {
90
+ removedMethod = true;
91
+ view.setTransformationMethod(null);
92
+ }
93
+ view.setText(spannable);
94
+ current = view.getText();
95
+ final long total = delay + duration + stagger * (spans.length - 1);
96
+ animator = ValueAnimator.ofFloat(0f, 1f);
97
+ animator.setDuration(Math.max(total, 1));
98
+ animator.setInterpolator(new LinearInterpolator());
99
+ animator.addUpdateListener(animation -> update(animation.getAnimatedFraction() * total));
100
+ animator.addListener(new AnimatorListenerAdapter() {
101
+ @Override
102
+ public void onAnimationEnd(Animator animation) {
103
+ finish(true);
104
+ }
105
+ });
106
+ animator.start();
107
+ }
108
+
109
+ public void cancel() {
110
+ finish(false);
111
+ if (animator != null) animator.cancel();
112
+ }
113
+
114
+ private void update(float elapsed) {
115
+ for (int i = 0; i < spans.length; i++) {
116
+ float local = elapsed - delay - stagger * i;
117
+ spans[i].set(sample(local), alpha(local));
118
+ }
119
+ view.invalidate();
120
+ }
121
+
122
+ private float sample(float local) {
123
+ if (local <= 0) return values[0];
124
+ if (local >= duration) return values[values.length - 1];
125
+ float t = local / duration;
126
+ int i = 1;
127
+ while (i < times.length - 1 && times[i] < t) i++;
128
+ float span = times[i] - times[i - 1];
129
+ float fraction = span > 0 ? (t - times[i - 1]) / span : 1f;
130
+ return values[i - 1] + (values[i] - values[i - 1]) * fraction;
131
+ }
132
+
133
+ private float alpha(float local) {
134
+ if (local <= 0) return 0f;
135
+ if (fade <= 0 || local >= fade) return 1f;
136
+ return ease.getInterpolation(local / fade);
137
+ }
138
+
139
+ private void finish(boolean finished) {
140
+ if (ended) return;
141
+ ended = true;
142
+ if (current != null && view.getText() == current) view.setText(original);
143
+ if (removedMethod) view.setTransformationMethod(method);
144
+ callback.onEnd(finished);
145
+ }
146
+
147
+ /** Draws the new glyphs sliding in over the old ones sliding out, clipped to the line. */
148
+ static final class DigitSpan extends ReplacementSpan {
149
+ private final String previous;
150
+ private final int direction;
151
+ private final float shift;
152
+ private float offset;
153
+ private float alpha;
154
+
155
+ DigitSpan(String previous, int direction, float shift, float offset) {
156
+ this.previous = previous;
157
+ this.direction = direction;
158
+ this.shift = shift;
159
+ this.offset = offset;
160
+ }
161
+
162
+ void set(float offset, float alpha) {
163
+ this.offset = offset;
164
+ this.alpha = alpha;
165
+ }
166
+
167
+ @Override
168
+ public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
169
+ if (fm != null) paint.getFontMetricsInt(fm);
170
+ return Math.round(Math.max(paint.measureText(text, start, end), paint.measureText(previous)));
171
+ }
172
+
173
+ @Override
174
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
175
+ int bottom, Paint paint) {
176
+ int base = paint.getAlpha();
177
+ float width = getSize(paint, text, start, end, null);
178
+ canvas.save();
179
+ canvas.clipRect(x, top, x + width, bottom);
180
+ paint.setAlpha(Math.round(base * alpha));
181
+ canvas.drawText(text, start, end, x, y + direction * offset, paint);
182
+ if (!previous.isEmpty()) {
183
+ paint.setAlpha(Math.round(base * (1f - alpha)));
184
+ canvas.drawText(previous, x, y + direction * (offset - shift), paint);
185
+ }
186
+ paint.setAlpha(base);
187
+ canvas.restore();
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,180 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.animation.Animator;
4
+ import android.animation.AnimatorListenerAdapter;
5
+ import android.animation.ValueAnimator;
6
+ import android.text.SpannableStringBuilder;
7
+ import android.text.Spanned;
8
+ import android.text.TextPaint;
9
+ import android.text.method.TransformationMethod;
10
+ import android.text.style.CharacterStyle;
11
+ import android.text.style.UpdateAppearance;
12
+ import android.view.animation.Interpolator;
13
+ import android.view.animation.LinearInterpolator;
14
+ import android.view.animation.PathInterpolator;
15
+ import android.widget.TextView;
16
+ import java.text.BreakIterator;
17
+ import java.util.ArrayList;
18
+ import java.util.List;
19
+
20
+ /** Fades a TextView's text in piece by piece. Layout, truncation and the real text stay as they are. */
21
+ public final class TextReveal {
22
+ public interface Callback {
23
+ void onEnd(boolean finished);
24
+ }
25
+
26
+ private static final int MAX_PIECES = 256;
27
+ private static final Interpolator EASE_OUT = new PathInterpolator(0f, 0f, 0.58f, 1f);
28
+ private final TextView view;
29
+ private final long duration;
30
+ private final long stagger;
31
+ private final long delay;
32
+ private final float offset;
33
+ private final Callback callback;
34
+ private final CharSequence original;
35
+ private final CharSequence shown;
36
+ private final TransformationMethod method;
37
+ private final Piece[] pieces;
38
+ private ValueAnimator animator;
39
+ private boolean settled;
40
+
41
+ public TextReveal(TextView view, boolean byWord, long duration, long stagger, long delay, float offset, Callback callback) {
42
+ this.view = view;
43
+ this.duration = Math.max(duration, 1);
44
+ this.stagger = stagger;
45
+ this.delay = delay;
46
+ this.offset = offset;
47
+ this.callback = callback;
48
+ CharSequence text = view.getText();
49
+ this.original = text == null ? "" : text;
50
+ // NativeScript's transformation method rebuilds the displayed text from its own model,
51
+ // so pieces are measured on what is drawn and the spans go on that text.
52
+ this.method = view.getTransformationMethod();
53
+ CharSequence displayed = method != null ? method.getTransformation(original, view) : original;
54
+ this.shown = displayed == null ? "" : displayed;
55
+ this.pieces = split(shown.toString(), byWord);
56
+ }
57
+
58
+ public boolean isSupported() {
59
+ return pieces.length <= MAX_PIECES;
60
+ }
61
+
62
+ public void start() {
63
+ if (settled || animator != null) return;
64
+ if (pieces.length == 0) {
65
+ settle(true);
66
+ return;
67
+ }
68
+ SpannableStringBuilder spannable = new SpannableStringBuilder(shown);
69
+ for (Piece piece : pieces) {
70
+ piece.alpha = 0f;
71
+ piece.shift = offset;
72
+ spannable.setSpan(piece, piece.start, piece.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
73
+ }
74
+ if (method != null) view.setTransformationMethod(null);
75
+ view.setText(spannable);
76
+ final long total = delay + duration + (pieces.length - 1) * stagger;
77
+ animator = ValueAnimator.ofFloat(0f, 1f);
78
+ animator.setDuration(total);
79
+ animator.setInterpolator(new LinearInterpolator());
80
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
81
+ @Override
82
+ public void onAnimationUpdate(ValueAnimator animation) {
83
+ update(animation.getAnimatedFraction() * total);
84
+ view.invalidate();
85
+ }
86
+ });
87
+ animator.addListener(new AnimatorListenerAdapter() {
88
+ @Override
89
+ public void onAnimationEnd(Animator animation) {
90
+ settle(true);
91
+ }
92
+ });
93
+ animator.start();
94
+ }
95
+
96
+ public void cancel() {
97
+ settle(false);
98
+ }
99
+
100
+ private void update(float time) {
101
+ for (int i = 0; i < pieces.length; i++) {
102
+ float local = (time - delay - i * stagger) / duration;
103
+ float eased = local <= 0f ? 0f : local >= 1f ? 1f : EASE_OUT.getInterpolation(local);
104
+ pieces[i].alpha = eased;
105
+ pieces[i].shift = offset * (1f - eased);
106
+ }
107
+ }
108
+
109
+ private void settle(boolean finished) {
110
+ if (settled) return;
111
+ settled = true;
112
+ ValueAnimator running = animator;
113
+ animator = null;
114
+ if (running != null) running.cancel();
115
+ CharSequence current = view.getText();
116
+ // An app change already replaced the spans; its text wins.
117
+ if (current instanceof Spanned && ((Spanned) current).getSpans(0, current.length(), Piece.class).length > 0) {
118
+ view.setText(original);
119
+ }
120
+ if (method != null && view.getTransformationMethod() == null) view.setTransformationMethod(method);
121
+ callback.onEnd(finished);
122
+ }
123
+
124
+ private static Piece[] split(String text, boolean byWord) {
125
+ List<Piece> pieces = new ArrayList<>();
126
+ BreakIterator iterator = byWord ? BreakIterator.getWordInstance() : BreakIterator.getCharacterInstance();
127
+ iterator.setText(text);
128
+ int start = iterator.first();
129
+ for (int end = iterator.next(); end != BreakIterator.DONE; start = end, end = iterator.next()) {
130
+ if (byWord ? isWord(text, start, end) : !isBlank(text, start, end)) pieces.add(new Piece(start, end));
131
+ }
132
+ if (byWord) {
133
+ // Spaces and punctuation stay with the word before them, so every visible mark is revealed.
134
+ for (int i = 0; i < pieces.size(); i++) {
135
+ Piece piece = pieces.get(i);
136
+ if (i == 0) piece.start = 0;
137
+ piece.end = i + 1 < pieces.size() ? pieces.get(i + 1).start : text.length();
138
+ }
139
+ if (pieces.isEmpty() && !isBlank(text, 0, text.length())) pieces.add(new Piece(0, text.length()));
140
+ }
141
+ return pieces.toArray(new Piece[0]);
142
+ }
143
+
144
+ private static boolean isWord(String text, int start, int end) {
145
+ for (int i = start; i < end; ) {
146
+ int point = text.codePointAt(i);
147
+ if (Character.isLetterOrDigit(point)) return true;
148
+ i += Character.charCount(point);
149
+ }
150
+ return false;
151
+ }
152
+
153
+ private static boolean isBlank(String text, int start, int end) {
154
+ for (int i = start; i < end; ) {
155
+ int point = text.codePointAt(i);
156
+ if (!Character.isWhitespace(point) && !Character.isSpaceChar(point)) return false;
157
+ i += Character.charCount(point);
158
+ }
159
+ return true;
160
+ }
161
+
162
+ /** Only changes how its range is drawn, so the TextView's layout never sees it. */
163
+ private static final class Piece extends CharacterStyle implements UpdateAppearance {
164
+ int start;
165
+ int end;
166
+ float alpha;
167
+ float shift;
168
+
169
+ Piece(int start, int end) {
170
+ this.start = start;
171
+ this.end = end;
172
+ }
173
+
174
+ @Override
175
+ public void updateDrawState(TextPaint paint) {
176
+ paint.setAlpha(Math.round(paint.getAlpha() * alpha));
177
+ paint.baselineShift += Math.round(shift);
178
+ }
179
+ }
180
+ }
@@ -4,3 +4,10 @@
4
4
  -keepclassmembers class org.nativescript.widgets.BoxShadowDrawable {
5
5
  public android.graphics.drawable.Drawable getWrappedDrawable();
6
6
  }
7
+ -keep class org.rafaelaugusto.jolt.DecorationTarget { *; }
8
+ -keep class org.rafaelaugusto.jolt.KeyboardFollower { *; }
9
+ -keep class org.rafaelaugusto.jolt.KeyboardFollower$* { *; }
10
+ -keep class org.rafaelaugusto.jolt.TextReveal { *; }
11
+ -keep class org.rafaelaugusto.jolt.TextReveal$* { *; }
12
+ -keep class org.rafaelaugusto.jolt.NumberRoll { *; }
13
+ -keep class org.rafaelaugusto.jolt.NumberRoll$* { *; }
@@ -0,0 +1,64 @@
1
+ declare namespace org.rafaelaugusto.jolt {
2
+ class KeyboardFollower {
3
+ constructor(
4
+ view: android.view.View,
5
+ offset: number,
6
+ listener: KeyboardFollower.Listener,
7
+ )
8
+ attach(): void
9
+ detach(): void
10
+ }
11
+ namespace KeyboardFollower {
12
+ class Listener {
13
+ constructor(implementation: {
14
+ onStart(): void
15
+ onSettled(translation: number): void
16
+ })
17
+ }
18
+ }
19
+ class TextReveal {
20
+ constructor(
21
+ view: android.widget.TextView,
22
+ byWord: boolean,
23
+ duration: number,
24
+ stagger: number,
25
+ delay: number,
26
+ offset: number,
27
+ callback: TextReveal.Callback,
28
+ )
29
+ isSupported(): boolean
30
+ start(): void
31
+ cancel(): void
32
+ }
33
+ namespace TextReveal {
34
+ class Callback {
35
+ constructor(implementation: { onEnd(finished: boolean): void })
36
+ }
37
+ }
38
+ class NumberRoll {
39
+ constructor(
40
+ view: android.widget.TextView,
41
+ starts: androidNative.Array<number>,
42
+ ends: androidNative.Array<number>,
43
+ previous: androidNative.Array<string>,
44
+ direction: number,
45
+ times: androidNative.Array<number>,
46
+ values: androidNative.Array<number>,
47
+ duration: number,
48
+ delay: number,
49
+ stagger: number,
50
+ shift: number,
51
+ fade: number,
52
+ tabular: boolean,
53
+ callback: NumberRoll.Callback,
54
+ )
55
+ isSupported(): boolean
56
+ start(): void
57
+ cancel(): void
58
+ }
59
+ namespace NumberRoll {
60
+ class Callback {
61
+ constructor(implementation: { onEnd(finished: boolean): void })
62
+ }
63
+ }
64
+ }
package/src/animate.ts CHANGED
@@ -1,3 +1,11 @@
1
+ import {
2
+ isDecorationProperty,
3
+ validateDecorationTarget,
4
+ validateDecorationFrames,
5
+ } from './decoration.js'
6
+ import { playbackGroup } from './playback-group.js'
7
+ import type { PlaybackAnimation } from './playback.js'
8
+ import { preparePath, type PreparedPath } from './path.js'
1
9
  import { View } from '@nativescript/core'
2
10
  import {
3
11
  isColorProperty,
@@ -31,13 +39,14 @@ import type {
31
39
  DecayVelocity,
32
40
  Keyframes,
33
41
  KeyframeValue,
42
+ KeyframeProperty,
34
43
  Stagger,
35
44
  Target,
36
45
  } from './types.js'
37
46
 
38
47
  interface Options {
39
48
  base: Transition
40
- overrides: Partial<Record<keyof Keyframes, Transition>>
49
+ overrides: Partial<Record<KeyframeProperty | 'path', Transition>>
41
50
  stagger: number | Stagger | undefined
42
51
  relative: boolean | undefined
43
52
  haptic: Haptic | undefined
@@ -57,8 +66,8 @@ export function animate(
57
66
  target: Target | readonly Target[],
58
67
  animation: Keyframes | PresetName,
59
68
  options?: AnimateOptions | SpringName,
60
- ): Animation {
61
- const { keyframes, base, overrides, stagger, relative, haptic } = prepare(
69
+ ): PlaybackAnimation {
70
+ const { keyframes, base, overrides, stagger, relative, haptic, path } = prepare(
62
71
  animation,
63
72
  options,
64
73
  )
@@ -68,9 +77,10 @@ export function animate(
68
77
  const view = toView(target)
69
78
  for (const property of Object.keys(keyframes))
70
79
  if (isColorProperty(property)) validateColorTarget(view, property)
80
+ else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
71
81
  return new ViewAnimation(
72
82
  view,
73
- plan(keyframes, base, overrides, 0, relative),
83
+ plan(keyframes, base, overrides, 0, relative, path),
74
84
  feedback,
75
85
  )
76
86
  }
@@ -79,22 +89,20 @@ export function animate(
79
89
  for (const view of views)
80
90
  for (const property of Object.keys(keyframes))
81
91
  if (isColorProperty(property)) validateColorTarget(view, property)
82
- return group(
83
- views.map(
84
- (view, index) =>
85
- new ViewAnimation(
86
- view,
87
- plan(
88
- keyframes,
89
- base,
90
- overrides,
91
- staggerDelay(stagger, index, views.length),
92
- relative,
93
- ),
94
- feedback,
95
- ),
92
+ else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
93
+ const plans = views.map((_, index) =>
94
+ plan(
95
+ keyframes,
96
+ base,
97
+ overrides,
98
+ staggerDelay(stagger, index, views.length),
99
+ relative,
100
+ path,
96
101
  ),
97
102
  )
103
+ return playbackGroup(
104
+ views.map((view, index) => new ViewAnimation(view, plans[index]!, feedback, true)),
105
+ )
98
106
  }
99
107
 
100
108
  /** Starts at the current translation and slows down from a velocity in DIPs per second. */
@@ -102,7 +110,7 @@ export function decay(
102
110
  target: Target | readonly Target[],
103
111
  velocity: DecayVelocity,
104
112
  options: DecayOptions = {},
105
- ): Animation {
113
+ ): PlaybackAnimation {
106
114
  const friction = options.friction ?? DEFAULT_FRICTION
107
115
  if (!Number.isFinite(friction) || friction <= 0 || friction >= 1) {
108
116
  throw new RangeError('Decay friction must be between 0 and 1 exclusively.')
@@ -164,14 +172,16 @@ export function decay(
164
172
  ]
165
173
  })
166
174
  const views = toViews(target)
167
- if (plans.length === 0) return group([])
168
- return group(views.map((view) => new ViewAnimation(view, plans)))
175
+ if (plans.length === 0) return playbackGroup([])
176
+ return playbackGroup(
177
+ views.map((view) => new ViewAnimation(view, plans, undefined, true)),
178
+ )
169
179
  }
170
180
 
171
181
  /** Stops all or selected properties at their current values. `scale` stops both axes. */
172
182
  export function stop(
173
183
  target: Target | readonly Target[],
174
- properties?: readonly (keyof Keyframes)[],
184
+ properties?: readonly KeyframeProperty[],
175
185
  ) {
176
186
  const expanded = properties?.flatMap<Property>((property) =>
177
187
  property === 'scale' ? ['scaleX', 'scaleY'] : [property],
@@ -213,6 +223,17 @@ function isList<T>(target: T | readonly T[]): target is readonly T[] {
213
223
  function prepare(
214
224
  animation: Keyframes | PresetName,
215
225
  options?: AnimateOptions | SpringName,
226
+ ) {
227
+ const prepared = resolveAnimation(animation, options)
228
+ return {
229
+ ...prepared,
230
+ path: preparePath(prepared.keyframes, prepared.base, prepared.overrides),
231
+ }
232
+ }
233
+
234
+ function resolveAnimation(
235
+ animation: Keyframes | PresetName,
236
+ options?: AnimateOptions | SpringName,
216
237
  ) {
217
238
  const user = normalize(options)
218
239
  if (typeof animation !== 'string') return { keyframes: animation, ...user }
@@ -259,7 +280,7 @@ function normalize(options: AnimateOptions | SpringName | undefined): Options {
259
280
  if (key === 'stagger') stagger = value as Options['stagger']
260
281
  else if (key === 'relative') relative = value as Options['relative']
261
282
  else if (key === 'haptic') haptic = value as Options['haptic']
262
- else if (key === 'scale' || isProperty(key)) {
283
+ else if (key === 'path' || key === 'scale' || isProperty(key)) {
263
284
  overrides[key] = typeof value === 'string' ? spring(value as SpringName) : value
264
285
  } else base[key] = value
265
286
  }
@@ -272,8 +293,25 @@ function plan(
272
293
  overrides: Options['overrides'],
273
294
  offset: number,
274
295
  relative = false,
296
+ path?: PreparedPath,
275
297
  ): Plan[] {
276
- return expand(keyframes).map(([property, frames]) => {
298
+ return expand(keyframes, path).map(([property, frames]) => {
299
+ if (path?.properties.includes(property as 'x' | 'y' | 'rotate')) {
300
+ if (
301
+ !Number.isFinite(path.transition.delay + offset) ||
302
+ path.transition.delay + offset < 0
303
+ )
304
+ throw new RangeError(
305
+ 'Path delays, including stagger, must be finite and nonnegative.',
306
+ )
307
+ return {
308
+ property,
309
+ keyframes: frames,
310
+ transition: { ...path.transition, delay: path.transition.delay + offset },
311
+ relative,
312
+ path,
313
+ }
314
+ }
277
315
  const override =
278
316
  overrides[property] ?? (property.startsWith('scale') ? overrides.scale : undefined)
279
317
  const transition = resolveTransition(
@@ -291,31 +329,55 @@ export function startingValues(
291
329
  animation: Keyframes | PresetName,
292
330
  options?: AnimateOptions | SpringName,
293
331
  ) {
294
- const { keyframes, relative } = prepare(animation, options)
332
+ const { keyframes, relative, path } = prepare(animation, options)
295
333
  if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
296
334
  return new Map(
297
- expand(keyframes).map(([property, [first]]) => [
335
+ expand(keyframes, path).map(([property, [first]]) => [
298
336
  property,
299
337
  relative && !isColorProperty(property) ? null : first!,
300
338
  ]),
301
339
  )
302
340
  }
303
341
 
304
- function expand(keyframes: Keyframes): [Property, (number | null)[]][] {
342
+ /** Each property the animation touches, with the value it leaves behind when that is known up front. */
343
+ export function endingValues(
344
+ animation: Keyframes | PresetName,
345
+ options?: AnimateOptions | SpringName,
346
+ ) {
347
+ const { keyframes, base, overrides, relative, path } = prepare(animation, options)
348
+ return new Map(
349
+ plan(keyframes, base, overrides, 0, relative, path).map(
350
+ ({ property, keyframes: frames, transition, path: owned }) => {
351
+ const finite = Number.isFinite(transition.repeat)
352
+ const backwards = transition.reverse && finite && transition.repeat % 2 === 1
353
+ const unknown = owned !== undefined || (relative && !isColorProperty(property))
354
+ const frame = !finite || backwards ? frames[0] : frames.at(-1)
355
+ return [property, unknown ? null : frame!]
356
+ },
357
+ ),
358
+ )
359
+ }
360
+
361
+ function expand(
362
+ keyframes: Keyframes,
363
+ path?: PreparedPath,
364
+ ): [Property, (number | null)[]][] {
305
365
  const expanded: [Property, (number | null)[]][] = []
306
- for (const [key, value] of Object.entries(keyframes) as [
307
- keyof Keyframes,
308
- KeyframeValue | ColorKeyframes | undefined,
309
- ][]) {
310
- if (value === undefined) continue
366
+ for (const [key, value] of Object.entries(keyframes)) {
367
+ if (value === undefined || key === 'path' || key === 'orient') continue
311
368
  if (key !== 'scale' && !isProperty(key)) throw new Error(`Can't animate "${key}".`)
312
369
 
313
370
  const properties: Property[] = key === 'scale' ? ['scaleX', 'scaleY'] : [key]
314
371
  for (const property of properties) {
315
372
  if (key === 'scale' && keyframes[property] !== undefined) continue
316
- expanded.push([property, toKeyframes(property, value)])
373
+ expanded.push([
374
+ property,
375
+ toKeyframes(property, value as KeyframeValue | ColorKeyframes),
376
+ ])
317
377
  }
318
378
  }
379
+ for (const [property, value] of path?.starts ?? [])
380
+ expanded.push([property, [value, value]])
319
381
  return expanded
320
382
  }
321
383
 
@@ -339,6 +401,7 @@ function toKeyframes(
339
401
  ) {
340
402
  throw new Error(`Invalid keyframes for "${property}": ${JSON.stringify(value)}.`)
341
403
  }
404
+ if (isDecorationProperty(property)) validateDecorationFrames(property, frames)
342
405
  return frames
343
406
  }
344
407