react-native-ease 0.1.0-alpha.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.
@@ -0,0 +1,233 @@
1
+ package com.ease
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReadableArray
5
+ import com.facebook.react.bridge.WritableMap
6
+ import com.facebook.react.module.annotations.ReactModule
7
+ import com.facebook.react.uimanager.PixelUtil
8
+ import com.facebook.react.uimanager.ThemedReactContext
9
+ import com.facebook.react.uimanager.UIManagerHelper
10
+ import com.facebook.react.uimanager.annotations.ReactProp
11
+ import com.facebook.react.uimanager.events.Event
12
+ import com.facebook.react.views.view.ReactViewGroup
13
+ import com.facebook.react.views.view.ReactViewManager
14
+
15
+ @ReactModule(name = EaseViewManager.NAME)
16
+ class EaseViewManager : ReactViewManager() {
17
+
18
+ override fun getName(): String = NAME
19
+
20
+ override fun createViewInstance(context: ThemedReactContext): EaseView {
21
+ val view = EaseView(context)
22
+ view.onTransitionEnd = { finished ->
23
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
24
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
25
+ eventDispatcher?.dispatchEvent(
26
+ TransitionEndEvent(surfaceId, view.id, finished)
27
+ )
28
+ }
29
+ return view
30
+ }
31
+
32
+ // --- Animated properties bitmask ---
33
+
34
+ @ReactProp(name = "animatedProperties", defaultInt = 0)
35
+ fun setAnimatedProperties(view: EaseView, value: Int) {
36
+ view.animatedProperties = value
37
+ }
38
+
39
+ // --- Animate value setters ---
40
+
41
+ @ReactProp(name = "animateOpacity", defaultFloat = 1f)
42
+ fun setAnimateOpacity(view: EaseView, value: Float) {
43
+ view.pendingOpacity = value
44
+ }
45
+
46
+ @ReactProp(name = "animateTranslateX", defaultFloat = 0f)
47
+ fun setAnimateTranslateX(view: EaseView, value: Float) {
48
+ view.pendingTranslateX = PixelUtil.toPixelFromDIP(value)
49
+ }
50
+
51
+ @ReactProp(name = "animateTranslateY", defaultFloat = 0f)
52
+ fun setAnimateTranslateY(view: EaseView, value: Float) {
53
+ view.pendingTranslateY = PixelUtil.toPixelFromDIP(value)
54
+ }
55
+
56
+ @ReactProp(name = "animateScaleX", defaultFloat = 1f)
57
+ fun setAnimateScaleX(view: EaseView, value: Float) {
58
+ view.pendingScaleX = value
59
+ }
60
+
61
+ @ReactProp(name = "animateScaleY", defaultFloat = 1f)
62
+ fun setAnimateScaleY(view: EaseView, value: Float) {
63
+ view.pendingScaleY = value
64
+ }
65
+
66
+ @ReactProp(name = "animateRotate", defaultFloat = 0f)
67
+ fun setAnimateRotate(view: EaseView, value: Float) {
68
+ view.pendingRotate = value
69
+ }
70
+
71
+ @ReactProp(name = "animateRotateX", defaultFloat = 0f)
72
+ fun setAnimateRotateX(view: EaseView, value: Float) {
73
+ view.pendingRotateX = value
74
+ }
75
+
76
+ @ReactProp(name = "animateRotateY", defaultFloat = 0f)
77
+ fun setAnimateRotateY(view: EaseView, value: Float) {
78
+ view.pendingRotateY = value
79
+ }
80
+
81
+ // --- Initial animate value setters ---
82
+
83
+ @ReactProp(name = "initialAnimateOpacity", defaultFloat = 1f)
84
+ fun setInitialAnimateOpacity(view: EaseView, value: Float) {
85
+ view.initialAnimateOpacity = value
86
+ }
87
+
88
+ @ReactProp(name = "initialAnimateTranslateX", defaultFloat = 0f)
89
+ fun setInitialAnimateTranslateX(view: EaseView, value: Float) {
90
+ view.initialAnimateTranslateX = PixelUtil.toPixelFromDIP(value)
91
+ }
92
+
93
+ @ReactProp(name = "initialAnimateTranslateY", defaultFloat = 0f)
94
+ fun setInitialAnimateTranslateY(view: EaseView, value: Float) {
95
+ view.initialAnimateTranslateY = PixelUtil.toPixelFromDIP(value)
96
+ }
97
+
98
+ @ReactProp(name = "initialAnimateScaleX", defaultFloat = 1f)
99
+ fun setInitialAnimateScaleX(view: EaseView, value: Float) {
100
+ view.initialAnimateScaleX = value
101
+ }
102
+
103
+ @ReactProp(name = "initialAnimateScaleY", defaultFloat = 1f)
104
+ fun setInitialAnimateScaleY(view: EaseView, value: Float) {
105
+ view.initialAnimateScaleY = value
106
+ }
107
+
108
+ @ReactProp(name = "initialAnimateRotate", defaultFloat = 0f)
109
+ fun setInitialAnimateRotate(view: EaseView, value: Float) {
110
+ view.initialAnimateRotate = value
111
+ }
112
+
113
+ @ReactProp(name = "initialAnimateRotateX", defaultFloat = 0f)
114
+ fun setInitialAnimateRotateX(view: EaseView, value: Float) {
115
+ view.initialAnimateRotateX = value
116
+ }
117
+
118
+ @ReactProp(name = "initialAnimateRotateY", defaultFloat = 0f)
119
+ fun setInitialAnimateRotateY(view: EaseView, value: Float) {
120
+ view.initialAnimateRotateY = value
121
+ }
122
+
123
+ @ReactProp(name = "initialAnimateBorderRadius", defaultFloat = 0f)
124
+ fun setInitialAnimateBorderRadius(view: EaseView, value: Float) {
125
+ view.initialAnimateBorderRadius = PixelUtil.toPixelFromDIP(value)
126
+ }
127
+
128
+ // --- Transition config setters ---
129
+
130
+ @ReactProp(name = "transitionType")
131
+ fun setTransitionType(view: EaseView, value: String?) {
132
+ view.transitionType = value ?: "timing"
133
+ }
134
+
135
+ @ReactProp(name = "transitionDuration", defaultInt = 300)
136
+ fun setTransitionDuration(view: EaseView, value: Int) {
137
+ view.transitionDuration = value
138
+ }
139
+
140
+ @ReactProp(name = "transitionEasingBezier")
141
+ fun setTransitionEasingBezier(view: EaseView, value: ReadableArray?) {
142
+ if (value != null && value.size() == 4) {
143
+ view.transitionEasingBezier = floatArrayOf(
144
+ value.getDouble(0).toFloat(),
145
+ value.getDouble(1).toFloat(),
146
+ value.getDouble(2).toFloat(),
147
+ value.getDouble(3).toFloat()
148
+ )
149
+ } else {
150
+ // Fallback: easeInOut
151
+ view.transitionEasingBezier = floatArrayOf(0.42f, 0f, 0.58f, 1.0f)
152
+ }
153
+ }
154
+
155
+ @ReactProp(name = "transitionDamping", defaultFloat = 15f)
156
+ fun setTransitionDamping(view: EaseView, value: Float) {
157
+ view.transitionDamping = value
158
+ }
159
+
160
+ @ReactProp(name = "transitionStiffness", defaultFloat = 120f)
161
+ fun setTransitionStiffness(view: EaseView, value: Float) {
162
+ view.transitionStiffness = value
163
+ }
164
+
165
+ @ReactProp(name = "transitionMass", defaultFloat = 1f)
166
+ fun setTransitionMass(view: EaseView, value: Float) {
167
+ view.transitionMass = value
168
+ }
169
+
170
+ @ReactProp(name = "transitionLoop")
171
+ fun setTransitionLoop(view: EaseView, value: String?) {
172
+ view.transitionLoop = value ?: "none"
173
+ }
174
+
175
+ // --- Border radius ---
176
+
177
+ @ReactProp(name = "animateBorderRadius", defaultFloat = 0f)
178
+ fun setAnimateBorderRadius(view: EaseView, value: Float) {
179
+ view.pendingBorderRadius = PixelUtil.toPixelFromDIP(value)
180
+ }
181
+
182
+ // --- Hardware layer ---
183
+
184
+ @ReactProp(name = "useHardwareLayer", defaultBoolean = false)
185
+ fun setUseHardwareLayer(view: EaseView, value: Boolean) {
186
+ view.useHardwareLayer = value
187
+ }
188
+
189
+ // --- Transform origin ---
190
+
191
+ @ReactProp(name = "transformOriginX", defaultFloat = 0.5f)
192
+ fun setTransformOriginX(view: EaseView, value: Float) {
193
+ view.transformOriginX = value
194
+ }
195
+
196
+ @ReactProp(name = "transformOriginY", defaultFloat = 0.5f)
197
+ fun setTransformOriginY(view: EaseView, value: Float) {
198
+ view.transformOriginY = value
199
+ }
200
+
201
+ // --- Lifecycle ---
202
+
203
+ override fun onAfterUpdateTransaction(view: ReactViewGroup) {
204
+ super.onAfterUpdateTransaction(view)
205
+ (view as? EaseView)?.applyPendingAnimateValues()
206
+ }
207
+
208
+ override fun onDropViewInstance(view: ReactViewGroup) {
209
+ super.onDropViewInstance(view)
210
+ (view as? EaseView)?.cleanup()
211
+ }
212
+
213
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
214
+ return mapOf(
215
+ "onTransitionEnd" to mapOf("registrationName" to "onTransitionEnd")
216
+ )
217
+ }
218
+
219
+ private class TransitionEndEvent(
220
+ surfaceId: Int,
221
+ viewId: Int,
222
+ private val finished: Boolean
223
+ ) : Event<TransitionEndEvent>(surfaceId, viewId) {
224
+ override fun getEventName() = "onTransitionEnd"
225
+ override fun getEventData(): WritableMap = Arguments.createMap().apply {
226
+ putBoolean("finished", finished)
227
+ }
228
+ }
229
+
230
+ companion object {
231
+ const val NAME = "EaseView"
232
+ }
233
+ }
package/ios/EaseView.h ADDED
@@ -0,0 +1,14 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ #ifndef EaseViewNativeComponent_h
5
+ #define EaseViewNativeComponent_h
6
+
7
+ NS_ASSUME_NONNULL_BEGIN
8
+
9
+ @interface EaseView : RCTViewComponentView <CAAnimationDelegate>
10
+ @end
11
+
12
+ NS_ASSUME_NONNULL_END
13
+
14
+ #endif /* EaseViewNativeComponent_h */
@@ -0,0 +1,435 @@
1
+ #import "EaseView.h"
2
+
3
+ #import <React/RCTConversions.h>
4
+
5
+ #import <react/renderer/components/EaseViewSpec/ComponentDescriptors.h>
6
+ #import <react/renderer/components/EaseViewSpec/EventEmitters.h>
7
+ #import <react/renderer/components/EaseViewSpec/Props.h>
8
+ #import <react/renderer/components/EaseViewSpec/RCTComponentViewHelpers.h>
9
+
10
+ #import "RCTFabricComponentsPlugins.h"
11
+
12
+ using namespace facebook::react;
13
+
14
+ // Animation key constants
15
+ static NSString *const kAnimKeyOpacity = @"ease_opacity";
16
+ static NSString *const kAnimKeyTransform = @"ease_transform";
17
+ static NSString *const kAnimKeyCornerRadius = @"ease_cornerRadius";
18
+
19
+ static inline CGFloat degreesToRadians(CGFloat degrees) {
20
+ return degrees * M_PI / 180.0;
21
+ }
22
+
23
+ // Compose a full CATransform3D from individual animate values.
24
+ // Order: Scale → RotateY → RotateX → RotateZ → Translate.
25
+ // Perspective (m34) is always included — invisible when no 3D rotation.
26
+ static CATransform3D composeTransform(CGFloat scaleX, CGFloat scaleY,
27
+ CGFloat translateX, CGFloat translateY,
28
+ CGFloat rotateZ, CGFloat rotateX,
29
+ CGFloat rotateY) {
30
+ CATransform3D t = CATransform3DIdentity;
31
+ t.m34 = -1.0 / 850.0;
32
+ t = CATransform3DTranslate(t, translateX, translateY, 0);
33
+ t = CATransform3DRotate(t, rotateZ, 0, 0, 1);
34
+ t = CATransform3DRotate(t, rotateX, 1, 0, 0);
35
+ t = CATransform3DRotate(t, rotateY, 0, 1, 0);
36
+ t = CATransform3DScale(t, scaleX, scaleY, 1);
37
+ return t;
38
+ }
39
+
40
+ // Bitmask flags — must match JS constants
41
+ static const int kMaskOpacity = 1 << 0;
42
+ static const int kMaskTranslateX = 1 << 1;
43
+ static const int kMaskTranslateY = 1 << 2;
44
+ static const int kMaskScaleX = 1 << 3;
45
+ static const int kMaskScaleY = 1 << 4;
46
+ static const int kMaskRotate = 1 << 5;
47
+ static const int kMaskRotateX = 1 << 6;
48
+ static const int kMaskRotateY = 1 << 7;
49
+ static const int kMaskBorderRadius = 1 << 8;
50
+ static const int kMaskAnyTransform = kMaskTranslateX | kMaskTranslateY |
51
+ kMaskScaleX | kMaskScaleY | kMaskRotate |
52
+ kMaskRotateX | kMaskRotateY;
53
+
54
+ @implementation EaseView {
55
+ BOOL _isFirstMount;
56
+ NSInteger _animationBatchId;
57
+ NSInteger _pendingAnimationCount;
58
+ BOOL _anyInterrupted;
59
+ CGFloat _transformOriginX;
60
+ CGFloat _transformOriginY;
61
+ }
62
+
63
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
64
+ return concreteComponentDescriptorProvider<EaseViewComponentDescriptor>();
65
+ }
66
+
67
+ - (instancetype)initWithFrame:(CGRect)frame {
68
+ if (self = [super initWithFrame:frame]) {
69
+ static const auto defaultProps = std::make_shared<const EaseViewProps>();
70
+ _props = defaultProps;
71
+ _isFirstMount = YES;
72
+ _transformOriginX = 0.5;
73
+ _transformOriginY = 0.5;
74
+ }
75
+ return self;
76
+ }
77
+
78
+ #pragma mark - Transform origin
79
+
80
+ - (void)updateAnchorPoint {
81
+ CGPoint newAnchor = CGPointMake(_transformOriginX, _transformOriginY);
82
+ if (CGPointEqualToPoint(newAnchor, self.layer.anchorPoint)) {
83
+ return;
84
+ }
85
+ CGPoint oldAnchor = self.layer.anchorPoint;
86
+ CGSize size = self.layer.bounds.size;
87
+ CGPoint pos = self.layer.position;
88
+ pos.x += (newAnchor.x - oldAnchor.x) * size.width;
89
+ pos.y += (newAnchor.y - oldAnchor.y) * size.height;
90
+ self.layer.anchorPoint = newAnchor;
91
+ self.layer.position = pos;
92
+ }
93
+
94
+ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
95
+ oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
96
+ // Temporarily reset to default anchorPoint so super's frame setting
97
+ // computes position correctly, then re-apply our custom anchorPoint.
98
+ CGPoint customAnchor = self.layer.anchorPoint;
99
+ BOOL hasCustomAnchor =
100
+ !CGPointEqualToPoint(customAnchor, CGPointMake(0.5, 0.5));
101
+ if (hasCustomAnchor) {
102
+ self.layer.anchorPoint = CGPointMake(0.5, 0.5);
103
+ }
104
+
105
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
106
+
107
+ if (hasCustomAnchor) {
108
+ CGSize size = self.layer.bounds.size;
109
+ CGPoint pos = self.layer.position;
110
+ pos.x += (customAnchor.x - 0.5) * size.width;
111
+ pos.y += (customAnchor.y - 0.5) * size.height;
112
+ self.layer.anchorPoint = customAnchor;
113
+ self.layer.position = pos;
114
+ }
115
+ }
116
+
117
+ #pragma mark - Animation helpers
118
+
119
+ - (CATransform3D)presentationTransform {
120
+ CALayer *pl = self.layer.presentationLayer;
121
+ return pl ? pl.transform : self.layer.transform;
122
+ }
123
+
124
+ - (NSValue *)presentationValueForKeyPath:(NSString *)keyPath {
125
+ CALayer *presentationLayer = self.layer.presentationLayer;
126
+ if (presentationLayer) {
127
+ return [presentationLayer valueForKeyPath:keyPath];
128
+ }
129
+ return [self.layer valueForKeyPath:keyPath];
130
+ }
131
+
132
+ - (CAAnimation *)createAnimationForKeyPath:(NSString *)keyPath
133
+ fromValue:(NSValue *)fromValue
134
+ toValue:(NSValue *)toValue
135
+ props:(const EaseViewProps &)props
136
+ loop:(BOOL)loop {
137
+ if (props.transitionType == EaseViewTransitionType::Spring) {
138
+ CASpringAnimation *spring =
139
+ [CASpringAnimation animationWithKeyPath:keyPath];
140
+ spring.fromValue = fromValue;
141
+ spring.toValue = toValue;
142
+ spring.damping = props.transitionDamping;
143
+ spring.stiffness = props.transitionStiffness;
144
+ spring.mass = props.transitionMass;
145
+ spring.initialVelocity = 0;
146
+ spring.duration = spring.settlingDuration;
147
+ return spring;
148
+ } else {
149
+ CABasicAnimation *timing = [CABasicAnimation animationWithKeyPath:keyPath];
150
+ timing.fromValue = fromValue;
151
+ timing.toValue = toValue;
152
+ timing.duration = props.transitionDuration / 1000.0;
153
+ {
154
+ const auto &b = props.transitionEasingBezier;
155
+ if (b.size() == 4) {
156
+ timing.timingFunction = [CAMediaTimingFunction
157
+ functionWithControlPoints:(float)b[0]:(float)b[1]:(float)b[2
158
+ ]:(float)b[3]];
159
+ } else {
160
+ // Fallback: easeInOut
161
+ timing.timingFunction =
162
+ [CAMediaTimingFunction functionWithControlPoints:0.42:0.0:0.58:1.0];
163
+ }
164
+ }
165
+ if (loop) {
166
+ if (props.transitionLoop == EaseViewTransitionLoop::Repeat) {
167
+ timing.repeatCount = HUGE_VALF;
168
+ } else if (props.transitionLoop == EaseViewTransitionLoop::Reverse) {
169
+ timing.repeatCount = HUGE_VALF;
170
+ timing.autoreverses = YES;
171
+ }
172
+ }
173
+ return timing;
174
+ }
175
+ }
176
+
177
+ - (void)applyAnimationForKeyPath:(NSString *)keyPath
178
+ animationKey:(NSString *)animationKey
179
+ fromValue:(NSValue *)fromValue
180
+ toValue:(NSValue *)toValue
181
+ props:(const EaseViewProps &)props
182
+ loop:(BOOL)loop {
183
+ _pendingAnimationCount++;
184
+
185
+ CAAnimation *animation = [self createAnimationForKeyPath:keyPath
186
+ fromValue:fromValue
187
+ toValue:toValue
188
+ props:props
189
+ loop:loop];
190
+ [animation setValue:@(_animationBatchId) forKey:@"easeBatchId"];
191
+ animation.delegate = self;
192
+ [self.layer addAnimation:animation forKey:animationKey];
193
+ }
194
+
195
+ /// Compose a CATransform3D from EaseViewProps target values.
196
+ - (CATransform3D)targetTransformFromProps:(const EaseViewProps &)p {
197
+ return composeTransform(
198
+ p.animateScaleX, p.animateScaleY, p.animateTranslateX,
199
+ p.animateTranslateY, degreesToRadians(p.animateRotate),
200
+ degreesToRadians(p.animateRotateX), degreesToRadians(p.animateRotateY));
201
+ }
202
+
203
+ /// Compose a CATransform3D from EaseViewProps initial values.
204
+ - (CATransform3D)initialTransformFromProps:(const EaseViewProps &)p {
205
+ return composeTransform(p.initialAnimateScaleX, p.initialAnimateScaleY,
206
+ p.initialAnimateTranslateX,
207
+ p.initialAnimateTranslateY,
208
+ degreesToRadians(p.initialAnimateRotate),
209
+ degreesToRadians(p.initialAnimateRotateX),
210
+ degreesToRadians(p.initialAnimateRotateY));
211
+ }
212
+
213
+ #pragma mark - Props update
214
+
215
+ - (void)updateProps:(const Props::Shared &)props
216
+ oldProps:(const Props::Shared &)oldProps {
217
+ const auto &newViewProps =
218
+ *std::static_pointer_cast<const EaseViewProps>(props);
219
+
220
+ [CATransaction begin];
221
+ [CATransaction setDisableActions:YES];
222
+
223
+ if (_transformOriginX != newViewProps.transformOriginX ||
224
+ _transformOriginY != newViewProps.transformOriginY) {
225
+ _transformOriginX = newViewProps.transformOriginX;
226
+ _transformOriginY = newViewProps.transformOriginY;
227
+ [self updateAnchorPoint];
228
+ }
229
+
230
+ if (_pendingAnimationCount > 0 && _eventEmitter) {
231
+ auto emitter =
232
+ std::static_pointer_cast<const EaseViewEventEmitter>(_eventEmitter);
233
+ emitter->onTransitionEnd(EaseViewEventEmitter::OnTransitionEnd{
234
+ .finished = false,
235
+ });
236
+ }
237
+
238
+ _animationBatchId++;
239
+ _pendingAnimationCount = 0;
240
+ _anyInterrupted = NO;
241
+
242
+ // Bitmask: which properties are animated. Non-animated = let style handle.
243
+ int mask = newViewProps.animatedProperties;
244
+ BOOL hasTransform = (mask & kMaskAnyTransform) != 0;
245
+
246
+ if (_isFirstMount) {
247
+ _isFirstMount = NO;
248
+
249
+ // Check if initial differs from target for any masked property
250
+ BOOL hasInitialOpacity =
251
+ (mask & kMaskOpacity) &&
252
+ newViewProps.initialAnimateOpacity != newViewProps.animateOpacity;
253
+
254
+ BOOL hasInitialBorderRadius =
255
+ (mask & kMaskBorderRadius) && newViewProps.initialAnimateBorderRadius !=
256
+ newViewProps.animateBorderRadius;
257
+
258
+ BOOL hasInitialTransform = NO;
259
+ CATransform3D initialT = CATransform3DIdentity;
260
+ CATransform3D targetT = CATransform3DIdentity;
261
+
262
+ if (hasTransform) {
263
+ initialT = [self initialTransformFromProps:newViewProps];
264
+ targetT = [self targetTransformFromProps:newViewProps];
265
+ hasInitialTransform = !CATransform3DEqualToTransform(initialT, targetT);
266
+ }
267
+
268
+ if (hasInitialOpacity || hasInitialTransform || hasInitialBorderRadius) {
269
+ // Set initial values
270
+ if (mask & kMaskOpacity)
271
+ self.layer.opacity = newViewProps.initialAnimateOpacity;
272
+ if (hasTransform)
273
+ self.layer.transform = initialT;
274
+ if (mask & kMaskBorderRadius) {
275
+ self.layer.cornerRadius = newViewProps.initialAnimateBorderRadius;
276
+ self.layer.masksToBounds =
277
+ newViewProps.initialAnimateBorderRadius > 0 ||
278
+ newViewProps.animateBorderRadius > 0;
279
+ }
280
+
281
+ // Animate from initial to target
282
+ if (hasInitialOpacity) {
283
+ self.layer.opacity = newViewProps.animateOpacity;
284
+ [self applyAnimationForKeyPath:@"opacity"
285
+ animationKey:kAnimKeyOpacity
286
+ fromValue:@(newViewProps.initialAnimateOpacity)
287
+ toValue:@(newViewProps.animateOpacity)
288
+ props:newViewProps
289
+ loop:YES];
290
+ }
291
+ if (hasInitialTransform) {
292
+ self.layer.transform = targetT;
293
+ [self applyAnimationForKeyPath:@"transform"
294
+ animationKey:kAnimKeyTransform
295
+ fromValue:[NSValue valueWithCATransform3D:initialT]
296
+ toValue:[NSValue valueWithCATransform3D:targetT]
297
+ props:newViewProps
298
+ loop:YES];
299
+ }
300
+ if (hasInitialBorderRadius) {
301
+ self.layer.cornerRadius = newViewProps.animateBorderRadius;
302
+ [self
303
+ applyAnimationForKeyPath:@"cornerRadius"
304
+ animationKey:kAnimKeyCornerRadius
305
+ fromValue:@(newViewProps.initialAnimateBorderRadius)
306
+ toValue:@(newViewProps.animateBorderRadius)
307
+ props:newViewProps
308
+ loop:YES];
309
+ }
310
+ } else {
311
+ // No initial animation — set target values directly
312
+ if (mask & kMaskOpacity)
313
+ self.layer.opacity = newViewProps.animateOpacity;
314
+ if (hasTransform)
315
+ self.layer.transform = targetT;
316
+ if (mask & kMaskBorderRadius) {
317
+ self.layer.cornerRadius = newViewProps.animateBorderRadius;
318
+ self.layer.masksToBounds = newViewProps.animateBorderRadius > 0;
319
+ }
320
+ }
321
+ } else if (newViewProps.transitionType == EaseViewTransitionType::None) {
322
+ // No transition — set values immediately
323
+ [self.layer removeAllAnimations];
324
+ if (mask & kMaskOpacity)
325
+ self.layer.opacity = newViewProps.animateOpacity;
326
+ if (hasTransform)
327
+ self.layer.transform = [self targetTransformFromProps:newViewProps];
328
+ if (mask & kMaskBorderRadius) {
329
+ self.layer.cornerRadius = newViewProps.animateBorderRadius;
330
+ self.layer.masksToBounds = newViewProps.animateBorderRadius > 0;
331
+ }
332
+ if (_eventEmitter) {
333
+ auto emitter =
334
+ std::static_pointer_cast<const EaseViewEventEmitter>(_eventEmitter);
335
+ emitter->onTransitionEnd(EaseViewEventEmitter::OnTransitionEnd{
336
+ .finished = true,
337
+ });
338
+ }
339
+ } else {
340
+ // Subsequent updates: animate changed properties
341
+ const auto &oldViewProps =
342
+ *std::static_pointer_cast<const EaseViewProps>(oldProps);
343
+
344
+ if ((mask & kMaskOpacity) &&
345
+ oldViewProps.animateOpacity != newViewProps.animateOpacity) {
346
+ self.layer.opacity = newViewProps.animateOpacity;
347
+ [self
348
+ applyAnimationForKeyPath:@"opacity"
349
+ animationKey:kAnimKeyOpacity
350
+ fromValue:[self presentationValueForKeyPath:@"opacity"]
351
+ toValue:@(newViewProps.animateOpacity)
352
+ props:newViewProps
353
+ loop:NO];
354
+ }
355
+
356
+ // Check if ANY transform-related property changed
357
+ if (hasTransform) {
358
+ BOOL anyTransformChanged =
359
+ oldViewProps.animateTranslateX != newViewProps.animateTranslateX ||
360
+ oldViewProps.animateTranslateY != newViewProps.animateTranslateY ||
361
+ oldViewProps.animateScaleX != newViewProps.animateScaleX ||
362
+ oldViewProps.animateScaleY != newViewProps.animateScaleY ||
363
+ oldViewProps.animateRotate != newViewProps.animateRotate ||
364
+ oldViewProps.animateRotateX != newViewProps.animateRotateX ||
365
+ oldViewProps.animateRotateY != newViewProps.animateRotateY;
366
+
367
+ if (anyTransformChanged) {
368
+ CATransform3D fromT = [self presentationTransform];
369
+ CATransform3D toT = [self targetTransformFromProps:newViewProps];
370
+ self.layer.transform = toT;
371
+ [self applyAnimationForKeyPath:@"transform"
372
+ animationKey:kAnimKeyTransform
373
+ fromValue:[NSValue valueWithCATransform3D:fromT]
374
+ toValue:[NSValue valueWithCATransform3D:toT]
375
+ props:newViewProps
376
+ loop:NO];
377
+ }
378
+ }
379
+
380
+ if ((mask & kMaskBorderRadius) &&
381
+ oldViewProps.animateBorderRadius != newViewProps.animateBorderRadius) {
382
+ self.layer.cornerRadius = newViewProps.animateBorderRadius;
383
+ self.layer.masksToBounds = newViewProps.animateBorderRadius > 0;
384
+ [self applyAnimationForKeyPath:@"cornerRadius"
385
+ animationKey:kAnimKeyCornerRadius
386
+ fromValue:[self presentationValueForKeyPath:
387
+ @"cornerRadius"]
388
+ toValue:@(newViewProps.animateBorderRadius)
389
+ props:newViewProps
390
+ loop:NO];
391
+ }
392
+ }
393
+
394
+ [CATransaction commit];
395
+
396
+ [super updateProps:props oldProps:oldProps];
397
+ }
398
+
399
+ #pragma mark - CAAnimationDelegate
400
+
401
+ - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
402
+ NSNumber *batchId = [anim valueForKey:@"easeBatchId"];
403
+ if (!batchId || batchId.integerValue != _animationBatchId || !_eventEmitter) {
404
+ return;
405
+ }
406
+
407
+ if (!flag) {
408
+ _anyInterrupted = YES;
409
+ }
410
+ _pendingAnimationCount--;
411
+ if (_pendingAnimationCount <= 0) {
412
+ auto emitter =
413
+ std::static_pointer_cast<const EaseViewEventEmitter>(_eventEmitter);
414
+ emitter->onTransitionEnd(EaseViewEventEmitter::OnTransitionEnd{
415
+ .finished = !_anyInterrupted,
416
+ });
417
+ }
418
+ }
419
+
420
+ - (void)prepareForRecycle {
421
+ [super prepareForRecycle];
422
+ [self.layer removeAllAnimations];
423
+ _isFirstMount = YES;
424
+ _pendingAnimationCount = 0;
425
+ _anyInterrupted = NO;
426
+ _transformOriginX = 0.5;
427
+ _transformOriginY = 0.5;
428
+ self.layer.anchorPoint = CGPointMake(0.5, 0.5);
429
+ self.layer.opacity = 1.0;
430
+ self.layer.transform = CATransform3DIdentity;
431
+ self.layer.cornerRadius = 0;
432
+ self.layer.masksToBounds = NO;
433
+ }
434
+
435
+ @end