Skip to content

Commit 7409877

Browse files
committed
feat: implement immediate transition end for 'none' config in per-property animations
1 parent bb0242a commit 7409877

2 files changed

Lines changed: 140 additions & 21 deletions

File tree

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

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
334334
if (mask and MASK_BACKGROUND_COLOR != 0 && initialAnimateBackgroundColor != backgroundColor) {
335335
animateBackgroundColor(initialAnimateBackgroundColor, backgroundColor, getTransitionConfigForProperty(PROP_INDEX_BACKGROUND_COLOR), loop = true)
336336
}
337+
338+
// If all per-property configs were 'none', no animations were queued.
339+
// Fire onTransitionEnd immediately to match the scalar 'none' contract.
340+
if (pendingBatchAnimationCount == 0) {
341+
onTransitionEnd?.invoke(true)
342+
}
337343
} else {
338344
// No initial animation — set target values directly (skip non-animated)
339345
if (mask and MASK_OPACITY != 0) this.alpha = opacity
@@ -363,9 +369,15 @@ class EaseView(context: Context) : ReactViewGroup(context) {
363369
onTransitionEnd?.invoke(true)
364370
} else {
365371
// Subsequent updates: animate changed properties (skip non-animated)
372+
var anyPropertyChanged = false
373+
366374
if (prevOpacity != null && mask and MASK_OPACITY != 0 && prevOpacity != opacity) {
375+
anyPropertyChanged = true
367376
val config = getTransitionConfigForProperty(PROP_INDEX_OPACITY)
368377
if (config.type == "none") {
378+
cancelSpringForProperty("alpha")
379+
runningAnimators["alpha"]?.cancel()
380+
runningAnimators.remove("alpha")
369381
this.alpha = opacity
370382
} else {
371383
val from = getCurrentValue("alpha")
@@ -374,8 +386,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
374386
}
375387

376388
if (prevTranslateX != null && mask and MASK_TRANSLATE_X != 0 && prevTranslateX != translateX) {
389+
anyPropertyChanged = true
377390
val config = getTransitionConfigForProperty(PROP_INDEX_TRANSLATE_X)
378391
if (config.type == "none") {
392+
cancelSpringForProperty("translationX")
393+
runningAnimators["translationX"]?.cancel()
394+
runningAnimators.remove("translationX")
379395
this.translationX = translateX
380396
} else {
381397
val from = getCurrentValue("translationX")
@@ -384,8 +400,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
384400
}
385401

386402
if (prevTranslateY != null && mask and MASK_TRANSLATE_Y != 0 && prevTranslateY != translateY) {
403+
anyPropertyChanged = true
387404
val config = getTransitionConfigForProperty(PROP_INDEX_TRANSLATE_Y)
388405
if (config.type == "none") {
406+
cancelSpringForProperty("translationY")
407+
runningAnimators["translationY"]?.cancel()
408+
runningAnimators.remove("translationY")
389409
this.translationY = translateY
390410
} else {
391411
val from = getCurrentValue("translationY")
@@ -394,8 +414,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
394414
}
395415

396416
if (prevScaleX != null && mask and MASK_SCALE_X != 0 && prevScaleX != scaleX) {
417+
anyPropertyChanged = true
397418
val config = getTransitionConfigForProperty(PROP_INDEX_SCALE_X)
398419
if (config.type == "none") {
420+
cancelSpringForProperty("scaleX")
421+
runningAnimators["scaleX"]?.cancel()
422+
runningAnimators.remove("scaleX")
399423
this.scaleX = scaleX
400424
} else {
401425
val from = getCurrentValue("scaleX")
@@ -404,8 +428,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
404428
}
405429

406430
if (prevScaleY != null && mask and MASK_SCALE_Y != 0 && prevScaleY != scaleY) {
431+
anyPropertyChanged = true
407432
val config = getTransitionConfigForProperty(PROP_INDEX_SCALE_Y)
408433
if (config.type == "none") {
434+
cancelSpringForProperty("scaleY")
435+
runningAnimators["scaleY"]?.cancel()
436+
runningAnimators.remove("scaleY")
409437
this.scaleY = scaleY
410438
} else {
411439
val from = getCurrentValue("scaleY")
@@ -414,8 +442,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
414442
}
415443

416444
if (prevRotate != null && mask and MASK_ROTATE != 0 && prevRotate != rotate) {
445+
anyPropertyChanged = true
417446
val config = getTransitionConfigForProperty(PROP_INDEX_ROTATE)
418447
if (config.type == "none") {
448+
cancelSpringForProperty("rotation")
449+
runningAnimators["rotation"]?.cancel()
450+
runningAnimators.remove("rotation")
419451
this.rotation = rotate
420452
} else {
421453
val from = getCurrentValue("rotation")
@@ -424,8 +456,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
424456
}
425457

426458
if (prevRotateX != null && mask and MASK_ROTATE_X != 0 && prevRotateX != rotateX) {
459+
anyPropertyChanged = true
427460
val config = getTransitionConfigForProperty(PROP_INDEX_ROTATE_X)
428461
if (config.type == "none") {
462+
cancelSpringForProperty("rotationX")
463+
runningAnimators["rotationX"]?.cancel()
464+
runningAnimators.remove("rotationX")
429465
this.rotationX = rotateX
430466
} else {
431467
val from = getCurrentValue("rotationX")
@@ -434,8 +470,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
434470
}
435471

436472
if (prevRotateY != null && mask and MASK_ROTATE_Y != 0 && prevRotateY != rotateY) {
473+
anyPropertyChanged = true
437474
val config = getTransitionConfigForProperty(PROP_INDEX_ROTATE_Y)
438475
if (config.type == "none") {
476+
cancelSpringForProperty("rotationY")
477+
runningAnimators["rotationY"]?.cancel()
478+
runningAnimators.remove("rotationY")
439479
this.rotationY = rotateY
440480
} else {
441481
val from = getCurrentValue("rotationY")
@@ -444,8 +484,11 @@ class EaseView(context: Context) : ReactViewGroup(context) {
444484
}
445485

446486
if (prevBorderRadius != null && mask and MASK_BORDER_RADIUS != 0 && prevBorderRadius != borderRadius) {
487+
anyPropertyChanged = true
447488
val config = getTransitionConfigForProperty(PROP_INDEX_BORDER_RADIUS)
448489
if (config.type == "none") {
490+
runningAnimators["animateBorderRadius"]?.cancel()
491+
runningAnimators.remove("animateBorderRadius")
449492
setAnimateBorderRadius(borderRadius)
450493
} else {
451494
val from = getCurrentValue("animateBorderRadius")
@@ -454,13 +497,22 @@ class EaseView(context: Context) : ReactViewGroup(context) {
454497
}
455498

456499
if (prevBackgroundColor != null && mask and MASK_BACKGROUND_COLOR != 0 && prevBackgroundColor != backgroundColor) {
500+
anyPropertyChanged = true
457501
val config = getTransitionConfigForProperty(PROP_INDEX_BACKGROUND_COLOR)
458502
if (config.type == "none") {
503+
runningAnimators["backgroundColor"]?.cancel()
504+
runningAnimators.remove("backgroundColor")
459505
applyBackgroundColor(backgroundColor)
460506
} else {
461507
animateBackgroundColor(getCurrentBackgroundColor(), backgroundColor, config)
462508
}
463509
}
510+
511+
// If per-property arrays are populated and all changed properties resolved
512+
// to 'none', no animations were queued. Fire onTransitionEnd immediately.
513+
if (hasPerPropertyArrays() && anyPropertyChanged && pendingBatchAnimationCount == 0) {
514+
onTransitionEnd?.invoke(true)
515+
}
464516
}
465517

466518
prevOpacity = opacity
@@ -549,6 +601,17 @@ class EaseView(context: Context) : ReactViewGroup(context) {
549601
config: TransitionConfig,
550602
loop: Boolean = false
551603
) {
604+
if (config.type == "none") {
605+
// Set immediately — cancel any running animation for this property
606+
cancelSpringForProperty(propertyName)
607+
runningAnimators[propertyName]?.cancel()
608+
runningAnimators.remove(propertyName)
609+
ObjectAnimator.ofFloat(this, propertyName, toValue).apply {
610+
duration = 0
611+
start()
612+
}
613+
return
614+
}
552615
if (config.type == "spring" && viewProperty != null) {
553616
animateSpring(viewProperty, toValue, config)
554617
} else {
@@ -605,11 +668,13 @@ class EaseView(context: Context) : ReactViewGroup(context) {
605668
private fun animateSpring(viewProperty: DynamicAnimation.ViewProperty, toValue: Float, config: TransitionConfig) {
606669
cancelTimingForViewProperty(viewProperty)
607670

608-
val existingSpring = runningSpringAnimations[viewProperty]
609-
if (existingSpring != null && existingSpring.isRunning) {
610-
existingSpring.animateToFinalPosition(toValue)
611-
return
671+
// Cancel any existing spring so we get a fresh end listener with the current batchId.
672+
runningSpringAnimations[viewProperty]?.let { existing ->
673+
if (existing.isRunning) {
674+
existing.cancel()
675+
}
612676
}
677+
runningSpringAnimations.remove(viewProperty)
613678

614679
val batchId = animationBatchId
615680
pendingBatchAnimationCount++

ios/EaseView.mm

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -409,49 +409,76 @@ - (void)updateProps:(const Props::Shared &)props
409409
newViewProps.initialAnimateBackgroundColor)
410410
.CGColor;
411411

412-
// Animate from initial to target
412+
// Animate from initial to target (skip if config is 'none')
413413
if (hasInitialOpacity) {
414414
EaseTransitionConfig opacityConfig =
415415
transitionConfigForPropertyIndex(kPropIndexOpacity, newViewProps);
416416
self.layer.opacity = newViewProps.animateOpacity;
417-
[self applyAnimationForKeyPath:@"opacity"
418-
animationKey:kAnimKeyOpacity
419-
fromValue:@(newViewProps.initialAnimateOpacity)
420-
toValue:@(newViewProps.animateOpacity)
421-
config:opacityConfig
422-
loop:YES];
417+
if (opacityConfig.type != EaseViewTransitionType::None) {
418+
[self applyAnimationForKeyPath:@"opacity"
419+
animationKey:kAnimKeyOpacity
420+
fromValue:@(newViewProps.initialAnimateOpacity)
421+
toValue:@(newViewProps.animateOpacity)
422+
config:opacityConfig
423+
loop:YES];
424+
}
423425
}
424426
if (hasInitialTransform) {
425-
int transformIdx = lowestTransformPropertyIndex(mask);
427+
// Build mask of which transform sub-properties actually changed
428+
int changedInitTransform = 0;
429+
if (newViewProps.initialAnimateTranslateX !=
430+
newViewProps.animateTranslateX)
431+
changedInitTransform |= kMaskTranslateX;
432+
if (newViewProps.initialAnimateTranslateY !=
433+
newViewProps.animateTranslateY)
434+
changedInitTransform |= kMaskTranslateY;
435+
if (newViewProps.initialAnimateScaleX != newViewProps.animateScaleX)
436+
changedInitTransform |= kMaskScaleX;
437+
if (newViewProps.initialAnimateScaleY != newViewProps.animateScaleY)
438+
changedInitTransform |= kMaskScaleY;
439+
if (newViewProps.initialAnimateRotate != newViewProps.animateRotate)
440+
changedInitTransform |= kMaskRotate;
441+
if (newViewProps.initialAnimateRotateX != newViewProps.animateRotateX)
442+
changedInitTransform |= kMaskRotateX;
443+
if (newViewProps.initialAnimateRotateY != newViewProps.animateRotateY)
444+
changedInitTransform |= kMaskRotateY;
445+
int transformIdx = lowestTransformPropertyIndex(changedInitTransform);
426446
EaseTransitionConfig transformConfig =
427447
transitionConfigForPropertyIndex(transformIdx, newViewProps);
428448
self.layer.transform = targetT;
429-
[self applyAnimationForKeyPath:@"transform"
449+
if (transformConfig.type != EaseViewTransitionType::None) {
450+
[self
451+
applyAnimationForKeyPath:@"transform"
430452
animationKey:kAnimKeyTransform
431453
fromValue:[NSValue valueWithCATransform3D:initialT]
432454
toValue:[NSValue valueWithCATransform3D:targetT]
433455
config:transformConfig
434456
loop:YES];
457+
}
435458
}
436459
if (hasInitialBorderRadius) {
437460
EaseTransitionConfig brConfig = transitionConfigForPropertyIndex(
438461
kPropIndexBorderRadius, newViewProps);
439462
self.layer.cornerRadius = newViewProps.animateBorderRadius;
440-
[self
441-
applyAnimationForKeyPath:@"cornerRadius"
442-
animationKey:kAnimKeyCornerRadius
443-
fromValue:@(newViewProps.initialAnimateBorderRadius)
444-
toValue:@(newViewProps.animateBorderRadius)
445-
config:brConfig
446-
loop:YES];
463+
if (brConfig.type != EaseViewTransitionType::None) {
464+
[self applyAnimationForKeyPath:@"cornerRadius"
465+
animationKey:kAnimKeyCornerRadius
466+
fromValue:@(newViewProps
467+
.initialAnimateBorderRadius)
468+
toValue:@(newViewProps.animateBorderRadius)
469+
config:brConfig
470+
loop:YES];
471+
}
447472
}
448473
if (hasInitialBackgroundColor) {
449474
EaseTransitionConfig bgConfig = transitionConfigForPropertyIndex(
450475
kPropIndexBackgroundColor, newViewProps);
451476
self.layer.backgroundColor =
452477
RCTUIColorFromSharedColor(newViewProps.animateBackgroundColor)
453478
.CGColor;
454-
[self applyAnimationForKeyPath:@"backgroundColor"
479+
if (bgConfig.type != EaseViewTransitionType::None) {
480+
[self
481+
applyAnimationForKeyPath:@"backgroundColor"
455482
animationKey:kAnimKeyBackgroundColor
456483
fromValue:(__bridge id)RCTUIColorFromSharedColor(
457484
newViewProps
@@ -462,6 +489,17 @@ - (void)updateProps:(const Props::Shared &)props
462489
.CGColor
463490
config:bgConfig
464491
loop:YES];
492+
}
493+
}
494+
495+
// If all per-property configs were 'none', no animations were queued.
496+
// Fire onTransitionEnd immediately to match the scalar 'none' contract.
497+
if (_pendingAnimationCount == 0 && _eventEmitter) {
498+
auto emitter =
499+
std::static_pointer_cast<const EaseViewEventEmitter>(_eventEmitter);
500+
emitter->onTransitionEnd(EaseViewEventEmitter::OnTransitionEnd{
501+
.finished = true,
502+
});
465503
}
466504
} else {
467505
// No initial animation — set target values directly
@@ -505,9 +543,11 @@ - (void)updateProps:(const Props::Shared &)props
505543
// Subsequent updates: animate changed properties
506544
const auto &oldViewProps =
507545
*std::static_pointer_cast<const EaseViewProps>(oldProps);
546+
BOOL anyPropertyChanged = NO;
508547

509548
if ((mask & kMaskOpacity) &&
510549
oldViewProps.animateOpacity != newViewProps.animateOpacity) {
550+
anyPropertyChanged = YES;
511551
EaseTransitionConfig opacityConfig =
512552
transitionConfigForPropertyIndex(kPropIndexOpacity, newViewProps);
513553
if (opacityConfig.type == EaseViewTransitionType::None) {
@@ -538,6 +578,7 @@ - (void)updateProps:(const Props::Shared &)props
538578
oldViewProps.animateRotateY != newViewProps.animateRotateY;
539579

540580
if (anyTransformChanged) {
581+
anyPropertyChanged = YES;
541582
// Determine which transform sub-properties changed for config selection
542583
int changedTransformMask = 0;
543584
if (oldViewProps.animateTranslateX != newViewProps.animateTranslateX)
@@ -578,6 +619,7 @@ - (void)updateProps:(const Props::Shared &)props
578619

579620
if ((mask & kMaskBorderRadius) &&
580621
oldViewProps.animateBorderRadius != newViewProps.animateBorderRadius) {
622+
anyPropertyChanged = YES;
581623
EaseTransitionConfig brConfig = transitionConfigForPropertyIndex(
582624
kPropIndexBorderRadius, newViewProps);
583625
self.layer.cornerRadius = newViewProps.animateBorderRadius;
@@ -598,6 +640,7 @@ - (void)updateProps:(const Props::Shared &)props
598640
if ((mask & kMaskBackgroundColor) &&
599641
oldViewProps.animateBackgroundColor !=
600642
newViewProps.animateBackgroundColor) {
643+
anyPropertyChanged = YES;
601644
EaseTransitionConfig bgConfig = transitionConfigForPropertyIndex(
602645
kPropIndexBackgroundColor, newViewProps);
603646
CGColorRef toColor =
@@ -617,6 +660,17 @@ - (void)updateProps:(const Props::Shared &)props
617660
loop:NO];
618661
}
619662
}
663+
664+
// If per-property arrays are populated and all changed properties resolved
665+
// to 'none', no animations were queued. Fire onTransitionEnd immediately.
666+
if (perProp && anyPropertyChanged && _pendingAnimationCount == 0 &&
667+
_eventEmitter) {
668+
auto emitter =
669+
std::static_pointer_cast<const EaseViewEventEmitter>(_eventEmitter);
670+
emitter->onTransitionEnd(EaseViewEventEmitter::OnTransitionEnd{
671+
.finished = true,
672+
});
673+
}
620674
}
621675

622676
[CATransaction commit];

0 commit comments

Comments
 (0)