@rafaelaugusto/jolt 0.1.0-beta.0 → 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.
- package/README.md +441 -25
- package/dist/animate.d.ts +21 -5
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +205 -25
- package/dist/animate.js.map +1 -1
- package/dist/color.d.ts +11 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +73 -0
- package/dist/color.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +73 -0
- package/dist/config.js.map +1 -0
- package/dist/decay.d.ts +11 -0
- package/dist/decay.d.ts.map +1 -0
- package/dist/decay.js +22 -0
- package/dist/decay.js.map +1 -0
- package/dist/decoration.d.ts +18 -0
- package/dist/decoration.d.ts.map +1 -0
- package/dist/decoration.js +155 -0
- package/dist/decoration.js.map +1 -0
- package/dist/drag-input.d.ts +15 -0
- package/dist/drag-input.d.ts.map +1 -0
- package/dist/drag-input.js +66 -0
- package/dist/drag-input.js.map +1 -0
- package/dist/drag-physics.d.ts +20 -0
- package/dist/drag-physics.d.ts.map +1 -0
- package/dist/drag-physics.js +75 -0
- package/dist/drag-physics.js.map +1 -0
- package/dist/drag.d.ts +58 -0
- package/dist/drag.d.ts.map +1 -0
- package/dist/drag.js +324 -0
- package/dist/drag.js.map +1 -0
- package/dist/driver/android-color.d.ts +20 -0
- package/dist/driver/android-color.d.ts.map +1 -0
- package/dist/driver/android-color.js +136 -0
- package/dist/driver/android-color.js.map +1 -0
- package/dist/driver/android-decoration.d.ts +22 -0
- package/dist/driver/android-decoration.d.ts.map +1 -0
- package/dist/driver/android-decoration.js +139 -0
- package/dist/driver/android-decoration.js.map +1 -0
- package/dist/driver/android-playback.d.ts +13 -0
- package/dist/driver/android-playback.d.ts.map +1 -0
- package/dist/driver/android-playback.js +141 -0
- package/dist/driver/android-playback.js.map +1 -0
- package/dist/driver/android.d.ts +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +165 -13
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +25 -2
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +16 -0
- package/dist/driver/ios-color.d.ts.map +1 -0
- package/dist/driver/ios-color.js +392 -0
- package/dist/driver/ios-color.js.map +1 -0
- package/dist/driver/ios-decoration.d.ts +17 -0
- package/dist/driver/ios-decoration.d.ts.map +1 -0
- package/dist/driver/ios-decoration.js +271 -0
- package/dist/driver/ios-decoration.js.map +1 -0
- package/dist/driver/ios-layout.d.ts +9 -0
- package/dist/driver/ios-layout.d.ts.map +1 -0
- package/dist/driver/ios-layout.js +52 -0
- package/dist/driver/ios-layout.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 +47 -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 +164 -20
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +76 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +303 -23
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +21 -0
- package/dist/haptics.d.ts.map +1 -0
- package/dist/haptics.js +120 -0
- package/dist/haptics.js.map +1 -0
- package/dist/image-effect-native.d.ts +26 -0
- package/dist/image-effect-native.d.ts.map +1 -0
- package/dist/image-effect-native.js +73 -0
- package/dist/image-effect-native.js.map +1 -0
- package/dist/image-effect.d.ts +50 -0
- package/dist/image-effect.d.ts.map +1 -0
- package/dist/image-effect.js +290 -0
- package/dist/image-effect.js.map +1 -0
- package/dist/image-owner.d.ts +3 -0
- package/dist/image-owner.d.ts.map +1 -0
- package/dist/image-owner.js +11 -0
- package/dist/image-owner.js.map +1 -0
- package/dist/index.d.ts +45 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -1
- package/dist/interaction-surface.d.ts +12 -0
- package/dist/interaction-surface.d.ts.map +1 -0
- package/dist/interaction-surface.js +36 -0
- package/dist/interaction-surface.js.map +1 -0
- 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 +41 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +256 -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/layout.d.ts +13 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +348 -0
- package/dist/layout.js.map +1 -0
- package/dist/liquid-grid.d.ts +3 -0
- package/dist/liquid-grid.d.ts.map +1 -0
- package/dist/liquid-grid.js +24 -0
- package/dist/liquid-grid.js.map +1 -0
- package/dist/liquid.d.ts +18 -0
- package/dist/liquid.d.ts.map +1 -0
- package/dist/liquid.js +234 -0
- package/dist/liquid.js.map +1 -0
- package/dist/loader.d.ts +19 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +173 -0
- package/dist/loader.js.map +1 -0
- package/dist/motion-scope.d.ts +39 -0
- package/dist/motion-scope.d.ts.map +1 -0
- package/dist/motion-scope.js +238 -0
- package/dist/motion-scope.js.map +1 -0
- package/dist/motion-states.d.ts +36 -0
- package/dist/motion-states.d.ts.map +1 -0
- package/dist/motion-states.js +165 -0
- package/dist/motion-states.js.map +1 -0
- package/dist/particles.d.ts +39 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +265 -0
- package/dist/particles.js.map +1 -0
- 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 +6 -0
- package/dist/playback-group.d.ts.map +1 -0
- package/dist/playback-group.js +141 -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/presets.d.ts +16 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +24 -6
- package/dist/presets.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +25 -7
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +6 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +26 -2
- package/dist/properties.js.map +1 -1
- package/dist/reveal.d.ts +25 -0
- package/dist/reveal.d.ts.map +1 -0
- package/dist/reveal.js +294 -0
- package/dist/reveal.js.map +1 -0
- 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 +13 -7
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +214 -19
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts +12 -0
- package/dist/shimmer.d.ts.map +1 -0
- package/dist/shimmer.js +161 -0
- package/dist/shimmer.js.map +1 -0
- 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 +23 -0
- package/dist/symbol-effect.d.ts.map +1 -0
- package/dist/symbol-effect.js +187 -0
- package/dist/symbol-effect.js.map +1 -0
- package/dist/text-layout.d.ts +30 -0
- package/dist/text-layout.d.ts.map +1 -0
- package/dist/text-layout.js +185 -0
- package/dist/text-layout.js.map +1 -0
- package/dist/text.d.ts +17 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +263 -0
- package/dist/text.js.map +1 -0
- package/dist/track.d.ts +3 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +266 -13
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +17 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +41 -6
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +50 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +95 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +163 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/zoom-native.d.ts +30 -0
- package/dist/zoom-native.d.ts.map +1 -0
- package/dist/zoom-native.js +56 -0
- package/dist/zoom-native.js.map +1 -0
- package/dist/zoomable.d.ts +48 -0
- package/dist/zoomable.d.ts.map +1 -0
- package/dist/zoomable.js +249 -0
- package/dist/zoomable.js.map +1 -0
- package/package.json +33 -5
- package/platforms/android/include.gradle +7 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +19 -0
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/JoltImageEffect.h +25 -0
- package/platforms/ios/src/JoltImageEffect.mm +242 -0
- package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
- package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
- package/platforms/ios/src/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +6 -0
- package/src/android-plugin.d.ts +155 -0
- package/src/animate.ts +278 -38
- package/src/color.ts +101 -0
- package/src/config.ts +84 -0
- package/src/decay.ts +45 -0
- package/src/decoration.ts +180 -0
- package/src/drag-input.ts +74 -0
- package/src/drag-physics.ts +110 -0
- package/src/drag.ts +395 -0
- package/src/driver/android-color.ts +170 -0
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +208 -20
- package/src/driver/index.ts +37 -2
- package/src/driver/ios-color.ts +441 -0
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-layout.ts +63 -0
- package/src/driver/ios-playback.ts +51 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +194 -25
- package/src/engine.ts +329 -27
- package/src/haptics.ts +148 -0
- package/src/image-effect-native.ts +129 -0
- package/src/image-effect.ts +336 -0
- package/src/image-owner.ts +12 -0
- package/src/index.ts +61 -4
- package/src/interaction-surface.ts +41 -0
- package/src/ios-plugin.d.ts +81 -0
- package/src/items.ts +140 -0
- package/src/keyboard.ts +291 -0
- package/src/label.ts +76 -0
- package/src/layout.ts +408 -0
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +287 -0
- package/src/loader.ts +202 -0
- package/src/motion-scope.ts +279 -0
- package/src/motion-states.ts +215 -0
- package/src/particles.ts +323 -0
- package/src/path.ts +124 -0
- package/src/playback-group.ts +143 -0
- package/src/playback.ts +62 -0
- package/src/presets.ts +31 -7
- package/src/press.ts +32 -10
- package/src/properties.ts +32 -6
- package/src/reveal.ts +374 -0
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +212 -19
- package/src/shimmer.ts +180 -0
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +226 -0
- package/src/text-layout.ts +228 -0
- package/src/text.ts +279 -0
- package/src/track.ts +347 -12
- package/src/transition.ts +63 -6
- package/src/types.ts +51 -1
- package/src/vue/index.ts +231 -0
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
package/src/roll.ts
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { Application, Label, Utils, isAndroid, isIOS } from '@nativescript/core'
|
|
2
|
+
import { toView } from './animate.js'
|
|
3
|
+
import type { Animation } from './engine.js'
|
|
4
|
+
import { reducedMotion } from './config.js'
|
|
5
|
+
import { completion, forgetCompletion, watchLabel } from './label.js'
|
|
6
|
+
import { diffFromEnd, type Grapheme } from './roll-diff.js'
|
|
7
|
+
import { textLayer, type TextPiece } from './text-layout.js'
|
|
8
|
+
import { createTrack } from './track.js'
|
|
9
|
+
import { resolveTransition } from './transition.js'
|
|
10
|
+
import type { Target } from './types.js'
|
|
11
|
+
|
|
12
|
+
export interface RollOptions {
|
|
13
|
+
/** Turns a number into the text to show. Defaults to `String`. */
|
|
14
|
+
format?: (value: number) => string
|
|
15
|
+
/** How long the spring feels, in milliseconds. Defaults to 600. */
|
|
16
|
+
duration?: number
|
|
17
|
+
/** Spring bounce, from -1 to 1. Defaults to 0.2. */
|
|
18
|
+
bounce?: number
|
|
19
|
+
/** Milliseconds between changed positions, counted from the right. Defaults to zero. */
|
|
20
|
+
stagger?: number
|
|
21
|
+
/** Milliseconds before the first position moves. Defaults to zero. */
|
|
22
|
+
delay?: number
|
|
23
|
+
/** `auto` rolls up to a higher number and down to a lower one. Defaults to `auto`. */
|
|
24
|
+
direction?: 'auto' | 'up' | 'down'
|
|
25
|
+
/** Tabular figures on the native label, so every digit takes the same width. Defaults to true. */
|
|
26
|
+
tabular?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Options {
|
|
30
|
+
duration: number
|
|
31
|
+
bounce: number
|
|
32
|
+
stagger: number
|
|
33
|
+
delay: number
|
|
34
|
+
direction: 1 | -1
|
|
35
|
+
tabular: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const running = new WeakMap<Label, RollAnimation>()
|
|
39
|
+
const lastValues = new WeakMap<Label, number>()
|
|
40
|
+
const MAX_POSITIONS = 256
|
|
41
|
+
const directions = ['auto', 'up', 'down']
|
|
42
|
+
const paintKeys = () => [
|
|
43
|
+
NSForegroundColorAttributeName,
|
|
44
|
+
NSStrokeColorAttributeName,
|
|
45
|
+
NSUnderlineColorAttributeName,
|
|
46
|
+
NSStrikethroughColorAttributeName,
|
|
47
|
+
NSShadowAttributeName,
|
|
48
|
+
]
|
|
49
|
+
// CoreText layout features: kNumberSpacingType and kMonospacedNumbersSelector.
|
|
50
|
+
const NUMBER_SPACING = 6
|
|
51
|
+
const MONOSPACED_NUMBERS = 0
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Changes a Label's text and rolls the digits that differ, each one sliding
|
|
55
|
+
* out while its replacement slides in. The label holds the final text from the
|
|
56
|
+
* start; only the drawing moves.
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* rollTo(balance, 1249.9, { format: (n) => n.toFixed(2) })
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function rollTo(
|
|
63
|
+
target: Target,
|
|
64
|
+
value: number | string,
|
|
65
|
+
options: RollOptions = {},
|
|
66
|
+
): Animation {
|
|
67
|
+
const config = resolveOptions(options)
|
|
68
|
+
const view = toView(target)
|
|
69
|
+
if (!(view instanceof Label)) throw new TypeError('Expected a NativeScript Label.')
|
|
70
|
+
const text = typeof value === 'string' ? value : (options.format ?? String)(value)
|
|
71
|
+
if (typeof text !== 'string') throw new TypeError('Roll format must return a string.')
|
|
72
|
+
running.get(view)?.stop()
|
|
73
|
+
const previous = currentText(view)
|
|
74
|
+
const direction = resolveDirection(options.direction, view, value, previous)
|
|
75
|
+
if (typeof value === 'number') lastValues.set(view, value)
|
|
76
|
+
else lastValues.delete(view)
|
|
77
|
+
if (view.formattedText) view.formattedText = undefined as never
|
|
78
|
+
view.text = text
|
|
79
|
+
return new RollAnimation(view, previous, text, { ...config, direction })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveOptions(options: RollOptions): Omit<Options, 'direction'> {
|
|
83
|
+
const duration = options.duration ?? 600
|
|
84
|
+
const bounce = options.bounce ?? 0.2
|
|
85
|
+
const stagger = options.stagger ?? 0
|
|
86
|
+
const delay = options.delay ?? 0
|
|
87
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
88
|
+
throw new RangeError('Roll duration must be a positive finite number.')
|
|
89
|
+
if (!Number.isFinite(bounce) || bounce < -1 || bounce > 1)
|
|
90
|
+
throw new RangeError('Roll bounce must be between -1 and 1.')
|
|
91
|
+
if (!Number.isFinite(stagger) || stagger < 0)
|
|
92
|
+
throw new RangeError('Roll stagger must be a nonnegative finite number.')
|
|
93
|
+
if (!Number.isFinite(delay) || delay < 0)
|
|
94
|
+
throw new RangeError('Roll delay must be a nonnegative finite number.')
|
|
95
|
+
if (options.direction !== undefined && !directions.includes(options.direction))
|
|
96
|
+
throw new TypeError('Roll direction must be auto, up or down.')
|
|
97
|
+
if (options.format !== undefined && typeof options.format !== 'function')
|
|
98
|
+
throw new TypeError('Roll format must be a function.')
|
|
99
|
+
return { duration, bounce, stagger, delay, tabular: options.tabular ?? true }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveDirection(
|
|
103
|
+
option: RollOptions['direction'],
|
|
104
|
+
view: Label,
|
|
105
|
+
value: number | string,
|
|
106
|
+
previous: string,
|
|
107
|
+
): 1 | -1 {
|
|
108
|
+
if (option === 'up') return 1
|
|
109
|
+
if (option === 'down') return -1
|
|
110
|
+
if (typeof value !== 'number') return 1
|
|
111
|
+
const before = lastValues.get(view) ?? Number(previous)
|
|
112
|
+
return Number.isFinite(before) && value < before ? -1 : 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function currentText(view: Label): string {
|
|
116
|
+
const native = view.nativeViewProtected as unknown
|
|
117
|
+
if (isIOS && native instanceof UILabel)
|
|
118
|
+
return native.attributedText?.string ?? native.text ?? ''
|
|
119
|
+
if (isAndroid && native instanceof android.widget.TextView)
|
|
120
|
+
return native.getText()?.toString() ?? ''
|
|
121
|
+
return view.text ?? ''
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function springTrack(shift: number, { duration, bounce }: Options) {
|
|
125
|
+
return createTrack(
|
|
126
|
+
'y',
|
|
127
|
+
[shift, 0],
|
|
128
|
+
resolveTransition({ type: 'spring', duration, bounce }, 'y', 2),
|
|
129
|
+
0,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fadeTrack({ duration }: Options) {
|
|
134
|
+
return createTrack(
|
|
135
|
+
'opacity',
|
|
136
|
+
[0, 1],
|
|
137
|
+
resolveTransition(
|
|
138
|
+
{ type: 'tween', duration: duration / 2, ease: 'easeOut' },
|
|
139
|
+
'opacity',
|
|
140
|
+
2,
|
|
141
|
+
),
|
|
142
|
+
0,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function pieceAt(pieces: readonly TextPiece[], grapheme: Grapheme) {
|
|
147
|
+
const index = pieces.findIndex(
|
|
148
|
+
({ location, length }) =>
|
|
149
|
+
grapheme.start >= location && grapheme.start < location + length,
|
|
150
|
+
)
|
|
151
|
+
return index === -1 ? undefined : index
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function layersOf(layer: CALayer | undefined): CALayer[] {
|
|
155
|
+
const sublayers = layer?.sublayers
|
|
156
|
+
if (!sublayers) return []
|
|
157
|
+
const layers: CALayer[] = []
|
|
158
|
+
for (let i = 0; i < sublayers.count; i++) layers.push(sublayers.objectAtIndex(i))
|
|
159
|
+
return layers
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isClear(text: NSAttributedString) {
|
|
163
|
+
if (!text.length) return true
|
|
164
|
+
const color = text.attributeAtIndexEffectiveRange(
|
|
165
|
+
NSForegroundColorAttributeName,
|
|
166
|
+
0,
|
|
167
|
+
null,
|
|
168
|
+
) as UIColor | null
|
|
169
|
+
return !color || color.isEqual(UIColor.clearColor)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function tabularFont(font: UIFont): UIFont {
|
|
173
|
+
const feature = NSDictionary.dictionaryWithObjectsForKeys(
|
|
174
|
+
[NUMBER_SPACING, MONOSPACED_NUMBERS],
|
|
175
|
+
[UIFontFeatureTypeIdentifierKey, UIFontFeatureSelectorIdentifierKey],
|
|
176
|
+
)
|
|
177
|
+
const descriptor = font.fontDescriptor.fontDescriptorByAddingAttributes(
|
|
178
|
+
NSDictionary.dictionaryWithObjectForKey(
|
|
179
|
+
[feature],
|
|
180
|
+
UIFontDescriptorFeatureSettingsAttribute,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
return UIFont.fontWithDescriptorSize(descriptor, font.pointSize)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class RollAnimation implements Animation {
|
|
187
|
+
readonly finished: Promise<boolean>
|
|
188
|
+
private resolve!: (finished: boolean) => void
|
|
189
|
+
private settled = false
|
|
190
|
+
private queued = false
|
|
191
|
+
private started = false
|
|
192
|
+
private unwatch: (() => void) | undefined
|
|
193
|
+
private native: UILabel | undefined
|
|
194
|
+
private container: CALayer | undefined
|
|
195
|
+
private pieces: CALayer[] = []
|
|
196
|
+
private attributed: NSAttributedString | null = null
|
|
197
|
+
private hidden: NSAttributedString | null = null
|
|
198
|
+
private color: UIColor | undefined
|
|
199
|
+
private shadow: UIColor | null = null
|
|
200
|
+
private delegate: NSObject | undefined
|
|
201
|
+
private observer: ReturnType<typeof Application.ios.addNotificationObserver> | undefined
|
|
202
|
+
private roll: org.rafaelaugusto.jolt.NumberRoll | undefined
|
|
203
|
+
|
|
204
|
+
constructor(
|
|
205
|
+
private readonly view: Label,
|
|
206
|
+
private readonly previous: string,
|
|
207
|
+
private readonly next: string,
|
|
208
|
+
private readonly options: Options,
|
|
209
|
+
) {
|
|
210
|
+
this.finished = new Promise((resolve) => {
|
|
211
|
+
this.resolve = resolve
|
|
212
|
+
})
|
|
213
|
+
running.set(view, this)
|
|
214
|
+
const { changes, removed } = diffFromEnd(previous, next)
|
|
215
|
+
if (!changes.length && !removed.length) {
|
|
216
|
+
this.end(true)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
if (!isIOS && !isAndroid) {
|
|
220
|
+
this.end(false)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if (reducedMotion()) {
|
|
224
|
+
this.end(true)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
view.on('loaded', this.schedule)
|
|
228
|
+
view.on('unloaded', this.stop)
|
|
229
|
+
if (isIOS) view.on('layoutChanged', this.layoutChanged)
|
|
230
|
+
this.unwatch = watchLabel(view, this.changed)
|
|
231
|
+
this.schedule()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
then<A = boolean, B = never>(
|
|
235
|
+
onfulfilled?: ((finished: boolean) => A | PromiseLike<A>) | null,
|
|
236
|
+
onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null,
|
|
237
|
+
): Promise<A | B> {
|
|
238
|
+
return this.finished.then(onfulfilled, onrejected)
|
|
239
|
+
}
|
|
240
|
+
/** Cancels the roll. The label keeps its final text. */
|
|
241
|
+
stop = () => this.end(false)
|
|
242
|
+
private changed = () => this.end(false)
|
|
243
|
+
private motionChanged = () => {
|
|
244
|
+
if (reducedMotion()) this.end(true)
|
|
245
|
+
}
|
|
246
|
+
private layoutChanged = () => {
|
|
247
|
+
if (!this.container) return this.schedule()
|
|
248
|
+
const native = this.view.nativeViewProtected
|
|
249
|
+
if (native !== this.native || !this.native?.window) return this.stop()
|
|
250
|
+
this.container.frame = this.native.bounds
|
|
251
|
+
}
|
|
252
|
+
private schedule = () => {
|
|
253
|
+
if (this.settled || this.queued) return
|
|
254
|
+
this.queued = true
|
|
255
|
+
void Promise.resolve().then(() => {
|
|
256
|
+
this.queued = false
|
|
257
|
+
if (this.settled || this.started || !this.view.isLoaded) return
|
|
258
|
+
try {
|
|
259
|
+
this.start()
|
|
260
|
+
} catch {
|
|
261
|
+
this.end(false)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
private start() {
|
|
266
|
+
if (reducedMotion()) return this.end(true)
|
|
267
|
+
if (this.view.visibility !== 'visible') return this.end(false)
|
|
268
|
+
if (isIOS) this.startIOS()
|
|
269
|
+
else this.startAndroid()
|
|
270
|
+
}
|
|
271
|
+
private startIOS() {
|
|
272
|
+
const native = this.view.nativeViewProtected as UILabel | undefined
|
|
273
|
+
if (!(native instanceof UILabel) || !native.window) return
|
|
274
|
+
if (native.bounds.size.width <= 0 || native.bounds.size.height <= 0) return
|
|
275
|
+
if (this.options.tabular) native.font = tabularFont(native.font)
|
|
276
|
+
const attributed = native.attributedText
|
|
277
|
+
let before: ReturnType<typeof textLayer>
|
|
278
|
+
try {
|
|
279
|
+
if (attributed) {
|
|
280
|
+
const copy =
|
|
281
|
+
NSMutableAttributedString.alloc().initWithAttributedString(attributed)
|
|
282
|
+
copy.replaceCharactersInRangeWithString(
|
|
283
|
+
{ location: 0, length: copy.length },
|
|
284
|
+
this.previous,
|
|
285
|
+
)
|
|
286
|
+
native.attributedText = copy
|
|
287
|
+
} else native.text = this.previous
|
|
288
|
+
before = textLayer(native, 'letter')
|
|
289
|
+
} finally {
|
|
290
|
+
if (attributed) native.attributedText = attributed
|
|
291
|
+
else native.text = this.next
|
|
292
|
+
}
|
|
293
|
+
const after = textLayer(native, 'letter')
|
|
294
|
+
if (!before || !after) return this.end(false)
|
|
295
|
+
this.started = true
|
|
296
|
+
|
|
297
|
+
const { changes, removed } = diffFromEnd(this.previous, this.next)
|
|
298
|
+
const entering = new Map<number, number>()
|
|
299
|
+
const exiting = new Map<number, number>()
|
|
300
|
+
const assign = (
|
|
301
|
+
map: Map<number, number>,
|
|
302
|
+
pieces: readonly TextPiece[],
|
|
303
|
+
grapheme: Grapheme,
|
|
304
|
+
step: number,
|
|
305
|
+
) => {
|
|
306
|
+
const index = pieceAt(pieces, grapheme)
|
|
307
|
+
if (index !== undefined && !map.has(index)) map.set(index, step)
|
|
308
|
+
}
|
|
309
|
+
changes.forEach(({ next, previous }, step) => {
|
|
310
|
+
assign(entering, after.pieces, next, step)
|
|
311
|
+
if (previous) assign(exiting, before.pieces, previous, step)
|
|
312
|
+
})
|
|
313
|
+
removed.forEach((grapheme, index) =>
|
|
314
|
+
assign(exiting, before.pieces, grapheme, changes.length + index),
|
|
315
|
+
)
|
|
316
|
+
if (!entering.size && !exiting.size) return this.end(true)
|
|
317
|
+
if (entering.size + exiting.size > MAX_POSITIONS) return this.end(false)
|
|
318
|
+
const last = Math.max(...entering.values(), ...exiting.values())
|
|
319
|
+
|
|
320
|
+
const { delay, stagger, direction } = this.options
|
|
321
|
+
const shift = native.font.lineHeight
|
|
322
|
+
const spring = springTrack(shift, this.options)
|
|
323
|
+
const fade = fadeTrack(this.options)
|
|
324
|
+
const container = CALayer.new()
|
|
325
|
+
container.name = 'jolt:roll'
|
|
326
|
+
container.frame = native.bounds
|
|
327
|
+
container.masksToBounds = true
|
|
328
|
+
const oldPieces = layersOf(before.layer)
|
|
329
|
+
const newPieces = layersOf(after.layer)
|
|
330
|
+
const start = CACurrentMediaTime()
|
|
331
|
+
this.native = native
|
|
332
|
+
this.container = container
|
|
333
|
+
CATransaction.begin()
|
|
334
|
+
CATransaction.setDisableActions(true)
|
|
335
|
+
try {
|
|
336
|
+
this.hide(native)
|
|
337
|
+
oldPieces.forEach((piece, index) => {
|
|
338
|
+
if (!exiting.has(index)) piece.removeFromSuperlayer()
|
|
339
|
+
})
|
|
340
|
+
if (before.layer) container.addSublayer(before.layer)
|
|
341
|
+
if (after.layer) container.addSublayer(after.layer)
|
|
342
|
+
native.layer.addSublayer(container)
|
|
343
|
+
const roll = (piece: CALayer, step: number, incoming: boolean) => {
|
|
344
|
+
if (!incoming) {
|
|
345
|
+
piece.opacity = 0
|
|
346
|
+
piece.transform = CATransform3DMakeTranslation(0, -direction * shift, 0)
|
|
347
|
+
}
|
|
348
|
+
const move = CAKeyframeAnimation.animationWithKeyPath('transform.translation.y')
|
|
349
|
+
move.values = NSArray.arrayWithArray(
|
|
350
|
+
spring.values.map((value) => direction * (incoming ? value : value - shift)),
|
|
351
|
+
)
|
|
352
|
+
move.keyTimes = NSArray.arrayWithArray(spring.times as number[])
|
|
353
|
+
move.duration = spring.duration / 1000
|
|
354
|
+
const opacity = CAKeyframeAnimation.animationWithKeyPath('opacity')
|
|
355
|
+
opacity.values = NSArray.arrayWithArray(
|
|
356
|
+
fade.values.map((value) => (incoming ? value : 1 - value)),
|
|
357
|
+
)
|
|
358
|
+
opacity.keyTimes = NSArray.arrayWithArray(fade.times as number[])
|
|
359
|
+
opacity.duration = fade.duration / 1000
|
|
360
|
+
const group = CAAnimationGroup.new()
|
|
361
|
+
group.animations = NSArray.arrayWithArray([opacity, move])
|
|
362
|
+
group.duration = Math.max(spring.duration, fade.duration) / 1000
|
|
363
|
+
group.beginTime = start + (delay + step * stagger) / 1000
|
|
364
|
+
group.fillMode = kCAFillModeBackwards
|
|
365
|
+
if (step === last && !this.delegate)
|
|
366
|
+
this.delegate = group.delegate = completion((finished) => this.end(finished))
|
|
367
|
+
piece.addAnimationForKey(group, 'jolt:roll')
|
|
368
|
+
this.pieces.push(piece)
|
|
369
|
+
}
|
|
370
|
+
for (const [index, step] of exiting) roll(oldPieces[index]!, step, false)
|
|
371
|
+
for (const [index, step] of entering) roll(newPieces[index]!, step, true)
|
|
372
|
+
this.observer = Application.ios.addNotificationObserver(
|
|
373
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
374
|
+
this.motionChanged,
|
|
375
|
+
)
|
|
376
|
+
} finally {
|
|
377
|
+
CATransaction.commit()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
private hide(native: UILabel) {
|
|
381
|
+
this.color = native.textColor
|
|
382
|
+
// UILabel synthesizes an attributed string for plain text, so this is the usual path.
|
|
383
|
+
const attributed = native.attributedText
|
|
384
|
+
if (attributed) {
|
|
385
|
+
const hidden =
|
|
386
|
+
NSMutableAttributedString.alloc().initWithAttributedString(attributed)
|
|
387
|
+
const full = { location: 0, length: hidden.length }
|
|
388
|
+
const shadow = NSShadow.new()
|
|
389
|
+
shadow.shadowColor = UIColor.clearColor
|
|
390
|
+
for (const key of paintKeys())
|
|
391
|
+
hidden.addAttributeValueRange(
|
|
392
|
+
key,
|
|
393
|
+
key === NSShadowAttributeName ? shadow : UIColor.clearColor,
|
|
394
|
+
full,
|
|
395
|
+
)
|
|
396
|
+
this.attributed = attributed
|
|
397
|
+
this.hidden = hidden
|
|
398
|
+
native.attributedText = hidden
|
|
399
|
+
} else native.textColor = UIColor.clearColor
|
|
400
|
+
this.shadow = native.shadowColor
|
|
401
|
+
native.shadowColor = null
|
|
402
|
+
}
|
|
403
|
+
private show(native: UILabel) {
|
|
404
|
+
// UIKit hands back a normalized copy of the attributed text and keeps the
|
|
405
|
+
// clear color for text the app sets later, so "still ours" means the text
|
|
406
|
+
// is still clear. A color the app painted meanwhile wins.
|
|
407
|
+
const current = native.attributedText
|
|
408
|
+
if (!current || isClear(current)) {
|
|
409
|
+
if (current && this.attributed && current.string === this.hidden?.string) {
|
|
410
|
+
const restored =
|
|
411
|
+
NSMutableAttributedString.alloc().initWithAttributedString(current)
|
|
412
|
+
const full = { location: 0, length: restored.length }
|
|
413
|
+
for (const key of paintKeys()) {
|
|
414
|
+
restored.removeAttributeRange(key, full)
|
|
415
|
+
this.attributed.enumerateAttributeInRangeOptionsUsingBlock(
|
|
416
|
+
key,
|
|
417
|
+
full,
|
|
418
|
+
0 as NSAttributedStringEnumerationOptions,
|
|
419
|
+
(value, range) => {
|
|
420
|
+
if (value != null) restored.addAttributeValueRange(key, value, range)
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
native.attributedText = restored
|
|
425
|
+
}
|
|
426
|
+
if (this.color && native.textColor?.isEqual(UIColor.clearColor))
|
|
427
|
+
native.textColor = this.color
|
|
428
|
+
}
|
|
429
|
+
if (!native.shadowColor) native.shadowColor = this.shadow
|
|
430
|
+
}
|
|
431
|
+
private startAndroid() {
|
|
432
|
+
const native = this.view.nativeViewProtected as android.widget.TextView | undefined
|
|
433
|
+
if (!(native instanceof android.widget.TextView)) return this.end(false)
|
|
434
|
+
const { changes } = diffFromEnd(this.previous, this.next)
|
|
435
|
+
if (!changes.length) return this.end(true)
|
|
436
|
+
if (changes.length > MAX_POSITIONS) return this.end(false)
|
|
437
|
+
this.started = true
|
|
438
|
+
const density = Utils.layout.getDisplayDensity()
|
|
439
|
+
const shift = native.getLineHeight()
|
|
440
|
+
const spring = springTrack(shift / density, this.options)
|
|
441
|
+
const fade = fadeTrack(this.options)
|
|
442
|
+
const starts = Array.create('int', changes.length)
|
|
443
|
+
const ends = Array.create('int', changes.length)
|
|
444
|
+
const previous = Array.create(java.lang.String, changes.length)
|
|
445
|
+
changes.forEach(({ next, previous: old }, index) => {
|
|
446
|
+
starts[index] = next.start
|
|
447
|
+
ends[index] = next.end
|
|
448
|
+
previous[index] = old?.text ?? ''
|
|
449
|
+
})
|
|
450
|
+
const times = Array.create('float', spring.times.length)
|
|
451
|
+
const values = Array.create('float', spring.values.length)
|
|
452
|
+
spring.times.forEach((time, index) => (times[index] = time))
|
|
453
|
+
spring.values.forEach((value, index) => (values[index] = value * density))
|
|
454
|
+
const { delay, stagger, direction, tabular } = this.options
|
|
455
|
+
let roll: org.rafaelaugusto.jolt.NumberRoll
|
|
456
|
+
try {
|
|
457
|
+
roll = new org.rafaelaugusto.jolt.NumberRoll(
|
|
458
|
+
native,
|
|
459
|
+
starts,
|
|
460
|
+
ends,
|
|
461
|
+
previous,
|
|
462
|
+
direction,
|
|
463
|
+
times,
|
|
464
|
+
values,
|
|
465
|
+
spring.duration,
|
|
466
|
+
Math.round(delay),
|
|
467
|
+
Math.round(stagger),
|
|
468
|
+
shift,
|
|
469
|
+
fade.duration,
|
|
470
|
+
tabular,
|
|
471
|
+
new org.rafaelaugusto.jolt.NumberRoll.Callback({
|
|
472
|
+
onEnd: (finished) => this.end(finished),
|
|
473
|
+
}),
|
|
474
|
+
)
|
|
475
|
+
} catch {
|
|
476
|
+
// A JS-only update cannot install the native plugin. The label shows the final text.
|
|
477
|
+
return this.end(false)
|
|
478
|
+
}
|
|
479
|
+
if (!roll.isSupported()) return this.end(false)
|
|
480
|
+
this.roll = roll
|
|
481
|
+
roll.start()
|
|
482
|
+
}
|
|
483
|
+
private end(finished: boolean) {
|
|
484
|
+
if (this.settled) return
|
|
485
|
+
this.settled = true
|
|
486
|
+
const { view, native, container } = this
|
|
487
|
+
view.off('loaded', this.schedule)
|
|
488
|
+
view.off('unloaded', this.stop)
|
|
489
|
+
view.off('layoutChanged', this.layoutChanged)
|
|
490
|
+
this.unwatch?.()
|
|
491
|
+
if (running.get(view) === this) running.delete(view)
|
|
492
|
+
if (this.observer)
|
|
493
|
+
Application.ios.removeNotificationObserver(
|
|
494
|
+
this.observer,
|
|
495
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
496
|
+
)
|
|
497
|
+
if (this.delegate) forgetCompletion(this.delegate)
|
|
498
|
+
if (native && container) {
|
|
499
|
+
CATransaction.begin()
|
|
500
|
+
CATransaction.setDisableActions(true)
|
|
501
|
+
try {
|
|
502
|
+
this.show(native)
|
|
503
|
+
container.removeFromSuperlayer()
|
|
504
|
+
for (const piece of this.pieces) piece.removeAnimationForKey('jolt:roll')
|
|
505
|
+
} finally {
|
|
506
|
+
CATransaction.commit()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
this.roll?.cancel()
|
|
510
|
+
this.pieces = []
|
|
511
|
+
this.native = this.container = this.delegate = this.observer = this.roll = undefined
|
|
512
|
+
this.attributed = this.hidden = null
|
|
513
|
+
this.resolve(finished)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ScrollView, type EventData, type ScrollEventData } from '@nativescript/core'
|
|
2
|
+
import { animate, toView } from './animate.js'
|
|
3
|
+
import type { Keyframes, Target } from './types.js'
|
|
4
|
+
|
|
5
|
+
export interface ScrollLinkedOptions {
|
|
6
|
+
/** Scroll offsets in DIPs, mapped to progress 0 and 1. Either order is allowed. */
|
|
7
|
+
range: readonly [number, number]
|
|
8
|
+
/** Offset to read. Defaults to the vertical axis. */
|
|
9
|
+
axis?: 'x' | 'y'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Builds one paused animation and scrubs it synchronously on scroll. */
|
|
13
|
+
export function scrollLinked(
|
|
14
|
+
scrollView: Target,
|
|
15
|
+
target: Target,
|
|
16
|
+
keyframes: Keyframes,
|
|
17
|
+
options: ScrollLinkedOptions,
|
|
18
|
+
): () => void {
|
|
19
|
+
const source = toView(scrollView)
|
|
20
|
+
if (!(source instanceof ScrollView))
|
|
21
|
+
throw new TypeError('scrollLinked requires a NativeScript ScrollView.')
|
|
22
|
+
const view = toView(target)
|
|
23
|
+
const range = options?.range
|
|
24
|
+
if (
|
|
25
|
+
!Array.isArray(range) ||
|
|
26
|
+
range.length !== 2 ||
|
|
27
|
+
!range.every(Number.isFinite) ||
|
|
28
|
+
!Number.isFinite(range[1]! - range[0]!) ||
|
|
29
|
+
range[0] === range[1]
|
|
30
|
+
)
|
|
31
|
+
throw new RangeError('Scroll range requires two distinct finite offsets in DIPs.')
|
|
32
|
+
const [start, end] = range as [number, number]
|
|
33
|
+
const axis = options.axis ?? 'y'
|
|
34
|
+
if (axis !== 'x' && axis !== 'y') throw new TypeError('Scroll axis must be "x" or "y".')
|
|
35
|
+
|
|
36
|
+
const animation = animate(view, keyframes, { duration: 1000, ease: 'linear' })
|
|
37
|
+
animation.pause()
|
|
38
|
+
let disposed = false
|
|
39
|
+
let previous: number | undefined
|
|
40
|
+
|
|
41
|
+
const update = (offset: number) => {
|
|
42
|
+
if (disposed || !Number.isFinite(offset)) return
|
|
43
|
+
const progress = Math.max(0, Math.min(1, (offset - start) / (end - start)))
|
|
44
|
+
if (progress === previous) return
|
|
45
|
+
animation.progress = progress
|
|
46
|
+
previous = progress
|
|
47
|
+
}
|
|
48
|
+
const current = () =>
|
|
49
|
+
update(axis === 'x' ? source.horizontalOffset : source.verticalOffset)
|
|
50
|
+
const onScroll = (event: EventData) => {
|
|
51
|
+
const scroll = event as ScrollEventData
|
|
52
|
+
update(axis === 'x' ? scroll.scrollX : scroll.scrollY)
|
|
53
|
+
}
|
|
54
|
+
const cleanup = () => {
|
|
55
|
+
if (disposed) return
|
|
56
|
+
disposed = true
|
|
57
|
+
source.off(ScrollView.scrollEvent, onScroll)
|
|
58
|
+
source.off('loaded', current)
|
|
59
|
+
source.off('unloaded', cleanup)
|
|
60
|
+
view.off('loaded', current)
|
|
61
|
+
view.off('unloaded', cleanup)
|
|
62
|
+
animation.stop()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
source.on(ScrollView.scrollEvent, onScroll)
|
|
66
|
+
source.on('loaded', current)
|
|
67
|
+
source.on('unloaded', cleanup)
|
|
68
|
+
view.on('loaded', current)
|
|
69
|
+
view.on('unloaded', cleanup)
|
|
70
|
+
void animation.finished.then(cleanup)
|
|
71
|
+
current()
|
|
72
|
+
return cleanup
|
|
73
|
+
}
|