@rafaelaugusto/jolt 0.1.0-beta.3 → 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 (132) hide show
  1. package/README.md +78 -13
  2. package/dist/animate.d.ts +3 -2
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +35 -5
  5. package/dist/animate.js.map +1 -1
  6. package/dist/color.d.ts +11 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +73 -0
  9. package/dist/color.js.map +1 -0
  10. package/dist/decoration.d.ts +18 -0
  11. package/dist/decoration.d.ts.map +1 -0
  12. package/dist/decoration.js +155 -0
  13. package/dist/decoration.js.map +1 -0
  14. package/dist/driver/android-color.d.ts +20 -0
  15. package/dist/driver/android-color.d.ts.map +1 -0
  16. package/dist/driver/android-color.js +136 -0
  17. package/dist/driver/android-color.js.map +1 -0
  18. package/dist/driver/android-decoration.d.ts +22 -0
  19. package/dist/driver/android-decoration.d.ts.map +1 -0
  20. package/dist/driver/android-decoration.js +139 -0
  21. package/dist/driver/android-decoration.js.map +1 -0
  22. package/dist/driver/android-playback.d.ts +13 -0
  23. package/dist/driver/android-playback.d.ts.map +1 -0
  24. package/dist/driver/android-playback.js +141 -0
  25. package/dist/driver/android-playback.js.map +1 -0
  26. package/dist/driver/android.d.ts.map +1 -1
  27. package/dist/driver/android.js +28 -35
  28. package/dist/driver/android.js.map +1 -1
  29. package/dist/driver/index.d.ts +8 -1
  30. package/dist/driver/index.d.ts.map +1 -1
  31. package/dist/driver/index.js.map +1 -1
  32. package/dist/driver/ios-color.d.ts +16 -0
  33. package/dist/driver/ios-color.d.ts.map +1 -0
  34. package/dist/driver/ios-color.js +392 -0
  35. package/dist/driver/ios-color.js.map +1 -0
  36. package/dist/driver/ios-decoration.d.ts +17 -0
  37. package/dist/driver/ios-decoration.d.ts.map +1 -0
  38. package/dist/driver/ios-decoration.js +271 -0
  39. package/dist/driver/ios-decoration.js.map +1 -0
  40. package/dist/driver/ios-playback.d.ts +10 -0
  41. package/dist/driver/ios-playback.d.ts.map +1 -0
  42. package/dist/driver/ios-playback.js +33 -0
  43. package/dist/driver/ios-playback.js.map +1 -0
  44. package/dist/driver/ios-shadow.d.ts +6 -0
  45. package/dist/driver/ios-shadow.d.ts.map +1 -0
  46. package/dist/driver/ios-shadow.js +52 -0
  47. package/dist/driver/ios-shadow.js.map +1 -0
  48. package/dist/driver/ios.d.ts.map +1 -1
  49. package/dist/driver/ios.js +78 -35
  50. package/dist/driver/ios.js.map +1 -1
  51. package/dist/engine.d.ts +48 -2
  52. package/dist/engine.d.ts.map +1 -1
  53. package/dist/engine.js +217 -21
  54. package/dist/engine.js.map +1 -1
  55. package/dist/haptics.d.ts +4 -1
  56. package/dist/haptics.d.ts.map +1 -1
  57. package/dist/haptics.js +27 -3
  58. package/dist/haptics.js.map +1 -1
  59. package/dist/index.d.ts +4 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/liquid-grid.d.ts +3 -0
  64. package/dist/liquid-grid.d.ts.map +1 -0
  65. package/dist/liquid-grid.js +24 -0
  66. package/dist/liquid-grid.js.map +1 -0
  67. package/dist/liquid.d.ts +18 -0
  68. package/dist/liquid.d.ts.map +1 -0
  69. package/dist/liquid.js +229 -0
  70. package/dist/liquid.js.map +1 -0
  71. package/dist/playback-group.d.ts +6 -0
  72. package/dist/playback-group.d.ts.map +1 -0
  73. package/dist/playback-group.js +113 -0
  74. package/dist/playback-group.js.map +1 -0
  75. package/dist/playback.d.ts +25 -0
  76. package/dist/playback.d.ts.map +1 -0
  77. package/dist/playback.js +35 -0
  78. package/dist/playback.js.map +1 -0
  79. package/dist/press.d.ts.map +1 -1
  80. package/dist/press.js +6 -1
  81. package/dist/press.js.map +1 -1
  82. package/dist/properties.d.ts +5 -2
  83. package/dist/properties.d.ts.map +1 -1
  84. package/dist/properties.js +25 -1
  85. package/dist/properties.js.map +1 -1
  86. package/dist/sequence.d.ts.map +1 -1
  87. package/dist/sequence.js +6 -0
  88. package/dist/sequence.js.map +1 -1
  89. package/dist/text.d.ts.map +1 -1
  90. package/dist/text.js +3 -0
  91. package/dist/text.js.map +1 -1
  92. package/dist/track.d.ts +1 -1
  93. package/dist/track.d.ts.map +1 -1
  94. package/dist/track.js +48 -3
  95. package/dist/track.js.map +1 -1
  96. package/dist/transition.d.ts.map +1 -1
  97. package/dist/transition.js +11 -2
  98. package/dist/transition.js.map +1 -1
  99. package/dist/types.d.ts +20 -0
  100. package/dist/types.d.ts.map +1 -1
  101. package/package.json +11 -2
  102. package/platforms/android/include.gradle +7 -0
  103. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  104. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  105. package/platforms/android/proguard-rules.pro +7 -0
  106. package/src/animate.ts +48 -9
  107. package/src/color.ts +101 -0
  108. package/src/decoration.ts +180 -0
  109. package/src/driver/android-color.ts +170 -0
  110. package/src/driver/android-decoration.ts +182 -0
  111. package/src/driver/android-playback.ts +157 -0
  112. package/src/driver/android.ts +50 -46
  113. package/src/driver/index.ts +19 -1
  114. package/src/driver/ios-color.ts +441 -0
  115. package/src/driver/ios-decoration.ts +332 -0
  116. package/src/driver/ios-playback.ts +37 -0
  117. package/src/driver/ios-shadow.ts +50 -0
  118. package/src/driver/ios.ts +91 -42
  119. package/src/engine.ts +227 -23
  120. package/src/haptics.ts +25 -2
  121. package/src/index.ts +4 -0
  122. package/src/liquid-grid.ts +28 -0
  123. package/src/liquid.ts +282 -0
  124. package/src/playback-group.ts +110 -0
  125. package/src/playback.ts +62 -0
  126. package/src/press.ts +8 -2
  127. package/src/properties.ts +31 -5
  128. package/src/sequence.ts +5 -0
  129. package/src/text.ts +2 -0
  130. package/src/track.ts +72 -3
  131. package/src/transition.ts +13 -2
  132. package/src/types.ts +20 -0
