@rafaelaugusto/jolt 0.1.0-beta.5 → 0.1.0-beta.6
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 +110 -3
- package/dist/animate.d.ts +4 -2
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +48 -11
- package/dist/animate.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.js.map +1 -0
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +7 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +2 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.js +1 -1
- package/dist/driver/ios-color.js.map +1 -1
- package/dist/driver/ios-decoration.js +1 -1
- package/dist/driver/ios-decoration.js.map +1 -1
- package/dist/driver/ios-playback.d.ts +1 -1
- package/dist/driver/ios-playback.d.ts.map +1 -1
- package/dist/driver/ios-playback.js +14 -0
- package/dist/driver/ios-playback.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +1 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +16 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +33 -1
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/items.d.ts +20 -0
- package/dist/items.d.ts.map +1 -0
- package/dist/items.js +109 -0
- package/dist/items.js.map +1 -0
- package/dist/keyboard.d.ts +17 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +116 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.d.ts.map +1 -0
- package/dist/label.js +60 -0
- package/dist/label.js.map +1 -0
- package/dist/liquid.d.ts.map +1 -1
- package/dist/liquid.js +2 -1
- package/dist/liquid.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +2 -1
- package/dist/loader.js.map +1 -1
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +4 -3
- package/dist/particles.js.map +1 -1
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +90 -0
- package/dist/path.js.map +1 -0
- package/dist/playback-group.d.ts.map +1 -1
- package/dist/playback-group.js +29 -1
- package/dist/playback-group.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +3 -3
- package/dist/press.js.map +1 -1
- package/dist/reveal.d.ts.map +1 -1
- package/dist/reveal.js +3 -2
- package/dist/reveal.js.map +1 -1
- package/dist/roll-diff.d.ts +22 -0
- package/dist/roll-diff.d.ts.map +1 -0
- package/dist/roll-diff.js +36 -0
- package/dist/roll-diff.js.map +1 -0
- package/dist/roll.d.ts +29 -0
- package/dist/roll.d.ts.map +1 -0
- package/dist/roll.js +442 -0
- package/dist/roll.js.map +1 -0
- package/dist/scroll-linked.d.ts +10 -0
- package/dist/scroll-linked.d.ts.map +1 -0
- package/dist/scroll-linked.js +58 -0
- package/dist/scroll-linked.js.map +1 -0
- package/dist/sequence.d.ts +10 -5
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +202 -14
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts.map +1 -1
- package/dist/shimmer.js +2 -1
- package/dist/shimmer.js.map +1 -1
- package/dist/svg-path.d.ts +14 -0
- package/dist/svg-path.d.ts.map +1 -0
- package/dist/svg-path.js +281 -0
- package/dist/svg-path.js.map +1 -0
- package/dist/symbol-effect.d.ts.map +1 -1
- package/dist/symbol-effect.js +4 -3
- package/dist/symbol-effect.js.map +1 -1
- package/dist/text-layout.d.ts +8 -0
- package/dist/text-layout.d.ts.map +1 -1
- package/dist/text-layout.js +8 -2
- package/dist/text-layout.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +60 -64
- package/dist/text.js.map +1 -1
- package/dist/track.d.ts +2 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +62 -15
- package/dist/track.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +89 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +134 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +15 -3
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +6 -0
- package/src/android-plugin.d.ts +64 -0
- package/src/animate.ts +80 -30
- package/src/config.ts +46 -0
- package/src/driver/android.ts +14 -0
- package/src/driver/index.ts +2 -0
- package/src/driver/ios-color.ts +1 -1
- package/src/driver/ios-decoration.ts +1 -1
- package/src/driver/ios-playback.ts +15 -1
- package/src/driver/ios.ts +2 -0
- package/src/engine.ts +47 -2
- package/src/index.ts +11 -1
- package/src/items.ts +140 -0
- package/src/keyboard.ts +132 -0
- package/src/label.ts +76 -0
- package/src/liquid.ts +2 -1
- package/src/loader.ts +2 -1
- package/src/particles.ts +4 -4
- package/src/path.ts +124 -0
- package/src/playback-group.ts +34 -1
- package/src/press.ts +3 -5
- package/src/reveal.ts +3 -3
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +199 -14
- package/src/shimmer.ts +2 -1
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +4 -3
- package/src/text-layout.ts +14 -2
- package/src/text.ts +62 -81
- package/src/track.ts +89 -24
- package/src/types.ts +8 -1
- package/src/vue/index.ts +189 -0
package/src/driver/android.ts
CHANGED
|
@@ -207,5 +207,19 @@ export function createAndroidDriver(): Driver {
|
|
|
207
207
|
marker.cancel()
|
|
208
208
|
}
|
|
209
209
|
},
|
|
210
|
+
|
|
211
|
+
reducedMotion() {
|
|
212
|
+
// Remove animations sets the animator duration scale to zero.
|
|
213
|
+
if (android.os.Build.VERSION.SDK_INT >= 26)
|
|
214
|
+
return !android.animation.ValueAnimator.areAnimatorsEnabled()
|
|
215
|
+
const { Global } = android.provider.Settings
|
|
216
|
+
return (
|
|
217
|
+
Global.getFloat(
|
|
218
|
+
Utils.android.getApplicationContext().getContentResolver(),
|
|
219
|
+
Global.ANIMATOR_DURATION_SCALE,
|
|
220
|
+
1,
|
|
221
|
+
) === 0
|
|
222
|
+
)
|
|
223
|
+
},
|
|
210
224
|
}
|
|
211
225
|
}
|
package/src/driver/index.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface Driver {
|
|
|
34
34
|
presented(view: View, property: Property): number | undefined
|
|
35
35
|
/** Calls back after `ms` of animation time, respecting platform speed settings. */
|
|
36
36
|
after(ms: number, callback: () => void): () => void
|
|
37
|
+
/** The platform's accessibility setting: Reduce Motion on iOS, Remove animations on Android. */
|
|
38
|
+
reducedMotion?(): boolean
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export function after(ms: number, callback: () => void) {
|
package/src/driver/ios-color.ts
CHANGED
|
@@ -203,7 +203,6 @@ export function playIOSColor(
|
|
|
203
203
|
)
|
|
204
204
|
animation.keyTimes = NSArray.arrayWithArray(track.times as number[])
|
|
205
205
|
animation.duration = Math.max(track.duration, 1) / 1000
|
|
206
|
-
timing.configure(animation, layer)
|
|
207
206
|
if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
|
|
208
207
|
const maximum = Math.min(
|
|
209
208
|
120,
|
|
@@ -220,6 +219,7 @@ export function playIOSColor(
|
|
|
220
219
|
animation.autoreverses = track.reverse
|
|
221
220
|
animation.repeatCount = track.reverse ? (track.repeat + 1) / 2 : track.repeat + 1
|
|
222
221
|
}
|
|
222
|
+
timing.configure(animation, layer)
|
|
223
223
|
CATransaction.begin()
|
|
224
224
|
if (!timing.paused && track.repeat !== Infinity)
|
|
225
225
|
CATransaction.setCompletionBlock(() => {
|
|
@@ -234,13 +234,13 @@ export function playIOSDecoration(
|
|
|
234
234
|
animation.values = NSArray.arrayWithArray(values)
|
|
235
235
|
animation.keyTimes = NSArray.arrayWithArray(times)
|
|
236
236
|
animation.duration = Math.max(1, track.duration) / 1000
|
|
237
|
-
timing.configure(animation, layer)
|
|
238
237
|
if (track.repeat > 0) {
|
|
239
238
|
animation.autoreverses = track.reverse
|
|
240
239
|
animation.repeatCount = track.reverse
|
|
241
240
|
? (track.repeat + 1) / 2
|
|
242
241
|
: track.repeat + 1
|
|
243
242
|
}
|
|
243
|
+
timing.configure(animation, layer)
|
|
244
244
|
if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
|
|
245
245
|
const max = Math.min(
|
|
246
246
|
120,
|
|
@@ -21,7 +21,7 @@ export function iosPlayback(track: Track, startedAt: number) {
|
|
|
21
21
|
anchor = CACurrentMediaTime() * 1000
|
|
22
22
|
paused = hold
|
|
23
23
|
},
|
|
24
|
-
configure(animation:
|
|
24
|
+
configure(animation: CAKeyframeAnimation, layer: CALayer) {
|
|
25
25
|
const now = CACurrentMediaTime()
|
|
26
26
|
const position = elapsed()
|
|
27
27
|
animation.speed = paused ? 0 : 1
|
|
@@ -32,6 +32,20 @@ export function iosPlayback(track: Track, startedAt: number) {
|
|
|
32
32
|
)
|
|
33
33
|
animation.fillMode = paused ? 'both' : kCAFillModeBackwards
|
|
34
34
|
animation.removedOnCompletion = !paused && track.repeat !== Infinity
|
|
35
|
+
// With speed 0, CA folds an exact duration boundary back to the first
|
|
36
|
+
// frame. Hold the actual endpoint with a constant copy, also when a
|
|
37
|
+
// shorter property waits at its end while another one is being scrubbed.
|
|
38
|
+
if (paused && Number.isFinite(track.end) && position >= track.end) {
|
|
39
|
+
const values = animation.values!
|
|
40
|
+
const index = track.reverse && track.repeat % 2 === 1 ? 0 : values.count - 1
|
|
41
|
+
const value = values.objectAtIndex(index)
|
|
42
|
+
animation.values = NSArray.arrayWithArray([value, value])
|
|
43
|
+
animation.keyTimes = NSArray.arrayWithArray([0, 1])
|
|
44
|
+
animation.duration = 1
|
|
45
|
+
animation.repeatCount = 0
|
|
46
|
+
animation.autoreverses = false
|
|
47
|
+
animation.timeOffset = 0
|
|
48
|
+
}
|
|
35
49
|
},
|
|
36
50
|
}
|
|
37
51
|
}
|
package/src/driver/ios.ts
CHANGED
package/src/engine.ts
CHANGED
|
@@ -12,12 +12,13 @@ import { readValue, writeValue, type Property } from './properties.js'
|
|
|
12
12
|
import { createTrack, type Track } from './track.js'
|
|
13
13
|
import type { ResolvedTransition } from './transition.js'
|
|
14
14
|
import { isColorProperty } from './color.js'
|
|
15
|
+
import type { PreparedPath, PathProperty } from './path.js'
|
|
15
16
|
|
|
16
17
|
export interface Animation extends PromiseLike<boolean> {
|
|
17
18
|
/**
|
|
18
19
|
* Resolves with `true` once the animation played to the end, or with `false`
|
|
19
20
|
* if it was stopped, superseded or skipped.
|
|
20
|
-
*
|
|
21
|
+
* Playback cancellation never rejects; sequences can reject setup/callback errors.
|
|
21
22
|
*/
|
|
22
23
|
readonly finished: Promise<boolean>
|
|
23
24
|
/** Stops playback. Each effect documents whether it preserves or restores its visual state. */
|
|
@@ -30,6 +31,7 @@ export interface Plan {
|
|
|
30
31
|
keyframes: readonly (number | null)[]
|
|
31
32
|
transition: ResolvedTransition
|
|
32
33
|
relative?: boolean
|
|
34
|
+
path?: PreparedPath
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
interface Entry {
|
|
@@ -49,6 +51,21 @@ const revisions = new WeakMap<View, Map<Property, symbol>>()
|
|
|
49
51
|
const running = new WeakMap<View, Map<Property, Entry>>()
|
|
50
52
|
const arrivals = new WeakMap<Animation, Promise<unknown>>()
|
|
51
53
|
|
|
54
|
+
/** Internal forecast for starting a timeline step before another one arrives. */
|
|
55
|
+
export interface ArrivalTiming {
|
|
56
|
+
read(): { remaining: number | undefined; paused: boolean; arrived: boolean }
|
|
57
|
+
subscribe(callback: () => void): () => void
|
|
58
|
+
}
|
|
59
|
+
const arrivalTimings = new WeakMap<Animation, ArrivalTiming>()
|
|
60
|
+
|
|
61
|
+
export function arrivalTimingOf(animation: Animation) {
|
|
62
|
+
return arrivalTimings.get(animation)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setArrivalTiming(animation: Animation, timing: ArrivalTiming) {
|
|
66
|
+
arrivalTimings.set(animation, timing)
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
/**
|
|
53
70
|
* Resolves once the animation looks done, which for a spring is well before
|
|
54
71
|
* `finished`. Sequences move on to the next step then.
|
|
@@ -83,6 +100,10 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
83
100
|
private idleListener: (() => void) | undefined
|
|
84
101
|
private readyListener: (() => void) | undefined
|
|
85
102
|
private readonly endless: boolean
|
|
103
|
+
private readonly timingListeners = new Set<() => void>()
|
|
104
|
+
private timingChanged() {
|
|
105
|
+
for (const callback of [...this.timingListeners]) callback()
|
|
106
|
+
}
|
|
86
107
|
|
|
87
108
|
get paused() {
|
|
88
109
|
return this._paused
|
|
@@ -136,6 +157,7 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
136
157
|
this.cancelArrival?.()
|
|
137
158
|
this.haptics?.pause()
|
|
138
159
|
if (this.started) this.controlTracks(this.time(), 'pause')
|
|
160
|
+
this.timingChanged()
|
|
139
161
|
}
|
|
140
162
|
play() {
|
|
141
163
|
if (this.settled || !this._paused) return
|
|
@@ -148,6 +170,7 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
148
170
|
this.haptics?.play()
|
|
149
171
|
if (this.pending === 0) this.becameIdle()
|
|
150
172
|
}
|
|
173
|
+
this.timingChanged()
|
|
151
174
|
}
|
|
152
175
|
seekTime(time: number) {
|
|
153
176
|
if (this.settled) return
|
|
@@ -173,6 +196,7 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
173
196
|
this.scheduleArrival()
|
|
174
197
|
this.haptics?.seek()
|
|
175
198
|
if (this.pending === 0) this.becameIdle()
|
|
199
|
+
this.timingChanged()
|
|
176
200
|
}
|
|
177
201
|
elapsed(entry: Entry, now: number) {
|
|
178
202
|
return this.controlled
|
|
@@ -203,10 +227,12 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
203
227
|
if (remaining <= 0) {
|
|
204
228
|
this.arrived = true
|
|
205
229
|
this.arrive()
|
|
230
|
+
this.timingChanged()
|
|
206
231
|
} else
|
|
207
232
|
this.cancelArrival = getDriver().after(remaining, () => {
|
|
208
233
|
this.arrived = true
|
|
209
234
|
this.arrive()
|
|
235
|
+
this.timingChanged()
|
|
210
236
|
})
|
|
211
237
|
}
|
|
212
238
|
private becameIdle() {
|
|
@@ -248,6 +274,17 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
248
274
|
)
|
|
249
275
|
this.finished = new Promise((resolve) => (this.resolve = resolve))
|
|
250
276
|
setArrival(this, new Promise<void>((resolve) => (this.arrive = resolve)))
|
|
277
|
+
setArrivalTiming(this, {
|
|
278
|
+
read: () => ({
|
|
279
|
+
remaining: this.started ? Math.max(0, this.arrival - this.time()) : undefined,
|
|
280
|
+
paused: this._paused,
|
|
281
|
+
arrived: this.arrived || this.settled,
|
|
282
|
+
}),
|
|
283
|
+
subscribe: (callback) => {
|
|
284
|
+
this.timingListeners.add(callback)
|
|
285
|
+
return () => this.timingListeners.delete(callback)
|
|
286
|
+
},
|
|
287
|
+
})
|
|
251
288
|
if (view.isLoaded && view.nativeViewProtected) this.start(plans)
|
|
252
289
|
else this.waitForLoad(plans)
|
|
253
290
|
}
|
|
@@ -285,7 +322,7 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
285
322
|
const now = driver.now()
|
|
286
323
|
const entries = entriesOf(this.view)
|
|
287
324
|
|
|
288
|
-
const tracks = plans.map(({ property, keyframes, transition, relative }) => {
|
|
325
|
+
const tracks = plans.map(({ property, keyframes, transition, relative, path }) => {
|
|
289
326
|
const previous = entries.get(property)
|
|
290
327
|
let current: number
|
|
291
328
|
let velocity = 0
|
|
@@ -300,6 +337,12 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
300
337
|
? driver.presented(this.view, property)
|
|
301
338
|
: undefined) ?? readValue(this.view, property)
|
|
302
339
|
}
|
|
340
|
+
if (path)
|
|
341
|
+
return path.track(
|
|
342
|
+
property as PathProperty,
|
|
343
|
+
relative ? current : 0,
|
|
344
|
+
transition.delay,
|
|
345
|
+
)
|
|
303
346
|
return createTrack(
|
|
304
347
|
property,
|
|
305
348
|
keyframes.map((value) => {
|
|
@@ -357,6 +400,7 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
357
400
|
} else if (this._paused) this.controlTracks(0, 'pause')
|
|
358
401
|
this.scheduleArrival()
|
|
359
402
|
this.readyListener?.()
|
|
403
|
+
this.timingChanged()
|
|
360
404
|
if (this.pending === 0) this.becameIdle()
|
|
361
405
|
}
|
|
362
406
|
|
|
@@ -388,6 +432,7 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
388
432
|
this.haptics = undefined
|
|
389
433
|
this.arrive()
|
|
390
434
|
this.resolve(this.complete)
|
|
435
|
+
this.timingChanged()
|
|
391
436
|
}
|
|
392
437
|
}
|
|
393
438
|
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export { animate, decay, stop } from './animate.js'
|
|
2
2
|
export { sequence } from './sequence.js'
|
|
3
|
-
export type { Step } from './sequence.js'
|
|
3
|
+
export type { Step, SequenceOptions, SequencePosition } from './sequence.js'
|
|
4
4
|
export { press } from './press.js'
|
|
5
|
+
export { scrollLinked } from './scroll-linked.js'
|
|
6
|
+
export type { ScrollLinkedOptions } from './scroll-linked.js'
|
|
7
|
+
export { animateItems } from './items.js'
|
|
8
|
+
export type { ItemsOptions } from './items.js'
|
|
9
|
+
export { followKeyboard } from './keyboard.js'
|
|
10
|
+
export type { KeyboardOptions } from './keyboard.js'
|
|
5
11
|
export type { Haptic, HapticOptions, HapticStyle } from './haptics.js'
|
|
6
12
|
export { confetti, particles } from './particles.js'
|
|
7
13
|
export type { ConfettiOptions, ParticleOptions } from './particles.js'
|
|
@@ -15,12 +21,16 @@ export { reveal, conceal } from './reveal.js'
|
|
|
15
21
|
export type { RevealOptions, ConcealOptions } from './reveal.js'
|
|
16
22
|
export { animateText } from './text.js'
|
|
17
23
|
export type { TextOptions } from './text.js'
|
|
24
|
+
export { rollTo } from './roll.js'
|
|
25
|
+
export type { RollOptions } from './roll.js'
|
|
18
26
|
export { symbolEffect } from './symbol-effect.js'
|
|
19
27
|
export type {
|
|
20
28
|
SymbolEffect,
|
|
21
29
|
SymbolEffectName,
|
|
22
30
|
SymbolEffectOptions,
|
|
23
31
|
} from './symbol-effect.js'
|
|
32
|
+
export { configure } from './config.js'
|
|
33
|
+
export type { JoltConfig } from './config.js'
|
|
24
34
|
export { definePreset, presets } from './presets.js'
|
|
25
35
|
export type { Preset, PresetName, Presets } from './presets.js'
|
|
26
36
|
export { springs } from './transition.js'
|
package/src/items.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ListView, View, type EventData, type ItemEventData } from '@nativescript/core'
|
|
2
|
+
import { animate, endingValues, startingValues, toView } from './animate.js'
|
|
3
|
+
import { getDriver } from './driver/index.js'
|
|
4
|
+
import { stopView } from './engine.js'
|
|
5
|
+
import type { PresetName } from './presets.js'
|
|
6
|
+
import { writeValue } from './properties.js'
|
|
7
|
+
import type { SpringName } from './transition.js'
|
|
8
|
+
import type { AnimateOptions, Keyframes, Target } from './types.js'
|
|
9
|
+
|
|
10
|
+
export type ItemsOptions = AnimateOptions & {
|
|
11
|
+
/** Animate each data item only the first time it shows up. Defaults to false. */
|
|
12
|
+
once?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Pending {
|
|
16
|
+
index: number
|
|
17
|
+
section: number | undefined
|
|
18
|
+
item: unknown
|
|
19
|
+
key: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Row = View & {
|
|
23
|
+
bindingContext?: unknown
|
|
24
|
+
_listViewItemIndex?: number
|
|
25
|
+
_listViewSectionIndex?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Source extends View {
|
|
29
|
+
items?: unknown
|
|
30
|
+
_getDataItem?(index: number): unknown
|
|
31
|
+
_getDataItemInSection?(section: number, index: number): unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Plays an animation on each row of a list as it comes on screen. Rows are
|
|
36
|
+
* recycled, so a row that is reused for another item starts over, or settles
|
|
37
|
+
* at the end values when that item was already shown and `once` is set.
|
|
38
|
+
* Returns a function that removes the effect.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* animateItems(listView, 'fadeInUp')
|
|
42
|
+
* animateItems(listView, 'fadeInUp', { once: true })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function animateItems(
|
|
46
|
+
list: Target,
|
|
47
|
+
animation: Keyframes | PresetName,
|
|
48
|
+
options?: ItemsOptions | SpringName,
|
|
49
|
+
): () => void {
|
|
50
|
+
const source = toView(list) as Source
|
|
51
|
+
const { once = false, ...rest } = typeof options === 'object' ? options : {}
|
|
52
|
+
if (rest.stagger !== undefined)
|
|
53
|
+
throw new TypeError('animateItems animates one row at a time; stagger has no effect.')
|
|
54
|
+
const transition: AnimateOptions | SpringName | undefined =
|
|
55
|
+
typeof options === 'string' ? options : rest
|
|
56
|
+
const properties = [...startingValues(animation, transition).keys()]
|
|
57
|
+
const ends = endingValues(animation, transition)
|
|
58
|
+
|
|
59
|
+
const rows = new Set<Row>()
|
|
60
|
+
const seenItems = new WeakSet<object>()
|
|
61
|
+
const seenKeys = new Set<string>()
|
|
62
|
+
const pending: Pending[] = []
|
|
63
|
+
let queued = false
|
|
64
|
+
let disposed = false
|
|
65
|
+
|
|
66
|
+
const settle = (row: Row) => {
|
|
67
|
+
stopView(row, properties)
|
|
68
|
+
for (const [property, value] of ends)
|
|
69
|
+
if (value !== null) writeValue(row, property, value)
|
|
70
|
+
getDriver().sync?.(row)
|
|
71
|
+
}
|
|
72
|
+
const show = (row: Row, { item, key }: Pending) => {
|
|
73
|
+
rows.add(row)
|
|
74
|
+
const tracked = typeof item === 'object' && item !== null
|
|
75
|
+
const seen = tracked ? seenItems.has(item) : seenKeys.has(key)
|
|
76
|
+
if (tracked) seenItems.add(item)
|
|
77
|
+
else seenKeys.add(key)
|
|
78
|
+
if (once && seen) settle(row)
|
|
79
|
+
else {
|
|
80
|
+
stopView(row, properties)
|
|
81
|
+
animate(row, animation, transition)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const flush = () => {
|
|
85
|
+
queued = false
|
|
86
|
+
if (disposed) return
|
|
87
|
+
const current = new Set<Row>()
|
|
88
|
+
const fresh: Row[] = []
|
|
89
|
+
source.eachChildView((child: Row) => {
|
|
90
|
+
current.add(child)
|
|
91
|
+
if (!rows.has(child)) fresh.push(child)
|
|
92
|
+
return true
|
|
93
|
+
})
|
|
94
|
+
for (const row of rows) if (!current.has(row)) rows.delete(row)
|
|
95
|
+
for (const entry of pending.splice(0)) {
|
|
96
|
+
const row =
|
|
97
|
+
fresh.find(
|
|
98
|
+
(candidate) =>
|
|
99
|
+
candidate._listViewItemIndex === entry.index &&
|
|
100
|
+
candidate._listViewSectionIndex === entry.section,
|
|
101
|
+
) ??
|
|
102
|
+
fresh.find(
|
|
103
|
+
(candidate) =>
|
|
104
|
+
entry.item !== undefined && candidate.bindingContext === entry.item,
|
|
105
|
+
)
|
|
106
|
+
if (!row) continue
|
|
107
|
+
fresh.splice(fresh.indexOf(row), 1)
|
|
108
|
+
show(row, entry)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const onItemLoading = (event: EventData) => {
|
|
112
|
+
const { index, section, view } = event as ItemEventData
|
|
113
|
+
const item =
|
|
114
|
+
section === undefined
|
|
115
|
+
? source._getDataItem?.(index)
|
|
116
|
+
: source._getDataItemInSection?.(section, index)
|
|
117
|
+
const entry: Pending = { index, section, item, key: `${section ?? ''}:${index}` }
|
|
118
|
+
if (view instanceof View) {
|
|
119
|
+
show(view, entry)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
pending.push(entry)
|
|
123
|
+
if (queued) return
|
|
124
|
+
queued = true
|
|
125
|
+
void Promise.resolve().then(flush)
|
|
126
|
+
}
|
|
127
|
+
const cleanup = () => {
|
|
128
|
+
if (disposed) return
|
|
129
|
+
disposed = true
|
|
130
|
+
source.off(ListView.itemLoadingEvent, onItemLoading)
|
|
131
|
+
source.off('unloaded', cleanup)
|
|
132
|
+
pending.length = 0
|
|
133
|
+
for (const row of rows) settle(row)
|
|
134
|
+
rows.clear()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
source.on(ListView.itemLoadingEvent, onItemLoading)
|
|
138
|
+
source.on('unloaded', cleanup)
|
|
139
|
+
return cleanup
|
|
140
|
+
}
|
package/src/keyboard.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Application, Utils, isAndroid, isIOS, type View } from '@nativescript/core'
|
|
2
|
+
import { stop, toView } from './animate.js'
|
|
3
|
+
import type { Target } from './types.js'
|
|
4
|
+
|
|
5
|
+
export interface KeyboardOptions {
|
|
6
|
+
/** DIPs kept between the view's bottom edge and the keyboard. Defaults to zero. */
|
|
7
|
+
offset?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const BEGIN_FROM_CURRENT_STATE = 4
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lifts the view by its overlap with the software keyboard, at the keyboard's
|
|
14
|
+
* own speed and curve, and lets it back down when the keyboard hides.
|
|
15
|
+
* Returns a function that removes the effect.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const release = followKeyboard(composer)
|
|
19
|
+
* followKeyboard(sendButton, { offset: 12 })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function followKeyboard(
|
|
23
|
+
target: Target,
|
|
24
|
+
options: KeyboardOptions = {},
|
|
25
|
+
): () => void {
|
|
26
|
+
const view = toView(target)
|
|
27
|
+
const offset = options.offset ?? 0
|
|
28
|
+
if (!Number.isFinite(offset)) throw new RangeError('Keyboard offset must be finite.')
|
|
29
|
+
if (isIOS) return followIOS(view, offset)
|
|
30
|
+
if (isAndroid) return followAndroid(view, offset)
|
|
31
|
+
return () => {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function followIOS(view: View, offset: number) {
|
|
35
|
+
let disposed = false
|
|
36
|
+
let current = 0
|
|
37
|
+
const onKeyboard = (notification: NSNotification) => {
|
|
38
|
+
if (disposed || !view.isLoaded) return
|
|
39
|
+
const native = view.nativeViewProtected as UIView | undefined
|
|
40
|
+
const window = native?.window
|
|
41
|
+
const info = notification.userInfo
|
|
42
|
+
if (!native || !window || !info) return
|
|
43
|
+
const end = info.objectForKey(UIKeyboardFrameEndUserInfoKey)?.CGRectValue as
|
|
44
|
+
CGRect | undefined
|
|
45
|
+
if (!end) return
|
|
46
|
+
const keyboard = window.convertRectFromWindow(end, null)
|
|
47
|
+
const hidden =
|
|
48
|
+
keyboard.size.height <= 0 || keyboard.origin.y >= window.bounds.size.height
|
|
49
|
+
const frame = native.convertRectToView(native.bounds, null)
|
|
50
|
+
const bottom = frame.origin.y + frame.size.height - view.translateY
|
|
51
|
+
const overlap = hidden ? 0 : Math.max(0, bottom + offset - keyboard.origin.y)
|
|
52
|
+
if (overlap === current) return
|
|
53
|
+
current = overlap
|
|
54
|
+
stop(view, ['y'])
|
|
55
|
+
const apply = () => {
|
|
56
|
+
view.translateY = overlap ? -overlap : 0
|
|
57
|
+
}
|
|
58
|
+
const duration = info.objectForKey(UIKeyboardAnimationDurationUserInfoKey)
|
|
59
|
+
?.doubleValue as number | undefined
|
|
60
|
+
if (!duration) return apply()
|
|
61
|
+
const curve =
|
|
62
|
+
(info.objectForKey(UIKeyboardAnimationCurveUserInfoKey)?.integerValue as
|
|
63
|
+
number | undefined) ?? 0
|
|
64
|
+
// The keyboard's curve is private; shifted into the options it still applies.
|
|
65
|
+
UIView.animateWithDurationDelayOptionsAnimationsCompletion(
|
|
66
|
+
duration,
|
|
67
|
+
0,
|
|
68
|
+
((curve << 16) | BEGIN_FROM_CURRENT_STATE) as UIViewAnimationOptions,
|
|
69
|
+
apply,
|
|
70
|
+
() => {},
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
const observer = Application.ios.addNotificationObserver(
|
|
74
|
+
UIKeyboardWillChangeFrameNotification,
|
|
75
|
+
onKeyboard,
|
|
76
|
+
)
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
if (disposed) return
|
|
79
|
+
disposed = true
|
|
80
|
+
Application.ios.removeNotificationObserver(
|
|
81
|
+
observer,
|
|
82
|
+
UIKeyboardWillChangeFrameNotification,
|
|
83
|
+
)
|
|
84
|
+
view.off('unloaded', cleanup)
|
|
85
|
+
if (current) view.translateY = 0
|
|
86
|
+
}
|
|
87
|
+
view.on('unloaded', cleanup)
|
|
88
|
+
return cleanup
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function followAndroid(view: View, offset: number) {
|
|
92
|
+
if (android.os.Build.VERSION.SDK_INT < 30) return () => {}
|
|
93
|
+
const density = Utils.layout.getDisplayDensity()
|
|
94
|
+
let disposed = false
|
|
95
|
+
let follower: org.rafaelaugusto.jolt.KeyboardFollower | undefined
|
|
96
|
+
const attach = () => {
|
|
97
|
+
if (disposed || follower) return
|
|
98
|
+
const native = view.nativeViewProtected as android.view.View | undefined
|
|
99
|
+
if (!native) return
|
|
100
|
+
try {
|
|
101
|
+
follower = new org.rafaelaugusto.jolt.KeyboardFollower(
|
|
102
|
+
native,
|
|
103
|
+
offset * density,
|
|
104
|
+
new org.rafaelaugusto.jolt.KeyboardFollower.Listener({
|
|
105
|
+
onStart: () => stop(view, ['y']),
|
|
106
|
+
onSettled: (translation) => {
|
|
107
|
+
view.translateY = translation / density
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
} catch {
|
|
112
|
+
// A JS-only update cannot install the native plugin. Leave the platform's resize in place.
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
follower.attach()
|
|
116
|
+
}
|
|
117
|
+
const cleanup = () => {
|
|
118
|
+
if (disposed) return
|
|
119
|
+
disposed = true
|
|
120
|
+
view.off('loaded', attach)
|
|
121
|
+
view.off('unloaded', cleanup)
|
|
122
|
+
if (follower) {
|
|
123
|
+
follower.detach()
|
|
124
|
+
follower = undefined
|
|
125
|
+
view.translateY = 0
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
view.on('loaded', attach)
|
|
129
|
+
view.on('unloaded', cleanup)
|
|
130
|
+
if (view.isLoaded) attach()
|
|
131
|
+
return cleanup
|
|
132
|
+
}
|
package/src/label.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Label } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
const callbacks = new WeakMap<NSObject, (finished: boolean) => void>()
|
|
4
|
+
let Delegate: typeof NSObject | undefined
|
|
5
|
+
const styleEvents = [
|
|
6
|
+
'fontInternalChange',
|
|
7
|
+
'colorChange',
|
|
8
|
+
'textAlignmentChange',
|
|
9
|
+
'textDecorationChange',
|
|
10
|
+
'textTransformChange',
|
|
11
|
+
'letterSpacingChange',
|
|
12
|
+
'lineHeightChange',
|
|
13
|
+
'textShadowChange',
|
|
14
|
+
'textStrokeChange',
|
|
15
|
+
'whiteSpaceChange',
|
|
16
|
+
'textOverflowChange',
|
|
17
|
+
'maxLinesChange',
|
|
18
|
+
'paddingLeftChange',
|
|
19
|
+
'paddingRightChange',
|
|
20
|
+
'paddingTopChange',
|
|
21
|
+
'paddingBottomChange',
|
|
22
|
+
'visibilityChange',
|
|
23
|
+
'directionChange',
|
|
24
|
+
'borderLeftWidthChange',
|
|
25
|
+
'borderRightWidthChange',
|
|
26
|
+
'borderTopWidthChange',
|
|
27
|
+
'borderBottomWidthChange',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/** Reports text, formatted text and style changes of a Label until the returned function is called. */
|
|
31
|
+
export function watchLabel(view: Label, onChange: () => void): () => void {
|
|
32
|
+
const formatted = view.formattedText
|
|
33
|
+
view.on('textChange', onChange)
|
|
34
|
+
view.on('formattedTextChange', onChange)
|
|
35
|
+
formatted?.on('propertyChange', onChange)
|
|
36
|
+
for (const event of styleEvents) view.style.on(event, onChange)
|
|
37
|
+
return () => {
|
|
38
|
+
view.off('textChange', onChange)
|
|
39
|
+
view.off('formattedTextChange', onChange)
|
|
40
|
+
formatted?.off('propertyChange', onChange)
|
|
41
|
+
for (const event of styleEvents) view.style.off(event, onChange)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A CAAnimationDelegate that reports when its animation stops. Release it with `forgetCompletion`. */
|
|
46
|
+
export function completion(
|
|
47
|
+
callback: (finished: boolean) => void,
|
|
48
|
+
): NSObject & CAAnimationDelegate {
|
|
49
|
+
if (!Delegate) {
|
|
50
|
+
const base = NSObject as typeof NSObject & {
|
|
51
|
+
extend(
|
|
52
|
+
methods: object,
|
|
53
|
+
options: { protocols: unknown[]; name: string },
|
|
54
|
+
): typeof NSObject
|
|
55
|
+
}
|
|
56
|
+
Delegate = base.extend(
|
|
57
|
+
{
|
|
58
|
+
animationDidStopFinished(
|
|
59
|
+
this: NSObject,
|
|
60
|
+
_animation: CAAnimation,
|
|
61
|
+
finished: boolean,
|
|
62
|
+
) {
|
|
63
|
+
callbacks.get(this)?.(finished)
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{ protocols: [CAAnimationDelegate], name: 'JoltLabelAnimationDelegate' },
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
const delegate = Delegate.new()
|
|
70
|
+
callbacks.set(delegate, callback)
|
|
71
|
+
return delegate
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function forgetCompletion(delegate: NSObject) {
|
|
75
|
+
callbacks.delete(delegate)
|
|
76
|
+
}
|
package/src/liquid.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { tintColorProperty } from '@nativescript/core/ui/image'
|
|
|
3
3
|
import { toView } from './animate.js'
|
|
4
4
|
import { liquidGrid } from './liquid-grid.js'
|
|
5
5
|
import type { Target } from './types.js'
|
|
6
|
+
import { reducedMotion } from './config.js'
|
|
6
7
|
|
|
7
8
|
export interface LiquidOptions {
|
|
8
9
|
/** Amount of internal deformation, from 0 to 1. Defaults to 0.7. */
|
|
@@ -134,7 +135,7 @@ class Liquid implements LiquidEffect {
|
|
|
134
135
|
!this.image.isLoaded ||
|
|
135
136
|
this.image.visibility !== 'visible' ||
|
|
136
137
|
this.image.tintColor ||
|
|
137
|
-
|
|
138
|
+
reducedMotion()
|
|
138
139
|
)
|
|
139
140
|
return this.remove()
|
|
140
141
|
const native = this.image.nativeViewProtected as UIImageView | undefined
|
package/src/loader.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Application, Color, isIOS, type View } from '@nativescript/core'
|
|
2
2
|
import { toView } from './animate.js'
|
|
3
3
|
import type { Target } from './types.js'
|
|
4
|
+
import { reducedMotion } from './config.js'
|
|
4
5
|
|
|
5
6
|
export type LoaderStyle = 'dots' | 'bars' | 'ring'
|
|
6
7
|
|
|
@@ -160,7 +161,7 @@ class Loader {
|
|
|
160
161
|
0,
|
|
161
162
|
)
|
|
162
163
|
}
|
|
163
|
-
const reduced =
|
|
164
|
+
const reduced = reducedMotion()
|
|
164
165
|
item.opacity = style === 'ring' ? 0.35 : 1
|
|
165
166
|
if (reduced && this.animating) {
|
|
166
167
|
item.removeAnimationForKey(key)
|