@rafaelaugusto/jolt 0.1.0-beta.0 → 0.1.0-beta.10
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 +441 -25
- package/dist/animate.d.ts +21 -5
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +205 -25
- package/dist/animate.js.map +1 -1
- package/dist/color.d.ts +11 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +73 -0
- package/dist/color.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +73 -0
- package/dist/config.js.map +1 -0
- package/dist/decay.d.ts +11 -0
- package/dist/decay.d.ts.map +1 -0
- package/dist/decay.js +22 -0
- package/dist/decay.js.map +1 -0
- package/dist/decoration.d.ts +18 -0
- package/dist/decoration.d.ts.map +1 -0
- package/dist/decoration.js +155 -0
- package/dist/decoration.js.map +1 -0
- package/dist/drag-input.d.ts +15 -0
- package/dist/drag-input.d.ts.map +1 -0
- package/dist/drag-input.js +66 -0
- package/dist/drag-input.js.map +1 -0
- package/dist/drag-physics.d.ts +20 -0
- package/dist/drag-physics.d.ts.map +1 -0
- package/dist/drag-physics.js +75 -0
- package/dist/drag-physics.js.map +1 -0
- package/dist/drag.d.ts +58 -0
- package/dist/drag.d.ts.map +1 -0
- package/dist/drag.js +324 -0
- package/dist/drag.js.map +1 -0
- package/dist/driver/android-color.d.ts +20 -0
- package/dist/driver/android-color.d.ts.map +1 -0
- package/dist/driver/android-color.js +136 -0
- package/dist/driver/android-color.js.map +1 -0
- package/dist/driver/android-decoration.d.ts +22 -0
- package/dist/driver/android-decoration.d.ts.map +1 -0
- package/dist/driver/android-decoration.js +139 -0
- package/dist/driver/android-decoration.js.map +1 -0
- package/dist/driver/android-playback.d.ts +13 -0
- package/dist/driver/android-playback.d.ts.map +1 -0
- package/dist/driver/android-playback.js +141 -0
- package/dist/driver/android-playback.js.map +1 -0
- package/dist/driver/android.d.ts +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +165 -13
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +25 -2
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +16 -0
- package/dist/driver/ios-color.d.ts.map +1 -0
- package/dist/driver/ios-color.js +392 -0
- package/dist/driver/ios-color.js.map +1 -0
- package/dist/driver/ios-decoration.d.ts +17 -0
- package/dist/driver/ios-decoration.d.ts.map +1 -0
- package/dist/driver/ios-decoration.js +271 -0
- package/dist/driver/ios-decoration.js.map +1 -0
- package/dist/driver/ios-layout.d.ts +9 -0
- package/dist/driver/ios-layout.d.ts.map +1 -0
- package/dist/driver/ios-layout.js +52 -0
- package/dist/driver/ios-layout.js.map +1 -0
- package/dist/driver/ios-playback.d.ts +10 -0
- package/dist/driver/ios-playback.d.ts.map +1 -0
- package/dist/driver/ios-playback.js +47 -0
- package/dist/driver/ios-playback.js.map +1 -0
- package/dist/driver/ios-shadow.d.ts +6 -0
- package/dist/driver/ios-shadow.d.ts.map +1 -0
- package/dist/driver/ios-shadow.js +52 -0
- package/dist/driver/ios-shadow.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +164 -20
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +76 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +303 -23
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +21 -0
- package/dist/haptics.d.ts.map +1 -0
- package/dist/haptics.js +120 -0
- package/dist/haptics.js.map +1 -0
- package/dist/image-effect-native.d.ts +26 -0
- package/dist/image-effect-native.d.ts.map +1 -0
- package/dist/image-effect-native.js +73 -0
- package/dist/image-effect-native.js.map +1 -0
- package/dist/image-effect.d.ts +50 -0
- package/dist/image-effect.d.ts.map +1 -0
- package/dist/image-effect.js +290 -0
- package/dist/image-effect.js.map +1 -0
- package/dist/image-owner.d.ts +3 -0
- package/dist/image-owner.d.ts.map +1 -0
- package/dist/image-owner.js +11 -0
- package/dist/image-owner.js.map +1 -0
- package/dist/index.d.ts +45 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -1
- package/dist/interaction-surface.d.ts +12 -0
- package/dist/interaction-surface.d.ts.map +1 -0
- package/dist/interaction-surface.js +36 -0
- package/dist/interaction-surface.js.map +1 -0
- 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 +41 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +256 -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/layout.d.ts +13 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +348 -0
- package/dist/layout.js.map +1 -0
- package/dist/liquid-grid.d.ts +3 -0
- package/dist/liquid-grid.d.ts.map +1 -0
- package/dist/liquid-grid.js +24 -0
- package/dist/liquid-grid.js.map +1 -0
- package/dist/liquid.d.ts +18 -0
- package/dist/liquid.d.ts.map +1 -0
- package/dist/liquid.js +234 -0
- package/dist/liquid.js.map +1 -0
- package/dist/loader.d.ts +19 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +173 -0
- package/dist/loader.js.map +1 -0
- package/dist/motion-scope.d.ts +39 -0
- package/dist/motion-scope.d.ts.map +1 -0
- package/dist/motion-scope.js +238 -0
- package/dist/motion-scope.js.map +1 -0
- package/dist/motion-states.d.ts +36 -0
- package/dist/motion-states.d.ts.map +1 -0
- package/dist/motion-states.js +165 -0
- package/dist/motion-states.js.map +1 -0
- package/dist/particles.d.ts +39 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +265 -0
- package/dist/particles.js.map +1 -0
- 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 +6 -0
- package/dist/playback-group.d.ts.map +1 -0
- package/dist/playback-group.js +141 -0
- package/dist/playback-group.js.map +1 -0
- package/dist/playback.d.ts +25 -0
- package/dist/playback.d.ts.map +1 -0
- package/dist/playback.js +35 -0
- package/dist/playback.js.map +1 -0
- package/dist/presets.d.ts +16 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +24 -6
- package/dist/presets.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +25 -7
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +6 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +26 -2
- package/dist/properties.js.map +1 -1
- package/dist/reveal.d.ts +25 -0
- package/dist/reveal.d.ts.map +1 -0
- package/dist/reveal.js +294 -0
- package/dist/reveal.js.map +1 -0
- 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 +13 -7
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +214 -19
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts +12 -0
- package/dist/shimmer.d.ts.map +1 -0
- package/dist/shimmer.js +161 -0
- package/dist/shimmer.js.map +1 -0
- 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 +23 -0
- package/dist/symbol-effect.d.ts.map +1 -0
- package/dist/symbol-effect.js +187 -0
- package/dist/symbol-effect.js.map +1 -0
- package/dist/text-layout.d.ts +30 -0
- package/dist/text-layout.d.ts.map +1 -0
- package/dist/text-layout.js +185 -0
- package/dist/text-layout.js.map +1 -0
- package/dist/text.d.ts +17 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +263 -0
- package/dist/text.js.map +1 -0
- package/dist/track.d.ts +3 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +266 -13
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +17 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +41 -6
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +50 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +95 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +163 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/zoom-native.d.ts +30 -0
- package/dist/zoom-native.d.ts.map +1 -0
- package/dist/zoom-native.js +56 -0
- package/dist/zoom-native.js.map +1 -0
- package/dist/zoomable.d.ts +48 -0
- package/dist/zoomable.d.ts.map +1 -0
- package/dist/zoomable.js +249 -0
- package/dist/zoomable.js.map +1 -0
- package/package.json +33 -5
- package/platforms/android/include.gradle +7 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +19 -0
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/JoltImageEffect.h +25 -0
- package/platforms/ios/src/JoltImageEffect.mm +242 -0
- package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
- package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
- package/platforms/ios/src/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +6 -0
- package/src/android-plugin.d.ts +155 -0
- package/src/animate.ts +278 -38
- package/src/color.ts +101 -0
- package/src/config.ts +84 -0
- package/src/decay.ts +45 -0
- package/src/decoration.ts +180 -0
- package/src/drag-input.ts +74 -0
- package/src/drag-physics.ts +110 -0
- package/src/drag.ts +395 -0
- package/src/driver/android-color.ts +170 -0
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +208 -20
- package/src/driver/index.ts +37 -2
- package/src/driver/ios-color.ts +441 -0
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-layout.ts +63 -0
- package/src/driver/ios-playback.ts +51 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +194 -25
- package/src/engine.ts +329 -27
- package/src/haptics.ts +148 -0
- package/src/image-effect-native.ts +129 -0
- package/src/image-effect.ts +336 -0
- package/src/image-owner.ts +12 -0
- package/src/index.ts +61 -4
- package/src/interaction-surface.ts +41 -0
- package/src/ios-plugin.d.ts +81 -0
- package/src/items.ts +140 -0
- package/src/keyboard.ts +291 -0
- package/src/label.ts +76 -0
- package/src/layout.ts +408 -0
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +287 -0
- package/src/loader.ts +202 -0
- package/src/motion-scope.ts +279 -0
- package/src/motion-states.ts +215 -0
- package/src/particles.ts +323 -0
- package/src/path.ts +124 -0
- package/src/playback-group.ts +143 -0
- package/src/playback.ts +62 -0
- package/src/presets.ts +31 -7
- package/src/press.ts +32 -10
- package/src/properties.ts +32 -6
- package/src/reveal.ts +374 -0
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +212 -19
- package/src/shimmer.ts +180 -0
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +226 -0
- package/src/text-layout.ts +228 -0
- package/src/text.ts +279 -0
- package/src/track.ts +347 -12
- package/src/transition.ts +63 -6
- package/src/types.ts +51 -1
- package/src/vue/index.ts +231 -0
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
|
@@ -0,0 +1,288 @@
|
|
|
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.*;
|
|
7
|
+
import android.graphics.drawable.*;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
import android.view.MotionEvent;
|
|
10
|
+
import android.view.View;
|
|
11
|
+
import android.widget.ImageView;
|
|
12
|
+
|
|
13
|
+
/** Image-only GPU effects. All animation and pointer samples stay in Java. */
|
|
14
|
+
public final class ImageEffect {
|
|
15
|
+
public interface Listener { void onEnd(boolean completed, float progress); }
|
|
16
|
+
private static final String SHADER =
|
|
17
|
+
"uniform shader source; uniform shader destination; uniform float2 size;" +
|
|
18
|
+
"uniform float2 origin; uniform float2 displacement; uniform float progress;" +
|
|
19
|
+
"uniform float amplitude; uniform float radius; uniform float seed; uniform float mode;" +
|
|
20
|
+
"half4 main(float2 p) { float2 uv=p/size; float2 q=p;" +
|
|
21
|
+
"if(mode<1.5){ float2 v=p-origin; float d=length(v);" +
|
|
22
|
+
"float edge=clamp(min(min(p.x,size.x-p.x),min(p.y,size.y-p.y))/max(amplitude*2.,1.),0.,1.);" +
|
|
23
|
+
"if(mode<.5){float wave=d-progress*length(size);" +
|
|
24
|
+
"float a=sin(wave/max(radius*.12,1.))*exp(-pow(wave/max(radius,1.),2.))*amplitude*sin(progress*3.14159265);" +
|
|
25
|
+
"q-=v/max(d,1.)*a*edge;}else{q-=displacement*exp(-d*d/max(radius*radius,1.))*edge;}" +
|
|
26
|
+
"return source.eval(clamp(q,float2(.5),size-float2(.5)));}" +
|
|
27
|
+
"if(progress<=0.)return source.eval(p); if(progress>=1.)return destination.eval(p);" +
|
|
28
|
+
"float n=fract(sin(dot(floor(p/3.),float2(12.9898,78.233))+seed)*43758.5453);" +
|
|
29
|
+
"float a=smoothstep(n-.08,n+.08,progress*1.16-.08);" +
|
|
30
|
+
"return mix(source.eval(p),destination.eval(p),a);}";
|
|
31
|
+
|
|
32
|
+
private final ImageView view;
|
|
33
|
+
private final Listener listener;
|
|
34
|
+
private final int mode;
|
|
35
|
+
private final float amplitude, radius;
|
|
36
|
+
private final long duration;
|
|
37
|
+
private final Drawable original;
|
|
38
|
+
private final Bitmap originalBitmap;
|
|
39
|
+
private final ImageView.ScaleType originalScale;
|
|
40
|
+
private final View.OnTouchListener previousTouch;
|
|
41
|
+
private final boolean originalClickable;
|
|
42
|
+
private Bitmap source, destination, placeholderBitmap;
|
|
43
|
+
private RuntimeShader shader;
|
|
44
|
+
private EffectDrawable drawable;
|
|
45
|
+
private Drawable placeholder;
|
|
46
|
+
private ValueAnimator animator;
|
|
47
|
+
private boolean disposed, paused, suppressEnd, attached;
|
|
48
|
+
private float progress, goal = 1, x, y, dx, dy;
|
|
49
|
+
private int pointer = -1;
|
|
50
|
+
private float startX, startY, rawX, rawY;
|
|
51
|
+
private long frames, lastFrame, longestFrame;
|
|
52
|
+
private String failureReason="";
|
|
53
|
+
private final Path contentClip=new Path();
|
|
54
|
+
|
|
55
|
+
public ImageEffect(ImageView view, Bitmap from, Bitmap to, float[] geometry, int mode, float x, float y,
|
|
56
|
+
float amplitude, float radius, long duration, float seed,
|
|
57
|
+
View.OnTouchListener previousTouch, Listener listener) {
|
|
58
|
+
this.view=view; this.listener=listener; this.mode=mode;
|
|
59
|
+
this.amplitude=amplitude; this.radius=radius; this.duration=duration;
|
|
60
|
+
this.x=x; this.y=y; this.previousTouch=previousTouch;
|
|
61
|
+
original=view.getDrawable(); originalScale=view.getScaleType();
|
|
62
|
+
originalBitmap=from;
|
|
63
|
+
originalClickable=view.isClickable();
|
|
64
|
+
if(Build.VERSION.SDK_INT<33 || !view.isHardwareAccelerated() || from==null || original==null || original instanceof Animatable) {
|
|
65
|
+
failureReason="Requires API 33, hardware rendering and a static drawable ("+(original==null?"null":original.getClass().getName())+")";
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
int w=view.getWidth(), h=view.getHeight();
|
|
69
|
+
if(w<=0 || h<=0 || (long)w*h>4194304L) {failureReason="Image surface exceeds the pixel budget";return;}
|
|
70
|
+
try {
|
|
71
|
+
float left=view.getPaddingLeft(),top=view.getPaddingTop(),right=view.getPaddingRight(),bottom=view.getPaddingBottom();
|
|
72
|
+
float[] radii={
|
|
73
|
+
Math.max(0,geometry[2]-left),Math.max(0,geometry[2]-top),
|
|
74
|
+
Math.max(0,geometry[3]-right),Math.max(0,geometry[3]-top),
|
|
75
|
+
Math.max(0,geometry[4]-right),Math.max(0,geometry[4]-bottom),
|
|
76
|
+
Math.max(0,geometry[5]-left),Math.max(0,geometry[5]-bottom)
|
|
77
|
+
};
|
|
78
|
+
contentClip.addRoundRect(new RectF(left,top,w-right,h-bottom),radii,Path.Direction.CW);
|
|
79
|
+
source=raster(from,geometry[0],w,h);
|
|
80
|
+
if(mode==2) {
|
|
81
|
+
if(to==null) { releaseBitmaps(); return; }
|
|
82
|
+
destination=raster(to,geometry[1],w,h);
|
|
83
|
+
}
|
|
84
|
+
shader=new RuntimeShader(SHADER);
|
|
85
|
+
shader.setInputShader("source",new BitmapShader(source,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP));
|
|
86
|
+
shader.setInputShader("destination",new BitmapShader(destination==null?source:destination,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP));
|
|
87
|
+
shader.setFloatUniform("size",w,h); shader.setFloatUniform("mode",mode);
|
|
88
|
+
shader.setFloatUniform("amplitude",amplitude); shader.setFloatUniform("radius",radius);
|
|
89
|
+
shader.setFloatUniform("seed",seed);
|
|
90
|
+
drawable=new EffectDrawable(w,h);
|
|
91
|
+
drawable.setBounds(0,0,w,h);
|
|
92
|
+
update();
|
|
93
|
+
// NativeScript's ImageView rasterizes custom drawables on a software
|
|
94
|
+
// canvas. An overlay keeps RuntimeShader on the hardware canvas.
|
|
95
|
+
view.getOverlay().add(drawable);
|
|
96
|
+
attached=true;
|
|
97
|
+
// NativeScript's onDraw clips the canvas to its bitmap's mapped
|
|
98
|
+
// rectangle, including overlays. A full-content transparent alpha
|
|
99
|
+
// bitmap keeps that clip open in every stretch/rotation mode.
|
|
100
|
+
// Clearing the drawable leaves its cached bitmap shader invalid.
|
|
101
|
+
int pw=Math.max(1,w-view.getPaddingLeft()-view.getPaddingRight());
|
|
102
|
+
int ph=Math.max(1,h-view.getPaddingTop()-view.getPaddingBottom());
|
|
103
|
+
float angle=Math.abs(geometry[0]%180);
|
|
104
|
+
if(angle>45 && angle<135){int swap=pw;pw=ph;ph=swap;}
|
|
105
|
+
placeholderBitmap=Bitmap.createBitmap(pw,ph,Bitmap.Config.ALPHA_8);
|
|
106
|
+
view.setImageBitmap(placeholderBitmap);
|
|
107
|
+
placeholder=view.getDrawable();
|
|
108
|
+
if(mode==1) {
|
|
109
|
+
view.setClickable(true);
|
|
110
|
+
view.setOnTouchListener(this::touch);
|
|
111
|
+
}
|
|
112
|
+
} catch (RuntimeException | OutOfMemoryError error) {
|
|
113
|
+
failureReason=error.toString();
|
|
114
|
+
if(attached) restore(true);
|
|
115
|
+
releaseBitmaps(); shader=null; drawable=null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Match NativeScript's bitmap mapping without baking backgrounds/borders
|
|
120
|
+
* into the texture. The stationary inner corner mask is applied at draw time. */
|
|
121
|
+
private Bitmap raster(Bitmap bitmap,float rotation,int width,int height) {
|
|
122
|
+
Bitmap result=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
|
|
123
|
+
Canvas canvas=new Canvas(result);
|
|
124
|
+
float left=view.getPaddingLeft(),top=view.getPaddingTop();
|
|
125
|
+
float w=width-left-view.getPaddingRight(),h=height-top-view.getPaddingBottom();
|
|
126
|
+
float iw=bitmap.getWidth(),ih=bitmap.getHeight();
|
|
127
|
+
Matrix matrix=new Matrix();matrix.postRotate(rotation,iw/2,ih/2);
|
|
128
|
+
float angle=Math.abs(rotation%180);
|
|
129
|
+
if(angle>45 && angle<135){matrix.postTranslate((ih-iw)/2,(iw-ih)/2);float swap=iw;iw=ih;ih=swap;}
|
|
130
|
+
float sx=w/iw,sy=h/ih;
|
|
131
|
+
RectF visible=new RectF(left,top,left+w,top+h);
|
|
132
|
+
if(originalScale==ImageView.ScaleType.FIT_XY) matrix.postScale(sx,sy);
|
|
133
|
+
else if(originalScale==ImageView.ScaleType.MATRIX) visible.intersect(left,top,left+iw,top+ih);
|
|
134
|
+
else {
|
|
135
|
+
float scale=originalScale==ImageView.ScaleType.CENTER?1:originalScale==ImageView.ScaleType.CENTER_CROP?Math.max(sx,sy):Math.min(sx,sy);
|
|
136
|
+
matrix.postTranslate((w-iw)/2,(h-ih)/2);
|
|
137
|
+
matrix.postScale(scale,scale,w/2,h/2);
|
|
138
|
+
visible.intersect(left+(w-iw*scale)/2,top+(h-ih*scale)/2,left+(w+iw*scale)/2,top+(h+ih*scale)/2);
|
|
139
|
+
}
|
|
140
|
+
matrix.postTranslate(left,top);
|
|
141
|
+
BitmapShader pixels=new BitmapShader(bitmap,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP);
|
|
142
|
+
pixels.setLocalMatrix(matrix);
|
|
143
|
+
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG);
|
|
144
|
+
paint.setShader(pixels);paint.setColorFilter(view.getColorFilter());
|
|
145
|
+
canvas.drawRect(visible,paint);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public boolean isSupported() { return drawable!=null && !disposed; }
|
|
150
|
+
public String getFailureReason() { return failureReason; }
|
|
151
|
+
public float getProgress() { return progress; }
|
|
152
|
+
public long getFrames() { return frames; }
|
|
153
|
+
public long getLongestFrameNanos() { return longestFrame; }
|
|
154
|
+
public long getTextureBytes() { return source==null?0:(long)source.getAllocationByteCount()+(destination==null?0:destination.getAllocationByteCount())+(placeholderBitmap==null?0:placeholderBitmap.getAllocationByteCount()); }
|
|
155
|
+
public boolean isPaused() { return paused; }
|
|
156
|
+
|
|
157
|
+
public void play(float destinationProgress) {
|
|
158
|
+
if(disposed || drawable==null || mode==1) return;
|
|
159
|
+
goal=destinationProgress; paused=false;
|
|
160
|
+
animate(progress,goal,Math.max(1,(long)(duration*Math.abs(goal-progress))),false);
|
|
161
|
+
}
|
|
162
|
+
public void pause() {
|
|
163
|
+
paused=true;
|
|
164
|
+
if(animator!=null) animator.pause();
|
|
165
|
+
}
|
|
166
|
+
public void resume() {
|
|
167
|
+
if(disposed) return;
|
|
168
|
+
paused=false;
|
|
169
|
+
lastFrame=0;
|
|
170
|
+
if(animator!=null && animator.isPaused()) animator.resume();
|
|
171
|
+
}
|
|
172
|
+
public void seek(float value) {
|
|
173
|
+
if(disposed || mode==1) return;
|
|
174
|
+
boolean hold=paused;
|
|
175
|
+
cancelAnimator(); progress=value; update();
|
|
176
|
+
if(!hold) play(goal);
|
|
177
|
+
}
|
|
178
|
+
public void finish(float endpoint) {
|
|
179
|
+
if(disposed || mode==1) return;
|
|
180
|
+
cancelAnimator(); progress=endpoint; update();
|
|
181
|
+
listener.onEnd(true,progress);
|
|
182
|
+
}
|
|
183
|
+
public void stop() {
|
|
184
|
+
detach(true);
|
|
185
|
+
}
|
|
186
|
+
public void stopPreservingImage() {
|
|
187
|
+
detach(false);
|
|
188
|
+
}
|
|
189
|
+
private void detach(boolean restoreImage) {
|
|
190
|
+
if(disposed) return;
|
|
191
|
+
disposed=true; cancelAnimator();
|
|
192
|
+
restore(restoreImage); shader=null; drawable=null; releaseBitmaps();
|
|
193
|
+
}
|
|
194
|
+
private void restore(boolean restoreImage) {
|
|
195
|
+
if(attached && drawable!=null) view.getOverlay().remove(drawable);
|
|
196
|
+
if(restoreImage && attached && view.getDrawable()==placeholder) {
|
|
197
|
+
// NativeScript caches mBitmap separately from its drawable. Restoring
|
|
198
|
+
// only the drawable leaves the placeholder's geometry behind.
|
|
199
|
+
view.setImageBitmap(originalBitmap);
|
|
200
|
+
}
|
|
201
|
+
if(mode==1 && drawable!=null) {
|
|
202
|
+
view.setOnTouchListener(previousTouch); view.setClickable(originalClickable);
|
|
203
|
+
if(view.getParent()!=null) view.getParent().requestDisallowInterceptTouchEvent(false);
|
|
204
|
+
}
|
|
205
|
+
attached=false;
|
|
206
|
+
}
|
|
207
|
+
private void releaseBitmaps() {
|
|
208
|
+
// Draw commands may still reference them; let Android retire the backing storage safely.
|
|
209
|
+
source=null; destination=null; placeholderBitmap=null;
|
|
210
|
+
}
|
|
211
|
+
private void cancelAnimator() {
|
|
212
|
+
if(animator==null) return;
|
|
213
|
+
suppressEnd=true; animator.cancel(); animator=null; suppressEnd=false;
|
|
214
|
+
}
|
|
215
|
+
private void animate(float from,float to,long milliseconds,boolean settle) {
|
|
216
|
+
cancelAnimator();
|
|
217
|
+
lastFrame=0;
|
|
218
|
+
final float startDx=dx,startDy=dy;
|
|
219
|
+
animator=ValueAnimator.ofFloat(from,to);
|
|
220
|
+
animator.setDuration(milliseconds);
|
|
221
|
+
animator.setInterpolator(new android.view.animation.LinearInterpolator());
|
|
222
|
+
animator.addUpdateListener(a->{
|
|
223
|
+
float value=(float)a.getAnimatedValue();
|
|
224
|
+
if(settle) {float t=value; float decay=(float)(Math.exp(-8*t)*(1-t)); dx=startDx*decay; dy=startDy*decay;}
|
|
225
|
+
else progress=value;
|
|
226
|
+
update();
|
|
227
|
+
});
|
|
228
|
+
animator.addListener(new AnimatorListenerAdapter(){
|
|
229
|
+
@Override public void onAnimationEnd(Animator a){
|
|
230
|
+
if(suppressEnd || disposed) return;
|
|
231
|
+
animator=null;
|
|
232
|
+
if(settle) {dx=dy=0;update();}
|
|
233
|
+
else listener.onEnd(true,progress);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
animator.start();
|
|
237
|
+
}
|
|
238
|
+
private boolean touch(View target,MotionEvent event) {
|
|
239
|
+
if(disposed || paused) return false;
|
|
240
|
+
int action=event.getActionMasked();
|
|
241
|
+
if(action==MotionEvent.ACTION_DOWN) {
|
|
242
|
+
cancelAnimator(); pointer=event.getPointerId(0);
|
|
243
|
+
x=startX=event.getX(); y=startY=event.getY(); dx=dy=rawX=rawY=0;
|
|
244
|
+
update(); return true;
|
|
245
|
+
}
|
|
246
|
+
int index=event.findPointerIndex(pointer);
|
|
247
|
+
if(action==MotionEvent.ACTION_MOVE && index>=0) {
|
|
248
|
+
float vx=event.getX(index)-startX,vy=event.getY(index)-startY;
|
|
249
|
+
rawX=vx;rawY=vy;
|
|
250
|
+
float length=(float)Math.hypot(vx,vy);
|
|
251
|
+
float factor=length==0?0:(float)(amplitude*Math.tanh(length/Math.max(amplitude,1))/length);
|
|
252
|
+
dx=vx*factor; dy=vy*factor; update(); return true;
|
|
253
|
+
}
|
|
254
|
+
if(action==MotionEvent.ACTION_POINTER_UP && event.getPointerId(event.getActionIndex())==pointer) {
|
|
255
|
+
int next=event.getActionIndex()==0?1:0;
|
|
256
|
+
if(next<event.getPointerCount()) {
|
|
257
|
+
pointer=event.getPointerId(next); startX=event.getX(next)-rawX;startY=event.getY(next)-rawY;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if(action==MotionEvent.ACTION_UP || action==MotionEvent.ACTION_CANCEL) {
|
|
261
|
+
pointer=-1; animate(0,1,Math.min(600,duration),true);
|
|
262
|
+
if(action==MotionEvent.ACTION_UP) view.performClick();
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
private void update() {
|
|
267
|
+
if(shader==null || disposed) return;
|
|
268
|
+
shader.setFloatUniform("origin",x,y);
|
|
269
|
+
shader.setFloatUniform("displacement",dx,dy);
|
|
270
|
+
shader.setFloatUniform("progress",progress);
|
|
271
|
+
if(drawable!=null) drawable.invalidateSelf();
|
|
272
|
+
}
|
|
273
|
+
private final class EffectDrawable extends Drawable {
|
|
274
|
+
private final Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
275
|
+
private final int width,height;
|
|
276
|
+
EffectDrawable(int width,int height) {this.width=width;this.height=height;paint.setShader(shader);}
|
|
277
|
+
@Override public int getIntrinsicWidth(){return width;}
|
|
278
|
+
@Override public int getIntrinsicHeight(){return height;}
|
|
279
|
+
@Override public void draw(Canvas canvas){
|
|
280
|
+
long now=System.nanoTime(); if(lastFrame!=0 && animator!=null && !paused) longestFrame=Math.max(longestFrame,now-lastFrame);
|
|
281
|
+
lastFrame=now; frames++;
|
|
282
|
+
canvas.drawPath(contentClip,paint);
|
|
283
|
+
}
|
|
284
|
+
@Override public void setAlpha(int alpha){paint.setAlpha(alpha);invalidateSelf();}
|
|
285
|
+
@Override public void setColorFilter(ColorFilter filter){paint.setColorFilter(filter);invalidateSelf();}
|
|
286
|
+
@Override public int getOpacity(){return PixelFormat.TRANSLUCENT;}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.view.View;
|
|
4
|
+
import android.view.WindowInsets;
|
|
5
|
+
import android.view.WindowInsetsAnimation;
|
|
6
|
+
import java.util.List;
|
|
7
|
+
|
|
8
|
+
/** Keeps a view above the keyboard while the IME animates. All frame updates stay in native code. */
|
|
9
|
+
public final class KeyboardFollower extends WindowInsetsAnimation.Callback {
|
|
10
|
+
public interface Listener {
|
|
11
|
+
void onStart();
|
|
12
|
+
void onSettled(float translation);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private final View view;
|
|
16
|
+
private final float offset;
|
|
17
|
+
private final Listener listener;
|
|
18
|
+
private float visualBottom;
|
|
19
|
+
private float from;
|
|
20
|
+
private float to;
|
|
21
|
+
private boolean tracking;
|
|
22
|
+
|
|
23
|
+
public KeyboardFollower(View view, float offset, Listener listener) {
|
|
24
|
+
super(DISPATCH_MODE_CONTINUE_ON_SUBTREE);
|
|
25
|
+
this.view = view;
|
|
26
|
+
this.offset = offset;
|
|
27
|
+
this.listener = listener;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public void attach() {
|
|
31
|
+
view.setWindowInsetsAnimationCallback(this);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public void detach() {
|
|
35
|
+
tracking = false;
|
|
36
|
+
view.setWindowInsetsAnimationCallback(null);
|
|
37
|
+
view.setTranslationY(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private static boolean keyboard(WindowInsetsAnimation animation) {
|
|
41
|
+
return (animation.getTypeMask() & WindowInsets.Type.ime()) != 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private float bottom() {
|
|
45
|
+
int[] location = new int[2];
|
|
46
|
+
view.getLocationInWindow(location);
|
|
47
|
+
return location[1] + view.getHeight();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Override
|
|
51
|
+
public void onPrepare(WindowInsetsAnimation animation) {
|
|
52
|
+
if (!keyboard(animation)) return;
|
|
53
|
+
visualBottom = bottom();
|
|
54
|
+
tracking = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Override
|
|
58
|
+
public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) {
|
|
59
|
+
if (!keyboard(animation) || !tracking) return bounds;
|
|
60
|
+
listener.onStart();
|
|
61
|
+
// The end state is laid out now; the view may already have moved with the layout.
|
|
62
|
+
float untranslated = bottom() - view.getTranslationY();
|
|
63
|
+
WindowInsets insets = view.getRootWindowInsets();
|
|
64
|
+
int height = insets == null ? 0 : insets.getInsets(WindowInsets.Type.ime()).bottom;
|
|
65
|
+
float top = view.getRootView().getHeight() - height;
|
|
66
|
+
to = -Math.max(0, untranslated + offset - top);
|
|
67
|
+
from = visualBottom - untranslated;
|
|
68
|
+
view.setTranslationY(from);
|
|
69
|
+
return bounds;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Override
|
|
73
|
+
public WindowInsets onProgress(WindowInsets insets, List<WindowInsetsAnimation> running) {
|
|
74
|
+
if (!tracking) return insets;
|
|
75
|
+
for (WindowInsetsAnimation animation : running) {
|
|
76
|
+
if (keyboard(animation)) {
|
|
77
|
+
view.setTranslationY(from + (to - from) * animation.getInterpolatedFraction());
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return insets;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Override
|
|
85
|
+
public void onEnd(WindowInsetsAnimation animation) {
|
|
86
|
+
if (!keyboard(animation) || !tracking) return;
|
|
87
|
+
tracking = false;
|
|
88
|
+
view.setTranslationY(to);
|
|
89
|
+
listener.onSettled(to);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.view.View;
|
|
4
|
+
import android.view.ViewTreeObserver;
|
|
5
|
+
import android.view.WindowInsets;
|
|
6
|
+
import android.view.WindowInsetsAnimation;
|
|
7
|
+
import android.view.animation.Interpolator;
|
|
8
|
+
import java.util.List;
|
|
9
|
+
|
|
10
|
+
/** Reports how much of a window the keyboard covers, and how it gets there. Moves nothing. */
|
|
11
|
+
public final class KeyboardObserver extends WindowInsetsAnimation.Callback
|
|
12
|
+
implements ViewTreeObserver.OnGlobalLayoutListener {
|
|
13
|
+
public interface Listener {
|
|
14
|
+
void onChange(int height, long duration, float[] curve);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private static final int SAMPLES = 32;
|
|
18
|
+
|
|
19
|
+
private final View root;
|
|
20
|
+
private final Listener listener;
|
|
21
|
+
private int height;
|
|
22
|
+
private boolean animating;
|
|
23
|
+
|
|
24
|
+
public KeyboardObserver(View root, Listener listener) {
|
|
25
|
+
super(DISPATCH_MODE_CONTINUE_ON_SUBTREE);
|
|
26
|
+
this.root = root;
|
|
27
|
+
this.listener = listener;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public void attach() {
|
|
31
|
+
height = covered();
|
|
32
|
+
root.setWindowInsetsAnimationCallback(this);
|
|
33
|
+
root.getViewTreeObserver().addOnGlobalLayoutListener(this);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public void detach() {
|
|
37
|
+
root.setWindowInsetsAnimationCallback(null);
|
|
38
|
+
root.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public int height() {
|
|
42
|
+
return height;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private int covered() {
|
|
46
|
+
WindowInsets insets = root.getRootWindowInsets();
|
|
47
|
+
return insets == null ? 0 : insets.getInsets(WindowInsets.Type.ime()).bottom;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static boolean keyboard(WindowInsetsAnimation animation) {
|
|
51
|
+
return (animation.getTypeMask() & WindowInsets.Type.ime()) != 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static float[] sample(Interpolator interpolator) {
|
|
55
|
+
if (interpolator == null) return null;
|
|
56
|
+
float[] curve = new float[SAMPLES + 1];
|
|
57
|
+
for (int i = 0; i <= SAMPLES; i++) curve[i] = interpolator.getInterpolation(i / (float) SAMPLES);
|
|
58
|
+
return curve;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private void report(int next, long duration, float[] curve) {
|
|
62
|
+
if (next == height) return;
|
|
63
|
+
height = next;
|
|
64
|
+
listener.onChange(next, duration, curve);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Override
|
|
68
|
+
public void onPrepare(WindowInsetsAnimation animation) {
|
|
69
|
+
// The layout for the end state comes before onStart; let onStart report it with its timing.
|
|
70
|
+
if (keyboard(animation)) animating = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Override
|
|
74
|
+
public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) {
|
|
75
|
+
if (!keyboard(animation)) return bounds;
|
|
76
|
+
// The end state is already in the root insets when the animation starts.
|
|
77
|
+
report(covered(), animation.getDurationMillis(), sample(animation.getInterpolator()));
|
|
78
|
+
return bounds;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Override
|
|
82
|
+
public WindowInsets onProgress(WindowInsets insets, List<WindowInsetsAnimation> running) {
|
|
83
|
+
return insets;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Override
|
|
87
|
+
public void onEnd(WindowInsetsAnimation animation) {
|
|
88
|
+
if (!keyboard(animation)) return;
|
|
89
|
+
animating = false;
|
|
90
|
+
report(covered(), 0, null);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Override
|
|
94
|
+
public void onGlobalLayout() {
|
|
95
|
+
if (!animating) report(covered(), 0, null);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -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
|
+
}
|