@@ -0,0 +1,170 @@
1
+ import { type View } from '@nativescript/core'
2
+ import { readColor, type ColorProperty } from '../color.js'
3
+ import { androidPlayback, type AndroidPlayback } from './android-playback.js'
4
+ import type { PlaybackCommand } from '../playback.js'
5
+ import type { Track } from '../track.js'
6
+
7
+ interface NativeTarget {
8
+ getColor(): number
9
+ setColor(value: number): void
10
+ isSupported(): boolean
11
+ }
12
+ interface ColorHandle {
13
+ control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
14
+ cancel(): void
15
+ elapsed(): number
16
+ target: NativeTarget
17
+ }
18
+ const playing = new WeakMap<View, Map<ColorProperty, ColorHandle>>()
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
+
25
+ export function presentedAndroidColor(view: View, property: ColorProperty): number {
26
+ const active = playing.get(view)?.get(property)
27
+ if (active) return active.target.getColor() >>> 0
28
+ if (property === 'color' && view.nativeViewProtected instanceof android.widget.TextView)
29
+ return view.nativeViewProtected.getCurrentTextColor() >>> 0
30
+ return readColor(view, property)
31
+ }
32
+
33
+ export function playAndroidColor(
34
+ view: View,
35
+ track: Track,
36
+ onEnd: (finished?: boolean) => void,
37
+ ): ColorHandle {
38
+ const property = track.property as ColorProperty
39
+ const changes =
40
+ property === 'borderColor'
41
+ ? [
42
+ 'borderTopColorChange',
43
+ 'borderRightColorChange',
44
+ 'borderBottomColorChange',
45
+ 'borderLeftColorChange',
46
+ ]
47
+ : [`${property}Change`]
48
+ const bridge = globalThis as unknown as {
49
+ org: {
50
+ rafaelaugusto: {
51
+ jolt: {
52
+ ColorTarget: new (
53
+ view: android.view.View,
54
+ property: number,
55
+ color: number,
56
+ ) => NativeTarget
57
+ }
58
+ }
59
+ }
60
+ }
61
+ let target: NativeTarget
62
+ try {
63
+ target = new bridge.org.rafaelaugusto.jolt.ColorTarget(
64
+ view.nativeViewProtected,
65
+ property === 'backgroundColor' ? 0 : property === 'borderColor' ? 1 : 2,
66
+ track.values[0]! | 0,
67
+ )
68
+ } catch {
69
+ // A JS-only update cannot install the native plugin. Keep the final model.
70
+ onEnd(false)
71
+ return {
72
+ cancel() {},
73
+ control() {},
74
+ elapsed: () => 0,
75
+ target: {
76
+ getColor: () => readColor(view, property),
77
+ setColor() {},
78
+ isSupported: () => false,
79
+ },
80
+ }
81
+ }
82
+ const frames = Array.create(android.animation.Keyframe, track.values.length)
83
+ track.values.forEach((value, index) => {
84
+ frames[index] = android.animation.Keyframe.ofInt(track.times[index]!, value | 0)
85
+ })
86
+ const holder = android.animation.PropertyValuesHolder.ofKeyframe('color', frames)
87
+ holder.setEvaluator(new android.animation.ArgbEvaluator())
88
+ const holders = Array.create(android.animation.PropertyValuesHolder, 1)
89
+ holders[0] = holder
90
+ const animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
91
+ target as unknown as java.lang.Object,
92
+ holders,
93
+ )
94
+ animator.setDuration(Math.max(track.duration, 1))
95
+ animator.setStartDelay(track.delay)
96
+ animator.setInterpolator(new android.view.animation.LinearInterpolator())
97
+ if (track.repeat > 0) {
98
+ animator.setRepeatCount(
99
+ Number.isFinite(track.repeat)
100
+ ? track.repeat
101
+ : android.animation.ValueAnimator.INFINITE,
102
+ )
103
+ animator.setRepeatMode(
104
+ track.reverse
105
+ ? android.animation.ValueAnimator.REVERSE
106
+ : android.animation.ValueAnimator.RESTART,
107
+ )
108
+ }
109
+ let stopped = false,
110
+ queued = false
111
+ const cleanup = () => {
112
+ if (stopped) return
113
+ stopped = true
114
+ view.off('unloaded', cancel)
115
+ if (property === 'color') {
116
+ view.off('textChange', invalidate)
117
+ view.off('formattedTextChange', invalidate)
118
+ }
119
+ for (const name of changes) view.style.off(name, invalidate)
120
+ if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
121
+ }
122
+ let player: AndroidPlayback | undefined
123
+ const cancel = () => {
124
+ if (stopped) return
125
+ cleanup()
126
+ player?.cancel()
127
+ onEnd(false)
128
+ }
129
+ const invalidate = () => {
130
+ if (stopped || queued) return
131
+ queued = true
132
+ void Promise.resolve().then(() => {
133
+ queued = false
134
+ if (!stopped) cancel()
135
+ })
136
+ }
137
+ const handle: ColorHandle = {
138
+ target,
139
+ elapsed: () => player?.elapsed() ?? 0,
140
+ control(time, paused, command) {
141
+ player?.control(time, paused, command)
142
+ },
143
+ cancel() {
144
+ cleanup()
145
+ player?.cancel()
146
+ },
147
+ }
148
+ let entries = playing.get(view)
149
+ if (!entries) playing.set(view, (entries = new Map()))
150
+ entries.set(property, handle)
151
+ view.on('unloaded', cancel)
152
+ if (property === 'color') {
153
+ view.on('textChange', invalidate)
154
+ view.on('formattedTextChange', invalidate)
155
+ }
156
+ for (const name of changes) view.style.on(name, invalidate)
157
+ if (target.isSupported()) {
158
+ target.setColor(track.values[0]! | 0)
159
+ player = androidPlayback(animator, track, () => {
160
+ if (!stopped) {
161
+ cleanup()
162
+ onEnd()
163
+ }
164
+ })
165
+ } else {
166
+ cleanup()
167
+ onEnd(false)
168
+ }
169
+ return handle
170
+ }
@@ -0,0 +1,182 @@
1
+ import { Utils, type View } from '@nativescript/core'
2
+ import {
3
+ isWritingDecoration,
4
+ radiusKeys,
5
+ widthKeys,
6
+ validateDecorationTarget,
7
+ type DecorationProperty,
8
+ } from '../decoration.js'
9
+ import { androidPlayback, type AndroidPlayback } from './android-playback.js'
10
+ import type { PlaybackCommand } from '../playback.js'
11
+ import type { Track } from '../track.js'
12
+ interface NativeTarget {
13
+ getValue(): number
14
+ setValue(value: number): void
15
+ isSupported(): boolean
16
+ acquire(): void
17
+ release(): void
18
+ }
19
+ interface Handle {
20
+ control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
21
+ cancel(): void
22
+ elapsed(): number
23
+ target: NativeTarget
24
+ }
25
+ const playing = new WeakMap<View, Map<DecorationProperty, Handle>>()
26
+ const ids = { borderRadius: 0, borderWidth: 1, elevation: 2, translationZ: 3 } as const
27
+ export function syncAndroidDecoration(view: View) {
28
+ for (const handle of playing.get(view)?.values() ?? [])
29
+ handle.target.setValue(handle.target.getValue())
30
+ }
31
+ export const presentedAndroidDecoration = (view: View, property: DecorationProperty) => {
32
+ const target = playing.get(view)?.get(property)?.target
33
+ return target ? target.getValue() / Utils.layout.getDisplayDensity() : undefined
34
+ }
35
+ export function playAndroidDecoration(
36
+ view: View,
37
+ track: Track,
38
+ onEnd: (finished?: boolean) => void,
39
+ ): Handle {
40
+ const property = track.property as keyof typeof ids
41
+ const density = Utils.layout.getDisplayDensity()
42
+ const bridge = globalThis as unknown as {
43
+ org: {
44
+ rafaelaugusto: {
45
+ jolt: {
46
+ DecorationTarget: new (
47
+ view: android.view.View,
48
+ property: number,
49
+ value: number,
50
+ borderColor: number,
51
+ ) => NativeTarget
52
+ }
53
+ }
54
+ }
55
+ }
56
+ let target: NativeTarget
57
+ try {
58
+ target = new bridge.org.rafaelaugusto.jolt.DecorationTarget(
59
+ view.nativeViewProtected,
60
+ ids[property],
61
+ track.values[0]! * density,
62
+ view.borderTopColor?.argb ?? 0,
63
+ )
64
+ } catch {
65
+ onEnd(false)
66
+ return {
67
+ cancel() {},
68
+ control() {},
69
+ elapsed: () => 0,
70
+ target: {
71
+ getValue: () => track.to * density,
72
+ setValue() {},
73
+ isSupported: () => false,
74
+ acquire() {},
75
+ release() {},
76
+ },
77
+ }
78
+ }
79
+ const frames = Array.create(android.animation.Keyframe, track.values.length)
80
+ track.values.forEach((value, index) => {
81
+ frames[index] = android.animation.Keyframe.ofFloat(
82
+ track.times[index]!,
83
+ value * density,
84
+ )
85
+ })
86
+ const holders = Array.create(android.animation.PropertyValuesHolder, 1)
87
+ holders[0] = android.animation.PropertyValuesHolder.ofKeyframe('value', frames)
88
+ const animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
89
+ target as unknown as java.lang.Object,
90
+ holders,
91
+ )
92
+ animator.setDuration(Math.max(track.duration, 1))
93
+ animator.setStartDelay(track.delay)
94
+ animator.setInterpolator(new android.view.animation.LinearInterpolator())
95
+ if (track.repeat > 0) {
96
+ animator.setRepeatCount(
97
+ Number.isFinite(track.repeat)
98
+ ? track.repeat
99
+ : android.animation.ValueAnimator.INFINITE,
100
+ )
101
+ animator.setRepeatMode(
102
+ track.reverse
103
+ ? android.animation.ValueAnimator.REVERSE
104
+ : android.animation.ValueAnimator.RESTART,
105
+ )
106
+ }
107
+ let stopped = false
108
+ const changes = [
109
+ ...radiusKeys,
110
+ ...widthKeys,
111
+ 'clipPath',
112
+ 'cornerShape',
113
+ 'boxShadow',
114
+ 'androidElevation',
115
+ ].map((name) => `${name}Change`)
116
+ const cleanup = () => {
117
+ if (stopped) return
118
+ stopped = true
119
+ view.off('unloaded', cancel)
120
+ for (const event of changes) view.style.off(event, external)
121
+ target.release()
122
+ if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
123
+ }
124
+ let player: AndroidPlayback | undefined
125
+ const cancel = () => {
126
+ if (!stopped) {
127
+ cleanup()
128
+ player?.cancel()
129
+ onEnd(false)
130
+ }
131
+ }
132
+ const external = (event: { eventName: string }) => {
133
+ if (isWritingDecoration(view)) return
134
+ const own =
135
+ property === 'borderRadius'
136
+ ? radiusKeys
137
+ : property === 'borderWidth'
138
+ ? widthKeys
139
+ : property === 'elevation'
140
+ ? ['androidElevation']
141
+ : []
142
+ void Promise.resolve().then(() => {
143
+ if (stopped) return
144
+ if (own.some((name) => event.eventName === `${name}Change`)) return cancel()
145
+ try {
146
+ validateDecorationTarget(view, property)
147
+ } catch {
148
+ cancel()
149
+ }
150
+ })
151
+ }
152
+ const handle: Handle = {
153
+ target,
154
+ elapsed: () => player?.elapsed() ?? 0,
155
+ control(time, paused, command) {
156
+ player?.control(time, paused, command)
157
+ },
158
+ cancel() {
159
+ cleanup()
160
+ player?.cancel()
161
+ },
162
+ }
163
+ const entries = playing.get(view) ?? new Map<DecorationProperty, Handle>()
164
+ playing.set(view, entries)
165
+ entries.set(property, handle)
166
+ view.on('unloaded', cancel)
167
+ for (const event of changes) view.style.on(event, external)
168
+ if (target.isSupported()) {
169
+ target.acquire()
170
+ target.setValue(track.values[0]! * density)
171
+ player = androidPlayback(animator, track, () => {
172
+ if (!stopped) {
173
+ cleanup()
174
+ onEnd()
175
+ }
176
+ })
177
+ } else {
178
+ cleanup()
179
+ onEnd(false)
180
+ }
181
+ return handle
182
+ }
@@ -0,0 +1,157 @@
1
+ import type { Track } from '../track.js'
2
+ import type { PlaybackClock, PlaybackCommand } from '../playback.js'
3
+
4
+ export interface AndroidPlayback {
5
+ cancel(): void
6
+ elapsed(): number
7
+ control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
8
+ }
9
+
10
+ /** Reuse native keyframes when seeking, including rewinding into a start delay. */
11
+ export function androidPlayback(
12
+ animator: android.animation.ValueAnimator,
13
+ track: Track,
14
+ onEnd: () => void,
15
+ onStart?: () => void,
16
+ onRelease?: () => void,
17
+ ): AndroidPlayback {
18
+ let suppressed = false
19
+ let stopped = false
20
+ let started = false
21
+ let acquired = false
22
+ let origin = 0
23
+ let delay = track.delay
24
+ let held: number | undefined
25
+ animator.addListener(
26
+ new android.animation.Animator.AnimatorListener({
27
+ onAnimationStart() {
28
+ if (!suppressed && !stopped) {
29
+ started = true
30
+ if (!acquired) {
31
+ acquired = true
32
+ onStart?.()
33
+ }
34
+ }
35
+ },
36
+ onAnimationRepeat() {},
37
+ onAnimationCancel() {
38
+ if (!suppressed && !stopped) onRelease?.()
39
+ },
40
+ onAnimationEnd() {
41
+ if (suppressed || stopped || held !== undefined) return
42
+ stopped = true
43
+ onRelease?.()
44
+ onEnd()
45
+ },
46
+ }),
47
+ )
48
+ const elapsed = () =>
49
+ held ?? (started ? origin + delay + animator.getCurrentPlayTime() : origin)
50
+ const control = (
51
+ position: number,
52
+ paused: boolean,
53
+ command: PlaybackCommand = 'seek',
54
+ ) => {
55
+ if (stopped) return
56
+ if (command === 'pause') {
57
+ held = position
58
+ animator.pause()
59
+ return
60
+ }
61
+ if (command === 'play') {
62
+ held = undefined
63
+ animator.resume()
64
+ return
65
+ }
66
+ suppressed = true
67
+ animator.cancel()
68
+ started = false
69
+ origin = Math.min(position, track.delay)
70
+ delay = Math.max(0, track.delay - position)
71
+ animator.setStartDelay(delay)
72
+ held = paused ? position : undefined
73
+ suppressed = false
74
+ animator.start()
75
+ if (paused) animator.pause()
76
+ if (position >= track.delay)
77
+ animator.setCurrentPlayTime(Math.max(0, position - track.delay))
78
+ }
79
+ animator.start()
80
+ return {
81
+ elapsed,
82
+ control,
83
+ cancel() {
84
+ if (stopped) return
85
+ stopped = true
86
+ onRelease?.()
87
+ animator.cancel()
88
+ },
89
+ }
90
+ }
91
+
92
+ /** Share one native clock among active animations. Pause it when every owner
93
+ * is paused, and dispose it when the last owner settles. No frame callbacks. */
94
+ export function androidClockFactory() {
95
+ let animator: android.animation.ValueAnimator | undefined
96
+ let owners = 0,
97
+ running = 0,
98
+ accumulated = 0
99
+ const now = () => accumulated + (animator?.getCurrentPlayTime() ?? 0)
100
+ const start = () => {
101
+ if (animator) return
102
+ const values = Array.create('float', 2)
103
+ values[0] = 0
104
+ values[1] = 1
105
+ animator = android.animation.ValueAnimator.ofFloat(values)
106
+ animator.setDuration(60_000)
107
+ animator.setRepeatCount(android.animation.ValueAnimator.INFINITE)
108
+ animator.start()
109
+ }
110
+ const stop = () => {
111
+ accumulated = now()
112
+ animator?.cancel()
113
+ animator = undefined
114
+ }
115
+ return (_duration: number): PlaybackClock => {
116
+ start()
117
+ owners++
118
+ running++
119
+ let offset = 0,
120
+ anchor = now(),
121
+ paused = false,
122
+ stopped = false
123
+ const time = () => offset + (paused || stopped ? 0 : now() - anchor)
124
+ const pause = () => {
125
+ if (paused || stopped) return
126
+ offset = time()
127
+ paused = true
128
+ // getCurrentPlayTime keeps counting while a ValueAnimator is paused and
129
+ // corrects itself only on its next frame. Start a fresh marker on resume
130
+ // so command-time reads cannot jump forward and then backward.
131
+ if (--running === 0) stop()
132
+ }
133
+ return {
134
+ time,
135
+ pause,
136
+ play() {
137
+ if (!paused || stopped) return
138
+ if (running++ === 0) start()
139
+ anchor = now()
140
+ paused = false
141
+ },
142
+ seek(ms) {
143
+ offset = ms
144
+ anchor = now()
145
+ },
146
+ cancel() {
147
+ if (stopped) return
148
+ pause()
149
+ stopped = true
150
+ if (--owners === 0) {
151
+ stop()
152
+ accumulated = 0
153
+ }
154
+ },
155
+ }
156
+ }
157
+ }
@@ -1,13 +1,27 @@
1
+ import {
2
+ androidPlayback,
3
+ androidClockFactory,
4
+ type AndroidPlayback,
5
+ } from './android-playback.js'
6
+ import { isDecorationProperty } from '../decoration.js'
7
+ import {
8
+ syncAndroidDecoration,
9
+ playAndroidDecoration,
10
+ presentedAndroidDecoration,
11
+ } from './android-decoration.js'
1
12
  import { Utils, type View } from '@nativescript/core'
