@rafaelaugusto/jolt 0.1.0-beta.4 → 0.1.0-beta.5
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 +24 -1
- package/dist/animate.d.ts +3 -2
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +11 -3
- package/dist/animate.js.map +1 -1
- 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/driver/android-color.d.ts +3 -0
- package/dist/driver/android-color.d.ts.map +1 -1
- package/dist/driver/android-color.js +20 -18
- package/dist/driver/android-color.js.map +1 -1
- 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.map +1 -1
- package/dist/driver/android.js +23 -36
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +7 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +2 -0
- package/dist/driver/ios-color.d.ts.map +1 -1
- package/dist/driver/ios-color.js +19 -8
- package/dist/driver/ios-color.js.map +1 -1
- 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-playback.d.ts +10 -0
- package/dist/driver/ios-playback.d.ts.map +1 -0
- package/dist/driver/ios-playback.js +33 -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 +75 -40
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +48 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +210 -18
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +4 -1
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +27 -3
- package/dist/haptics.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/playback-group.d.ts +6 -0
- package/dist/playback-group.d.ts.map +1 -0
- package/dist/playback-group.js +113 -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/properties.d.ts +2 -1
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +14 -1
- package/dist/properties.js.map +1 -1
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +6 -0
- package/dist/sequence.js.map +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +10 -1
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +4 -1
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +2 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/proguard-rules.pro +1 -0
- package/src/animate.ts +18 -5
- package/src/decoration.ts +180 -0
- package/src/driver/android-color.ts +22 -20
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +43 -43
- package/src/driver/index.ts +13 -0
- package/src/driver/ios-color.ts +19 -10
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-playback.ts +37 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +85 -46
- package/src/engine.ts +220 -20
- package/src/haptics.ts +25 -2
- package/src/index.ts +1 -0
- package/src/playback-group.ts +110 -0
- package/src/playback.ts +62 -0
- package/src/properties.ts +18 -2
- package/src/sequence.ts +5 -0
- package/src/track.ts +9 -1
- package/src/transition.ts +4 -1
- package/src/types.ts +15 -0
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import java.lang.reflect.Method;
|
|
|
12
12
|
/** ObjectAnimator target. All frame updates stay in native code. */
|
|
13
13
|
public final class ColorTarget {
|
|
14
14
|
private static final String BORDER = "org.nativescript.widgets.BorderDrawable";
|
|
15
|
-
|
|
15
|
+
static final Field[] FIELDS = fields();
|
|
16
16
|
private static final String SHADOW = "org.nativescript.widgets.BoxShadowDrawable";
|
|
17
17
|
private static final Method UNWRAP = unwrapMethod();
|
|
18
18
|
private final WeakReference<View> view;
|
|
@@ -49,7 +49,7 @@ public final class ColorTarget {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
static Drawable background(View nativeView) {
|
|
53
53
|
Drawable drawable = nativeView.getBackground();
|
|
54
54
|
// Avoid reflective method lookups and exceptions in the frame loop.
|
|
55
55
|
if (drawable != null && drawable.getClass().getName().equals(SHADOW) && UNWRAP != null) {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.animation.StateListAnimator;
|
|
4
|
+
import android.graphics.drawable.Drawable;
|
|
5
|
+
import android.graphics.drawable.ColorDrawable;
|
|
6
|
+
import android.view.View;
|
|
7
|
+
import java.lang.ref.WeakReference;
|
|
8
|
+
import java.lang.reflect.Constructor;
|
|
9
|
+
import java.lang.reflect.Field;
|
|
10
|
+
import java.util.WeakHashMap;
|
|
11
|
+
|
|
12
|
+
/** ObjectAnimator's float target. No JavaScript enters the native frame loop. */
|
|
13
|
+
public final class DecorationTarget {
|
|
14
|
+
private static final String BORDER = "org.nativescript.widgets.BorderDrawable";
|
|
15
|
+
private static final Field[][] FIELDS = fields();
|
|
16
|
+
private static final Constructor<?> CONSTRUCTOR = constructor();
|
|
17
|
+
private static final WeakHashMap<View, BackgroundLease> BACKGROUNDS = new WeakHashMap<>();
|
|
18
|
+
private static final WeakHashMap<View, Lease> LEASES = new WeakHashMap<>();
|
|
19
|
+
private final WeakReference<View> view;
|
|
20
|
+
private final int property;
|
|
21
|
+
private final int borderColor;
|
|
22
|
+
private float value;
|
|
23
|
+
private boolean acquired;
|
|
24
|
+
|
|
25
|
+
// 0 = uniform radius, 1 = uniform width, 2 = elevation, 3 = translationZ.
|
|
26
|
+
public DecorationTarget(View view, int property, float value, int borderColor) {
|
|
27
|
+
this.view = new WeakReference<>(view);
|
|
28
|
+
this.property = property;
|
|
29
|
+
this.value = value;
|
|
30
|
+
this.borderColor = borderColor;
|
|
31
|
+
}
|
|
32
|
+
private static Field[][] fields() {
|
|
33
|
+
try {
|
|
34
|
+
Class<?> type = Class.forName(BORDER);
|
|
35
|
+
String[][] names = {
|
|
36
|
+
{"borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius"},
|
|
37
|
+
{"borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth"}
|
|
38
|
+
};
|
|
39
|
+
Field[][] result = new Field[2][4];
|
|
40
|
+
for (int i = 0; i < 2; i++) for (int j = 0; j < 4; j++) {
|
|
41
|
+
result[i][j] = type.getDeclaredField(names[i][j]);
|
|
42
|
+
result[i][j].setAccessible(true);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
} catch (ReflectiveOperationException | SecurityException exception) { return null; }
|
|
46
|
+
}
|
|
47
|
+
private static Constructor<?> constructor() {
|
|
48
|
+
try { return Class.forName(BORDER).getConstructor(float.class); }
|
|
49
|
+
catch (ReflectiveOperationException | SecurityException exception) { return null; }
|
|
50
|
+
}
|
|
51
|
+
public boolean isSupported() {
|
|
52
|
+
View nativeView = view.get();
|
|
53
|
+
if (nativeView == null) return false;
|
|
54
|
+
if (property >= 2) return true;
|
|
55
|
+
Drawable drawable = ColorTarget.background(nativeView);
|
|
56
|
+
return FIELDS != null && ((drawable != null && drawable.getClass().getName().equals(BORDER)) ||
|
|
57
|
+
((drawable == null || drawable.getClass() == ColorDrawable.class) && CONSTRUCTOR != null && ColorTarget.FIELDS != null));
|
|
58
|
+
}
|
|
59
|
+
public float getValue() { return value; }
|
|
60
|
+
public void setValue(float next) {
|
|
61
|
+
View nativeView = view.get();
|
|
62
|
+
if (nativeView == null) return;
|
|
63
|
+
value = next;
|
|
64
|
+
if (property >= 2) suspendStateAnimator(nativeView);
|
|
65
|
+
if (property == 2) { nativeView.setElevation(next); return; }
|
|
66
|
+
if (property == 3) { nativeView.setTranslationZ(next); return; }
|
|
67
|
+
ensureBackground(nativeView);
|
|
68
|
+
Drawable drawable = ColorTarget.background(nativeView);
|
|
69
|
+
if (drawable == null || !drawable.getClass().getName().equals(BORDER) || FIELDS == null) return;
|
|
70
|
+
try {
|
|
71
|
+
// Core rebuilds the paths from these fields in draw() and getOutline().
|
|
72
|
+
// Keep its bitmap, gradient, color and all other state on the same drawable.
|
|
73
|
+
for (Field field : FIELDS[property]) field.setFloat(drawable, next);
|
|
74
|
+
drawable.invalidateSelf();
|
|
75
|
+
nativeView.invalidateOutline();
|
|
76
|
+
} catch (IllegalAccessException exception) { /* Keep Core's final model. */ }
|
|
77
|
+
}
|
|
78
|
+
public void acquire() {
|
|
79
|
+
View nativeView = view.get();
|
|
80
|
+
if (nativeView == null || acquired) return;
|
|
81
|
+
acquired = true;
|
|
82
|
+
if (property < 2) {
|
|
83
|
+
BackgroundLease lease = BACKGROUNDS.get(nativeView);
|
|
84
|
+
if (lease == null) { lease = new BackgroundLease(); BACKGROUNDS.put(nativeView, lease); }
|
|
85
|
+
lease.count++;
|
|
86
|
+
ensureBackground(nativeView);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
Lease lease = LEASES.get(nativeView);
|
|
90
|
+
if (lease == null) {
|
|
91
|
+
lease = new Lease(nativeView.getStateListAnimator());
|
|
92
|
+
LEASES.put(nativeView, lease);
|
|
93
|
+
nativeView.setStateListAnimator(null);
|
|
94
|
+
}
|
|
95
|
+
lease.count++;
|
|
96
|
+
suspendStateAnimator(nativeView);
|
|
97
|
+
}
|
|
98
|
+
public void release() {
|
|
99
|
+
View nativeView = view.get();
|
|
100
|
+
if (!acquired || nativeView == null) return;
|
|
101
|
+
acquired = false;
|
|
102
|
+
if (property < 2) {
|
|
103
|
+
BackgroundLease lease = BACKGROUNDS.get(nativeView);
|
|
104
|
+
if (lease != null && --lease.count == 0) {
|
|
105
|
+
BACKGROUNDS.remove(nativeView);
|
|
106
|
+
if (lease.drawable != null && nativeView.getBackground() == lease.drawable) nativeView.setBackground(lease.original);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
suspendStateAnimator(nativeView);
|
|
111
|
+
Lease lease = LEASES.get(nativeView);
|
|
112
|
+
if (lease != null && --lease.count == 0) {
|
|
113
|
+
LEASES.remove(nativeView);
|
|
114
|
+
if (nativeView.getStateListAnimator() == null) {
|
|
115
|
+
float elevation = nativeView.getElevation(), z = nativeView.getTranslationZ();
|
|
116
|
+
nativeView.setStateListAnimator(lease.animator);
|
|
117
|
+
if (lease.animator != null) lease.animator.jumpToCurrentState();
|
|
118
|
+
nativeView.setElevation(elevation);
|
|
119
|
+
nativeView.setTranslationZ(z);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
private void ensureBackground(View nativeView) {
|
|
124
|
+
BackgroundLease lease = BACKGROUNDS.get(nativeView);
|
|
125
|
+
Drawable original = nativeView.getBackground();
|
|
126
|
+
if (lease == null || (original != null && original.getClass() != ColorDrawable.class) || CONSTRUCTOR == null || ColorTarget.FIELDS == null) return;
|
|
127
|
+
try {
|
|
128
|
+
Drawable drawable = (Drawable) CONSTRUCTOR.newInstance(nativeView.getResources().getDisplayMetrics().density);
|
|
129
|
+
ColorTarget.FIELDS[0].setInt(drawable, original == null ? 0 : ((ColorDrawable) original).getColor());
|
|
130
|
+
for (int i = 1; i < ColorTarget.FIELDS.length; i++) ColorTarget.FIELDS[i].setInt(drawable, borderColor);
|
|
131
|
+
lease.original = original;
|
|
132
|
+
lease.drawable = drawable;
|
|
133
|
+
nativeView.setBackground(drawable);
|
|
134
|
+
} catch (ReflectiveOperationException exception) { /* Unsupported Core. */ }
|
|
135
|
+
}
|
|
136
|
+
private static final class BackgroundLease {
|
|
137
|
+
Drawable original, drawable;
|
|
138
|
+
int count;
|
|
139
|
+
}
|
|
140
|
+
private static void suspendStateAnimator(View view) {
|
|
141
|
+
Lease lease = LEASES.get(view);
|
|
142
|
+
StateListAnimator current = view.getStateListAnimator();
|
|
143
|
+
if (lease != null && current != null) {
|
|
144
|
+
// A Core model write may have installed an updated state animator.
|
|
145
|
+
lease.animator = current;
|
|
146
|
+
view.setStateListAnimator(null);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
private static final class Lease {
|
|
150
|
+
StateListAnimator animator;
|
|
151
|
+
int count;
|
|
152
|
+
Lease(StateListAnimator animator) { this.animator = animator; }
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/animate.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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'
|
|
1
8
|
import { View } from '@nativescript/core'
|
|
2
9
|
import {
|
|
3
10
|
isColorProperty,
|
|
@@ -57,7 +64,7 @@ export function animate(
|
|
|
57
64
|
target: Target | readonly Target[],
|
|
58
65
|
animation: Keyframes | PresetName,
|
|
59
66
|
options?: AnimateOptions | SpringName,
|
|
60
|
-
):
|
|
67
|
+
): PlaybackAnimation {
|
|
61
68
|
const { keyframes, base, overrides, stagger, relative, haptic } = prepare(
|
|
62
69
|
animation,
|
|
63
70
|
options,
|
|
@@ -68,6 +75,7 @@ export function animate(
|
|
|
68
75
|
const view = toView(target)
|
|
69
76
|
for (const property of Object.keys(keyframes))
|
|
70
77
|
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
78
|
+
else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
71
79
|
return new ViewAnimation(
|
|
72
80
|
view,
|
|
73
81
|
plan(keyframes, base, overrides, 0, relative),
|
|
@@ -79,7 +87,8 @@ export function animate(
|
|
|
79
87
|
for (const view of views)
|
|
80
88
|
for (const property of Object.keys(keyframes))
|
|
81
89
|
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
82
|
-
|
|
90
|
+
else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
91
|
+
return playbackGroup(
|
|
83
92
|
views.map(
|
|
84
93
|
(view, index) =>
|
|
85
94
|
new ViewAnimation(
|
|
@@ -92,6 +101,7 @@ export function animate(
|
|
|
92
101
|
relative,
|
|
93
102
|
),
|
|
94
103
|
feedback,
|
|
104
|
+
true,
|
|
95
105
|
),
|
|
96
106
|
),
|
|
97
107
|
)
|
|
@@ -102,7 +112,7 @@ export function decay(
|
|
|
102
112
|
target: Target | readonly Target[],
|
|
103
113
|
velocity: DecayVelocity,
|
|
104
114
|
options: DecayOptions = {},
|
|
105
|
-
):
|
|
115
|
+
): PlaybackAnimation {
|
|
106
116
|
const friction = options.friction ?? DEFAULT_FRICTION
|
|
107
117
|
if (!Number.isFinite(friction) || friction <= 0 || friction >= 1) {
|
|
108
118
|
throw new RangeError('Decay friction must be between 0 and 1 exclusively.')
|
|
@@ -164,8 +174,10 @@ export function decay(
|
|
|
164
174
|
]
|
|
165
175
|
})
|
|
166
176
|
const views = toViews(target)
|
|
167
|
-
if (plans.length === 0) return
|
|
168
|
-
return
|
|
177
|
+
if (plans.length === 0) return playbackGroup([])
|
|
178
|
+
return playbackGroup(
|
|
179
|
+
views.map((view) => new ViewAnimation(view, plans, undefined, true)),
|
|
180
|
+
)
|
|
169
181
|
}
|
|
170
182
|
|
|
171
183
|
/** Stops all or selected properties at their current values. `scale` stops both axes. */
|
|
@@ -339,6 +351,7 @@ function toKeyframes(
|
|
|
339
351
|
) {
|
|
340
352
|
throw new Error(`Invalid keyframes for "${property}": ${JSON.stringify(value)}.`)
|
|
341
353
|
}
|
|
354
|
+
if (isDecorationProperty(property)) validateDecorationFrames(property, frames)
|
|
342
355
|
return frames
|
|
343
356
|
}
|
|
344
357
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { preserveZeroShadow } from './driver/ios-shadow.js'
|
|
2
|
+
import { Color, Utils, isAndroid, type View } from '@nativescript/core'
|
|
3
|
+
|
|
4
|
+
type Shadow = Exclude<View['boxShadow'], string>[number]
|
|
5
|
+
export type DecorationProperty =
|
|
6
|
+
| 'borderRadius'
|
|
7
|
+
| 'borderWidth'
|
|
8
|
+
| 'shadowOpacity'
|
|
9
|
+
| 'shadowRadius'
|
|
10
|
+
| 'shadowOffsetX'
|
|
11
|
+
| 'shadowOffsetY'
|
|
12
|
+
| 'elevation'
|
|
13
|
+
| 'translationZ'
|
|
14
|
+
|
|
15
|
+
export const decorationProperties: readonly DecorationProperty[] = [
|
|
16
|
+
'borderRadius',
|
|
17
|
+
'borderWidth',
|
|
18
|
+
'shadowOpacity',
|
|
19
|
+
'shadowRadius',
|
|
20
|
+
'shadowOffsetX',
|
|
21
|
+
'shadowOffsetY',
|
|
22
|
+
'elevation',
|
|
23
|
+
'translationZ',
|
|
24
|
+
]
|
|
25
|
+
export const radiusKeys = [
|
|
26
|
+
'borderTopLeftRadius',
|
|
27
|
+
'borderTopRightRadius',
|
|
28
|
+
'borderBottomRightRadius',
|
|
29
|
+
'borderBottomLeftRadius',
|
|
30
|
+
] as const
|
|
31
|
+
export const widthKeys = [
|
|
32
|
+
'borderTopWidth',
|
|
33
|
+
'borderRightWidth',
|
|
34
|
+
'borderBottomWidth',
|
|
35
|
+
'borderLeftWidth',
|
|
36
|
+
] as const
|
|
37
|
+
const writing = new WeakSet<View>()
|
|
38
|
+
export const isWritingDecoration = (view: View) => writing.has(view)
|
|
39
|
+
export const isDecorationProperty = (key: string): key is DecorationProperty =>
|
|
40
|
+
decorationProperties.includes(key as DecorationProperty)
|
|
41
|
+
|
|
42
|
+
/** Core lengths may be numbers, px strings or parsed { value, unit } lengths. */
|
|
43
|
+
export function dip(value: unknown): number {
|
|
44
|
+
if (value === undefined || value === null) return 0
|
|
45
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
const match = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(dip|px)?$/.exec(value.trim())
|
|
48
|
+
if (match)
|
|
49
|
+
return Number(match[1]) / (match[2] === 'px' ? Utils.layout.getDisplayDensity() : 1)
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'object' && 'value' in value && 'unit' in value) {
|
|
52
|
+
const length = value as { value: number; unit: string }
|
|
53
|
+
if (Number.isFinite(length.value) && (length.unit === 'dip' || length.unit === 'px'))
|
|
54
|
+
return length.value / (length.unit === 'px' ? Utils.layout.getDisplayDensity() : 1)
|
|
55
|
+
}
|
|
56
|
+
throw new TypeError('Decoration animations require finite DIP or px lengths.')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function singleShadow(view: View): Shadow {
|
|
60
|
+
const shadows = view.boxShadow
|
|
61
|
+
if (!Array.isArray(shadows) || shadows.length !== 1 || shadows[0]!.inset)
|
|
62
|
+
throw new TypeError('Shadow animation requires one existing outer boxShadow.')
|
|
63
|
+
return shadows[0]!
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateDecorationTarget(view: View, property: DecorationProperty) {
|
|
67
|
+
const shadow = property.startsWith('shadow')
|
|
68
|
+
const z = property === 'elevation' || property === 'translationZ'
|
|
69
|
+
if ((shadow && isAndroid) || (z && !isAndroid))
|
|
70
|
+
throw new TypeError(`"${property}" is only supported on ${z ? 'Android' : 'iOS'}.`)
|
|
71
|
+
if (z) return
|
|
72
|
+
if (view.style.clipPath || view.style.cornerShape === 'squircle')
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
'Decoration animations require circular corners without a clipPath.',
|
|
75
|
+
)
|
|
76
|
+
for (const keys of [radiusKeys, widthKeys]) {
|
|
77
|
+
const values = keys.map((key) => dip(view[key]))
|
|
78
|
+
if (values.some((value) => value !== values[0]))
|
|
79
|
+
throw new TypeError(
|
|
80
|
+
'Decoration animations require uniform border widths and radii.',
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
const colors = [
|
|
84
|
+
view.borderTopColor,
|
|
85
|
+
view.borderRightColor,
|
|
86
|
+
view.borderBottomColor,
|
|
87
|
+
view.borderLeftColor,
|
|
88
|
+
].map((color) => color?.argb ?? 0)
|
|
89
|
+
if (colors.some((color) => color !== colors[0]))
|
|
90
|
+
throw new TypeError('Decoration animations require uniform border colors.')
|
|
91
|
+
if (shadow) {
|
|
92
|
+
const value = singleShadow(view)
|
|
93
|
+
for (const length of [
|
|
94
|
+
value.offsetX,
|
|
95
|
+
value.offsetY,
|
|
96
|
+
value.blurRadius,
|
|
97
|
+
value.spreadRadius,
|
|
98
|
+
])
|
|
99
|
+
dip(length)
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
property === 'borderRadius' &&
|
|
103
|
+
isAndroid &&
|
|
104
|
+
view.boxShadow &&
|
|
105
|
+
(!Array.isArray(view.boxShadow) || view.boxShadow.length)
|
|
106
|
+
)
|
|
107
|
+
throw new TypeError(
|
|
108
|
+
'Animate Android radius with elevation shadows; CSS boxShadow caches its shape.',
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function validateDecorationFrames(
|
|
113
|
+
property: DecorationProperty,
|
|
114
|
+
frames: readonly (number | null)[],
|
|
115
|
+
) {
|
|
116
|
+
if (
|
|
117
|
+
frames.some((value) => value !== null && clampDecoration(property, value) !== value)
|
|
118
|
+
)
|
|
119
|
+
throw new RangeError(`Invalid keyframes for "${property}".`)
|
|
120
|
+
}
|
|
121
|
+
export function clampDecoration(property: DecorationProperty, value: number): number {
|
|
122
|
+
if (property === 'shadowOpacity') return Math.max(0, Math.min(1, value))
|
|
123
|
+
return property === 'borderRadius' ||
|
|
124
|
+
property === 'borderWidth' ||
|
|
125
|
+
property === 'shadowRadius' ||
|
|
126
|
+
property === 'elevation'
|
|
127
|
+
? Math.max(0, value)
|
|
128
|
+
: value
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function readDecoration(view: View, property: DecorationProperty): number {
|
|
132
|
+
if (property === 'borderRadius') {
|
|
133
|
+
const radius = !isAndroid ? view.nativeViewProtected?.layer?.cornerRadius : undefined
|
|
134
|
+
return typeof radius === 'number' ? radius : dip(view.borderTopLeftRadius)
|
|
135
|
+
}
|
|
136
|
+
if (property === 'borderWidth') return dip(view.borderTopWidth)
|
|
137
|
+
if (property === 'elevation')
|
|
138
|
+
return view.nativeViewProtected?.getElevation
|
|
139
|
+
? view.nativeViewProtected.getElevation() / Utils.layout.getDisplayDensity()
|
|
140
|
+
: view.androidElevation || 0
|
|
141
|
+
if (property === 'translationZ')
|
|
142
|
+
return (
|
|
143
|
+
view.nativeViewProtected?.getTranslationZ?.() / Utils.layout.getDisplayDensity() ||
|
|
144
|
+
0
|
|
145
|
+
)
|
|
146
|
+
const shadow = singleShadow(view)
|
|
147
|
+
if (property === 'shadowOpacity')
|
|
148
|
+
return ((shadow.color?.argb ?? 0xff000000) >>> 24) / 255
|
|
149
|
+
return property === 'shadowRadius'
|
|
150
|
+
? dip(shadow.blurRadius) / 2
|
|
151
|
+
: dip(property === 'shadowOffsetX' ? shadow.offsetX : shadow.offsetY)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function writeDecoration(view: View, property: DecorationProperty, raw: number) {
|
|
155
|
+
const value = clampDecoration(property, raw)
|
|
156
|
+
writing.add(view)
|
|
157
|
+
try {
|
|
158
|
+
if (property === 'borderRadius' || property === 'borderWidth') view[property] = value
|
|
159
|
+
else if (property === 'elevation') view.androidElevation = value
|
|
160
|
+
else if (property === 'translationZ')
|
|
161
|
+
view.nativeViewProtected?.setTranslationZ?.(
|
|
162
|
+
value * Utils.layout.getDisplayDensity(),
|
|
163
|
+
)
|
|
164
|
+
else {
|
|
165
|
+
const shadow = { ...singleShadow(view) }
|
|
166
|
+
if (property === 'shadowOpacity')
|
|
167
|
+
shadow.color = new Color(
|
|
168
|
+
((Math.round(value * 255) << 24) | ((shadow.color?.argb ?? 0) & 0xffffff)) >>>
|
|
169
|
+
0,
|
|
170
|
+
)
|
|
171
|
+
else if (property === 'shadowRadius') shadow.blurRadius = value * 2
|
|
172
|
+
else if (property === 'shadowOffsetX') shadow.offsetX = value
|
|
173
|
+
else shadow.offsetY = value
|
|
174
|
+
view.boxShadow = [shadow]
|
|
175
|
+
if (!isAndroid) preserveZeroShadow(view)
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
writing.delete(view)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type View } from '@nativescript/core'
|
|
2
2
|
import { readColor, type ColorProperty } from '../color.js'
|
|
3
|
+
import { androidPlayback, type AndroidPlayback } from './android-playback.js'
|
|
4
|
+
import type { PlaybackCommand } from '../playback.js'
|
|
3
5
|
import type { Track } from '../track.js'
|
|
4
6
|
|
|
5
7
|
interface NativeTarget {
|
|
@@ -8,12 +10,18 @@ interface NativeTarget {
|
|
|
8
10
|
isSupported(): boolean
|
|
9
11
|
}
|
|
10
12
|
interface ColorHandle {
|
|
13
|
+
control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
|
|
11
14
|
cancel(): void
|
|
12
15
|
elapsed(): number
|
|
13
16
|
target: NativeTarget
|
|
14
17
|
}
|
|
15
18
|
const playing = new WeakMap<View, Map<ColorProperty, ColorHandle>>()
|
|
16
19
|
|
|
20
|
+
export function syncAndroidColor(view: View) {
|
|
21
|
+
for (const handle of playing.get(view)?.values() ?? [])
|
|
22
|
+
handle.target.setColor(handle.target.getColor())
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
export function presentedAndroidColor(view: View, property: ColorProperty): number {
|
|
18
26
|
const active = playing.get(view)?.get(property)
|
|
19
27
|
if (active) return active.target.getColor() >>> 0
|
|
@@ -62,6 +70,7 @@ export function playAndroidColor(
|
|
|
62
70
|
onEnd(false)
|
|
63
71
|
return {
|
|
64
72
|
cancel() {},
|
|
73
|
+
control() {},
|
|
65
74
|
elapsed: () => 0,
|
|
66
75
|
target: {
|
|
67
76
|
getColor: () => readColor(view, property),
|
|
@@ -98,7 +107,6 @@ export function playAndroidColor(
|
|
|
98
107
|
)
|
|
99
108
|
}
|
|
100
109
|
let stopped = false,
|
|
101
|
-
started = false,
|
|
102
110
|
queued = false
|
|
103
111
|
const cleanup = () => {
|
|
104
112
|
if (stopped) return
|
|
@@ -111,10 +119,11 @@ export function playAndroidColor(
|
|
|
111
119
|
for (const name of changes) view.style.off(name, invalidate)
|
|
112
120
|
if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
|
|
113
121
|
}
|
|
122
|
+
let player: AndroidPlayback | undefined
|
|
114
123
|
const cancel = () => {
|
|
115
124
|
if (stopped) return
|
|
116
125
|
cleanup()
|
|
117
|
-
|
|
126
|
+
player?.cancel()
|
|
118
127
|
onEnd(false)
|
|
119
128
|
}
|
|
120
129
|
const invalidate = () => {
|
|
@@ -127,30 +136,18 @@ export function playAndroidColor(
|
|
|
127
136
|
}
|
|
128
137
|
const handle: ColorHandle = {
|
|
129
138
|
target,
|
|
130
|
-
elapsed: () => (
|
|
139
|
+
elapsed: () => player?.elapsed() ?? 0,
|
|
140
|
+
control(time, paused, command) {
|
|
141
|
+
player?.control(time, paused, command)
|
|
142
|
+
},
|
|
131
143
|
cancel() {
|
|
132
144
|
cleanup()
|
|
133
|
-
|
|
145
|
+
player?.cancel()
|
|
134
146
|
},
|
|
135
147
|
}
|
|
136
148
|
let entries = playing.get(view)
|
|
137
149
|
if (!entries) playing.set(view, (entries = new Map()))
|
|
138
150
|
entries.set(property, handle)
|
|
139
|
-
animator.addListener(
|
|
140
|
-
new android.animation.Animator.AnimatorListener({
|
|
141
|
-
onAnimationStart() {
|
|
142
|
-
started = true
|
|
143
|
-
},
|
|
144
|
-
onAnimationRepeat() {},
|
|
145
|
-
onAnimationCancel() {},
|
|
146
|
-
onAnimationEnd() {
|
|
147
|
-
if (!stopped) {
|
|
148
|
-
cleanup()
|
|
149
|
-
onEnd()
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
}),
|
|
153
|
-
)
|
|
154
151
|
view.on('unloaded', cancel)
|
|
155
152
|
if (property === 'color') {
|
|
156
153
|
view.on('textChange', invalidate)
|
|
@@ -159,7 +156,12 @@ export function playAndroidColor(
|
|
|
159
156
|
for (const name of changes) view.style.on(name, invalidate)
|
|
160
157
|
if (target.isSupported()) {
|
|
161
158
|
target.setColor(track.values[0]! | 0)
|
|
162
|
-
animator
|
|
159
|
+
player = androidPlayback(animator, track, () => {
|
|
160
|
+
if (!stopped) {
|
|
161
|
+
cleanup()
|
|
162
|
+
onEnd()
|
|
163
|
+
}
|
|
164
|
+
})
|
|
163
165
|
} else {
|
|
164
166
|
cleanup()
|
|
165
167
|
onEnd(false)
|