@rafaelaugusto/jolt 0.1.0-beta.1 → 0.1.0-beta.10

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 (302) hide show
  1. package/README.md +367 -18
  2. package/dist/animate.d.ts +19 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +186 -14
  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/config.d.ts +31 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +73 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/decay.d.ts +11 -0
  15. package/dist/decay.d.ts.map +1 -0
  16. package/dist/decay.js +22 -0
  17. package/dist/decay.js.map +1 -0
  18. package/dist/decoration.d.ts +18 -0
  19. package/dist/decoration.d.ts.map +1 -0
  20. package/dist/decoration.js +155 -0
  21. package/dist/decoration.js.map +1 -0
  22. package/dist/drag-input.d.ts +15 -0
  23. package/dist/drag-input.d.ts.map +1 -0
  24. package/dist/drag-input.js +66 -0
  25. package/dist/drag-input.js.map +1 -0
  26. package/dist/drag-physics.d.ts +20 -0
  27. package/dist/drag-physics.d.ts.map +1 -0
  28. package/dist/drag-physics.js +75 -0
  29. package/dist/drag-physics.js.map +1 -0
  30. package/dist/drag.d.ts +58 -0
  31. package/dist/drag.d.ts.map +1 -0
  32. package/dist/drag.js +324 -0
  33. package/dist/drag.js.map +1 -0
  34. package/dist/driver/android-color.d.ts +20 -0
  35. package/dist/driver/android-color.d.ts.map +1 -0
  36. package/dist/driver/android-color.js +136 -0
  37. package/dist/driver/android-color.js.map +1 -0
  38. package/dist/driver/android-decoration.d.ts +22 -0
  39. package/dist/driver/android-decoration.d.ts.map +1 -0
  40. package/dist/driver/android-decoration.js +139 -0
  41. package/dist/driver/android-decoration.js.map +1 -0
  42. package/dist/driver/android-playback.d.ts +13 -0
  43. package/dist/driver/android-playback.d.ts.map +1 -0
  44. package/dist/driver/android-playback.js +141 -0
  45. package/dist/driver/android-playback.js.map +1 -0
  46. package/dist/driver/android.d.ts +1 -1
  47. package/dist/driver/android.d.ts.map +1 -1
  48. package/dist/driver/android.js +165 -13
  49. package/dist/driver/android.js.map +1 -1
  50. package/dist/driver/index.d.ts +25 -2
  51. package/dist/driver/index.d.ts.map +1 -1
  52. package/dist/driver/index.js.map +1 -1
  53. package/dist/driver/ios-color.d.ts +16 -0
  54. package/dist/driver/ios-color.d.ts.map +1 -0
  55. package/dist/driver/ios-color.js +392 -0
  56. package/dist/driver/ios-color.js.map +1 -0
  57. package/dist/driver/ios-decoration.d.ts +17 -0
  58. package/dist/driver/ios-decoration.d.ts.map +1 -0
  59. package/dist/driver/ios-decoration.js +271 -0
  60. package/dist/driver/ios-decoration.js.map +1 -0
  61. package/dist/driver/ios-layout.d.ts +9 -0
  62. package/dist/driver/ios-layout.d.ts.map +1 -0
  63. package/dist/driver/ios-layout.js +52 -0
  64. package/dist/driver/ios-layout.js.map +1 -0
  65. package/dist/driver/ios-playback.d.ts +10 -0
  66. package/dist/driver/ios-playback.d.ts.map +1 -0
  67. package/dist/driver/ios-playback.js +47 -0
  68. package/dist/driver/ios-playback.js.map +1 -0
  69. package/dist/driver/ios-shadow.d.ts +6 -0
  70. package/dist/driver/ios-shadow.d.ts.map +1 -0
  71. package/dist/driver/ios-shadow.js +52 -0
  72. package/dist/driver/ios-shadow.js.map +1 -0
  73. package/dist/driver/ios.d.ts.map +1 -1
  74. package/dist/driver/ios.js +163 -28
  75. package/dist/driver/ios.js.map +1 -1
  76. package/dist/engine.d.ts +75 -5
  77. package/dist/engine.d.ts.map +1 -1
  78. package/dist/engine.js +293 -23
  79. package/dist/engine.js.map +1 -1
  80. package/dist/haptics.d.ts +21 -0
  81. package/dist/haptics.d.ts.map +1 -0
  82. package/dist/haptics.js +120 -0
  83. package/dist/haptics.js.map +1 -0
  84. package/dist/image-effect-native.d.ts +26 -0
  85. package/dist/image-effect-native.d.ts.map +1 -0
  86. package/dist/image-effect-native.js +73 -0
  87. package/dist/image-effect-native.js.map +1 -0
  88. package/dist/image-effect.d.ts +50 -0
  89. package/dist/image-effect.d.ts.map +1 -0
  90. package/dist/image-effect.js +290 -0
  91. package/dist/image-effect.js.map +1 -0
  92. package/dist/image-owner.d.ts +3 -0
  93. package/dist/image-owner.d.ts.map +1 -0
  94. package/dist/image-owner.js +11 -0
  95. package/dist/image-owner.js.map +1 -0
  96. package/dist/index.d.ts +43 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +20 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/interaction-surface.d.ts +12 -0
  101. package/dist/interaction-surface.d.ts.map +1 -0
  102. package/dist/interaction-surface.js +36 -0
  103. package/dist/interaction-surface.js.map +1 -0
  104. package/dist/items.d.ts +20 -0
  105. package/dist/items.d.ts.map +1 -0
  106. package/dist/items.js +109 -0
  107. package/dist/items.js.map +1 -0
  108. package/dist/keyboard.d.ts +41 -0
  109. package/dist/keyboard.d.ts.map +1 -0
  110. package/dist/keyboard.js +256 -0
  111. package/dist/keyboard.js.map +1 -0
  112. package/dist/label.d.ts +7 -0
  113. package/dist/label.d.ts.map +1 -0
  114. package/dist/label.js +60 -0
  115. package/dist/label.js.map +1 -0
  116. package/dist/layout.d.ts +13 -0
  117. package/dist/layout.d.ts.map +1 -0
  118. package/dist/layout.js +348 -0
  119. package/dist/layout.js.map +1 -0
  120. package/dist/liquid-grid.d.ts +3 -0
  121. package/dist/liquid-grid.d.ts.map +1 -0
  122. package/dist/liquid-grid.js +24 -0
  123. package/dist/liquid-grid.js.map +1 -0
  124. package/dist/liquid.d.ts +18 -0
  125. package/dist/liquid.d.ts.map +1 -0
  126. package/dist/liquid.js +234 -0
  127. package/dist/liquid.js.map +1 -0
  128. package/dist/loader.d.ts +19 -0
  129. package/dist/loader.d.ts.map +1 -0
  130. package/dist/loader.js +173 -0
  131. package/dist/loader.js.map +1 -0
  132. package/dist/motion-scope.d.ts +39 -0
  133. package/dist/motion-scope.d.ts.map +1 -0
  134. package/dist/motion-scope.js +238 -0
  135. package/dist/motion-scope.js.map +1 -0
  136. package/dist/motion-states.d.ts +36 -0
  137. package/dist/motion-states.d.ts.map +1 -0
  138. package/dist/motion-states.js +165 -0
  139. package/dist/motion-states.js.map +1 -0
  140. package/dist/particles.d.ts +39 -0
  141. package/dist/particles.d.ts.map +1 -0
  142. package/dist/particles.js +265 -0
  143. package/dist/particles.js.map +1 -0
  144. package/dist/path.d.ts +12 -0
  145. package/dist/path.d.ts.map +1 -0
  146. package/dist/path.js +90 -0
  147. package/dist/path.js.map +1 -0
  148. package/dist/playback-group.d.ts +6 -0
  149. package/dist/playback-group.d.ts.map +1 -0
  150. package/dist/playback-group.js +141 -0
  151. package/dist/playback-group.js.map +1 -0
  152. package/dist/playback.d.ts +25 -0
  153. package/dist/playback.d.ts.map +1 -0
  154. package/dist/playback.js +35 -0
  155. package/dist/playback.js.map +1 -0
  156. package/dist/press.d.ts.map +1 -1
  157. package/dist/press.js +23 -7
  158. package/dist/press.js.map +1 -1
  159. package/dist/properties.d.ts +6 -3
  160. package/dist/properties.d.ts.map +1 -1
  161. package/dist/properties.js +26 -2
  162. package/dist/properties.js.map +1 -1
  163. package/dist/reveal.d.ts +25 -0
  164. package/dist/reveal.d.ts.map +1 -0
  165. package/dist/reveal.js +294 -0
  166. package/dist/reveal.js.map +1 -0
  167. package/dist/roll-diff.d.ts +22 -0
  168. package/dist/roll-diff.d.ts.map +1 -0
  169. package/dist/roll-diff.js +36 -0
  170. package/dist/roll-diff.js.map +1 -0
  171. package/dist/roll.d.ts +29 -0
  172. package/dist/roll.d.ts.map +1 -0
  173. package/dist/roll.js +442 -0
  174. package/dist/roll.js.map +1 -0
  175. package/dist/scroll-linked.d.ts +10 -0
  176. package/dist/scroll-linked.d.ts.map +1 -0
  177. package/dist/scroll-linked.js +58 -0
  178. package/dist/scroll-linked.js.map +1 -0
  179. package/dist/sequence.d.ts +10 -5
  180. package/dist/sequence.d.ts.map +1 -1
  181. package/dist/sequence.js +208 -14
  182. package/dist/sequence.js.map +1 -1
  183. package/dist/shimmer.d.ts +12 -0
  184. package/dist/shimmer.d.ts.map +1 -0
  185. package/dist/shimmer.js +161 -0
  186. package/dist/shimmer.js.map +1 -0
  187. package/dist/svg-path.d.ts +14 -0
  188. package/dist/svg-path.d.ts.map +1 -0
  189. package/dist/svg-path.js +281 -0
  190. package/dist/svg-path.js.map +1 -0
  191. package/dist/symbol-effect.d.ts +23 -0
  192. package/dist/symbol-effect.d.ts.map +1 -0
  193. package/dist/symbol-effect.js +187 -0
  194. package/dist/symbol-effect.js.map +1 -0
  195. package/dist/text-layout.d.ts +30 -0
  196. package/dist/text-layout.d.ts.map +1 -0
  197. package/dist/text-layout.js +185 -0
  198. package/dist/text-layout.js.map +1 -0
  199. package/dist/text.d.ts +17 -0
  200. package/dist/text.d.ts.map +1 -0
  201. package/dist/text.js +263 -0
  202. package/dist/text.js.map +1 -0
  203. package/dist/track.d.ts +3 -1
  204. package/dist/track.d.ts.map +1 -1
  205. package/dist/track.js +266 -13
  206. package/dist/track.js.map +1 -1
  207. package/dist/transition.d.ts +17 -1
  208. package/dist/transition.d.ts.map +1 -1
  209. package/dist/transition.js +41 -6
  210. package/dist/transition.js.map +1 -1
  211. package/dist/types.d.ts +48 -1
  212. package/dist/types.d.ts.map +1 -1
  213. package/dist/vue/index.d.ts +95 -0
  214. package/dist/vue/index.d.ts.map +1 -0
  215. package/dist/vue/index.js +163 -0
  216. package/dist/vue/index.js.map +1 -0
  217. package/dist/zoom-native.d.ts +30 -0
  218. package/dist/zoom-native.d.ts.map +1 -0
  219. package/dist/zoom-native.js +56 -0
  220. package/dist/zoom-native.js.map +1 -0
  221. package/dist/zoomable.d.ts +48 -0
  222. package/dist/zoomable.d.ts.map +1 -0
  223. package/dist/zoomable.js +249 -0
  224. package/dist/zoomable.js.map +1 -0
  225. package/package.json +29 -5
  226. package/platforms/android/include.gradle +7 -0
  227. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  228. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  229. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  230. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  231. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  232. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  233. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  234. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  235. package/platforms/android/proguard-rules.pro +19 -0
  236. package/platforms/ios/build.xcconfig +1 -0
  237. package/platforms/ios/src/JoltImageEffect.h +25 -0
  238. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  239. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  240. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  241. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  242. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  243. package/platforms/ios/src/module.modulemap +6 -0
  244. package/src/android-plugin.d.ts +155 -0
  245. package/src/animate.ts +249 -36
  246. package/src/color.ts +101 -0
  247. package/src/config.ts +84 -0
  248. package/src/decay.ts +45 -0
  249. package/src/decoration.ts +180 -0
  250. package/src/drag-input.ts +74 -0
  251. package/src/drag-physics.ts +110 -0
  252. package/src/drag.ts +395 -0
  253. package/src/driver/android-color.ts +170 -0
  254. package/src/driver/android-decoration.ts +182 -0
  255. package/src/driver/android-playback.ts +157 -0
  256. package/src/driver/android.ts +208 -20
  257. package/src/driver/index.ts +37 -2
  258. package/src/driver/ios-color.ts +441 -0
  259. package/src/driver/ios-decoration.ts +332 -0
  260. package/src/driver/ios-layout.ts +63 -0
  261. package/src/driver/ios-playback.ts +51 -0
  262. package/src/driver/ios-shadow.ts +50 -0
  263. package/src/driver/ios.ts +193 -33
  264. package/src/engine.ts +321 -27
  265. package/src/haptics.ts +148 -0
  266. package/src/image-effect-native.ts +129 -0
  267. package/src/image-effect.ts +336 -0
  268. package/src/image-owner.ts +12 -0
  269. package/src/index.ts +59 -2
  270. package/src/interaction-surface.ts +41 -0
  271. package/src/ios-plugin.d.ts +81 -0
  272. package/src/items.ts +140 -0
  273. package/src/keyboard.ts +291 -0
  274. package/src/label.ts +76 -0
  275. package/src/layout.ts +408 -0
  276. package/src/liquid-grid.ts +28 -0
  277. package/src/liquid.ts +287 -0
  278. package/src/loader.ts +202 -0
  279. package/src/motion-scope.ts +279 -0
  280. package/src/motion-states.ts +215 -0
  281. package/src/particles.ts +323 -0
  282. package/src/path.ts +124 -0
  283. package/src/playback-group.ts +143 -0
  284. package/src/playback.ts +62 -0
  285. package/src/press.ts +26 -10
  286. package/src/properties.ts +32 -6
  287. package/src/reveal.ts +374 -0
  288. package/src/roll-diff.ts +57 -0
  289. package/src/roll.ts +515 -0
  290. package/src/scroll-linked.ts +73 -0
  291. package/src/sequence.ts +204 -14
  292. package/src/shimmer.ts +180 -0
  293. package/src/svg-path.ts +355 -0
  294. package/src/symbol-effect.ts +226 -0
  295. package/src/text-layout.ts +228 -0
  296. package/src/text.ts +279 -0
  297. package/src/track.ts +347 -12
  298. package/src/transition.ts +63 -6
  299. package/src/types.ts +49 -1
  300. package/src/vue/index.ts +231 -0
  301. package/src/zoom-native.ts +124 -0
  302. package/src/zoomable.ts +330 -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