2
- import type { Property } from '../properties.js'
13
+ import type { NumericProperty } from '../properties.js'
3
14
  import type { Driver } from './index.js'
15
+ import { isColorProperty } from '../color.js'
16
+ import {
17
+ playAndroidColor,
18
+ presentedAndroidColor,
19
+ syncAndroidColor,
20
+ } from './android-color.js'
4
21
 
5
22
  type NativeView = android.view.View
6
23
 
7
- interface Handle {
8
- cancel(): void
9
- elapsed(): number
10
- }
24
+ type Handle = AndroidPlayback
11
25
 
12
26
  /** Own only layers we create; keep app-configured hardware/software layers and paints intact. */
13
27
  function fadeLayer(view: View, nativeView: NativeView): (() => void) | undefined {
@@ -44,7 +58,7 @@ function fadeLayer(view: View, nativeView: NativeView): (() => void) | undefined
44
58
  }
45
59
 
46
60
  const accessors: Record<
47
- Property,
61
+ NumericProperty,
48
62
  { get(view: NativeView): number; set(view: NativeView, value: number): void }
49
63
  > = {
50
64
  opacity: { get: (v) => v.getAlpha(), set: (v, value) => v.setAlpha(value) },
@@ -57,7 +71,7 @@ const accessors: Record<
57
71
  rotateY: { get: (v) => v.getRotationY(), set: (v, value) => v.setRotationY(value) },
58
72
  }
