@rafaelaugusto/jolt 0.1.0-beta.5 → 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.
- package/README.md +110 -3
- package/dist/animate.d.ts +4 -2
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +48 -11
- package/dist/animate.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.js.map +1 -0
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +7 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +2 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.js +1 -1
- package/dist/driver/ios-color.js.map +1 -1
- package/dist/driver/ios-decoration.js +1 -1
- package/dist/driver/ios-decoration.js.map +1 -1
- package/dist/driver/ios-playback.d.ts +1 -1
- package/dist/driver/ios-playback.d.ts.map +1 -1
- package/dist/driver/ios-playback.js +14 -0
- package/dist/driver/ios-playback.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +1 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +16 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +33 -1
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/items.d.ts +20 -0
- package/dist/items.d.ts.map +1 -0
- package/dist/items.js +109 -0
- package/dist/items.js.map +1 -0
- package/dist/keyboard.d.ts +17 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +116 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.d.ts.map +1 -0
- package/dist/label.js +60 -0
- package/dist/label.js.map +1 -0
- package/dist/liquid.d.ts.map +1 -1
- package/dist/liquid.js +2 -1
- package/dist/liquid.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +2 -1
- package/dist/loader.js.map +1 -1
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +4 -3
- package/dist/particles.js.map +1 -1
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +90 -0
- package/dist/path.js.map +1 -0
- package/dist/playback-group.d.ts.map +1 -1
- package/dist/playback-group.js +29 -1
- package/dist/playback-group.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +3 -3
- package/dist/press.js.map +1 -1
- package/dist/reveal.d.ts.map +1 -1
- package/dist/reveal.js +3 -2
- package/dist/reveal.js.map +1 -1
- package/dist/roll-diff.d.ts +22 -0
- package/dist/roll-diff.d.ts.map +1 -0
- package/dist/roll-diff.js +36 -0
- package/dist/roll-diff.js.map +1 -0
- package/dist/roll.d.ts +29 -0
- package/dist/roll.d.ts.map +1 -0
- package/dist/roll.js +442 -0
- package/dist/roll.js.map +1 -0
- package/dist/scroll-linked.d.ts +10 -0
- package/dist/scroll-linked.d.ts.map +1 -0
- package/dist/scroll-linked.js +58 -0
- package/dist/scroll-linked.js.map +1 -0
- package/dist/sequence.d.ts +10 -5
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +202 -14
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts.map +1 -1
- package/dist/shimmer.js +2 -1
- package/dist/shimmer.js.map +1 -1
- package/dist/svg-path.d.ts +14 -0
- package/dist/svg-path.d.ts.map +1 -0
- package/dist/svg-path.js +281 -0
- package/dist/svg-path.js.map +1 -0
- package/dist/symbol-effect.d.ts.map +1 -1
- package/dist/symbol-effect.js +4 -3
- package/dist/symbol-effect.js.map +1 -1
- package/dist/text-layout.d.ts +8 -0
- package/dist/text-layout.d.ts.map +1 -1
- package/dist/text-layout.js +8 -2
- package/dist/text-layout.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +60 -64
- package/dist/text.js.map +1 -1
- package/dist/track.d.ts +2 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +62 -15
- package/dist/track.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +89 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +134 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +15 -3
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +6 -0
- package/src/android-plugin.d.ts +64 -0
- package/src/animate.ts +80 -30
- package/src/config.ts +46 -0
- package/src/driver/android.ts +14 -0
- package/src/driver/index.ts +2 -0
- package/src/driver/ios-color.ts +1 -1
- package/src/driver/ios-decoration.ts +1 -1
- package/src/driver/ios-playback.ts +15 -1
- package/src/driver/ios.ts +2 -0
- package/src/engine.ts +47 -2
- package/src/index.ts +11 -1
- package/src/items.ts +140 -0
- package/src/keyboard.ts +132 -0
- package/src/label.ts +76 -0
- package/src/liquid.ts +2 -1
- package/src/loader.ts +2 -1
- package/src/particles.ts +4 -4
- package/src/path.ts +124 -0
- package/src/playback-group.ts +34 -1
- package/src/press.ts +3 -5
- package/src/reveal.ts +3 -3
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +199 -14
- package/src/shimmer.ts +2 -1
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +4 -3
- package/src/text-layout.ts +14 -2
- package/src/text.ts +62 -81
- package/src/track.ts +89 -24
- package/src/types.ts +8 -1
- 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
|
+
}
|
|
@@ -5,3 +5,9 @@
|
|
|
5
5
|
public android.graphics.drawable.Drawable getWrappedDrawable();
|
|
6
6
|
}
|
|
7
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
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from './decoration.js'
|
|
6
6
|
import { playbackGroup } from './playback-group.js'
|
|
7
7
|
import type { PlaybackAnimation } from './playback.js'
|
|
8
|
+
import { preparePath, type PreparedPath } from './path.js'
|
|
8
9
|
import { View } from '@nativescript/core'
|
|
9
10
|
import {
|
|
10
11
|
isColorProperty,
|
|
@@ -38,13 +39,14 @@ import type {
|
|
|
38
39
|
DecayVelocity,
|
|
39
40
|
Keyframes,
|
|
40
41
|
KeyframeValue,
|
|
42
|
+
KeyframeProperty,
|
|
41
43
|
Stagger,
|
|
42
44
|
Target,
|
|
43
45
|
} from './types.js'
|
|
44
46
|
|
|
45
47
|
interface Options {
|
|
46
48
|
base: Transition
|
|
47
|
-
overrides: Partial<Record<
|
|
49
|
+
overrides: Partial<Record<KeyframeProperty | 'path', Transition>>
|
|
48
50
|
stagger: number | Stagger | undefined
|
|
49
51
|
relative: boolean | undefined
|
|
50
52
|
haptic: Haptic | undefined
|
|
@@ -65,7 +67,7 @@ export function animate(
|
|
|
65
67
|
animation: Keyframes | PresetName,
|
|
66
68
|
options?: AnimateOptions | SpringName,
|
|
67
69
|
): PlaybackAnimation {
|
|
68
|
-
const { keyframes, base, overrides, stagger, relative, haptic } = prepare(
|
|
70
|
+
const { keyframes, base, overrides, stagger, relative, haptic, path } = prepare(
|
|
69
71
|
animation,
|
|
70
72
|
options,
|
|
71
73
|
)
|
|
@@ -78,7 +80,7 @@ export function animate(
|
|
|
78
80
|
else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
79
81
|
return new ViewAnimation(
|
|
80
82
|
view,
|
|
81
|
-
plan(keyframes, base, overrides, 0, relative),
|
|
83
|
+
plan(keyframes, base, overrides, 0, relative, path),
|
|
82
84
|
feedback,
|
|
83
85
|
)
|
|
84
86
|
}
|
|
@@ -88,23 +90,19 @@ export function animate(
|
|
|
88
90
|
for (const property of Object.keys(keyframes))
|
|
89
91
|
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
90
92
|
else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
overrides,
|
|
100
|
-
staggerDelay(stagger, index, views.length),
|
|
101
|
-
relative,
|
|
102
|
-
),
|
|
103
|
-
feedback,
|
|
104
|
-
true,
|
|
105
|
-
),
|
|
93
|
+
const plans = views.map((_, index) =>
|
|
94
|
+
plan(
|
|
95
|
+
keyframes,
|
|
96
|
+
base,
|
|
97
|
+
overrides,
|
|
98
|
+
staggerDelay(stagger, index, views.length),
|
|
99
|
+
relative,
|
|
100
|
+
path,
|
|
106
101
|
),
|
|
107
102
|
)
|
|
103
|
+
return playbackGroup(
|
|
104
|
+
views.map((view, index) => new ViewAnimation(view, plans[index]!, feedback, true)),
|
|
105
|
+
)
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
/** Starts at the current translation and slows down from a velocity in DIPs per second. */
|
|
@@ -183,7 +181,7 @@ export function decay(
|
|
|
183
181
|
/** Stops all or selected properties at their current values. `scale` stops both axes. */
|
|
184
182
|
export function stop(
|
|
185
183
|
target: Target | readonly Target[],
|
|
186
|
-
properties?: readonly
|
|
184
|
+
properties?: readonly KeyframeProperty[],
|
|
187
185
|
) {
|
|
188
186
|
const expanded = properties?.flatMap<Property>((property) =>
|
|
189
187
|
property === 'scale' ? ['scaleX', 'scaleY'] : [property],
|
|
@@ -225,6 +223,17 @@ function isList<T>(target: T | readonly T[]): target is readonly T[] {
|
|
|
225
223
|
function prepare(
|
|
226
224
|
animation: Keyframes | PresetName,
|
|
227
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,
|
|
228
237
|
) {
|
|
229
238
|
const user = normalize(options)
|
|
230
239
|
if (typeof animation !== 'string') return { keyframes: animation, ...user }
|
|
@@ -271,7 +280,7 @@ function normalize(options: AnimateOptions | SpringName | undefined): Options {
|
|
|
271
280
|
if (key === 'stagger') stagger = value as Options['stagger']
|
|
272
281
|
else if (key === 'relative') relative = value as Options['relative']
|
|
273
282
|
else if (key === 'haptic') haptic = value as Options['haptic']
|
|
274
|
-
else if (key === 'scale' || isProperty(key)) {
|
|
283
|
+
else if (key === 'path' || key === 'scale' || isProperty(key)) {
|
|
275
284
|
overrides[key] = typeof value === 'string' ? spring(value as SpringName) : value
|
|
276
285
|
} else base[key] = value
|
|
277
286
|
}
|
|
@@ -284,8 +293,25 @@ function plan(
|
|
|
284
293
|
overrides: Options['overrides'],
|
|
285
294
|
offset: number,
|
|
286
295
|
relative = false,
|
|
296
|
+
path?: PreparedPath,
|
|
287
297
|
): Plan[] {
|
|
288
|
-
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
|
+
}
|
|
289
315
|
const override =
|
|
290
316
|
overrides[property] ?? (property.startsWith('scale') ? overrides.scale : undefined)
|
|
291
317
|
const transition = resolveTransition(
|
|
@@ -303,31 +329,55 @@ export function startingValues(
|
|
|
303
329
|
animation: Keyframes | PresetName,
|
|
304
330
|
options?: AnimateOptions | SpringName,
|
|
305
331
|
) {
|
|
306
|
-
const { keyframes, relative } = prepare(animation, options)
|
|
332
|
+
const { keyframes, relative, path } = prepare(animation, options)
|
|
307
333
|
if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
|
|
308
334
|
return new Map(
|
|
309
|
-
expand(keyframes).map(([property, [first]]) => [
|
|
335
|
+
expand(keyframes, path).map(([property, [first]]) => [
|
|
310
336
|
property,
|
|
311
337
|
relative && !isColorProperty(property) ? null : first!,
|
|
312
338
|
]),
|
|
313
339
|
)
|
|
314
340
|
}
|
|
315
341
|
|
|
316
|
-
|
|
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)[]][] {
|
|
317
365
|
const expanded: [Property, (number | null)[]][] = []
|
|
318
|
-
for (const [key, value] of Object.entries(keyframes)
|
|
319
|
-
|
|
320
|
-
KeyframeValue | ColorKeyframes | undefined,
|
|
321
|
-
][]) {
|
|
322
|
-
if (value === undefined) continue
|
|
366
|
+
for (const [key, value] of Object.entries(keyframes)) {
|
|
367
|
+
if (value === undefined || key === 'path' || key === 'orient') continue
|
|
323
368
|
if (key !== 'scale' && !isProperty(key)) throw new Error(`Can't animate "${key}".`)
|
|
324
369
|
|
|
325
370
|
const properties: Property[] = key === 'scale' ? ['scaleX', 'scaleY'] : [key]
|
|
326
371
|
for (const property of properties) {
|
|
327
372
|
if (key === 'scale' && keyframes[property] !== undefined) continue
|
|
328
|
-
expanded.push([
|
|
373
|
+
expanded.push([
|
|
374
|
+
property,
|
|
375
|
+
toKeyframes(property, value as KeyframeValue | ColorKeyframes),
|
|
376
|
+
])
|
|
329
377
|
}
|
|
330
378
|
}
|
|
379
|
+
for (const [property, value] of path?.starts ?? [])
|
|
380
|
+
expanded.push([property, [value, value]])
|
|
331
381
|
return expanded
|
|
332
382
|
}
|
|
333
383
|
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getDriver } from './driver/index.js'
|
|
2
|
+
|
|
3
|
+
export interface JoltConfig {
|
|
4
|
+
/** Playback speed for every engine animation. `0.2` plays everything five times slower. */
|
|
5
|
+
timeScale?: number
|
|
6
|
+
/**
|
|
7
|
+
* When transforms, paths, decorations and decay jump to their end instead of animating.
|
|
8
|
+
* `user` follows the platform setting. Opacity and colors still animate.
|
|
9
|
+
*/
|
|
10
|
+
reducedMotion?: 'user' | 'always' | 'never'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const settings: Required<JoltConfig> = { timeScale: 1, reducedMotion: 'user' }
|
|
14
|
+
const preferences: readonly JoltConfig['reducedMotion'][] = ['user', 'always', 'never']
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Global settings, applied to animations that start after the call.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* configure({ timeScale: 0.2 }) // tune a spring in slow motion
|
|
21
|
+
* configure({ reducedMotion: 'always' }) // check the reduced layout
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function configure(options: JoltConfig): void {
|
|
25
|
+
if (options === null || typeof options !== 'object')
|
|
26
|
+
throw new TypeError('configure() takes an options object.')
|
|
27
|
+
const { timeScale, reducedMotion } = options
|
|
28
|
+
if (timeScale !== undefined && (!Number.isFinite(timeScale) || timeScale <= 0))
|
|
29
|
+
throw new RangeError(`Time scale must be a positive finite number, got ${timeScale}.`)
|
|
30
|
+
if (reducedMotion !== undefined && !preferences.includes(reducedMotion))
|
|
31
|
+
throw new TypeError(
|
|
32
|
+
`Reduced motion must be 'user', 'always' or 'never', got ${String(reducedMotion)}.`,
|
|
33
|
+
)
|
|
34
|
+
if (timeScale !== undefined) settings.timeScale = timeScale
|
|
35
|
+
if (reducedMotion !== undefined) settings.reducedMotion = reducedMotion
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function timeScale(): number {
|
|
39
|
+
return settings.timeScale
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Whether motion is reduced right now. `user` asks the platform each time. */
|
|
43
|
+
export function reducedMotion(): boolean {
|
|
44
|
+
if (settings.reducedMotion === 'user') return getDriver().reducedMotion?.() ?? false
|
|
45
|
+
return settings.reducedMotion === 'always'
|
|
46
|
+
}
|