react-native-ease 0.2.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.
- package/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +5 -0
- package/README.md +218 -74
- package/android/src/main/java/com/ease/EaseView.kt +275 -78
- package/android/src/main/java/com/ease/EaseViewManager.kt +5 -44
- package/ios/EaseView.mm +277 -76
- package/lib/module/EaseView.js +85 -26
- package/lib/module/EaseView.js.map +1 -1
- package/lib/module/EaseView.web.js +351 -0
- package/lib/module/EaseView.web.js.map +1 -0
- package/lib/module/EaseViewNativeComponent.ts +24 -15
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/EaseView.d.ts +2 -0
- package/lib/typescript/src/EaseView.d.ts.map +1 -1
- package/lib/typescript/src/EaseView.web.d.ts +16 -0
- package/lib/typescript/src/EaseView.web.d.ts.map +1 -0
- package/lib/typescript/src/EaseViewNativeComponent.d.ts +20 -7
- package/lib/typescript/src/EaseViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +21 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +7 -5
- package/skills/react-native-ease-refactor/SKILL.md +405 -0
- package/src/EaseView.tsx +116 -48
- package/src/EaseView.web.tsx +462 -0
- package/src/EaseViewNativeComponent.ts +24 -15
- package/src/index.tsx +2 -0
- package/src/types.ts +26 -2
|
@@ -13,6 +13,7 @@ import android.view.animation.PathInterpolator
|
|
|
13
13
|
import androidx.dynamicanimation.animation.DynamicAnimation
|
|
14
14
|
import androidx.dynamicanimation.animation.SpringAnimation
|
|
15
15
|
import androidx.dynamicanimation.animation.SpringForce
|
|
16
|
+
import com.facebook.react.bridge.ReadableMap
|
|
16
17
|
import com.facebook.react.views.view.ReactViewGroup
|
|
17
18
|
import kotlin.math.sqrt
|
|
18
19
|
|
|
@@ -34,14 +35,90 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
34
35
|
// --- First mount tracking ---
|
|
35
36
|
private var isFirstMount: Boolean = true
|
|
36
37
|
|
|
37
|
-
// --- Transition
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
// --- Transition configs (set by ViewManager via ReadableMap) ---
|
|
39
|
+
private var transitionConfigs: Map<String, TransitionConfig> = emptyMap()
|
|
40
|
+
|
|
41
|
+
data class TransitionConfig(
|
|
42
|
+
val type: String,
|
|
43
|
+
val duration: Int,
|
|
44
|
+
val easingBezier: FloatArray,
|
|
45
|
+
val damping: Float,
|
|
46
|
+
val stiffness: Float,
|
|
47
|
+
val mass: Float,
|
|
48
|
+
val loop: String,
|
|
49
|
+
val delay: Long
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
fun setTransitionsFromMap(map: ReadableMap?) {
|
|
53
|
+
if (map == null) {
|
|
54
|
+
transitionConfigs = emptyMap()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
val configs = mutableMapOf<String, TransitionConfig>()
|
|
58
|
+
val keys = listOf("defaultConfig", "transform", "opacity", "borderRadius", "backgroundColor")
|
|
59
|
+
for (key in keys) {
|
|
60
|
+
if (map.hasKey(key)) {
|
|
61
|
+
val configMap = map.getMap(key) ?: continue
|
|
62
|
+
val bezierArray = configMap.getArray("easingBezier")!!
|
|
63
|
+
configs[key] = TransitionConfig(
|
|
64
|
+
type = configMap.getString("type")!!,
|
|
65
|
+
duration = configMap.getInt("duration"),
|
|
66
|
+
easingBezier = floatArrayOf(
|
|
67
|
+
bezierArray.getDouble(0).toFloat(),
|
|
68
|
+
bezierArray.getDouble(1).toFloat(),
|
|
69
|
+
bezierArray.getDouble(2).toFloat(),
|
|
70
|
+
bezierArray.getDouble(3).toFloat()
|
|
71
|
+
),
|
|
72
|
+
damping = configMap.getDouble("damping").toFloat(),
|
|
73
|
+
stiffness = configMap.getDouble("stiffness").toFloat(),
|
|
74
|
+
mass = configMap.getDouble("mass").toFloat(),
|
|
75
|
+
loop = configMap.getString("loop")!!,
|
|
76
|
+
delay = configMap.getInt("delay").toLong()
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
transitionConfigs = configs
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Map property name to category key, then fall back to defaultConfig. */
|
|
84
|
+
fun getTransitionConfig(name: String): TransitionConfig {
|
|
85
|
+
val categoryKey = when (name) {
|
|
86
|
+
"opacity" -> "opacity"
|
|
87
|
+
"translateX", "translateY", "scaleX", "scaleY",
|
|
88
|
+
"rotate", "rotateX", "rotateY" -> "transform"
|
|
89
|
+
"borderRadius" -> "borderRadius"
|
|
90
|
+
"backgroundColor" -> "backgroundColor"
|
|
91
|
+
else -> null
|
|
92
|
+
}
|
|
93
|
+
if (categoryKey != null) {
|
|
94
|
+
transitionConfigs[categoryKey]?.let { return it }
|
|
95
|
+
}
|
|
96
|
+
return transitionConfigs["defaultConfig"]!!
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun allTransitionsNone(): Boolean {
|
|
100
|
+
val defaultConfig = transitionConfigs["defaultConfig"]
|
|
101
|
+
if (defaultConfig == null || defaultConfig.type != "none") return false
|
|
102
|
+
val categories = listOf("transform", "opacity", "borderRadius", "backgroundColor")
|
|
103
|
+
return categories.all { key ->
|
|
104
|
+
val config = transitionConfigs[key]
|
|
105
|
+
config == null || config.type == "none"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
companion object {
|
|
110
|
+
// Bitmask flags — must match JS constants
|
|
111
|
+
const val MASK_OPACITY = 1 shl 0
|
|
112
|
+
const val MASK_TRANSLATE_X = 1 shl 1
|
|
113
|
+
const val MASK_TRANSLATE_Y = 1 shl 2
|
|
114
|
+
const val MASK_SCALE_X = 1 shl 3
|
|
115
|
+
const val MASK_SCALE_Y = 1 shl 4
|
|
116
|
+
const val MASK_ROTATE = 1 shl 5
|
|
117
|
+
const val MASK_ROTATE_X = 1 shl 6
|
|
118
|
+
const val MASK_ROTATE_Y = 1 shl 7
|
|
119
|
+
const val MASK_BORDER_RADIUS = 1 shl 8
|
|
120
|
+
const val MASK_BACKGROUND_COLOR = 1 shl 9
|
|
121
|
+
}
|
|
45
122
|
|
|
46
123
|
// --- Transform origin (0–1 fractions) ---
|
|
47
124
|
var transformOriginX: Float = 0.5f
|
|
@@ -112,25 +189,11 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
112
189
|
// --- Running animations ---
|
|
113
190
|
private val runningAnimators = mutableMapOf<String, Animator>()
|
|
114
191
|
private val runningSpringAnimations = mutableMapOf<DynamicAnimation.ViewProperty, SpringAnimation>()
|
|
192
|
+
private val pendingDelayedRunnables = mutableListOf<Runnable>()
|
|
115
193
|
|
|
116
194
|
// --- Animated properties bitmask (set by ViewManager) ---
|
|
117
195
|
var animatedProperties: Int = 0
|
|
118
196
|
|
|
119
|
-
// --- Easing interpolators (lazy singletons shared across all instances) ---
|
|
120
|
-
companion object {
|
|
121
|
-
// Bitmask flags — must match JS constants
|
|
122
|
-
const val MASK_OPACITY = 1 shl 0
|
|
123
|
-
const val MASK_TRANSLATE_X = 1 shl 1
|
|
124
|
-
const val MASK_TRANSLATE_Y = 1 shl 2
|
|
125
|
-
const val MASK_SCALE_X = 1 shl 3
|
|
126
|
-
const val MASK_SCALE_Y = 1 shl 4
|
|
127
|
-
const val MASK_ROTATE = 1 shl 5
|
|
128
|
-
const val MASK_ROTATE_X = 1 shl 6
|
|
129
|
-
const val MASK_ROTATE_Y = 1 shl 7
|
|
130
|
-
const val MASK_BORDER_RADIUS = 1 shl 8
|
|
131
|
-
const val MASK_BACKGROUND_COLOR = 1 shl 9
|
|
132
|
-
}
|
|
133
|
-
|
|
134
197
|
init {
|
|
135
198
|
// Set camera distance for 3D perspective rotations (rotateX/rotateY)
|
|
136
199
|
cameraDistance = resources.displayMetrics.density * 850f
|
|
@@ -234,34 +297,40 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
234
297
|
|
|
235
298
|
// Animate properties that differ from initial to target
|
|
236
299
|
if (mask and MASK_OPACITY != 0 && initialAnimateOpacity != opacity) {
|
|
237
|
-
animateProperty("alpha", DynamicAnimation.ALPHA, initialAnimateOpacity, opacity, loop = true)
|
|
300
|
+
animateProperty("alpha", DynamicAnimation.ALPHA, initialAnimateOpacity, opacity, getTransitionConfig("opacity"), loop = true)
|
|
238
301
|
}
|
|
239
302
|
if (mask and MASK_TRANSLATE_X != 0 && initialAnimateTranslateX != translateX) {
|
|
240
|
-
animateProperty("translationX", DynamicAnimation.TRANSLATION_X, initialAnimateTranslateX, translateX, loop = true)
|
|
303
|
+
animateProperty("translationX", DynamicAnimation.TRANSLATION_X, initialAnimateTranslateX, translateX, getTransitionConfig("translateX"), loop = true)
|
|
241
304
|
}
|
|
242
305
|
if (mask and MASK_TRANSLATE_Y != 0 && initialAnimateTranslateY != translateY) {
|
|
243
|
-
animateProperty("translationY", DynamicAnimation.TRANSLATION_Y, initialAnimateTranslateY, translateY, loop = true)
|
|
306
|
+
animateProperty("translationY", DynamicAnimation.TRANSLATION_Y, initialAnimateTranslateY, translateY, getTransitionConfig("translateY"), loop = true)
|
|
244
307
|
}
|
|
245
308
|
if (mask and MASK_SCALE_X != 0 && initialAnimateScaleX != scaleX) {
|
|
246
|
-
animateProperty("scaleX", DynamicAnimation.SCALE_X, initialAnimateScaleX, scaleX, loop = true)
|
|
309
|
+
animateProperty("scaleX", DynamicAnimation.SCALE_X, initialAnimateScaleX, scaleX, getTransitionConfig("scaleX"), loop = true)
|
|
247
310
|
}
|
|
248
311
|
if (mask and MASK_SCALE_Y != 0 && initialAnimateScaleY != scaleY) {
|
|
249
|
-
animateProperty("scaleY", DynamicAnimation.SCALE_Y, initialAnimateScaleY, scaleY, loop = true)
|
|
312
|
+
animateProperty("scaleY", DynamicAnimation.SCALE_Y, initialAnimateScaleY, scaleY, getTransitionConfig("scaleY"), loop = true)
|
|
250
313
|
}
|
|
251
314
|
if (mask and MASK_ROTATE != 0 && initialAnimateRotate != rotate) {
|
|
252
|
-
animateProperty("rotation", DynamicAnimation.ROTATION, initialAnimateRotate, rotate, loop = true)
|
|
315
|
+
animateProperty("rotation", DynamicAnimation.ROTATION, initialAnimateRotate, rotate, getTransitionConfig("rotate"), loop = true)
|
|
253
316
|
}
|
|
254
317
|
if (mask and MASK_ROTATE_X != 0 && initialAnimateRotateX != rotateX) {
|
|
255
|
-
animateProperty("rotationX", DynamicAnimation.ROTATION_X, initialAnimateRotateX, rotateX, loop = true)
|
|
318
|
+
animateProperty("rotationX", DynamicAnimation.ROTATION_X, initialAnimateRotateX, rotateX, getTransitionConfig("rotateX"), loop = true)
|
|
256
319
|
}
|
|
257
320
|
if (mask and MASK_ROTATE_Y != 0 && initialAnimateRotateY != rotateY) {
|
|
258
|
-
animateProperty("rotationY", DynamicAnimation.ROTATION_Y, initialAnimateRotateY, rotateY, loop = true)
|
|
321
|
+
animateProperty("rotationY", DynamicAnimation.ROTATION_Y, initialAnimateRotateY, rotateY, getTransitionConfig("rotateY"), loop = true)
|
|
259
322
|
}
|
|
260
323
|
if (mask and MASK_BORDER_RADIUS != 0 && initialAnimateBorderRadius != borderRadius) {
|
|
261
|
-
animateProperty("animateBorderRadius", null, initialAnimateBorderRadius, borderRadius, loop = true)
|
|
324
|
+
animateProperty("animateBorderRadius", null, initialAnimateBorderRadius, borderRadius, getTransitionConfig("borderRadius"), loop = true)
|
|
262
325
|
}
|
|
263
326
|
if (mask and MASK_BACKGROUND_COLOR != 0 && initialAnimateBackgroundColor != backgroundColor) {
|
|
264
|
-
animateBackgroundColor(initialAnimateBackgroundColor, backgroundColor, loop = true)
|
|
327
|
+
animateBackgroundColor(initialAnimateBackgroundColor, backgroundColor, getTransitionConfig("backgroundColor"), loop = true)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// If all per-property configs were 'none', no animations were queued.
|
|
331
|
+
// Fire onTransitionEnd immediately to match the scalar 'none' contract.
|
|
332
|
+
if (pendingBatchAnimationCount == 0) {
|
|
333
|
+
onTransitionEnd?.invoke(true)
|
|
265
334
|
}
|
|
266
335
|
} else {
|
|
267
336
|
// No initial animation — set target values directly (skip non-animated)
|
|
@@ -276,8 +345,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
276
345
|
if (mask and MASK_BORDER_RADIUS != 0) setAnimateBorderRadius(borderRadius)
|
|
277
346
|
if (mask and MASK_BACKGROUND_COLOR != 0) applyBackgroundColor(backgroundColor)
|
|
278
347
|
}
|
|
279
|
-
} else if (
|
|
280
|
-
// No transition — set values immediately, cancel running animations
|
|
348
|
+
} else if (allTransitionsNone()) {
|
|
349
|
+
// No transition (scalar) — set values immediately, cancel running animations
|
|
281
350
|
cancelAllAnimations()
|
|
282
351
|
if (mask and MASK_OPACITY != 0) this.alpha = opacity
|
|
283
352
|
if (mask and MASK_TRANSLATE_X != 0) this.translationX = translateX
|
|
@@ -292,53 +361,149 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
292
361
|
onTransitionEnd?.invoke(true)
|
|
293
362
|
} else {
|
|
294
363
|
// Subsequent updates: animate changed properties (skip non-animated)
|
|
364
|
+
var anyPropertyChanged = false
|
|
365
|
+
|
|
295
366
|
if (prevOpacity != null && mask and MASK_OPACITY != 0 && prevOpacity != opacity) {
|
|
296
|
-
|
|
297
|
-
|
|
367
|
+
anyPropertyChanged = true
|
|
368
|
+
val config = getTransitionConfig("opacity")
|
|
369
|
+
if (config.type == "none") {
|
|
370
|
+
cancelSpringForProperty("alpha")
|
|
371
|
+
runningAnimators["alpha"]?.cancel()
|
|
372
|
+
runningAnimators.remove("alpha")
|
|
373
|
+
this.alpha = opacity
|
|
374
|
+
} else {
|
|
375
|
+
val from = getCurrentValue("alpha")
|
|
376
|
+
animateProperty("alpha", DynamicAnimation.ALPHA, from, opacity, config)
|
|
377
|
+
}
|
|
298
378
|
}
|
|
299
379
|
|
|
300
380
|
if (prevTranslateX != null && mask and MASK_TRANSLATE_X != 0 && prevTranslateX != translateX) {
|
|
301
|
-
|
|
302
|
-
|
|
381
|
+
anyPropertyChanged = true
|
|
382
|
+
val config = getTransitionConfig("translateX")
|
|
383
|
+
if (config.type == "none") {
|
|
384
|
+
cancelSpringForProperty("translationX")
|
|
385
|
+
runningAnimators["translationX"]?.cancel()
|
|
386
|
+
runningAnimators.remove("translationX")
|
|
387
|
+
this.translationX = translateX
|
|
388
|
+
} else {
|
|
389
|
+
val from = getCurrentValue("translationX")
|
|
390
|
+
animateProperty("translationX", DynamicAnimation.TRANSLATION_X, from, translateX, config)
|
|
391
|
+
}
|
|
303
392
|
}
|
|
304
393
|
|
|
305
394
|
if (prevTranslateY != null && mask and MASK_TRANSLATE_Y != 0 && prevTranslateY != translateY) {
|
|
306
|
-
|
|
307
|
-
|
|
395
|
+
anyPropertyChanged = true
|
|
396
|
+
val config = getTransitionConfig("translateY")
|
|
397
|
+
if (config.type == "none") {
|
|
398
|
+
cancelSpringForProperty("translationY")
|
|
399
|
+
runningAnimators["translationY"]?.cancel()
|
|
400
|
+
runningAnimators.remove("translationY")
|
|
401
|
+
this.translationY = translateY
|
|
402
|
+
} else {
|
|
403
|
+
val from = getCurrentValue("translationY")
|
|
404
|
+
animateProperty("translationY", DynamicAnimation.TRANSLATION_Y, from, translateY, config)
|
|
405
|
+
}
|
|
308
406
|
}
|
|
309
407
|
|
|
310
408
|
if (prevScaleX != null && mask and MASK_SCALE_X != 0 && prevScaleX != scaleX) {
|
|
311
|
-
|
|
312
|
-
|
|
409
|
+
anyPropertyChanged = true
|
|
410
|
+
val config = getTransitionConfig("scaleX")
|
|
411
|
+
if (config.type == "none") {
|
|
412
|
+
cancelSpringForProperty("scaleX")
|
|
413
|
+
runningAnimators["scaleX"]?.cancel()
|
|
414
|
+
runningAnimators.remove("scaleX")
|
|
415
|
+
this.scaleX = scaleX
|
|
416
|
+
} else {
|
|
417
|
+
val from = getCurrentValue("scaleX")
|
|
418
|
+
animateProperty("scaleX", DynamicAnimation.SCALE_X, from, scaleX, config)
|
|
419
|
+
}
|
|
313
420
|
}
|
|
314
421
|
|
|
315
422
|
if (prevScaleY != null && mask and MASK_SCALE_Y != 0 && prevScaleY != scaleY) {
|
|
316
|
-
|
|
317
|
-
|
|
423
|
+
anyPropertyChanged = true
|
|
424
|
+
val config = getTransitionConfig("scaleY")
|
|
425
|
+
if (config.type == "none") {
|
|
426
|
+
cancelSpringForProperty("scaleY")
|
|
427
|
+
runningAnimators["scaleY"]?.cancel()
|
|
428
|
+
runningAnimators.remove("scaleY")
|
|
429
|
+
this.scaleY = scaleY
|
|
430
|
+
} else {
|
|
431
|
+
val from = getCurrentValue("scaleY")
|
|
432
|
+
animateProperty("scaleY", DynamicAnimation.SCALE_Y, from, scaleY, config)
|
|
433
|
+
}
|
|
318
434
|
}
|
|
319
435
|
|
|
320
436
|
if (prevRotate != null && mask and MASK_ROTATE != 0 && prevRotate != rotate) {
|
|
321
|
-
|
|
322
|
-
|
|
437
|
+
anyPropertyChanged = true
|
|
438
|
+
val config = getTransitionConfig("rotate")
|
|
439
|
+
if (config.type == "none") {
|
|
440
|
+
cancelSpringForProperty("rotation")
|
|
441
|
+
runningAnimators["rotation"]?.cancel()
|
|
442
|
+
runningAnimators.remove("rotation")
|
|
443
|
+
this.rotation = rotate
|
|
444
|
+
} else {
|
|
445
|
+
val from = getCurrentValue("rotation")
|
|
446
|
+
animateProperty("rotation", DynamicAnimation.ROTATION, from, rotate, config)
|
|
447
|
+
}
|
|
323
448
|
}
|
|
324
449
|
|
|
325
450
|
if (prevRotateX != null && mask and MASK_ROTATE_X != 0 && prevRotateX != rotateX) {
|
|
326
|
-
|
|
327
|
-
|
|
451
|
+
anyPropertyChanged = true
|
|
452
|
+
val config = getTransitionConfig("rotateX")
|
|
453
|
+
if (config.type == "none") {
|
|
454
|
+
cancelSpringForProperty("rotationX")
|
|
455
|
+
runningAnimators["rotationX"]?.cancel()
|
|
456
|
+
runningAnimators.remove("rotationX")
|
|
457
|
+
this.rotationX = rotateX
|
|
458
|
+
} else {
|
|
459
|
+
val from = getCurrentValue("rotationX")
|
|
460
|
+
animateProperty("rotationX", DynamicAnimation.ROTATION_X, from, rotateX, config)
|
|
461
|
+
}
|
|
328
462
|
}
|
|
329
463
|
|
|
330
464
|
if (prevRotateY != null && mask and MASK_ROTATE_Y != 0 && prevRotateY != rotateY) {
|
|
331
|
-
|
|
332
|
-
|
|
465
|
+
anyPropertyChanged = true
|
|
466
|
+
val config = getTransitionConfig("rotateY")
|
|
467
|
+
if (config.type == "none") {
|
|
468
|
+
cancelSpringForProperty("rotationY")
|
|
469
|
+
runningAnimators["rotationY"]?.cancel()
|
|
470
|
+
runningAnimators.remove("rotationY")
|
|
471
|
+
this.rotationY = rotateY
|
|
472
|
+
} else {
|
|
473
|
+
val from = getCurrentValue("rotationY")
|
|
474
|
+
animateProperty("rotationY", DynamicAnimation.ROTATION_Y, from, rotateY, config)
|
|
475
|
+
}
|
|
333
476
|
}
|
|
334
477
|
|
|
335
478
|
if (prevBorderRadius != null && mask and MASK_BORDER_RADIUS != 0 && prevBorderRadius != borderRadius) {
|
|
336
|
-
|
|
337
|
-
|
|
479
|
+
anyPropertyChanged = true
|
|
480
|
+
val config = getTransitionConfig("borderRadius")
|
|
481
|
+
if (config.type == "none") {
|
|
482
|
+
runningAnimators["animateBorderRadius"]?.cancel()
|
|
483
|
+
runningAnimators.remove("animateBorderRadius")
|
|
484
|
+
setAnimateBorderRadius(borderRadius)
|
|
485
|
+
} else {
|
|
486
|
+
val from = getCurrentValue("animateBorderRadius")
|
|
487
|
+
animateProperty("animateBorderRadius", null, from, borderRadius, config)
|
|
488
|
+
}
|
|
338
489
|
}
|
|
339
490
|
|
|
340
491
|
if (prevBackgroundColor != null && mask and MASK_BACKGROUND_COLOR != 0 && prevBackgroundColor != backgroundColor) {
|
|
341
|
-
|
|
492
|
+
anyPropertyChanged = true
|
|
493
|
+
val config = getTransitionConfig("backgroundColor")
|
|
494
|
+
if (config.type == "none") {
|
|
495
|
+
runningAnimators["backgroundColor"]?.cancel()
|
|
496
|
+
runningAnimators.remove("backgroundColor")
|
|
497
|
+
applyBackgroundColor(backgroundColor)
|
|
498
|
+
} else {
|
|
499
|
+
animateBackgroundColor(getCurrentBackgroundColor(), backgroundColor, config)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// If all changed properties resolved to 'none', no animations were queued.
|
|
504
|
+
// Fire onTransitionEnd immediately.
|
|
505
|
+
if (anyPropertyChanged && pendingBatchAnimationCount == 0) {
|
|
506
|
+
onTransitionEnd?.invoke(true)
|
|
342
507
|
}
|
|
343
508
|
}
|
|
344
509
|
|
|
@@ -376,21 +541,23 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
376
541
|
setBackgroundColor(color)
|
|
377
542
|
}
|
|
378
543
|
|
|
379
|
-
private fun animateBackgroundColor(fromColor: Int, toColor: Int, loop: Boolean = false) {
|
|
544
|
+
private fun animateBackgroundColor(fromColor: Int, toColor: Int, config: TransitionConfig, loop: Boolean = false) {
|
|
380
545
|
runningAnimators["backgroundColor"]?.cancel()
|
|
381
546
|
|
|
382
547
|
val batchId = animationBatchId
|
|
383
548
|
pendingBatchAnimationCount++
|
|
384
549
|
|
|
385
550
|
val animator = ValueAnimator.ofArgb(fromColor, toColor).apply {
|
|
386
|
-
duration =
|
|
551
|
+
duration = config.duration.toLong()
|
|
552
|
+
startDelay = config.delay
|
|
553
|
+
|
|
387
554
|
interpolator = PathInterpolator(
|
|
388
|
-
|
|
389
|
-
|
|
555
|
+
config.easingBezier[0], config.easingBezier[1],
|
|
556
|
+
config.easingBezier[2], config.easingBezier[3]
|
|
390
557
|
)
|
|
391
|
-
if (loop &&
|
|
558
|
+
if (loop && config.loop != "none") {
|
|
392
559
|
repeatCount = ValueAnimator.INFINITE
|
|
393
|
-
repeatMode = if (
|
|
560
|
+
repeatMode = if (config.loop == "reverse") ValueAnimator.REVERSE else ValueAnimator.RESTART
|
|
394
561
|
}
|
|
395
562
|
addUpdateListener { animation ->
|
|
396
563
|
val color = animation.animatedValue as Int
|
|
@@ -425,16 +592,28 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
425
592
|
viewProperty: DynamicAnimation.ViewProperty?,
|
|
426
593
|
fromValue: Float,
|
|
427
594
|
toValue: Float,
|
|
595
|
+
config: TransitionConfig,
|
|
428
596
|
loop: Boolean = false
|
|
429
597
|
) {
|
|
430
|
-
if (
|
|
431
|
-
|
|
598
|
+
if (config.type == "none") {
|
|
599
|
+
// Set immediately — cancel any running animation for this property
|
|
600
|
+
cancelSpringForProperty(propertyName)
|
|
601
|
+
runningAnimators[propertyName]?.cancel()
|
|
602
|
+
runningAnimators.remove(propertyName)
|
|
603
|
+
ObjectAnimator.ofFloat(this, propertyName, toValue).apply {
|
|
604
|
+
duration = 0
|
|
605
|
+
start()
|
|
606
|
+
}
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
if (config.type == "spring" && viewProperty != null) {
|
|
610
|
+
animateSpring(viewProperty, toValue, config)
|
|
432
611
|
} else {
|
|
433
|
-
animateTiming(propertyName, fromValue, toValue, loop)
|
|
612
|
+
animateTiming(propertyName, fromValue, toValue, config, loop)
|
|
434
613
|
}
|
|
435
614
|
}
|
|
436
615
|
|
|
437
|
-
private fun animateTiming(propertyName: String, fromValue: Float, toValue: Float, loop: Boolean = false) {
|
|
616
|
+
private fun animateTiming(propertyName: String, fromValue: Float, toValue: Float, config: TransitionConfig, loop: Boolean = false) {
|
|
438
617
|
cancelSpringForProperty(propertyName)
|
|
439
618
|
runningAnimators[propertyName]?.cancel()
|
|
440
619
|
|
|
@@ -442,14 +621,16 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
442
621
|
pendingBatchAnimationCount++
|
|
443
622
|
|
|
444
623
|
val animator = ObjectAnimator.ofFloat(this, propertyName, fromValue, toValue).apply {
|
|
445
|
-
duration =
|
|
624
|
+
duration = config.duration.toLong()
|
|
625
|
+
startDelay = config.delay
|
|
626
|
+
|
|
446
627
|
interpolator = PathInterpolator(
|
|
447
|
-
|
|
448
|
-
|
|
628
|
+
config.easingBezier[0], config.easingBezier[1],
|
|
629
|
+
config.easingBezier[2], config.easingBezier[3]
|
|
449
630
|
)
|
|
450
|
-
if (loop &&
|
|
631
|
+
if (loop && config.loop != "none") {
|
|
451
632
|
repeatCount = ObjectAnimator.INFINITE
|
|
452
|
-
repeatMode = if (
|
|
633
|
+
repeatMode = if (config.loop == "reverse") {
|
|
453
634
|
ObjectAnimator.REVERSE
|
|
454
635
|
} else {
|
|
455
636
|
ObjectAnimator.RESTART
|
|
@@ -480,25 +661,27 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
480
661
|
animator.start()
|
|
481
662
|
}
|
|
482
663
|
|
|
483
|
-
private fun animateSpring(viewProperty: DynamicAnimation.ViewProperty, toValue: Float) {
|
|
664
|
+
private fun animateSpring(viewProperty: DynamicAnimation.ViewProperty, toValue: Float, config: TransitionConfig) {
|
|
484
665
|
cancelTimingForViewProperty(viewProperty)
|
|
485
666
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
667
|
+
// Cancel any existing spring so we get a fresh end listener with the current batchId.
|
|
668
|
+
runningSpringAnimations[viewProperty]?.let { existing ->
|
|
669
|
+
if (existing.isRunning) {
|
|
670
|
+
existing.cancel()
|
|
671
|
+
}
|
|
490
672
|
}
|
|
673
|
+
runningSpringAnimations.remove(viewProperty)
|
|
491
674
|
|
|
492
675
|
val batchId = animationBatchId
|
|
493
676
|
pendingBatchAnimationCount++
|
|
494
677
|
|
|
495
|
-
val dampingRatio = (
|
|
678
|
+
val dampingRatio = (config.damping / (2.0f * sqrt(config.stiffness * config.mass)))
|
|
496
679
|
.coerceAtLeast(0.01f)
|
|
497
680
|
|
|
498
681
|
val spring = SpringAnimation(this, viewProperty).apply {
|
|
499
682
|
spring = SpringForce(toValue).apply {
|
|
500
683
|
this.dampingRatio = dampingRatio
|
|
501
|
-
this.stiffness =
|
|
684
|
+
this.stiffness = config.stiffness
|
|
502
685
|
}
|
|
503
686
|
addUpdateListener { _, _, _ ->
|
|
504
687
|
// First update — enable hardware layer
|
|
@@ -520,10 +703,20 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
520
703
|
|
|
521
704
|
onEaseAnimationStart()
|
|
522
705
|
runningSpringAnimations[viewProperty] = spring
|
|
523
|
-
|
|
706
|
+
if (config.delay > 0) {
|
|
707
|
+
val runnable = Runnable { spring.start() }
|
|
708
|
+
pendingDelayedRunnables.add(runnable)
|
|
709
|
+
postDelayed(runnable, config.delay)
|
|
710
|
+
} else {
|
|
711
|
+
spring.start()
|
|
712
|
+
}
|
|
524
713
|
}
|
|
525
714
|
|
|
526
715
|
private fun cancelAllAnimations() {
|
|
716
|
+
for (runnable in pendingDelayedRunnables) {
|
|
717
|
+
removeCallbacks(runnable)
|
|
718
|
+
}
|
|
719
|
+
pendingDelayedRunnables.clear()
|
|
527
720
|
for (animator in runningAnimators.values) {
|
|
528
721
|
animator.cancel()
|
|
529
722
|
}
|
|
@@ -573,6 +766,10 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
573
766
|
}
|
|
574
767
|
|
|
575
768
|
fun cleanup() {
|
|
769
|
+
for (runnable in pendingDelayedRunnables) {
|
|
770
|
+
removeCallbacks(runnable)
|
|
771
|
+
}
|
|
772
|
+
pendingDelayedRunnables.clear()
|
|
576
773
|
for (animator in runningAnimators.values) {
|
|
577
774
|
animator.cancel()
|
|
578
775
|
}
|
|
@@ -613,6 +810,6 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
613
810
|
applyBackgroundColor(Color.TRANSPARENT)
|
|
614
811
|
|
|
615
812
|
isFirstMount = true
|
|
616
|
-
|
|
813
|
+
transitionConfigs = emptyMap()
|
|
617
814
|
}
|
|
618
815
|
}
|
|
@@ -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,51 +127,11 @@ class EaseViewManager : ReactViewManager() {
|
|
|
126
127
|
view.initialAnimateBorderRadius = PixelUtil.toPixelFromDIP(value)
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
// ---
|
|
130
|
+
// --- Transitions config (single ReadableMap) ---
|
|
130
131
|
|
|
131
|
-
@ReactProp(name = "
|
|
132
|
-
fun
|
|
133
|
-
view.
|
|
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"
|
|
132
|
+
@ReactProp(name = "transitions")
|
|
133
|
+
fun setTransitions(view: EaseView, value: ReadableMap?) {
|
|
134
|
+
view.setTransitionsFromMap(value)
|
|
174
135
|
}
|
|
175
136
|
|
|
176
137
|
// --- Border radius ---
|