59
73
 
60
- const unit = (property: Property) =>
74
+ const unit = (property: NumericProperty) =>
61
75
  property === 'x' || property === 'y' ? Utils.layout.getDisplayDensity() : 1
62
76
 
63
77
  export function createAndroidDriver(): Driver {
@@ -71,13 +85,21 @@ export function createAndroidDriver(): Driver {
71
85
  rotate: View.ROTATION,
72
86
  rotateX: View.ROTATION_X,
73
87
  rotateY: View.ROTATION_Y,
74
- } satisfies Record<Property, unknown>
88
+ } satisfies Record<NumericProperty, unknown>
89
+ const clock = androidClockFactory()
75
90
  const interpolator = new android.view.animation.LinearInterpolator()
76
91
 
77
92
  return {
78
93
  now: () => android.os.SystemClock.uptimeMillis(),
94
+ clock,
95
+ control(_view, handle, elapsed, paused, command) {
96
+ ;(handle as Handle).control(elapsed, paused, command)
97
+ },
79
98
 
80
99
  play(view, track, _startedAt, onEnd) {
100
+ if (isDecorationProperty(track.property))
101
+ return playAndroidDecoration(view, track, onEnd)
102
+ if (isColorProperty(track.property)) return playAndroidColor(view, track, onEnd)
81
103
  const nativeView = view.nativeViewProtected as NativeView
82
104
  const scale = unit(track.property)
83
105
 
@@ -101,9 +123,6 @@ export function createAndroidDriver(): Driver {
101
123
  nativeView,
102
124
  holders,
103
125
  )
104
- let cancelled = false
105
- let ended = false
106
- let started = false
107
126
  let restoreLayer: (() => void) | undefined
108
127
  const releaseLayer = () => {
109
128
  restoreLayer?.()
@@ -124,52 +143,37 @@ export function createAndroidDriver(): Driver {
124
143
  : android.animation.ValueAnimator.RESTART,
125
144
  )
126
145
  }
127
- animator.addListener(
128
- new android.animation.Animator.AnimatorListener({
129
- onAnimationStart() {
130
- started = true
131
- if (
132
- !cancelled &&
133
- !ended &&
134
- track.property === 'opacity' &&
135
- track.values.some((value) => value !== track.values[0])
136
- )
137
- restoreLayer = fadeLayer(view, nativeView)
138
- },
139
- onAnimationRepeat() {},
140
- onAnimationCancel() {
141
- cancelled = true
142
- releaseLayer()
143
- },
144
- onAnimationEnd() {
145
- if (ended) return
146
- ended = true
147
- releaseLayer()
148
- onEnd()
149
- },
150
- }),
151
- )
152
-
153
146
  accessors[track.property].set(nativeView, track.values[0]! * scale)
154
- animator.start()
155
- return {
156
- elapsed: () => (started ? track.delay + animator.getCurrentPlayTime() : 0),
157
- cancel() {
158
- // Cancel before native callbacks: Android can send onStart while cancelling a delay.
159
- cancelled = true
160
- releaseLayer()
161
- animator.cancel()
147
+ return androidPlayback(
148
+ animator,
149
+ track,
150
+ onEnd,
151
+ () => {
152
+ if (
153
+ track.property === 'opacity' &&
154
+ track.values.some((value) => value !== track.values[0])
155
+ )
156
+ restoreLayer = fadeLayer(view, nativeView)
162
157
  },
163
- } satisfies Handle
158
+ releaseLayer,
159
+ )
164
160
  },
165
161
 
166
162
  stop(_view, handle) {
167
163
  ;(handle as Handle).cancel()
168
164
  },
169
165
 
166
+ sync(view) {
167
+ syncAndroidDecoration(view)
168
+ syncAndroidColor(view)
169
+ },
170
+
170
171
  elapsed: (handle) => (handle as Handle).elapsed(),
171
172
 
172
173
  presented(view, property) {
174
+ if (isDecorationProperty(property))
175
+ return presentedAndroidDecoration(view, property)
176
+ if (isColorProperty(property)) return presentedAndroidColor(view, property)
173
177
  const nativeView = view.nativeViewProtected as NativeView | undefined
174
178
  return nativeView ? accessors[property].get(nativeView) / unit(property) : undefined
175
179
  },
@@ -1,3 +1,4 @@
1
+ import type { PlaybackClock, PlaybackCommand } from '../playback.js'
1
2
  import { isAndroid, type View } from '@nativescript/core'
2
3
  import type { Property } from '../properties.js'
3
4
  import type { Track } from '../track.js'
@@ -8,8 +9,25 @@ export interface Driver {
8
9
  /** Milliseconds, on the clock the platform's animations run on. */
9
10
  now(): number
10
11
  /** Starts the track at `startedAt + track.delay` and calls `onEnd` once, however it ends. */
11
- play(view: View, track: Track, startedAt: number, onEnd: () => void): unknown
12
+ play(
13
+ view: View,
14
+ track: Track,
15
+ startedAt: number,
16
+ onEnd: (finished?: boolean) => void,
17
+ ): unknown
12
18
  stop(view: View, handle: unknown): void
19
+ /** A native elapsed-time marker when system animation speed affects time. */
20
+ clock?(duration: number): PlaybackClock
21
+ /** Move only this native track to an elapsed time, optionally holding it. */
22
+ control?(
23
+ view: View,
24
+ handle: unknown,
25
+ elapsed: number,
26
+ paused: boolean,
27
+ command?: PlaybackCommand,
28
+ ): void
29
+ /** Reapply native presentation after a Core model write rebuilds its drawable/layers. */
30
+ sync?(view: View, excluding?: Property): void
13
31
  /** Track time, including its delay, when the native clock differs from `now()`. */
14
32
  elapsed?(handle: unknown): number
15
33
  /** The value on screen, when the platform knows it better than the track does. */