Skip to content

Commit 0d0adda

Browse files
feat: add transition delay + document backgroundColor spring limitation (#2)
* feat: add transition delay prop and document backgroundColor spring limitation Add `delay` option (milliseconds) to TimingTransition and SpringTransition types. Implemented via CAAnimation.beginTime on iOS, ObjectAnimator.startDelay on Android (timing/color), and postDelayed for SpringAnimation. Added a callout in README about Android backgroundColor being timing-only, and a delay demo in the example app. * style: fix clang-format violation in EaseView.mm * chore(example): increase delay to 2s for more obvious demo
1 parent 28bebd2 commit 0d0adda

9 files changed

Lines changed: 68 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ When `borderRadius` is in `animate`, any `borderRadius` in `style` is automatica
198198

199199
On Android, background color uses `ValueAnimator.ofArgb()` (timing only — spring is not supported for colors). On iOS, it uses `CAAnimation` on the `backgroundColor` layer key path and supports both timing and spring transitions.
200200

201+
> **Note:** On Android, background color animation uses `ValueAnimator.ofArgb()` which only supports timing transitions. Spring transitions for `backgroundColor` are not supported on Android and will fall back to timing with the default duration. On iOS, both timing and spring transitions work for background color.
202+
201203
When `backgroundColor` is in `animate`, any `backgroundColor` in `style` is automatically stripped to avoid conflicts.
202204

203205
### Animatable Properties

android/src/main/java/com/ease/EaseView.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
4242
var transitionStiffness: Float = 120.0f
4343
var transitionMass: Float = 1.0f
4444
var transitionLoop: String = "none"
45+
var transitionDelay: Long = 0L
4546

4647
// --- Transform origin (0–1 fractions) ---
4748
var transformOriginX: Float = 0.5f
@@ -112,6 +113,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
112113
// --- Running animations ---
113114
private val runningAnimators = mutableMapOf<String, Animator>()
114115
private val runningSpringAnimations = mutableMapOf<DynamicAnimation.ViewProperty, SpringAnimation>()
116+
private val pendingDelayedRunnables = mutableListOf<Runnable>()
115117

116118
// --- Animated properties bitmask (set by ViewManager) ---
117119
var animatedProperties: Int = 0
@@ -384,6 +386,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
384386

385387
val animator = ValueAnimator.ofArgb(fromColor, toColor).apply {
386388
duration = transitionDuration.toLong()
389+
startDelay = transitionDelay
387390
interpolator = PathInterpolator(
388391
transitionEasingBezier[0], transitionEasingBezier[1],
389392
transitionEasingBezier[2], transitionEasingBezier[3]
@@ -443,6 +446,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
443446

444447
val animator = ObjectAnimator.ofFloat(this, propertyName, fromValue, toValue).apply {
445448
duration = transitionDuration.toLong()
449+
startDelay = transitionDelay
446450
interpolator = PathInterpolator(
447451
transitionEasingBezier[0], transitionEasingBezier[1],
448452
transitionEasingBezier[2], transitionEasingBezier[3]
@@ -520,10 +524,20 @@ class EaseView(context: Context) : ReactViewGroup(context) {
520524

521525
onEaseAnimationStart()
522526
runningSpringAnimations[viewProperty] = spring
523-
spring.start()
527+
if (transitionDelay > 0) {
528+
val runnable = Runnable { spring.start() }
529+
pendingDelayedRunnables.add(runnable)
530+
postDelayed(runnable, transitionDelay)
531+
} else {
532+
spring.start()
533+
}
524534
}
525535

526536
private fun cancelAllAnimations() {
537+
for (runnable in pendingDelayedRunnables) {
538+
removeCallbacks(runnable)
539+
}
540+
pendingDelayedRunnables.clear()
527541
for (animator in runningAnimators.values) {
528542
animator.cancel()
529543
}
@@ -573,6 +587,10 @@ class EaseView(context: Context) : ReactViewGroup(context) {
573587
}
574588

575589
fun cleanup() {
590+
for (runnable in pendingDelayedRunnables) {
591+
removeCallbacks(runnable)
592+
}
593+
pendingDelayedRunnables.clear()
576594
for (animator in runningAnimators.values) {
577595
animator.cancel()
578596
}

android/src/main/java/com/ease/EaseViewManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ class EaseViewManager : ReactViewManager() {
173173
view.transitionLoop = value ?: "none"
174174
}
175175

176+
@ReactProp(name = "transitionDelay", defaultInt = 0)
177+
fun setTransitionDelay(view: EaseView, value: Int) {
178+
view.transitionDelay = value.toLong()
179+
}
180+
176181
// --- Border radius ---
177182

178183
@ReactProp(name = "animateBorderRadius", defaultFloat = 0f)

example/ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PODS:
22
- boost (1.84.0)
33
- DoubleConversion (1.1.6)
4-
- Ease (0.1.0):
4+
- Ease (0.2.0):
55
- boost
66
- DoubleConversion
77
- fast_float
@@ -3005,12 +3005,12 @@ EXTERNAL SOURCES:
30053005
SPEC CHECKSUMS:
30063006
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
30073007
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
3008-
Ease: e0e726adb99768bed143604b0b07661422921519
3008+
Ease: 1926ac476b99e4d4bbe830c0f8f7edf5d3990cdb
30093009
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
30103010
FBLazyVector: a293a88992c4c33f0aee184acab0b64a08ff9458
30113011
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
30123012
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
3013-
hermes-engine: 3992289ac7e8cd4a0bb902711038e16b94212779
3013+
hermes-engine: a3bc462fbff75c23f8cb35ae50c37c12905b7423
30143014
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
30153015
RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36
30163016
RCTRequired: f3540eee8094231581d40c5c6d41b0f170237a81
@@ -3088,4 +3088,4 @@ SPEC CHECKSUMS:
30883088

30893089
PODFILE CHECKSUM: b945c918935ce26cb0357d98636dc7184bab408a
30903090

3091-
COCOAPODS: 1.15.2
3091+
COCOAPODS: 1.16.2

example/src/App.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,28 @@ function BackgroundColorDemo() {
383383
);
384384
}
385385

386+
function DelayDemo() {
387+
const [active, setActive] = useState(false);
388+
return (
389+
<Section title="Delay">
390+
<EaseView
391+
animate={{ opacity: active ? 1 : 0.3, translateX: active ? 100 : 0 }}
392+
transition={{
393+
type: 'timing',
394+
duration: 400,
395+
delay: 2000,
396+
easing: 'easeOut',
397+
}}
398+
style={styles.box}
399+
/>
400+
<Button
401+
label={active ? 'Reset' : 'Delayed Move'}
402+
onPress={() => setActive((v) => !v)}
403+
/>
404+
</Section>
405+
);
406+
}
407+
386408
function CombinedDemo() {
387409
const [active, setActive] = useState(false);
388410
return (
@@ -457,6 +479,7 @@ function DemosScreen() {
457479
<StyledCardDemo />
458480
<BorderRadiusDemo />
459481
<BackgroundColorDemo />
482+
<DelayDemo />
460483
<CustomEasingDemo />
461484
<CombinedDemo />
462485
<StyleReRenderDemo />

ios/EaseView.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ - (void)applyAnimationForKeyPath:(NSString *)keyPath
194194
toValue:toValue
195195
props:props
196196
loop:loop];
197+
if (props.transitionDelay > 0) {
198+
animation.beginTime =
199+
CACurrentMediaTime() + (props.transitionDelay / 1000.0);
200+
animation.fillMode = kCAFillModeBackwards;
201+
}
197202
[animation setValue:@(_animationBatchId) forKey:@"easeBatchId"];
198203
animation.delegate = self;
199204
[self.layer addAnimation:animation forKey:animationKey];

src/EaseView.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ export function EaseView({
219219
transition?.type === 'spring' ? transition.mass ?? 1 : 1;
220220
const transitionLoop =
221221
transition?.type === 'timing' ? transition.loop ?? 'none' : 'none';
222+
const transitionDelay =
223+
transition?.type === 'timing' || transition?.type === 'spring'
224+
? transition.delay ?? 0
225+
: 0;
222226

223227
const handleTransitionEnd = onTransitionEnd
224228
? (event: { nativeEvent: { finished: boolean } }) =>
@@ -257,6 +261,7 @@ export function EaseView({
257261
transitionStiffness={transitionStiffness}
258262
transitionMass={transitionMass}
259263
transitionLoop={transitionLoop}
264+
transitionDelay={transitionDelay}
260265
useHardwareLayer={useHardwareLayer}
261266
transformOriginX={transformOrigin?.x ?? 0.5}
262267
transformOriginY={transformOrigin?.y ?? 0.5}

src/EaseViewNativeComponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface NativeProps extends ViewProps {
5252
'none' | 'repeat' | 'reverse',
5353
'none'
5454
>;
55+
transitionDelay?: CodegenTypes.WithDefault<CodegenTypes.Int32, 0>;
5556

5657
// Transform origin (0–1 fractions, default center)
5758
transformOriginX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export type TimingTransition = {
2020
easing?: EasingType;
2121
/** Loop mode — 'repeat' restarts from the beginning, 'reverse' alternates direction. */
2222
loop?: 'repeat' | 'reverse';
23+
/** Delay in milliseconds before the animation starts. @default 0 */
24+
delay?: number;
2325
};
2426

2527
/** Physics-based spring transition. */
@@ -31,6 +33,8 @@ export type SpringTransition = {
3133
stiffness?: number;
3234
/** Mass of the object — higher values mean slower, more momentum. @default 1 */
3335
mass?: number;
36+
/** Delay in milliseconds before the animation starts. @default 0 */
37+
delay?: number;
3438
};
3539

3640
/** No transition — values are applied immediately without animation. */

0 commit comments

Comments
 (0)