@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.
- package/README.md +78 -13
- package/dist/animate.d.ts +3 -2
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +35 -5
- 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/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 +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.map +1 -1
- package/dist/driver/android.js +28 -35
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +8 -1
- 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-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 +78 -35
- 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 +217 -21
- 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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- 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 +229 -0
- package/dist/liquid.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 +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/press.d.ts.map +1 -1
- package/dist/press.js +6 -1
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +5 -2
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +25 -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/text.d.ts.map +1 -1
- package/dist/text.js +3 -0
- package/dist/text.js.map +1 -1
- package/dist/track.d.ts +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +48 -3
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +11 -2
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
- 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/proguard-rules.pro +7 -0
- package/src/animate.ts +48 -9
- package/src/color.ts +101 -0
- package/src/decoration.ts +180 -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 +50 -46
- package/src/driver/index.ts +19 -1
- package/src/driver/ios-color.ts +441 -0
- 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 +91 -42
- package/src/engine.ts +227 -23
- package/src/haptics.ts +25 -2
- package/src/index.ts +4 -0
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +282 -0
- package/src/playback-group.ts +110 -0
- package/src/playback.ts +62 -0
- package/src/press.ts +8 -2
- package/src/properties.ts +31 -5
- package/src/sequence.ts +5 -0
- package/src/text.ts +2 -0
- package/src/track.ts +72 -3
- package/src/transition.ts +13 -2
- 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
|
+
}
|
package/src/driver/android.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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<
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
},
|
package/src/driver/index.ts
CHANGED
|
@@ -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(
|
|
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. */
|