+ }
@@ -1,11 +1,82 @@
1
- import { Utils, type View } from '@nativescript/core'
2
- import type { Property } from '../properties.js'
3
- import { after, type Driver } from './index.js'
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'
12
+ import { Application, Utils, type View } from '@nativescript/core'
13
+ import type { NumericProperty } from '../properties.js'
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
 
24
+ type Handle = AndroidPlayback
25
+
26
+ /** Own only layers we create; keep app-configured hardware/software layers and paints intact. */
27
+ function fadeLayer(view: View, nativeView: NativeView): (() => void) | undefined {
28
+ const { View, ViewGroup } = android.view
29
+ if (
30
+ !view.isLoaded ||
31
+ view.nativeViewProtected !== nativeView ||
32
+ !(nativeView instanceof ViewGroup) ||
33
+ nativeView.getChildCount() === 0 ||
34
+ !nativeView.getClipChildren() ||
35
+ !nativeView.isHardwareAccelerated() ||
36
+ nativeView.getLayerType() !== View.LAYER_TYPE_NONE
37
+ )
38
+ return undefined
39
+
40
+ // Do not cache an entire oversized scrolling/content tree into a GPU texture.
41
+ const { widthPixels, heightPixels } = nativeView.getResources().getDisplayMetrics()
42
+ const width = nativeView.getWidth()
43
+ const height = nativeView.getHeight()
44
+ if (width <= 0 || height <= 0 || width > widthPixels || height > heightPixels)
45
+ return undefined
46
+
47
+ nativeView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
48
+ let active = true
49
+ const restore = () => {
50
+ if (!active) return
51
+ active = false
52
+ view.off('unloaded', restore)
53
+ if (nativeView.getLayerType() === View.LAYER_TYPE_HARDWARE) {
54
+ nativeView.setLayerType(View.LAYER_TYPE_NONE, null)
55
+ }
56
+ }
57
+ view.on('unloaded', restore)
58
+ return restore
59
+ }
60
+
61
+ /**
62
+ * A translucent view is drawn into a buffer of its own size, which cuts off children
63
+ * that draw outside it. Fade those children one by one instead.
64
+ */
65
+ function keepOverflow(nativeView: NativeView) {
66
+ if (
67
+ android.os.Build.VERSION.SDK_INT >= 24 &&
68
+ nativeView instanceof android.view.ViewGroup &&
69
+ !nativeView.getClipChildren() &&
70
+ nativeView.getHasOverlappingRendering()
71
+ ) {
72
+ nativeView.forceHasOverlappingRendering(false)
73
+ // The render node only picks the flag up when the view draws again.
74
+ nativeView.invalidate()
75
+ }
76
+ }
77
+
7
78
  const accessors: Record<
