react-native-ease 0.3.0 → 0.4.0

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.
@@ -3,6 +3,7 @@ package com.ease
3
3
  import android.graphics.Color
4
4
  import com.facebook.react.bridge.Arguments
5
5
  import com.facebook.react.bridge.ReadableArray
6
+ import com.facebook.react.bridge.ReadableMap
6
7
  import com.facebook.react.bridge.WritableMap
7
8
  import com.facebook.react.module.annotations.ReactModule
8
9
  import com.facebook.react.uimanager.PixelUtil
@@ -126,56 +127,11 @@ class EaseViewManager : ReactViewManager() {
126
127
  view.initialAnimateBorderRadius = PixelUtil.toPixelFromDIP(value)
127
128
  }
128
129
 
129
- // --- Transition config setters ---
130
+ // --- Transitions config (single ReadableMap) ---
130
131
 
131
- @ReactProp(name = "transitionType")
132
- fun setTransitionType(view: EaseView, value: String?) {
133
- view.transitionType = value ?: "timing"
134
- }
135
-
136
- @ReactProp(name = "transitionDuration", defaultInt = 300)
137
- fun setTransitionDuration(view: EaseView, value: Int) {
138
- view.transitionDuration = value
139
- }
140
-
141
- @ReactProp(name = "transitionEasingBezier")
142
- fun setTransitionEasingBezier(view: EaseView, value: ReadableArray?) {
143
- if (value != null && value.size() == 4) {
144
- view.transitionEasingBezier = floatArrayOf(
145
- value.getDouble(0).toFloat(),
146
- value.getDouble(1).toFloat(),
147
- value.getDouble(2).toFloat(),
148
- value.getDouble(3).toFloat()
149
- )
150
- } else {
151
- // Fallback: easeInOut
152
- view.transitionEasingBezier = floatArrayOf(0.42f, 0f, 0.58f, 1.0f)
153
- }
154
- }
155
-
156
- @ReactProp(name = "transitionDamping", defaultFloat = 15f)
157
- fun setTransitionDamping(view: EaseView, value: Float) {
158
- view.transitionDamping = value
159
- }
160
-
161
- @ReactProp(name = "transitionStiffness", defaultFloat = 120f)
162
- fun setTransitionStiffness(view: EaseView, value: Float) {
163
- view.transitionStiffness = value
164
- }
165
-
166
- @ReactProp(name = "transitionMass", defaultFloat = 1f)
167
- fun setTransitionMass(view: EaseView, value: Float) {
168
- view.transitionMass = value
169
- }
170
-
171
- @ReactProp(name = "transitionLoop")
172
- fun setTransitionLoop(view: EaseView, value: String?) {
173
- view.transitionLoop = value ?: "none"
174
- }
175
-
176
- @ReactProp(name = "transitionDelay", defaultInt = 0)
177
- fun setTransitionDelay(view: EaseView, value: Int) {
178
- view.transitionDelay = value.toLong()
132
+ @ReactProp(name = "transitions")
133
+ fun setTransitions(view: EaseView, value: ReadableMap?) {
134
+ view.setTransitionsFromMap(value)
179
135
  }
180
136
 
181
137
  // --- Border radius ---
package/ios/EaseView.mm CHANGED
@@ -58,6 +58,92 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
58
58
  kMaskScaleX | kMaskScaleY | kMaskRotate |
59
59
  kMaskRotateX | kMaskRotateY;
60
60
 
61
+ // Per-property transition config resolved from the transitions struct
62
+ struct EaseTransitionConfig {
63
+ std::string type;
64
+ int duration;
65
+ float bezier[4];
66
+ float damping;
67
+ float stiffness;
68
+ float mass;
69
+ std::string loop;
70
+ int delay;
71
+ };
72
+
73
+ // Convert from a codegen-generated transition config struct to our local
74
+ // EaseTransitionConfig
75
+ template <typename T>
76
+ static EaseTransitionConfig transitionConfigFromStruct(const T &src) {
77
+ EaseTransitionConfig config;
78
+ config.type = src.type;
79
+ config.duration = src.duration;
80
+ const auto &b = src.easingBezier;
81
+ if (b.size() == 4) {
82
+ config.bezier[0] = b[0];
83
+ config.bezier[1] = b[1];
84
+ config.bezier[2] = b[2];
85
+ config.bezier[3] = b[3];
86
+ } else {
87
+ config.bezier[0] = 0.42f;
88
+ config.bezier[1] = 0.0f;
89
+ config.bezier[2] = 0.58f;
90
+ config.bezier[3] = 1.0f;
91
+ }
92
+ config.damping = src.damping;
93
+ config.stiffness = src.stiffness;
94
+ config.mass = src.mass;
95
+ config.loop = src.loop;
96
+ config.delay = src.delay;
97
+ return config;
98
+ }
99
+
100
+ // Check if a category config was explicitly set (non-empty type means JS sent
101
+ // it)
102
+ template <typename T> static bool hasConfig(const T &cfg) {
103
+ return !cfg.type.empty();
104
+ }
105
+
106
+ static EaseTransitionConfig
107
+ transitionConfigForProperty(const std::string &name,
108
+ const EaseViewProps &props) {
109
+ const auto &t = props.transitions;
110
+
111
+ // Map property name to category, check if category override exists
112
+ if (name == "opacity" && hasConfig(t.opacity)) {
113
+ return transitionConfigFromStruct(t.opacity);
114
+ } else if ((name == "translateX" || name == "translateY" ||
115
+ name == "scaleX" || name == "scaleY" || name == "rotate" ||
116
+ name == "rotateX" || name == "rotateY") &&
117
+ hasConfig(t.transform)) {
118
+ return transitionConfigFromStruct(t.transform);
119
+ } else if (name == "borderRadius" && hasConfig(t.borderRadius)) {
120
+ return transitionConfigFromStruct(t.borderRadius);
121
+ } else if (name == "backgroundColor" && hasConfig(t.backgroundColor)) {
122
+ return transitionConfigFromStruct(t.backgroundColor);
123
+ }
124
+ // Fallback to defaultConfig
125
+ return transitionConfigFromStruct(t.defaultConfig);
126
+ }
127
+
128
+ // Find lowest property name with a set mask bit among transform properties
129
+ static std::string lowestTransformPropertyName(int mask) {
130
+ if (mask & kMaskTranslateX)
131
+ return "translateX";
132
+ if (mask & kMaskTranslateY)
133
+ return "translateY";
134
+ if (mask & kMaskScaleX)
135
+ return "scaleX";
136
+ if (mask & kMaskScaleY)
137
+ return "scaleY";
138
+ if (mask & kMaskRotate)
139
+ return "rotate";
140
+ if (mask & kMaskRotateX)
141
+ return "rotateX";
142
+ if (mask & kMaskRotateY)
143
+ return "rotateY";
144
+ return "translateX"; // fallback
145
+ }
146
+
61
147
  @implementation EaseView {
62
148
  BOOL _isFirstMount;
63
149
  NSInteger _animationBatchId;
@@ -139,16 +225,16 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
139
225
  - (CAAnimation *)createAnimationForKeyPath:(NSString *)keyPath
140
226
  fromValue:(NSValue *)fromValue
141
227
  toValue:(NSValue *)toValue
142
- props:(const EaseViewProps &)props
228
+ config:(EaseTransitionConfig)config
143
229
  loop:(BOOL)loop {
144
- if (props.transitionType == EaseViewTransitionType::Spring) {
230
+ if (config.type == "spring") {
145
231
  CASpringAnimation *spring =
146
232
  [CASpringAnimation animationWithKeyPath:keyPath];
147
233
  spring.fromValue = fromValue;
148
234
  spring.toValue = toValue;
149
- spring.damping = props.transitionDamping;
150
- spring.stiffness = props.transitionStiffness;
151
- spring.mass = props.transitionMass;
235
+ spring.damping = config.damping;
236
+ spring.stiffness = config.stiffness;
237
+ spring.mass = config.mass;
152
238
  spring.initialVelocity = 0;
153
239
  spring.duration = spring.settlingDuration;
154
240
  return spring;
@@ -156,23 +242,14 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
156
242
  CABasicAnimation *timing = [CABasicAnimation animationWithKeyPath:keyPath];
157
243
  timing.fromValue = fromValue;
158
244
  timing.toValue = toValue;
159
- timing.duration = props.transitionDuration / 1000.0;
160
- {
161
- const auto &b = props.transitionEasingBezier;
162
- if (b.size() == 4) {
163
- timing.timingFunction = [CAMediaTimingFunction
164
- functionWithControlPoints:(float)b[0]:(float)b[1]:(float)b[2
165
- ]:(float)b[3]];
166
- } else {
167
- // Fallback: easeInOut
168
- timing.timingFunction =
169
- [CAMediaTimingFunction functionWithControlPoints:0.42:0.0:0.58:1.0];
170
- }
171
- }
245
+ timing.duration = config.duration / 1000.0;
246
+ timing.timingFunction = [CAMediaTimingFunction
247
+ functionWithControlPoints:config.bezier[0]:config.bezier[1
248
+ ]:config.bezier[2]:config.bezier[3]];
172
249
  if (loop) {
173
- if (props.transitionLoop == EaseViewTransitionLoop::Repeat) {
250
+ if (config.loop == "repeat") {
174
251
  timing.repeatCount = HUGE_VALF;
175
- } else if (props.transitionLoop == EaseViewTransitionLoop::Reverse) {
252
+ } else if (config.loop == "reverse") {
176
253
  timing.repeatCount = HUGE_VALF;
177
254
  timing.autoreverses = YES;
178
255
  }
@@ -185,18 +262,17 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
185
262
  animationKey:(NSString *)animationKey
186
263
  fromValue:(NSValue *)fromValue
187
264
  toValue:(NSValue *)toValue
188
- props:(const EaseViewProps &)props
265
+ config:(EaseTransitionConfig)config
189
266
  loop:(BOOL)loop {
190
267
  _pendingAnimationCount++;
191
268
 
192
269
  CAAnimation *animation = [self createAnimationForKeyPath:keyPath
193
270
  fromValue:fromValue
194
271
  toValue:toValue
195
- props:props
272
+ config:config
196
273
  loop:loop];
197
- if (props.transitionDelay > 0) {
198
- animation.beginTime =
199
- CACurrentMediaTime() + (props.transitionDelay / 1000.0);
274
+ if (config.delay > 0) {
275
+ animation.beginTime = CACurrentMediaTime() + (config.delay / 1000.0);
200
276
  animation.fillMode = kCAFillModeBackwards;
201
277
  }
202
278
  [animation setValue:@(_animationBatchId) forKey:@"easeBatchId"];
@@ -229,6 +305,10 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
229
305
  const auto &newViewProps =
230
306
  *std::static_pointer_cast<const EaseViewProps>(props);
231
307
 
308
+ // oldProps can be null. Fall back to props so the diff is a no-op.
309
+ const auto &oldViewProps = *std::static_pointer_cast<const EaseViewProps>(
310
+ oldProps ? oldProps : props);
311
+
232
312
  [super updateProps:props oldProps:oldProps];
233
313
 
234
314
  [CATransaction begin];
@@ -303,40 +383,77 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
303
383
  newViewProps.initialAnimateBackgroundColor)
304
384
  .CGColor;
305
385
 
306
- // Animate from initial to target
386
+ // Animate from initial to target (skip if config is 'none')
307
387
  if (hasInitialOpacity) {
388
+ EaseTransitionConfig opacityConfig =
389
+ transitionConfigForProperty("opacity", newViewProps);
308
390
  self.layer.opacity = newViewProps.animateOpacity;
309
- [self applyAnimationForKeyPath:@"opacity"
310
- animationKey:kAnimKeyOpacity
311
- fromValue:@(newViewProps.initialAnimateOpacity)
312
- toValue:@(newViewProps.animateOpacity)
313
- props:newViewProps
314
- loop:YES];
391
+ if (opacityConfig.type != "none") {
392
+ [self applyAnimationForKeyPath:@"opacity"
393
+ animationKey:kAnimKeyOpacity
394
+ fromValue:@(newViewProps.initialAnimateOpacity)
395
+ toValue:@(newViewProps.animateOpacity)
396
+ config:opacityConfig
397
+ loop:YES];
398
+ }
315
399
  }
316
400
  if (hasInitialTransform) {
401
+ // Build mask of which transform sub-properties actually changed
402
+ int changedInitTransform = 0;
403
+ if (newViewProps.initialAnimateTranslateX !=
404
+ newViewProps.animateTranslateX)
405
+ changedInitTransform |= kMaskTranslateX;
406
+ if (newViewProps.initialAnimateTranslateY !=
407
+ newViewProps.animateTranslateY)
408
+ changedInitTransform |= kMaskTranslateY;
409
+ if (newViewProps.initialAnimateScaleX != newViewProps.animateScaleX)
410
+ changedInitTransform |= kMaskScaleX;
411
+ if (newViewProps.initialAnimateScaleY != newViewProps.animateScaleY)
412
+ changedInitTransform |= kMaskScaleY;
413
+ if (newViewProps.initialAnimateRotate != newViewProps.animateRotate)
414
+ changedInitTransform |= kMaskRotate;
415
+ if (newViewProps.initialAnimateRotateX != newViewProps.animateRotateX)
416
+ changedInitTransform |= kMaskRotateX;
417
+ if (newViewProps.initialAnimateRotateY != newViewProps.animateRotateY)
418
+ changedInitTransform |= kMaskRotateY;
419
+ std::string transformName =
420
+ lowestTransformPropertyName(changedInitTransform);
421
+ EaseTransitionConfig transformConfig =
422
+ transitionConfigForProperty(transformName, newViewProps);
317
423
  self.layer.transform = targetT;
318
- [self applyAnimationForKeyPath:@"transform"
424
+ if (transformConfig.type != "none") {
425
+ [self
426
+ applyAnimationForKeyPath:@"transform"
319
427
  animationKey:kAnimKeyTransform
320
428
  fromValue:[NSValue valueWithCATransform3D:initialT]
321
429
  toValue:[NSValue valueWithCATransform3D:targetT]
322
- props:newViewProps
430
+ config:transformConfig
323
431
  loop:YES];
432
+ }
324
433
  }
325
434
  if (hasInitialBorderRadius) {
435
+ EaseTransitionConfig brConfig =
436
+ transitionConfigForProperty("borderRadius", newViewProps);
326
437
  self.layer.cornerRadius = newViewProps.animateBorderRadius;
327
- [self
328
- applyAnimationForKeyPath:@"cornerRadius"
329
- animationKey:kAnimKeyCornerRadius
330
- fromValue:@(newViewProps.initialAnimateBorderRadius)
331
- toValue:@(newViewProps.animateBorderRadius)
332
- props:newViewProps
333
- loop:YES];
438
+ if (brConfig.type != "none") {
439
+ [self applyAnimationForKeyPath:@"cornerRadius"
440
+ animationKey:kAnimKeyCornerRadius
441
+ fromValue:@(newViewProps
442
+ .initialAnimateBorderRadius)
443
+ toValue:@(newViewProps.animateBorderRadius)
444
+ config:brConfig
445
+ loop:YES];
446
+ }
334
447
  }
335
448
  if (hasInitialBackgroundColor) {
449
+ EaseTransitionConfig bgConfig =
450
+ transitionConfigForProperty("backgroundColor", newViewProps);
336
451
  self.layer.backgroundColor =
337
452
  RCTUIColorFromSharedColor(newViewProps.animateBackgroundColor)
338
453
  .CGColor;
339
- [self applyAnimationForKeyPath:@"backgroundColor"
454
+ if (bgConfig.type != "none") {
455
+ [self
456
+ applyAnimationForKeyPath:@"backgroundColor"
340
457
  animationKey:kAnimKeyBackgroundColor
341
458
  fromValue:(__bridge id)RCTUIColorFromSharedColor(
342
459
  newViewProps
@@ -345,8 +462,19 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
345
462
  toValue:(__bridge id)RCTUIColorFromSharedColor(
346
463
  newViewProps.animateBackgroundColor)
347
464
  .CGColor
348
- props:newViewProps
465
+ config:bgConfig
349
466
  loop:YES];
467
+ }
468
+ }
469
+
470
+ // If all per-property configs were 'none', no animations were queued.
471
+ // Fire onTransitionEnd immediately to match the scalar 'none' contract.
472
+ if (_pendingAnimationCount == 0 && _eventEmitter) {
473
+ auto emitter =
474
+ std::static_pointer_cast<const EaseViewEventEmitter>(_eventEmitter);
475
+ emitter->onTransitionEnd(EaseViewEventEmitter::OnTransitionEnd{
476
+ .finished = true,
477
+ });
350
478
  }
351
479
  } else {
352
480
  // No initial animation — set target values directly
@@ -363,8 +491,16 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
363
491
  RCTUIColorFromSharedColor(newViewProps.animateBackgroundColor)
364
492
  .CGColor;
365
493
  }
366
- } else if (newViewProps.transitionType == EaseViewTransitionType::None) {
367
- // No transition — set values immediately
494
+ } else if (newViewProps.transitions.defaultConfig.type == "none" &&
495
+ (!hasConfig(newViewProps.transitions.transform) ||
496
+ newViewProps.transitions.transform.type == "none") &&
497
+ (!hasConfig(newViewProps.transitions.opacity) ||
498
+ newViewProps.transitions.opacity.type == "none") &&
499
+ (!hasConfig(newViewProps.transitions.borderRadius) ||
500
+ newViewProps.transitions.borderRadius.type == "none") &&
501
+ (!hasConfig(newViewProps.transitions.backgroundColor) ||
502
+ newViewProps.transitions.backgroundColor.type == "none")) {
503
+ // All transitions are 'none' — set values immediately
368
504
  [self.layer removeAllAnimations];
369
505
  if (mask & kMaskOpacity)
370
506
  self.layer.opacity = newViewProps.animateOpacity;
@@ -387,19 +523,27 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
387
523
  }
388
524
  } else {
389
525
  // Subsequent updates: animate changed properties
390
- const auto &oldViewProps =
391
- *std::static_pointer_cast<const EaseViewProps>(oldProps);
526
+ BOOL anyPropertyChanged = NO;
392
527
 
393
528
  if ((mask & kMaskOpacity) &&
394
529
  oldViewProps.animateOpacity != newViewProps.animateOpacity) {
395
- self.layer.opacity = newViewProps.animateOpacity;
396
- [self
397
- applyAnimationForKeyPath:@"opacity"
398
- animationKey:kAnimKeyOpacity
399
- fromValue:[self presentationValueForKeyPath:@"opacity"]
400
- toValue:@(newViewProps.animateOpacity)
401
- props:newViewProps
402
- loop:NO];
530
+ anyPropertyChanged = YES;
531
+ EaseTransitionConfig opacityConfig =
532
+ transitionConfigForProperty("opacity", newViewProps);
533
+ if (opacityConfig.type == "none") {
534
+ self.layer.opacity = newViewProps.animateOpacity;
535
+ [self.layer removeAnimationForKey:kAnimKeyOpacity];
536
+ } else {
537
+ self.layer.opacity = newViewProps.animateOpacity;
538
+ [self
539
+ applyAnimationForKeyPath:@"opacity"
540
+ animationKey:kAnimKeyOpacity
541
+ fromValue:[self
542
+ presentationValueForKeyPath:@"opacity"]
543
+ toValue:@(newViewProps.animateOpacity)
544
+ config:opacityConfig
545
+ loop:NO];
546
+ }
403
547
  }
404
548
 
405
549
  // Check if ANY transform-related property changed
@@ -414,46 +558,98 @@ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
414
558
  oldViewProps.animateRotateY != newViewProps.animateRotateY;
415
559
 
416
560
  if (anyTransformChanged) {
417
- CATransform3D fromT = [self presentationTransform];
418
- CATransform3D toT = [self targetTransformFromProps:newViewProps];
419
- self.layer.transform = toT;
420
- [self applyAnimationForKeyPath:@"transform"
421
- animationKey:kAnimKeyTransform
422
- fromValue:[NSValue valueWithCATransform3D:fromT]
423
- toValue:[NSValue valueWithCATransform3D:toT]
424
- props:newViewProps
425
- loop:NO];
561
+ anyPropertyChanged = YES;
562
+ // Determine which transform sub-properties changed for config selection
563
+ int changedTransformMask = 0;
564
+ if (oldViewProps.animateTranslateX != newViewProps.animateTranslateX)
565
+ changedTransformMask |= kMaskTranslateX;
566
+ if (oldViewProps.animateTranslateY != newViewProps.animateTranslateY)
567
+ changedTransformMask |= kMaskTranslateY;
568
+ if (oldViewProps.animateScaleX != newViewProps.animateScaleX)
569
+ changedTransformMask |= kMaskScaleX;
570
+ if (oldViewProps.animateScaleY != newViewProps.animateScaleY)
571
+ changedTransformMask |= kMaskScaleY;
572
+ if (oldViewProps.animateRotate != newViewProps.animateRotate)
573
+ changedTransformMask |= kMaskRotate;
574
+ if (oldViewProps.animateRotateX != newViewProps.animateRotateX)
575
+ changedTransformMask |= kMaskRotateX;
576
+ if (oldViewProps.animateRotateY != newViewProps.animateRotateY)
577
+ changedTransformMask |= kMaskRotateY;
578
+
579
+ std::string transformName =
580
+ lowestTransformPropertyName(changedTransformMask);
581
+ EaseTransitionConfig transformConfig =
582
+ transitionConfigForProperty(transformName, newViewProps);
583
+
584
+ if (transformConfig.type == "none") {
585
+ self.layer.transform = [self targetTransformFromProps:newViewProps];
586
+ [self.layer removeAnimationForKey:kAnimKeyTransform];
587
+ } else {
588
+ CATransform3D fromT = [self presentationTransform];
589
+ CATransform3D toT = [self targetTransformFromProps:newViewProps];
590
+ self.layer.transform = toT;
591
+ [self applyAnimationForKeyPath:@"transform"
592
+ animationKey:kAnimKeyTransform
593
+ fromValue:[NSValue valueWithCATransform3D:fromT]
594
+ toValue:[NSValue valueWithCATransform3D:toT]
595
+ config:transformConfig
596
+ loop:NO];
597
+ }
426
598
  }
427
599
  }
428
600
 
429
601
  if ((mask & kMaskBorderRadius) &&
430
602
  oldViewProps.animateBorderRadius != newViewProps.animateBorderRadius) {
603
+ anyPropertyChanged = YES;
604
+ EaseTransitionConfig brConfig =
605
+ transitionConfigForProperty("borderRadius", newViewProps);
431
606
  self.layer.cornerRadius = newViewProps.animateBorderRadius;
432
607
  self.layer.masksToBounds = newViewProps.animateBorderRadius > 0;
433
- [self applyAnimationForKeyPath:@"cornerRadius"
434
- animationKey:kAnimKeyCornerRadius
435
- fromValue:[self presentationValueForKeyPath:
436
- @"cornerRadius"]
437
- toValue:@(newViewProps.animateBorderRadius)
438
- props:newViewProps
439
- loop:NO];
608
+ if (brConfig.type == "none") {
609
+ [self.layer removeAnimationForKey:kAnimKeyCornerRadius];
610
+ } else {
611
+ [self applyAnimationForKeyPath:@"cornerRadius"
612
+ animationKey:kAnimKeyCornerRadius
613
+ fromValue:[self presentationValueForKeyPath:
614
+ @"cornerRadius"]
615
+ toValue:@(newViewProps.animateBorderRadius)
616
+ config:brConfig
617
+ loop:NO];
618
+ }
440
619
  }
441
620
 
442
621
  if ((mask & kMaskBackgroundColor) &&
443
622
  oldViewProps.animateBackgroundColor !=
444
623
  newViewProps.animateBackgroundColor) {
445
- CGColorRef fromColor = (__bridge CGColorRef)
446
- [self presentationValueForKeyPath:@"backgroundColor"];
624
+ anyPropertyChanged = YES;
625
+ EaseTransitionConfig bgConfig =
626
+ transitionConfigForProperty("backgroundColor", newViewProps);
447
627
  CGColorRef toColor =
448
628
  RCTUIColorFromSharedColor(newViewProps.animateBackgroundColor)
449
629
  .CGColor;
450
630
  self.layer.backgroundColor = toColor;
451
- [self applyAnimationForKeyPath:@"backgroundColor"
452
- animationKey:kAnimKeyBackgroundColor
453
- fromValue:(__bridge id)fromColor
454
- toValue:(__bridge id)toColor
455
- props:newViewProps
456
- loop:NO];
631
+ if (bgConfig.type == "none") {
632
+ [self.layer removeAnimationForKey:kAnimKeyBackgroundColor];
633
+ } else {
634
+ CGColorRef fromColor = (__bridge CGColorRef)
635
+ [self presentationValueForKeyPath:@"backgroundColor"];
636
+ [self applyAnimationForKeyPath:@"backgroundColor"
637
+ animationKey:kAnimKeyBackgroundColor
638
+ fromValue:(__bridge id)fromColor
639
+ toValue:(__bridge id)toColor
640
+ config:bgConfig
641
+ loop:NO];
642
+ }
643
+ }
644
+
645
+ // If all changed properties resolved to 'none', no animations were queued.
646
+ // Fire onTransitionEnd immediately.
647
+ if (anyPropertyChanged && _pendingAnimationCount == 0 && _eventEmitter) {
648
+ auto emitter =
649
+ std::static_pointer_cast<const EaseViewEventEmitter>(_eventEmitter);
650
+ emitter->onTransitionEnd(EaseViewEventEmitter::OnTransitionEnd{
651
+ .finished = true,
652
+ });
457
653
  }
458
654
  }
459
655
 
@@ -52,6 +52,88 @@ const EASING_PRESETS = {
52
52
  easeOut: [0, 0, 0.58, 1],
53
53
  easeInOut: [0.42, 0, 0.58, 1]
54
54
  };
55
+
56
+ /** Returns true if the transition is a SingleTransition (has a `type` field). */
57
+ function isSingleTransition(t) {
58
+ return 'type' in t;
59
+ }
60
+ /** Default config: timing 300ms easeInOut. */
61
+ const DEFAULT_CONFIG = {
62
+ type: 'timing',
63
+ duration: 300,
64
+ easingBezier: [0.42, 0, 0.58, 1],
65
+ damping: 15,
66
+ stiffness: 120,
67
+ mass: 1,
68
+ loop: 'none',
69
+ delay: 0
70
+ };
71
+
72
+ /** Resolve a SingleTransition into a native config object. */
73
+ function resolveSingleConfig(config) {
74
+ const type = config.type;
75
+ const duration = config.type === 'timing' ? config.duration ?? 300 : 300;
76
+ const rawEasing = config.type === 'timing' ? config.easing ?? 'easeInOut' : 'easeInOut';
77
+ if (__DEV__) {
78
+ if (Array.isArray(rawEasing)) {
79
+ if (rawEasing.length !== 4) {
80
+ console.warn('react-native-ease: Custom easing must be a [x1, y1, x2, y2] tuple (got length ' + rawEasing.length + ').');
81
+ }
82
+ if (rawEasing[0] < 0 || rawEasing[0] > 1 || rawEasing[2] < 0 || rawEasing[2] > 1) {
83
+ console.warn('react-native-ease: Easing x-values (x1, x2) must be between 0 and 1.');
84
+ }
85
+ }
86
+ }
87
+ const easingBezier = Array.isArray(rawEasing) ? rawEasing : EASING_PRESETS[rawEasing];
88
+ const damping = config.type === 'spring' ? config.damping ?? 15 : 15;
89
+ const stiffness = config.type === 'spring' ? config.stiffness ?? 120 : 120;
90
+ const mass = config.type === 'spring' ? config.mass ?? 1 : 1;
91
+ const loop = config.type === 'timing' ? config.loop ?? 'none' : 'none';
92
+ const delay = config.type === 'timing' || config.type === 'spring' ? config.delay ?? 0 : 0;
93
+ return {
94
+ type,
95
+ duration,
96
+ easingBezier,
97
+ damping,
98
+ stiffness,
99
+ mass,
100
+ loop,
101
+ delay
102
+ };
103
+ }
104
+
105
+ /** Category keys that map to optional NativeTransitions fields. */
106
+ const CATEGORY_KEYS = ['transform', 'opacity', 'borderRadius', 'backgroundColor'];
107
+
108
+ /** Resolve the transition prop into a NativeTransitions struct. */
109
+ function resolveTransitions(transition) {
110
+ // No transition: timing default for all properties
111
+ if (transition == null) {
112
+ return {
113
+ defaultConfig: DEFAULT_CONFIG
114
+ };
115
+ }
116
+
117
+ // Single transition: set as defaultConfig only
118
+ if (isSingleTransition(transition)) {
119
+ return {
120
+ defaultConfig: resolveSingleConfig(transition)
121
+ };
122
+ }
123
+
124
+ // TransitionMap: resolve defaultConfig + only specified category keys
125
+ const defaultConfig = transition.default ? resolveSingleConfig(transition.default) : DEFAULT_CONFIG;
126
+ const result = {
127
+ defaultConfig
128
+ };
129
+ for (const key of CATEGORY_KEYS) {
130
+ const specific = transition[key];
131
+ if (specific != null) {
132
+ result[key] = resolveSingleConfig(specific);
133
+ }
134
+ }
135
+ return result;
136
+ }
55
137
  export function EaseView({
56
138
  animate,
57
139
  initialAnimate,
@@ -135,26 +217,8 @@ export function EaseView({
135
217
  }
136
218
  }
137
219
 
138
- // Resolve transition config
139
- const transitionType = transition?.type ?? 'timing';
140
- const transitionDuration = transition?.type === 'timing' ? transition.duration ?? 300 : 300;
141
- const rawEasing = transition?.type === 'timing' ? transition.easing ?? 'easeInOut' : 'easeInOut';
142
- if (__DEV__) {
143
- if (Array.isArray(rawEasing)) {
144
- if (rawEasing.length !== 4) {
145
- console.warn('react-native-ease: Custom easing must be a [x1, y1, x2, y2] tuple (got length ' + rawEasing.length + ').');
146
- }
147
- if (rawEasing[0] < 0 || rawEasing[0] > 1 || rawEasing[2] < 0 || rawEasing[2] > 1) {
148
- console.warn('react-native-ease: Easing x-values (x1, x2) must be between 0 and 1.');
149
- }
150
- }
151
- }
152
- const bezier = Array.isArray(rawEasing) ? rawEasing : EASING_PRESETS[rawEasing];
153
- const transitionDamping = transition?.type === 'spring' ? transition.damping ?? 15 : 15;
154
- const transitionStiffness = transition?.type === 'spring' ? transition.stiffness ?? 120 : 120;
155
- const transitionMass = transition?.type === 'spring' ? transition.mass ?? 1 : 1;
156
- const transitionLoop = transition?.type === 'timing' ? transition.loop ?? 'none' : 'none';
157
- const transitionDelay = transition?.type === 'timing' || transition?.type === 'spring' ? transition.delay ?? 0 : 0;
220
+ // Resolve transition config into a fully-populated struct
221
+ const transitions = resolveTransitions(transition);
158
222
  const handleTransitionEnd = onTransitionEnd ? event => onTransitionEnd(event.nativeEvent) : undefined;
159
223
  return /*#__PURE__*/_jsx(NativeEaseView, {
160
224
  style: cleanStyle,
@@ -180,14 +244,7 @@ export function EaseView({
180
244
  initialAnimateRotateY: resolvedInitial.rotateY,
181
245
  initialAnimateBorderRadius: resolvedInitial.borderRadius,
182
246
  initialAnimateBackgroundColor: initialBgColor,
183
- transitionType: transitionType,
184
- transitionDuration: transitionDuration,
185
- transitionEasingBezier: bezier,
186
- transitionDamping: transitionDamping,
187
- transitionStiffness: transitionStiffness,
188
- transitionMass: transitionMass,
189
- transitionLoop: transitionLoop,
190
- transitionDelay: transitionDelay,
247
+ transitions: transitions,
191
248
  useHardwareLayer: useHardwareLayer,
192
249
  transformOriginX: transformOrigin?.x ?? 0.5,
193
250
  transformOriginY: transformOrigin?.y ?? 0.5,