@rafaelaugusto/jolt 0.1.0-beta.0 → 0.1.0-beta.2
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 +134 -14
- package/dist/animate.d.ts +3 -3
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +40 -19
- package/dist/animate.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +19 -3
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +7 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +28 -5
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +18 -0
- package/dist/haptics.d.ts.map +1 -0
- package/dist/haptics.js +96 -0
- package/dist/haptics.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/particles.d.ts +39 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +264 -0
- package/dist/particles.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 +3 -1
- package/dist/press.js.map +1 -1
- package/dist/sequence.d.ts +3 -2
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +6 -5
- 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 +160 -0
- package/dist/shimmer.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 +186 -0
- package/dist/symbol-effect.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/animate.ts +64 -19
- package/src/driver/ios.ts +23 -3
- package/src/engine.ts +26 -6
- package/src/haptics.ts +125 -0
- package/src/index.ts +13 -2
- package/src/particles.ts +323 -0
- package/src/presets.ts +31 -7
- package/src/press.ts +7 -1
- package/src/sequence.ts +8 -5
- package/src/shimmer.ts +179 -0
- package/src/symbol-effect.ts +225 -0
- package/src/types.ts +5 -0
package/src/shimmer.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Application, isIOS, type View } from '@nativescript/core'
|
|
2
|
+
import { toView } from './animate.js'
|
|
3
|
+
import type { Target } from './types.js'
|
|
4
|
+
|
|
5
|
+
export interface ShimmerOptions {
|
|
6
|
+
/** Milliseconds per sweep. Defaults to 1500. */
|
|
7
|
+
duration?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const running = new WeakMap<View, () => void>()
|
|
11
|
+
const key = 'jolt:shimmer'
|
|
12
|
+
|
|
13
|
+
// NativeScript saves a mask here while applying clip-path or non-uniform borders.
|
|
14
|
+
type MaskedView = UIView & { originalMask?: CALayer | null }
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sweeps a native gradient mask across an iOS placeholder until stopped.
|
|
18
|
+
* Waits for layout, respects Reduce Motion and pauses while the view is unloaded.
|
|
19
|
+
* Android is a no-op. Returns an idempotent cleanup function.
|
|
20
|
+
*/
|
|
21
|
+
export function shimmer(target: Target, options: ShimmerOptions = {}): () => void {
|
|
22
|
+
const view = toView(target)
|
|
23
|
+
const duration = options.duration ?? 1500
|
|
24
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
25
|
+
throw new RangeError('Shimmer duration must be a positive finite number.')
|
|
26
|
+
running.get(view)?.()
|
|
27
|
+
if (!isIOS) return () => {}
|
|
28
|
+
return new Shimmer(view, duration).stop
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class Shimmer {
|
|
32
|
+
private stopped = false
|
|
33
|
+
private queued = false
|
|
34
|
+
private waitingForLoad: boolean
|
|
35
|
+
private native: MaskedView | undefined
|
|
36
|
+
private owner: CALayer | undefined
|
|
37
|
+
private gradient: CAGradientLayer | undefined
|
|
38
|
+
private observer: ReturnType<typeof Application.ios.addNotificationObserver> | undefined
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
private readonly view: View,
|
|
42
|
+
private readonly duration: number,
|
|
43
|
+
) {
|
|
44
|
+
this.waitingForLoad = !view.isLoaded
|
|
45
|
+
running.set(view, this.stop)
|
|
46
|
+
view.on('loaded', this.loaded)
|
|
47
|
+
view.on('layoutChanged', this.schedule)
|
|
48
|
+
view.on('unloaded', this.unloaded)
|
|
49
|
+
view.style.on('backgroundInternalChange', this.schedule)
|
|
50
|
+
this.update()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
stop = () => {
|
|
54
|
+
if (this.stopped) return
|
|
55
|
+
this.stopped = true
|
|
56
|
+
const { view } = this
|
|
57
|
+
view.off('loaded', this.loaded)
|
|
58
|
+
view.off('layoutChanged', this.schedule)
|
|
59
|
+
view.off('unloaded', this.unloaded)
|
|
60
|
+
view.style.off('backgroundInternalChange', this.schedule)
|
|
61
|
+
if (running.get(view) === this.stop) running.delete(view)
|
|
62
|
+
this.unloaded()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private loaded = () => {
|
|
66
|
+
this.waitingForLoad = false
|
|
67
|
+
this.schedule()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private unloaded = () => {
|
|
71
|
+
this.waitingForLoad = true
|
|
72
|
+
if (this.observer) {
|
|
73
|
+
Application.ios.removeNotificationObserver(
|
|
74
|
+
this.observer,
|
|
75
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
76
|
+
)
|
|
77
|
+
this.observer = undefined
|
|
78
|
+
}
|
|
79
|
+
this.remove()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private schedule = () => {
|
|
83
|
+
if (this.stopped || this.queued) return
|
|
84
|
+
this.queued = true
|
|
85
|
+
// Core raises layoutChanged before redrawing the background and its mask.
|
|
86
|
+
// Attach after that pass, coalescing events from the same layout.
|
|
87
|
+
void Promise.resolve().then(() => {
|
|
88
|
+
this.queued = false
|
|
89
|
+
this.update()
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private update = () => {
|
|
94
|
+
if (this.stopped || this.waitingForLoad || !this.view.isLoaded) return
|
|
95
|
+
const native = this.view.nativeViewProtected as MaskedView | undefined
|
|
96
|
+
if (!native) return
|
|
97
|
+
this.observer ??= Application.ios.addNotificationObserver(
|
|
98
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
99
|
+
this.update,
|
|
100
|
+
)
|
|
101
|
+
if (UIAccessibilityIsReduceMotionEnabled()) return this.remove()
|
|
102
|
+
const bounds = native.layer.bounds
|
|
103
|
+
if (bounds.size.width <= 0 || bounds.size.height <= 0) return this.remove()
|
|
104
|
+
if (this.native && this.native !== native) this.remove()
|
|
105
|
+
|
|
106
|
+
CATransaction.begin()
|
|
107
|
+
CATransaction.setDisableActions(true)
|
|
108
|
+
try {
|
|
109
|
+
this.native = native
|
|
110
|
+
const gradient = (this.gradient ??= this.createGradient())
|
|
111
|
+
// Leave Core's mask at the root so it can keep updating its path on resize.
|
|
112
|
+
let owner = native.layer
|
|
113
|
+
while (owner.mask && owner.mask !== gradient) owner = owner.mask
|
|
114
|
+
if (this.owner && this.owner !== owner && this.owner.mask === gradient)
|
|
115
|
+
this.owner.mask = null
|
|
116
|
+
this.owner = owner
|
|
117
|
+
owner.mask = gradient
|
|
118
|
+
// Don't let a later background redraw resurrect our mask after cleanup.
|
|
119
|
+
if (native.originalMask === gradient) native.originalMask = null
|
|
120
|
+
|
|
121
|
+
// Core's CAShapeLayer masks draw a path with zero bounds. In that case the
|
|
122
|
+
// path uses the view's coordinates; other masks have their own bounds.
|
|
123
|
+
const area =
|
|
124
|
+
owner.bounds.size.width > 0 && owner.bounds.size.height > 0
|
|
125
|
+
? owner.bounds
|
|
126
|
+
: bounds
|
|
127
|
+
gradient.frame = CGRectMake(
|
|
128
|
+
area.origin.x - area.size.width,
|
|
129
|
+
area.origin.y,
|
|
130
|
+
area.size.width * 3,
|
|
131
|
+
area.size.height,
|
|
132
|
+
)
|
|
133
|
+
} finally {
|
|
134
|
+
CATransaction.commit()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private createGradient(): CAGradientLayer {
|
|
139
|
+
const gradient = CAGradientLayer.new()
|
|
140
|
+
const dim = UIColor.colorWithWhiteAlpha(1, 0.35).CGColor
|
|
141
|
+
gradient.colors = NSArray.arrayWithArray([dim, UIColor.whiteColor.CGColor, dim])
|
|
142
|
+
gradient.startPoint = CGPointMake(0, 0.5)
|
|
143
|
+
gradient.endPoint = CGPointMake(1, 0.5)
|
|
144
|
+
|
|
145
|
+
// A three-width mask lets the band enter and leave while every stop stays
|
|
146
|
+
// within the documented 0...1 range for CAGradientLayer.locations.
|
|
147
|
+
const from = NSArray.arrayWithArray([0, 0.125, 0.25])
|
|
148
|
+
const to = NSArray.arrayWithArray([0.75, 0.875, 1])
|
|
149
|
+
gradient.locations = from
|
|
150
|
+
const animation = CABasicAnimation.animationWithKeyPath('locations')
|
|
151
|
+
animation.fromValue = from
|
|
152
|
+
animation.toValue = to
|
|
153
|
+
animation.duration = this.duration / 1000
|
|
154
|
+
animation.repeatCount = Infinity
|
|
155
|
+
animation.removedOnCompletion = false
|
|
156
|
+
animation.timingFunction = CAMediaTimingFunction.functionWithName(
|
|
157
|
+
kCAMediaTimingFunctionLinear,
|
|
158
|
+
)
|
|
159
|
+
gradient.addAnimationForKey(animation, key)
|
|
160
|
+
return gradient
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private remove() {
|
|
164
|
+
const { gradient, owner, native } = this
|
|
165
|
+
if (!gradient) return
|
|
166
|
+
CATransaction.begin()
|
|
167
|
+
CATransaction.setDisableActions(true)
|
|
168
|
+
try {
|
|
169
|
+
gradient.removeAnimationForKey(key)
|
|
170
|
+
if (owner?.mask === gradient) owner.mask = null
|
|
171
|
+
if (native?.originalMask === gradient) native.originalMask = null
|
|
172
|
+
} finally {
|
|
173
|
+
CATransaction.commit()
|
|
174
|
+
}
|
|
175
|
+
this.owner = undefined
|
|
176
|
+
this.gradient = undefined
|
|
177
|
+
this.native = undefined
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Image, ImageSource, isIOS } from '@nativescript/core'
|
|
2
|
+
import { toView } from './animate.js'
|
|
3
|
+
import type { Animation } from './engine.js'
|
|
4
|
+
import type { Target } from './types.js'
|
|
5
|
+
|
|
6
|
+
export type SymbolEffectName = 'bounce' | 'pulse' | 'scale' | 'variableColor'
|
|
7
|
+
|
|
8
|
+
export type SymbolEffect = SymbolEffectName | { replace: `sys://${string}` }
|
|
9
|
+
|
|
10
|
+
export interface SymbolEffectOptions {
|
|
11
|
+
/** Native playback speed multiplier. Defaults to 1. */
|
|
12
|
+
speed?: number
|
|
13
|
+
/** Repeat bounce, pulse or variableColor until stopped. Defaults to false. */
|
|
14
|
+
repeat?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const running = new WeakMap<Image, SymbolAnimation>()
|
|
18
|
+
const names: readonly string[] = ['bounce', 'pulse', 'scale', 'variableColor']
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Plays an iOS 17+ SF Symbol effect on a NativeScript Image.
|
|
22
|
+
* Scale holds until stopped; replacements also update the Image's src.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* symbolEffect(icon, 'bounce')
|
|
26
|
+
* symbolEffect(icon, { replace: 'sys://heart.fill' })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function symbolEffect(
|
|
30
|
+
target: Target,
|
|
31
|
+
effect: SymbolEffect,
|
|
32
|
+
options: SymbolEffectOptions = {},
|
|
33
|
+
): Animation {
|
|
34
|
+
const view = toView(target)
|
|
35
|
+
if (!(view instanceof Image)) throw new TypeError('symbolEffect expects an Image.')
|
|
36
|
+
if (
|
|
37
|
+
typeof effect === 'string'
|
|
38
|
+
? !names.includes(effect)
|
|
39
|
+
: !effect ||
|
|
40
|
+
typeof effect.replace !== 'string' ||
|
|
41
|
+
!/^sys:\/\/[^\s/]+$/.test(effect.replace)
|
|
42
|
+
) {
|
|
43
|
+
throw new TypeError('Expected a symbol effect name or { replace: "sys://name" }.')
|
|
44
|
+
}
|
|
45
|
+
const speed = options.speed ?? 1
|
|
46
|
+
if (!Number.isFinite(speed) || speed <= 0)
|
|
47
|
+
throw new RangeError('Symbol effect speed must be a positive finite number.')
|
|
48
|
+
if (options.repeat !== undefined && typeof options.repeat !== 'boolean')
|
|
49
|
+
throw new TypeError('Symbol effect repeat must be a boolean.')
|
|
50
|
+
if (options.repeat && (typeof effect !== 'string' || effect === 'scale'))
|
|
51
|
+
throw new TypeError('Only bounce, pulse and variableColor can repeat.')
|
|
52
|
+
|
|
53
|
+
return new SymbolAnimation(view, effect, speed, options.repeat ?? false)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class SymbolAnimation implements Animation {
|
|
57
|
+
readonly finished: Promise<boolean>
|
|
58
|
+
private resolve!: (finished: boolean) => void
|
|
59
|
+
private settled = false
|
|
60
|
+
private started = false
|
|
61
|
+
private changingSource = false
|
|
62
|
+
private waitingForLoad: boolean
|
|
63
|
+
private native: UIImageView | undefined
|
|
64
|
+
private nativeEffect: NSSymbolEffect | undefined
|
|
65
|
+
private replacement: UIImage | undefined
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly view: Image,
|
|
69
|
+
private readonly effect: SymbolEffect,
|
|
70
|
+
private readonly speed: number,
|
|
71
|
+
private readonly repeat: boolean,
|
|
72
|
+
) {
|
|
73
|
+
this.finished = new Promise((resolve) => (this.resolve = resolve))
|
|
74
|
+
this.waitingForLoad = !view.isLoaded
|
|
75
|
+
running.get(view)?.stop()
|
|
76
|
+
running.set(view, this)
|
|
77
|
+
view.on('unloaded', this.stop)
|
|
78
|
+
view.on('srcChange', this.sourceChanged)
|
|
79
|
+
view.on('imageSourceChange', this.imageChanged)
|
|
80
|
+
view.on('loaded', this.loaded)
|
|
81
|
+
view.on('isLoadingChange', this.start)
|
|
82
|
+
this.start()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
then<A = boolean, B = never>(
|
|
86
|
+
onfulfilled?: ((finished: boolean) => A | PromiseLike<A>) | null,
|
|
87
|
+
onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null,
|
|
88
|
+
): Promise<A | B> {
|
|
89
|
+
return this.finished.then(onfulfilled, onrejected)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
stop = () => this.end(false, true)
|
|
93
|
+
|
|
94
|
+
private sourceChanged = () => {
|
|
95
|
+
if (!this.changingSource) this.stop()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private imageChanged = () => {
|
|
99
|
+
if (this.changingSource) return
|
|
100
|
+
if (this.started) this.stop()
|
|
101
|
+
else this.start()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private loaded = () => {
|
|
105
|
+
this.waitingForLoad = false
|
|
106
|
+
this.start()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private start = () => {
|
|
110
|
+
if (this.settled || this.started) return
|
|
111
|
+
if (!isIOS) return this.end(false)
|
|
112
|
+
const { view, effect } = this
|
|
113
|
+
// isLoaded becomes true before NativeScript resumes native property updates.
|
|
114
|
+
// Wait for the loaded event so intermediate image resets don't start playback.
|
|
115
|
+
if (
|
|
116
|
+
this.waitingForLoad ||
|
|
117
|
+
!view.isLoaded ||
|
|
118
|
+
!view.nativeViewProtected ||
|
|
119
|
+
view.isLoading
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
this.started = true
|
|
123
|
+
const native = (this.native = view.nativeViewProtected as UIImageView)
|
|
124
|
+
const reducedMotion = UIAccessibilityIsReduceMotionEnabled()
|
|
125
|
+
|
|
126
|
+
if (typeof effect !== 'string') {
|
|
127
|
+
// Resolve first so a symbol absent on this OS doesn't clear the current image.
|
|
128
|
+
if (!ImageSource.fromSystemImageSync(effect.replace.slice(6))?.ios)
|
|
129
|
+
return this.end(false)
|
|
130
|
+
const previous = native.image
|
|
131
|
+
const loadMode = view.loadMode
|
|
132
|
+
this.changingSource = true
|
|
133
|
+
try {
|
|
134
|
+
// Let NativeScript update its source, tint, configuration and pending loads.
|
|
135
|
+
// Both assignments happen in this turn, before UIKit draws the next frame.
|
|
136
|
+
view.loadMode = 'sync'
|
|
137
|
+
view.src = effect.replace
|
|
138
|
+
} finally {
|
|
139
|
+
view.loadMode = loadMode
|
|
140
|
+
this.changingSource = false
|
|
141
|
+
}
|
|
142
|
+
// A source-change listener may have started another effect or changed the src.
|
|
143
|
+
if (this.settled) return
|
|
144
|
+
if (view.src !== effect.replace) return this.end(false)
|
|
145
|
+
const image = native.image
|
|
146
|
+
if (!image) return this.end(false)
|
|
147
|
+
if (
|
|
148
|
+
reducedMotion ||
|
|
149
|
+
!previous?.symbolImage ||
|
|
150
|
+
!native.respondsToSelector(
|
|
151
|
+
'setSymbolImage:withContentTransition:options:completion:',
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
return this.end(true)
|
|
155
|
+
|
|
156
|
+
this.replacement = image
|
|
157
|
+
native.image = previous
|
|
158
|
+
native.setSymbolImageWithContentTransitionOptionsCompletion(
|
|
159
|
+
image,
|
|
160
|
+
NSSymbolReplaceContentTransition.transition(),
|
|
161
|
+
this.options(),
|
|
162
|
+
this.completed,
|
|
163
|
+
)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
reducedMotion ||
|
|
169
|
+
!native.image?.symbolImage ||
|
|
170
|
+
!native.respondsToSelector('addSymbolEffect:options:animated:completion:')
|
|
171
|
+
)
|
|
172
|
+
return this.end(false)
|
|
173
|
+
|
|
174
|
+
const factories = {
|
|
175
|
+
bounce: NSSymbolBounceEffect,
|
|
176
|
+
pulse: NSSymbolPulseEffect,
|
|
177
|
+
scale: NSSymbolScaleEffect,
|
|
178
|
+
variableColor: NSSymbolVariableColorEffect,
|
|
179
|
+
}
|
|
180
|
+
this.nativeEffect = factories[effect].effect()
|
|
181
|
+
native.addSymbolEffectOptionsAnimatedCompletion(
|
|
182
|
+
this.nativeEffect,
|
|
183
|
+
this.options(),
|
|
184
|
+
true,
|
|
185
|
+
this.completed,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private options(): NSSymbolEffectOptions {
|
|
190
|
+
const options = this.repeat
|
|
191
|
+
? NSSymbolEffectOptions.optionsWithRepeating()
|
|
192
|
+
: NSSymbolEffectOptions.optionsWithNonRepeating()
|
|
193
|
+
return options.optionsWithSpeed(this.speed)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private completed = (context: UISymbolEffectCompletionContext) => {
|
|
197
|
+
// Scale's entrance can complete while the effect is still holding its state.
|
|
198
|
+
if (context.finished && (this.effect === 'scale' || this.repeat)) return
|
|
199
|
+
this.end(context.finished)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private end(finished: boolean, remove = false) {
|
|
203
|
+
if (this.settled) return
|
|
204
|
+
this.settled = true
|
|
205
|
+
const { view, native, nativeEffect, replacement } = this
|
|
206
|
+
view.off('loaded', this.loaded)
|
|
207
|
+
view.off('isLoadingChange', this.start)
|
|
208
|
+
view.off('unloaded', this.stop)
|
|
209
|
+
view.off('srcChange', this.sourceChanged)
|
|
210
|
+
view.off('imageSourceChange', this.imageChanged)
|
|
211
|
+
if (running.get(view) === this) running.delete(view)
|
|
212
|
+
if (remove && native) {
|
|
213
|
+
if (nativeEffect) native.removeSymbolEffectOfType(nativeEffect)
|
|
214
|
+
// Content transitions have no removal API. Keep the destination icon.
|
|
215
|
+
if (replacement && native.image === replacement) {
|
|
216
|
+
native.image = null
|
|
217
|
+
native.image = replacement
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
this.native = undefined
|
|
221
|
+
this.nativeEffect = undefined
|
|
222
|
+
this.replacement = undefined
|
|
223
|
+
this.resolve(finished)
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { View } from '@nativescript/core'
|
|
2
|
+
import type { Haptic } from './haptics.js'
|
|
2
3
|
import type { SpringName, Transition } from './transition.js'
|
|
3
4
|
|
|
4
5
|
/** A value to animate to, or keyframes. `null` stands for the value the view has when the animation starts. */
|
|
@@ -34,6 +35,10 @@ export interface Stagger {
|
|
|
34
35
|
export type AnimateOptions = Transition & {
|
|
35
36
|
[K in keyof Keyframes]?: Transition | SpringName
|
|
36
37
|
} & {
|
|
38
|
+
/** Offset translations/rotations and multiply scale/opacity by their values at the start. */
|
|
39
|
+
relative?: boolean
|
|
37
40
|
/** With several targets, delays each one by this much more than the previous. */
|
|
38
41
|
stagger?: number | Stagger
|
|
42
|
+
/** iOS impact feedback at arrival, or at explicit points in a finite animation. */
|
|
43
|
+
haptic?: Haptic
|
|
39
44
|
}
|