@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.
Files changed (105) hide show
  1. package/README.md +24 -1
  2. package/dist/animate.d.ts +3 -2
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +11 -3
  5. package/dist/animate.js.map +1 -1
  6. package/dist/decoration.d.ts +18 -0
  7. package/dist/decoration.d.ts.map +1 -0
  8. package/dist/decoration.js +155 -0
  9. package/dist/decoration.js.map +1 -0
  10. package/dist/driver/android-color.d.ts +3 -0
  11. package/dist/driver/android-color.d.ts.map +1 -1
  12. package/dist/driver/android-color.js +20 -18
  13. package/dist/driver/android-color.js.map +1 -1
  14. package/dist/driver/android-decoration.d.ts +22 -0
  15. package/dist/driver/android-decoration.d.ts.map +1 -0
  16. package/dist/driver/android-decoration.js +139 -0
  17. package/dist/driver/android-decoration.js.map +1 -0
  18. package/dist/driver/android-playback.d.ts +13 -0
  19. package/dist/driver/android-playback.d.ts.map +1 -0
  20. package/dist/driver/android-playback.js +141 -0
  21. package/dist/driver/android-playback.js.map +1 -0
  22. package/dist/driver/android.d.ts.map +1 -1
  23. package/dist/driver/android.js +23 -36
  24. package/dist/driver/android.js.map +1 -1
  25. package/dist/driver/index.d.ts +7 -0
  26. package/dist/driver/index.d.ts.map +1 -1
  27. package/dist/driver/index.js.map +1 -1
  28. package/dist/driver/ios-color.d.ts +2 -0
  29. package/dist/driver/ios-color.d.ts.map +1 -1
  30. package/dist/driver/ios-color.js +19 -8
  31. package/dist/driver/ios-color.js.map +1 -1
  32. package/dist/driver/ios-decoration.d.ts +17 -0
  33. package/dist/driver/ios-decoration.d.ts.map +1 -0
  34. package/dist/driver/ios-decoration.js +271 -0
  35. package/dist/driver/ios-decoration.js.map +1 -0
  36. package/dist/driver/ios-playback.d.ts +10 -0
  37. package/dist/driver/ios-playback.d.ts.map +1 -0
  38. package/dist/driver/ios-playback.js +33 -0
  39. package/dist/driver/ios-playback.js.map +1 -0
  40. package/dist/driver/ios-shadow.d.ts +6 -0
  41. package/dist/driver/ios-shadow.d.ts.map +1 -0
  42. package/dist/driver/ios-shadow.js +52 -0
  43. package/dist/driver/ios-shadow.js.map +1 -0
  44. package/dist/driver/ios.d.ts.map +1 -1
  45. package/dist/driver/ios.js +75 -40
  46. package/dist/driver/ios.js.map +1 -1
  47. package/dist/engine.d.ts +48 -2
  48. package/dist/engine.d.ts.map +1 -1
  49. package/dist/engine.js +210 -18
  50. package/dist/engine.js.map +1 -1
  51. package/dist/haptics.d.ts +4 -1
  52. package/dist/haptics.d.ts.map +1 -1
  53. package/dist/haptics.js +27 -3
  54. package/dist/haptics.js.map +1 -1
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/playback-group.d.ts +6 -0
  58. package/dist/playback-group.d.ts.map +1 -0
  59. package/dist/playback-group.js +113 -0
  60. package/dist/playback-group.js.map +1 -0
  61. package/dist/playback.d.ts +25 -0
  62. package/dist/playback.d.ts.map +1 -0
  63. package/dist/playback.js +35 -0
  64. package/dist/playback.js.map +1 -0
  65. package/dist/properties.d.ts +2 -1
  66. package/dist/properties.d.ts.map +1 -1
  67. package/dist/properties.js +14 -1
  68. package/dist/properties.js.map +1 -1
  69. package/dist/sequence.d.ts.map +1 -1
  70. package/dist/sequence.js +6 -0
  71. package/dist/sequence.js.map +1 -1
  72. package/dist/track.d.ts.map +1 -1
  73. package/dist/track.js +10 -1
  74. package/dist/track.js.map +1 -1
  75. package/dist/transition.d.ts.map +1 -1
  76. package/dist/transition.js +4 -1
  77. package/dist/transition.js.map +1 -1
  78. package/dist/types.d.ts +15 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/package.json +1 -1
  81. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +2 -2
  82. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  83. package/platforms/android/proguard-rules.pro +1 -0
  84. package/src/animate.ts +18 -5
  85. package/src/decoration.ts +180 -0
  86. package/src/driver/android-color.ts +22 -20
  87. package/src/driver/android-decoration.ts +182 -0
  88. package/src/driver/android-playback.ts +157 -0
  89. package/src/driver/android.ts +43 -43
  90. package/src/driver/index.ts +13 -0
  91. package/src/driver/ios-color.ts +19 -10
  92. package/src/driver/ios-decoration.ts +332 -0
  93. package/src/driver/ios-playback.ts +37 -0
  94. package/src/driver/ios-shadow.ts +50 -0
  95. package/src/driver/ios.ts +85 -46
  96. package/src/engine.ts +220 -20
  97. package/src/haptics.ts +25 -2
  98. package/src/index.ts +1 -0
  99. package/src/playback-group.ts +110 -0
  100. package/src/playback.ts +62 -0
  101. package/src/properties.ts +18 -2
  102. package/src/sequence.ts +5 -0
  103. package/src/track.ts +9 -1
  104. package/src/transition.ts +4 -1
  105. package/src/types.ts +15 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafaelaugusto/jolt",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "Native animations for NativeScript: presets, springs, keyframes and stagger in one call.",
5
5
  "keywords": [
6
6
  "nativescript",
@@ -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
- private static final Field[] FIELDS = fields();
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
- private Drawable background(View nativeView) {
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
+ }
@@ -4,3 +4,4 @@
4
4
  -keepclassmembers class org.nativescript.widgets.BoxShadowDrawable {
5
5
  public android.graphics.drawable.Drawable getWrappedDrawable();
6
6
  }
7
+ -keep class org.rafaelaugusto.jolt.DecorationTarget { *; }
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
- ): Animation {
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
- return group(
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
- ): Animation {
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 group([])
168
- return group(views.map((view) => new ViewAnimation(view, plans)))
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
- animator.cancel()
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: () => (started ? track.delay + animator.getCurrentPlayTime() : 0),
139
+ elapsed: () => player?.elapsed() ?? 0,
140
+ control(time, paused, command) {
141
+ player?.control(time, paused, command)
142
+ },
131
143
  cancel() {
132
144
  cleanup()
133
- animator.cancel()
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.start()
159
+ player = androidPlayback(animator, track, () => {
160
+ if (!stopped) {
161
+ cleanup()
162
+ onEnd()
163
+ }
164
+ })
163
165
  } else {
164
166
  cleanup()
165
167
  onEnd(false)