@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
|
@@ -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,15 +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
13
|
import type { NumericProperty } from '../properties.js'
|
|
3
14
|
import type { Driver } from './index.js'
|
|
4
15
|
import { isColorProperty } from '../color.js'
|
|
5
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
playAndroidColor,
|
|
18
|
+
presentedAndroidColor,
|
|
19
|
+
syncAndroidColor,
|
|
20
|
+
} from './android-color.js'
|
|
6
21
|
|
|
7
22
|
type NativeView = android.view.View
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
cancel(): void
|
|
11
|
-
elapsed(): number
|
|
12
|
-
}
|
|
24
|
+
type Handle = AndroidPlayback
|
|
13
25
|
|
|
14
26
|
/** Own only layers we create; keep app-configured hardware/software layers and paints intact. */
|
|
15
27
|
function fadeLayer(view: View, nativeView: NativeView): (() => void) | undefined {
|
|
@@ -74,12 +86,19 @@ export function createAndroidDriver(): Driver {
|
|
|
74
86
|
rotateX: View.ROTATION_X,
|
|
75
87
|
rotateY: View.ROTATION_Y,
|
|
76
88
|
} satisfies Record<NumericProperty, unknown>
|
|
89
|
+
const clock = androidClockFactory()
|
|
77
90
|
const interpolator = new android.view.animation.LinearInterpolator()
|
|
78
91
|
|
|
79
92
|
return {
|
|
80
93
|
now: () => android.os.SystemClock.uptimeMillis(),
|
|
94
|
+
clock,
|
|
95
|
+
control(_view, handle, elapsed, paused, command) {
|
|
96
|
+
;(handle as Handle).control(elapsed, paused, command)
|
|
97
|
+
},
|
|
81
98
|
|
|
82
99
|
play(view, track, _startedAt, onEnd) {
|
|
100
|
+
if (isDecorationProperty(track.property))
|
|
101
|
+
return playAndroidDecoration(view, track, onEnd)
|
|
83
102
|
if (isColorProperty(track.property)) return playAndroidColor(view, track, onEnd)
|
|
84
103
|
const nativeView = view.nativeViewProtected as NativeView
|
|
85
104
|
const scale = unit(track.property)
|
|
@@ -104,9 +123,6 @@ export function createAndroidDriver(): Driver {
|
|
|
104
123
|
nativeView,
|
|
105
124
|
holders,
|
|
106
125
|
)
|
|
107
|
-
let cancelled = false
|
|
108
|
-
let ended = false
|
|
109
|
-
let started = false
|
|
110
126
|
let restoreLayer: (() => void) | undefined
|
|
111
127
|
const releaseLayer = () => {
|
|
112
128
|
restoreLayer?.()
|
|
@@ -127,52 +143,36 @@ export function createAndroidDriver(): Driver {
|
|
|
127
143
|
: android.animation.ValueAnimator.RESTART,
|
|
128
144
|
)
|
|
129
145
|
}
|
|
130
|
-
animator.addListener(
|
|
131
|
-
new android.animation.Animator.AnimatorListener({
|
|
132
|
-
onAnimationStart() {
|
|
133
|
-
started = true
|
|
134
|
-
if (
|
|
135
|
-
!cancelled &&
|
|
136
|
-
!ended &&
|
|
137
|
-
track.property === 'opacity' &&
|
|
138
|
-
track.values.some((value) => value !== track.values[0])
|
|
139
|
-
)
|
|
140
|
-
restoreLayer = fadeLayer(view, nativeView)
|
|
141
|
-
},
|
|
142
|
-
onAnimationRepeat() {},
|
|
143
|
-
onAnimationCancel() {
|
|
144
|
-
cancelled = true
|
|
145
|
-
releaseLayer()
|
|
146
|
-
},
|
|
147
|
-
onAnimationEnd() {
|
|
148
|
-
if (ended) return
|
|
149
|
-
ended = true
|
|
150
|
-
releaseLayer()
|
|
151
|
-
onEnd()
|
|
152
|
-
},
|
|
153
|
-
}),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
146
|
accessors[track.property].set(nativeView, track.values[0]! * scale)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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)
|
|
165
157
|
},
|
|
166
|
-
|
|
158
|
+
releaseLayer,
|
|
159
|
+
)
|
|
167
160
|
},
|
|
168
161
|
|
|
169
162
|
stop(_view, handle) {
|
|
170
163
|
;(handle as Handle).cancel()
|
|
171
164
|
},
|
|
172
165
|
|
|
166
|
+
sync(view) {
|
|
167
|
+
syncAndroidDecoration(view)
|
|
168
|
+
syncAndroidColor(view)
|
|
169
|
+
},
|
|
170
|
+
|
|
173
171
|
elapsed: (handle) => (handle as Handle).elapsed(),
|
|
174
172
|
|
|
175
173
|
presented(view, property) {
|
|
174
|
+
if (isDecorationProperty(property))
|
|
175
|
+
return presentedAndroidDecoration(view, property)
|
|
176
176
|
if (isColorProperty(property)) return presentedAndroidColor(view, property)
|
|
177
177
|
const nativeView = view.nativeViewProtected as NativeView | undefined
|
|
178
178
|
return nativeView ? accessors[property].get(nativeView) / unit(property) : undefined
|
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'
|
|
@@ -15,6 +16,18 @@ export interface Driver {
|
|
|
15
16
|
onEnd: (finished?: boolean) => void,
|
|
16
17
|
): unknown
|
|
17
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
|
|
18
31
|
/** Track time, including its delay, when the native clock differs from `now()`. */
|
|
19
32
|
elapsed?(handle: unknown): number
|
|
20
33
|
/** The value on screen, when the platform knows it better than the track does. */
|
package/src/driver/ios-color.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Color, type View } from '@nativescript/core'
|
|
|
2
2
|
import { colorProperty } from '@nativescript/core/ui/styling/style-properties'
|
|
3
3
|
import { formattedTextProperty, textProperty } from '@nativescript/core/ui/text-base'
|
|
4
4
|
import { readColor, type ColorProperty } from '../color.js'
|
|
5
|
+
import { iosPlayback } from './ios-playback.js'
|
|
5
6
|
import type { Track } from '../track.js'
|
|
6
7
|
|
|
7
8
|
type TextView = View & {
|
|
@@ -18,6 +19,8 @@ type PaintLayer = {
|
|
|
18
19
|
}
|
|
19
20
|
type NativeView = UIView & { borderLayer?: CAShapeLayer }
|
|
20
21
|
interface ColorHandle {
|
|
22
|
+
elapsed(): number
|
|
23
|
+
control(elapsed: number, paused: boolean): void
|
|
21
24
|
stop(): void
|
|
22
25
|
presented(): number | undefined
|
|
23
26
|
}
|
|
@@ -35,8 +38,8 @@ export function presentedIOSColor(
|
|
|
35
38
|
view: View,
|
|
36
39
|
property: ColorProperty,
|
|
37
40
|
): number | undefined {
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
41
|
+
const active = playing.get(view)?.get(property)
|
|
42
|
+
if (active) return active.presented()
|
|
40
43
|
if (property === 'color' && !view.color) {
|
|
41
44
|
const label = labelOf(view)
|
|
42
45
|
if (label?.textColor) return Color.fromIosColor(label.textColor).argb >>> 0
|
|
@@ -56,6 +59,7 @@ export function playIOSColor(
|
|
|
56
59
|
startedAt: number,
|
|
57
60
|
onEnd: (finished?: boolean) => void,
|
|
58
61
|
): ColorHandle {
|
|
62
|
+
const timing = iosPlayback(track, startedAt)
|
|
59
63
|
const property = track.property as ColorProperty
|
|
60
64
|
let target: PaintLayer | undefined
|
|
61
65
|
let restore: (() => void) | undefined
|
|
@@ -107,11 +111,21 @@ export function playIOSColor(
|
|
|
107
111
|
}
|
|
108
112
|
const handle: ColorHandle = {
|
|
109
113
|
stop: cancel,
|
|
114
|
+
elapsed: timing.elapsed,
|
|
115
|
+
control(time, paused) {
|
|
116
|
+
if (!stopped) {
|
|
117
|
+
timing.control(time, paused)
|
|
118
|
+
attach()
|
|
119
|
+
}
|
|
120
|
+
},
|
|
110
121
|
presented() {
|
|
122
|
+
// A new CA copy commits at the end of the turn. After controls, the engine
|
|
123
|
+
// clock is authoritative even before presentationLayer catches up.
|
|
124
|
+
if (timing.controlled) return undefined
|
|
111
125
|
if (!target) return track.values[0]
|
|
112
126
|
const layer = target.layer.presentationLayer()
|
|
113
127
|
// Before CA commits its first frame, the model already contains the target.
|
|
114
|
-
if (!layer) return track.valueAt(
|
|
128
|
+
if (!layer) return track.valueAt(timing.elapsed())
|
|
115
129
|
const color = (layer as unknown as Record<string, interop.Pointer | null>)[
|
|
116
130
|
target.path
|
|
117
131
|
]
|
|
@@ -189,12 +203,7 @@ export function playIOSColor(
|
|
|
189
203
|
)
|
|
190
204
|
animation.keyTimes = NSArray.arrayWithArray(track.times as number[])
|
|
191
205
|
animation.duration = Math.max(track.duration, 1) / 1000
|
|
192
|
-
animation
|
|
193
|
-
(startedAt + track.delay) / 1000,
|
|
194
|
-
null,
|
|
195
|
-
)
|
|
196
|
-
animation.fillMode = kCAFillModeBackwards
|
|
197
|
-
animation.removedOnCompletion = track.repeat !== Infinity
|
|
206
|
+
timing.configure(animation, layer)
|
|
198
207
|
if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
|
|
199
208
|
const maximum = Math.min(
|
|
200
209
|
120,
|
|
@@ -212,7 +221,7 @@ export function playIOSColor(
|
|
|
212
221
|
animation.repeatCount = track.reverse ? (track.repeat + 1) / 2 : track.repeat + 1
|
|
213
222
|
}
|
|
214
223
|
CATransaction.begin()
|
|
215
|
-
if (track.repeat !== Infinity)
|
|
224
|
+
if (!timing.paused && track.repeat !== Infinity)
|
|
216
225
|
CATransaction.setCompletionBlock(() => {
|
|
217
226
|
if (!stopped && instance === generation) {
|
|
218
227
|
cleanup()
|