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.
- package/Ease.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +411 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/ease/EasePackage.kt +17 -0
- package/android/src/main/java/com/ease/EaseView.kt +541 -0
- package/android/src/main/java/com/ease/EaseViewManager.kt +233 -0
- package/ios/EaseView.h +14 -0
- package/ios/EaseView.mm +435 -0
- package/lib/module/EaseView.js +186 -0
- package/lib/module/EaseView.js.map +1 -0
- package/lib/module/EaseViewNativeComponent.ts +68 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/EaseView.d.ts +33 -0
- package/lib/typescript/src/EaseView.d.ts.map +1 -0
- package/lib/typescript/src/EaseViewNativeComponent.d.ts +38 -0
- package/lib/typescript/src/EaseViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +66 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +187 -0
- package/src/EaseView.tsx +256 -0
- package/src/EaseViewNativeComponent.ts +68 -0
- package/src/index.tsx +13 -0
- package/src/types.ts +78 -0
|
@@ -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 */
|
package/ios/EaseView.mm
ADDED
|
@@ -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
|