8
- Property,
79
+ NumericProperty,
9
80
  { get(view: NativeView): number; set(view: NativeView, value: number): void }
10
81
  > = {
11
82
  opacity: { get: (v) => v.getAlpha(), set: (v, value) => v.setAlpha(value) },
@@ -18,7 +89,7 @@ const accessors: Record<
18
89
  rotateY: { get: (v) => v.getRotationY(), set: (v, value) => v.setRotationY(value) },
19
90
  }
20
91
 
21
- const unit = (property: Property) =>
92
+ const unit = (property: NumericProperty) =>
22
93
  property === 'x' || property === 'y' ? Utils.layout.getDisplayDensity() : 1
23
94
 
24
95
  export function createAndroidDriver(): Driver {
@@ -32,16 +103,30 @@ export function createAndroidDriver(): Driver {
32
103
  rotate: View.ROTATION,
33
104
  rotateX: View.ROTATION_X,
34
105
  rotateY: View.ROTATION_Y,
35
- } satisfies Record<Property, unknown>
106
+ } satisfies Record<NumericProperty, unknown>
107
+ const clock = androidClockFactory()
36
108
  const interpolator = new android.view.animation.LinearInterpolator()
37
109
 
38
110
  return {
39
111
  now: () => android.os.SystemClock.uptimeMillis(),
112
+ clock,
113
+ control(_view, handle, elapsed, paused, command) {
114
+ ;(handle as Handle).control(elapsed, paused, command)
115
+ },
40
116
 
41
117
  play(view, track, _startedAt, onEnd) {
118
+ if (isDecorationProperty(track.property))
119
+ return playAndroidDecoration(view, track, onEnd)
120
+ if (isColorProperty(track.property)) return playAndroidColor(view, track, onEnd)
42
121
  const nativeView = view.nativeViewProtected as NativeView
43
122
  const scale = unit(track.property)
44
123
 
124
+ if (track.property === 'rotateX' || track.property === 'rotateY') {
125
+ nativeView.setCameraDistance(
126
+ (view.perspective || 300) * Utils.layout.getDisplayDensity(),
127
+ )
128
+ }
129
+
45
130
  const keyframes = Array.create(android.animation.Keyframe, track.values.length)
46
131
  track.values.forEach((value, i) => {
47
132
  keyframes[i] = android.animation.Keyframe.ofFloat(track.times[i]!, value * scale)
@@ -56,6 +141,11 @@ export function createAndroidDriver(): Driver {
56
141
  nativeView,
57
142
  holders,
58
143
  )
144
+ let restoreLayer: (() => void) | undefined
145
+ const releaseLayer = () => {
146
+ restoreLayer?.()
147
+ restoreLayer = undefined
148
+ }
59
149
  animator.setDuration(Math.max(track.duration, 1))
60
150
  animator.setStartDelay(track.delay)
61
151
  animator.setInterpolator(interpolator)
@@ -71,31 +161,129 @@ export function createAndroidDriver(): Driver {
71
161
  : android.animation.ValueAnimator.RESTART,
72
162
  )
73
163
  }
74
- animator.addListener(
164
+ if (track.property === 'opacity' && track.values.some((value) => value < 1))
165
+ keepOverflow(nativeView)
166
+ accessors[track.property].set(nativeView, track.values[0]! * scale)
167
+ return androidPlayback(
168
+ animator,
169
+ track,
170
+ onEnd,
171
+ () => {
172
+ if (
173
+ track.property === 'opacity' &&
174
+ track.values.some((value) => value !== track.values[0])
175
+ )
176
+ restoreLayer = fadeLayer(view, nativeView)
177
+ },
178
+ releaseLayer,
179
+ )
180
+ },
181
+
182
+ stop(_view, handle) {
183
+ ;(handle as Handle).cancel()
184
+ },
185
+
186
+ sync(view) {
187
+ syncAndroidDecoration(view)
188
+ syncAndroidColor(view)
189
+ },
190
+
191
+ elapsed: (handle) => (handle as Handle).elapsed(),
192
+
193
+ presented(view, property) {
194
+ if (isDecorationProperty(property))
195
+ return presentedAndroidDecoration(view, property)
196
+ if (isColorProperty(property)) return presentedAndroidColor(view, property)
197
+ const nativeView = view.nativeViewProtected as NativeView | undefined
198
+ return nativeView ? accessors[property].get(nativeView) / unit(property) : undefined
199
+ },
200
+
201
+ layoutFrame(view) {
202
+ const native = view.nativeViewProtected as android.view.View | undefined
203
+ if (!native) return undefined
204
+ const dip = Utils.layout.toDeviceIndependentPixels
205
+ return {
206
+ x: dip(native.getLeft()),
207
+ y: dip(native.getTop()),
208
+ width: dip(native.getWidth()),
209
+ height: dip(native.getHeight()),
210
+ }
211
+ },
212
+
213
+ afterLayout(view, callback) {
214
+ const native = view.nativeViewProtected as android.view.View
215
+ const observer = native.getViewTreeObserver()
216
+ let active = true
217
+ const detach = () => {
218
+ if (!active) return
219
+ active = false
220
+ if (observer.isAlive()) observer.removeOnPreDrawListener(listener)
221
+ else {
222
+ const current = native.getViewTreeObserver()
223
+ if (current.isAlive()) current.removeOnPreDrawListener(listener)
224
+ }
225
+ }
226
+ const listener = new android.view.ViewTreeObserver.OnPreDrawListener({
227
+ onPreDraw() {
228
+ if (active) {
229
+ detach()
230
+ callback()
231
+ }
232
+ return true
233
+ },
234
+ })
235
+ observer.addOnPreDrawListener(listener)
236
+ return detach
237
+ },
238
+
239
+ after(ms, callback) {
240
+ // A native marker follows animator duration scale, including changes during playback.
241
+ // Only completion crosses the bridge; there is no JavaScript update listener.
242
+ const values = Array.create('float', 2)
243
+ values[0] = 0
244
+ values[1] = 1
245
+ const marker = android.animation.ValueAnimator.ofFloat(values)
246
+ marker.setDuration(ms)
247
+ let active = true
248
+ marker.addListener(
75
249
  new android.animation.Animator.AnimatorListener({
76
250
  onAnimationStart() {},
77
251
  onAnimationRepeat() {},
78
- onAnimationCancel() {},
252
+ onAnimationCancel() {
253
+ active = false
254
+ },
79
255
  onAnimationEnd() {
80
- onEnd()
256
+ if (!active) return
257
+ active = false
258
+ callback()
81
259
  },
82
260
  }),
83
261
  )
84
-
85
- accessors[track.property].set(nativeView, track.values[0]! * scale)
86
- animator.start()
87
- return animator
262
+ marker.start()
263
+ return () => {
264
+ active = false
265
+ marker.cancel()
266
+ }
88
267
  },
89
268
 
90
- stop(_view, handle) {
91
- ;(handle as android.animation.ObjectAnimator).cancel()
269
+ reducedMotion() {
270
+ // Remove animations sets the animator duration scale to zero.
271
+ if (android.os.Build.VERSION.SDK_INT >= 26)
272
+ return !android.animation.ValueAnimator.areAnimatorsEnabled()
273
+ const { Global } = android.provider.Settings
274
+ return (
275
+ Global.getFloat(
276
+ Utils.android.getApplicationContext().getContentResolver(),
277
+ Global.ANIMATOR_DURATION_SCALE,
278
+ 1,
279
+ ) === 0
280
+ )
92
281
  },
93
282
 
94
- presented(view, property) {
95
- const nativeView = view.nativeViewProtected as NativeView | undefined
96
- return nativeView ? accessors[property].get(nativeView) / unit(property) : undefined
283
+ watchReducedMotion(callback) {
284
+ // Android has no broadcast for the animator scale; it can only change in Settings.
285
+ Application.on(Application.resumeEvent, callback)
286
+ return () => Application.off(Application.resumeEvent, callback)
97
287
  },
98
-
99
- after,
100
288
  }
101
289
  }
@@ -1,19 +1,54 @@
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'
4
5
  import { createAndroidDriver } from './android.js'
5
6
  import { createIOSDriver } from './ios.js'
6
7
 
8
+ export interface LayoutFrame {
9
+ x: number
10
+ y: number
11
+ width: number
12
+ height: number
13
+ }
14
+
7
15
  export interface Driver {
8
16
  /** Milliseconds, on the clock the platform's animations run on. */
9
17
  now(): number
10
18
  /** 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
19
+ play(
20
+ view: View,
21
+ track: Track,
22
+ startedAt: number,
23
+ onEnd: (finished?: boolean) => void,
24
+ ): unknown
12
25
  stop(view: View, handle: unknown): void
26
+ /** A native elapsed-time marker when system animation speed affects time. */
27
+ clock?(duration: number): PlaybackClock
28
+ /** Move only this native track to an elapsed time, optionally holding it. */
29
+ control?(
30
+ view: View,
31
+ handle: unknown,
32
+ elapsed: number,
33
+ paused: boolean,
34
+ command?: PlaybackCommand,
35
+ ): void
36
+ /** Reapply native presentation after a Core model write rebuilds its drawable/layers. */
37
+ sync?(view: View, excluding?: Property): void
38
+ /** Track time, including its delay, when the native clock differs from `now()`. */
39
+ elapsed?(handle: unknown): number
13
40
  /** The value on screen, when the platform knows it better than the track does. */
14
41
  presented(view: View, property: Property): number | undefined
15
- /** Calls back after `ms` on the same clock, and returns a function that cancels it. */
42
+ /** Calls back after `ms` of animation time, respecting platform speed settings. */
16
43
  after(ms: number, callback: () => void): () => void
44
+ /** Untransformed native layout bounds in parent DIPs. */
45
+ layoutFrame?(view: View): LayoutFrame | undefined
46
+ /** One callback after layout, before drawing; caller requests layout and owns cancellation. */
47
+ afterLayout?(view: View, callback: () => void): () => void
48
+ /** The platform's accessibility setting: Reduce Motion on iOS, Remove animations on Android. */
49
+ reducedMotion?(): boolean
50
+ /** Calls back when that setting may have changed; returns a cleanup function. */
51
+ watchReducedMotion?(callback: () => void): () => void
17
52
  }
18
53
 
19
54
  export function after(ms: number, callback: () => void) {