react-native-ease 0.1.0-alpha.0 → 0.3.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 +122 -74
- package/android/src/main/java/com/ease/EaseView.kt +111 -16
- package/android/src/main/java/com/ease/EaseViewManager.kt +18 -0
- package/ios/EaseView.mm +99 -2
- package/lib/module/EaseView.js +12 -1
- package/lib/module/EaseView.js.map +1 -1
- package/lib/module/EaseView.web.js +210 -0
- package/lib/module/EaseView.web.js.map +1 -0
- package/lib/module/EaseViewNativeComponent.ts +4 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +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 +4 -1
- package/lib/typescript/src/EaseViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +7 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +7 -5
- package/skills/react-native-ease-refactor/SKILL.md +399 -0
- package/src/EaseView.tsx +15 -0
- package/src/EaseView.web.tsx +295 -0
- package/src/EaseViewNativeComponent.ts +4 -0
- package/src/types.ts +8 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-ease-plugins",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "AppAndFlow",
|
|
5
|
+
"email": "devops@appandflow.com"
|
|
6
|
+
},
|
|
7
|
+
"metadata": {
|
|
8
|
+
"description": "Claude Code skills for react-native-ease — migrate Reanimated/Animated code to react-native-ease"
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "react-native-ease",
|
|
13
|
+
"source": "./",
|
|
14
|
+
"description": "Scan for Animated/Reanimated code and migrate to react-native-ease",
|
|
15
|
+
"version": "0.2.0",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "AppAndFlow"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/README.md
CHANGED
|
@@ -1,32 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
<img width="100%" height="auto" alt="react-native-ease by App & Flow" src="https://github.com/user-attachments/assets/8006ed51-d373-4c97-9e80-9937eb9a569e" />
|
|
2
2
|
|
|
3
3
|
Lightweight declarative animations powered by platform APIs. Uses Core Animation on iOS and Animator on Android — zero JS overhead.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## About
|
|
6
|
+
App & Flow is a Montreal-based React Native engineering and consulting studio. We partner with the world’s top companies and are recommended by [Expo](https://expo.dev/consultants). Need a hand? Let’s build together. team@appandflow.com
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- **Simple** — CSS-transition-like API. Set target values, get smooth animations. One component, a few props.
|
|
9
|
-
- **Lightweight** — Minimal native code, no C++ runtime, no custom animation engine. Just a thin declarative wrapper around what the OS already provides.
|
|
10
|
-
- **Interruptible** — Changing values mid-animation smoothly redirects to the new target. No jumps.
|
|
8
|
+
## Demo
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+

|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
- **Layout animations** — Animating width/height/layout changes is not supported.
|
|
16
|
-
- **Shared element transitions** — Use Reanimated or React Navigation's shared element transitions.
|
|
17
|
-
- **Old architecture** — Fabric (new architecture) only.
|
|
18
|
-
|
|
19
|
-
## When to use this vs Reanimated
|
|
20
|
-
|
|
21
|
-
| Use react-native-ease | Use Reanimated |
|
|
22
|
-
|---|---|
|
|
23
|
-
| Fade in a view | Gesture-driven animations |
|
|
24
|
-
| Slide/translate on state change | Complex interpolations |
|
|
25
|
-
| Scale/rotate on press | Shared values across components |
|
|
26
|
-
| Simple enter animations | Layout animations |
|
|
27
|
-
| You want zero config | You need animation worklets |
|
|
12
|
+
## Getting started
|
|
28
13
|
|
|
29
|
-
|
|
14
|
+
### Installation
|
|
30
15
|
|
|
31
16
|
```bash
|
|
32
17
|
npm install react-native-ease
|
|
@@ -34,7 +19,25 @@ npm install react-native-ease
|
|
|
34
19
|
yarn add react-native-ease
|
|
35
20
|
```
|
|
36
21
|
|
|
37
|
-
|
|
22
|
+
### Migration Skill
|
|
23
|
+
|
|
24
|
+
If you're already using `react-native-reanimated` or React Native's `Animated` API, this project includes an [Agent Skill](https://agentskills.io) that scans your codebase for animations that can be replaced with `react-native-ease` and migrates them automatically.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx skills add appandflow/react-native-ease
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then invoke the skill in your agent (e.g., `/react-native-ease-refactor` in Claude Code).
|
|
31
|
+
|
|
32
|
+
The skill will:
|
|
33
|
+
|
|
34
|
+
1. Scan your project for Reanimated/Animated code
|
|
35
|
+
2. Classify which animations can be migrated (and which can't, with reasons)
|
|
36
|
+
3. Show a migration report with before/after details
|
|
37
|
+
4. Let you select which components to migrate
|
|
38
|
+
5. Apply the changes, preserving all non-animation logic
|
|
39
|
+
|
|
40
|
+
### Example
|
|
38
41
|
|
|
39
42
|
```tsx
|
|
40
43
|
import { EaseView } from 'react-native-ease';
|
|
@@ -54,6 +57,32 @@ function FadeCard({ visible, children }) {
|
|
|
54
57
|
|
|
55
58
|
`EaseView` works like a regular `View` — it accepts children, styles, and all standard view props. When values in `animate` change, it smoothly transitions to the new values using native platform animations.
|
|
56
59
|
|
|
60
|
+
## Why
|
|
61
|
+
|
|
62
|
+
### Goals
|
|
63
|
+
|
|
64
|
+
- **Fast** — Animations run entirely on native platform APIs (CAAnimation, ObjectAnimator/SpringAnimation). No JS animation loop, no worklets, no shared values.
|
|
65
|
+
- **Simple** — CSS-transition-like API. Set target values, get smooth animations. One component, a few props.
|
|
66
|
+
- **Lightweight** — Minimal native code, no C++ runtime, no custom animation engine. Just a thin declarative wrapper around what the OS already provides.
|
|
67
|
+
- **Interruptible** — Changing values mid-animation smoothly redirects to the new target. No jumps.
|
|
68
|
+
|
|
69
|
+
### Non-Goals
|
|
70
|
+
|
|
71
|
+
- **Complex gesture-driven animations** — If you need pan/pinch-driven animations, animation worklets, or shared values across components, use [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated).
|
|
72
|
+
- **Layout animations** — Animating width/height/layout changes is not supported.
|
|
73
|
+
- **Shared element transitions** — Use Reanimated or React Navigation's shared element transitions.
|
|
74
|
+
- **Old architecture** — Fabric (new architecture) only.
|
|
75
|
+
|
|
76
|
+
### When to use this vs Reanimated
|
|
77
|
+
|
|
78
|
+
| Use case | Ease | Reanimated |
|
|
79
|
+
| -------------------------------------- | ---- | ---------- |
|
|
80
|
+
| Fade/slide/scale on state change | ✅ | |
|
|
81
|
+
| Enter/exit animations | ✅ | |
|
|
82
|
+
| Gesture-driven animations (pan, pinch) | | ✅ |
|
|
83
|
+
| Layout animations (width, height) | | ✅ |
|
|
84
|
+
| Complex interpolations & chaining | | ✅ |
|
|
85
|
+
|
|
57
86
|
## Guide
|
|
58
87
|
|
|
59
88
|
### Timing Animations
|
|
@@ -67,11 +96,11 @@ Timing animations transition from one value to another over a fixed duration wit
|
|
|
67
96
|
/>
|
|
68
97
|
```
|
|
69
98
|
|
|
70
|
-
| Parameter
|
|
71
|
-
|
|
72
|
-
| `duration` | `number`
|
|
73
|
-
| `easing`
|
|
74
|
-
| `loop`
|
|
99
|
+
| Parameter | Type | Default | Description |
|
|
100
|
+
| ---------- | ------------ | ------------- | ------------------------------------------------------------------------ |
|
|
101
|
+
| `duration` | `number` | `300` | Duration in milliseconds |
|
|
102
|
+
| `easing` | `EasingType` | `'easeInOut'` | Easing curve (preset name or `[x1, y1, x2, y2]` cubic bezier) |
|
|
103
|
+
| `loop` | `string` | — | `'repeat'` restarts from the beginning, `'reverse'` alternates direction |
|
|
75
104
|
|
|
76
105
|
Available easing curves:
|
|
77
106
|
|
|
@@ -112,11 +141,11 @@ Spring animations use a physics-based model for natural-feeling motion. Great fo
|
|
|
112
141
|
/>
|
|
113
142
|
```
|
|
114
143
|
|
|
115
|
-
| Parameter
|
|
116
|
-
|
|
117
|
-
| `damping`
|
|
118
|
-
| `stiffness` | `number` | `120`
|
|
119
|
-
| `mass`
|
|
144
|
+
| Parameter | Type | Default | Description |
|
|
145
|
+
| ----------- | -------- | ------- | ------------------------------------------------------------- |
|
|
146
|
+
| `damping` | `number` | `15` | Friction — higher values reduce oscillation |
|
|
147
|
+
| `stiffness` | `number` | `120` | Spring constant — higher values mean faster animation |
|
|
148
|
+
| `mass` | `number` | `1` | Mass of the object — higher values mean slower, more momentum |
|
|
120
149
|
|
|
121
150
|
Spring presets for common feels:
|
|
122
151
|
|
|
@@ -164,6 +193,26 @@ Use `{ type: 'none' }` to apply values immediately without animation. Useful for
|
|
|
164
193
|
|
|
165
194
|
When `borderRadius` is in `animate`, any `borderRadius` in `style` is automatically stripped to avoid conflicts.
|
|
166
195
|
|
|
196
|
+
### Background Color
|
|
197
|
+
|
|
198
|
+
`backgroundColor` can be animated using any React Native color value. Colors are converted to native ARGB integers via `processColor()`.
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<EaseView
|
|
202
|
+
animate={{ backgroundColor: isActive ? '#3B82F6' : '#E5E7EB' }}
|
|
203
|
+
transition={{ type: 'timing', duration: 300 }}
|
|
204
|
+
style={styles.card}
|
|
205
|
+
>
|
|
206
|
+
<Text>Tap to change color</Text>
|
|
207
|
+
</EaseView>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
On Android, background color uses `ValueAnimator.ofArgb()` (timing only — spring is not supported for colors). On iOS, it uses `CAAnimation` on the `backgroundColor` layer key path and supports both timing and spring transitions.
|
|
211
|
+
|
|
212
|
+
> **Note:** On Android, background color animation uses `ValueAnimator.ofArgb()` which only supports timing transitions. Spring transitions for `backgroundColor` are not supported on Android and will fall back to timing with the default duration. On iOS, both timing and spring transitions work for background color.
|
|
213
|
+
|
|
214
|
+
When `backgroundColor` is in `animate`, any `backgroundColor` in `style` is automatically stripped to avoid conflicts.
|
|
215
|
+
|
|
167
216
|
### Animatable Properties
|
|
168
217
|
|
|
169
218
|
All properties are set in the `animate` prop as flat values (no transform array).
|
|
@@ -171,16 +220,17 @@ All properties are set in the `animate` prop as flat values (no transform array)
|
|
|
171
220
|
```tsx
|
|
172
221
|
<EaseView
|
|
173
222
|
animate={{
|
|
174
|
-
opacity: 1,
|
|
175
|
-
translateX: 0,
|
|
176
|
-
translateY: 0,
|
|
177
|
-
scale: 1,
|
|
178
|
-
scaleX: 1,
|
|
179
|
-
scaleY: 1,
|
|
180
|
-
rotate: 0,
|
|
181
|
-
rotateX: 0,
|
|
182
|
-
rotateY: 0,
|
|
183
|
-
borderRadius: 0,
|
|
223
|
+
opacity: 1, // 0 to 1
|
|
224
|
+
translateX: 0, // pixels
|
|
225
|
+
translateY: 0, // pixels
|
|
226
|
+
scale: 1, // 1 = normal size (shorthand for scaleX + scaleY)
|
|
227
|
+
scaleX: 1, // horizontal scale
|
|
228
|
+
scaleY: 1, // vertical scale
|
|
229
|
+
rotate: 0, // Z-axis rotation in degrees
|
|
230
|
+
rotateX: 0, // X-axis rotation in degrees (3D)
|
|
231
|
+
rotateY: 0, // Y-axis rotation in degrees (3D)
|
|
232
|
+
borderRadius: 0, // pixels (hardware-accelerated, clips children)
|
|
233
|
+
backgroundColor: 'transparent', // any RN color value
|
|
184
234
|
}}
|
|
185
235
|
/>
|
|
186
236
|
```
|
|
@@ -261,11 +311,11 @@ By default, scale and rotation animate from the view's center. Use `transformOri
|
|
|
261
311
|
/>
|
|
262
312
|
```
|
|
263
313
|
|
|
264
|
-
| Value
|
|
265
|
-
|
|
266
|
-
| `{ x: 0, y: 0 }`
|
|
314
|
+
| Value | Position |
|
|
315
|
+
| -------------------- | ---------------- |
|
|
316
|
+
| `{ x: 0, y: 0 }` | Top-left |
|
|
267
317
|
| `{ x: 0.5, y: 0.5 }` | Center (default) |
|
|
268
|
-
| `{ x: 1, y: 1 }`
|
|
318
|
+
| `{ x: 1, y: 1 }` | Bottom-right |
|
|
269
319
|
|
|
270
320
|
### Style Handling
|
|
271
321
|
|
|
@@ -299,32 +349,33 @@ By default, scale and rotation animate from the view's center. Use `transformOri
|
|
|
299
349
|
|
|
300
350
|
A `View` that animates property changes using native platform APIs.
|
|
301
351
|
|
|
302
|
-
| Prop
|
|
303
|
-
|
|
304
|
-
| `animate`
|
|
305
|
-
| `initialAnimate`
|
|
306
|
-
| `transition`
|
|
307
|
-
| `onTransitionEnd`
|
|
308
|
-
| `transformOrigin`
|
|
309
|
-
| `useHardwareLayer` | `boolean`
|
|
310
|
-
| `style`
|
|
311
|
-
| `children`
|
|
312
|
-
| ...rest
|
|
352
|
+
| Prop | Type | Description |
|
|
353
|
+
| ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
354
|
+
| `animate` | `AnimateProps` | Target values for animated properties |
|
|
355
|
+
| `initialAnimate` | `AnimateProps` | Starting values for enter animations (animates to `animate` on mount) |
|
|
356
|
+
| `transition` | `Transition` | Animation configuration (timing, spring, or none) |
|
|
357
|
+
| `onTransitionEnd` | `(event) => void` | Called when all animations complete with `{ finished: boolean }` |
|
|
358
|
+
| `transformOrigin` | `{ x?: number; y?: number }` | Pivot point for scale/rotation as 0–1 fractions. Default: `{ x: 0.5, y: 0.5 }` (center) |
|
|
359
|
+
| `useHardwareLayer` | `boolean` | Android only — rasterize to GPU texture during animations. See [Hardware Layers](#hardware-layers-android). Default: `false` |
|
|
360
|
+
| `style` | `ViewStyle` | Non-animated styles (layout, colors, borders, etc.) |
|
|
361
|
+
| `children` | `ReactNode` | Child elements |
|
|
362
|
+
| ...rest | `ViewProps` | All other standard View props |
|
|
313
363
|
|
|
314
364
|
### `AnimateProps`
|
|
315
365
|
|
|
316
|
-
| Property
|
|
317
|
-
|
|
318
|
-
| `opacity`
|
|
319
|
-
| `translateX`
|
|
320
|
-
| `translateY`
|
|
321
|
-
| `scale`
|
|
322
|
-
| `scaleX`
|
|
323
|
-
| `scaleY`
|
|
324
|
-
| `rotate`
|
|
325
|
-
| `rotateX`
|
|
326
|
-
| `rotateY`
|
|
327
|
-
| `borderRadius`
|
|
366
|
+
| Property | Type | Default | Description |
|
|
367
|
+
| ----------------- | ------------ | --------------- | ------------------------------------------------------------------------------------ |
|
|
368
|
+
| `opacity` | `number` | `1` | View opacity (0–1) |
|
|
369
|
+
| `translateX` | `number` | `0` | Horizontal translation in pixels |
|
|
370
|
+
| `translateY` | `number` | `0` | Vertical translation in pixels |
|
|
371
|
+
| `scale` | `number` | `1` | Uniform scale factor (shorthand for `scaleX` + `scaleY`) |
|
|
372
|
+
| `scaleX` | `number` | `1` | Horizontal scale factor (overrides `scale` for X axis) |
|
|
373
|
+
| `scaleY` | `number` | `1` | Vertical scale factor (overrides `scale` for Y axis) |
|
|
374
|
+
| `rotate` | `number` | `0` | Z-axis rotation in degrees |
|
|
375
|
+
| `rotateX` | `number` | `0` | X-axis rotation in degrees (3D) |
|
|
376
|
+
| `rotateY` | `number` | `0` | Y-axis rotation in degrees (3D) |
|
|
377
|
+
| `borderRadius` | `number` | `0` | Border radius in pixels (hardware-accelerated, clips children) |
|
|
378
|
+
| `backgroundColor` | `ColorValue` | `'transparent'` | Background color (any RN color value). Timing-only on Android, spring+timing on iOS. |
|
|
328
379
|
|
|
329
380
|
Properties not specified in `animate` default to their identity values.
|
|
330
381
|
|
|
@@ -365,10 +416,7 @@ Applies values instantly with no animation. `onTransitionEnd` fires immediately
|
|
|
365
416
|
Setting `useHardwareLayer` rasterizes the view into a GPU texture for the duration of the animation. This means animated property changes (opacity, scale, rotation) are composited on the RenderThread without redrawing the view hierarchy — useful for complex views with many children.
|
|
366
417
|
|
|
367
418
|
```tsx
|
|
368
|
-
<EaseView
|
|
369
|
-
animate={{ opacity: isVisible ? 1 : 0 }}
|
|
370
|
-
useHardwareLayer
|
|
371
|
-
/>
|
|
419
|
+
<EaseView animate={{ opacity: isVisible ? 1 : 0 }} useHardwareLayer />
|
|
372
420
|
```
|
|
373
421
|
|
|
374
422
|
**Trade-offs:**
|
|
@@ -3,7 +3,9 @@ package com.ease
|
|
|
3
3
|
import android.animation.Animator
|
|
4
4
|
import android.animation.AnimatorListenerAdapter
|
|
5
5
|
import android.animation.ObjectAnimator
|
|
6
|
+
import android.animation.ValueAnimator
|
|
6
7
|
import android.content.Context
|
|
8
|
+
import android.graphics.Color
|
|
7
9
|
import android.graphics.Outline
|
|
8
10
|
import android.view.View
|
|
9
11
|
import android.view.ViewOutlineProvider
|
|
@@ -26,6 +28,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
26
28
|
private var prevRotateX: Float? = null
|
|
27
29
|
private var prevRotateY: Float? = null
|
|
28
30
|
private var prevBorderRadius: Float? = null
|
|
31
|
+
private var prevBackgroundColor: Int? = null
|
|
32
|
+
private var currentBackgroundColor: Int = Color.TRANSPARENT
|
|
29
33
|
|
|
30
34
|
// --- First mount tracking ---
|
|
31
35
|
private var isFirstMount: Boolean = true
|
|
@@ -38,6 +42,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
38
42
|
var transitionStiffness: Float = 120.0f
|
|
39
43
|
var transitionMass: Float = 1.0f
|
|
40
44
|
var transitionLoop: String = "none"
|
|
45
|
+
var transitionDelay: Long = 0L
|
|
41
46
|
|
|
42
47
|
// --- Transform origin (0–1 fractions) ---
|
|
43
48
|
var transformOriginX: Float = 0.5f
|
|
@@ -52,11 +57,13 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
// --- Border radius (hardware-accelerated via outline clipping) ---
|
|
55
|
-
// Animated via ObjectAnimator("
|
|
60
|
+
// Animated via ObjectAnimator("animateBorderRadius") — setter invalidates outline each frame.
|
|
56
61
|
private var _borderRadius: Float = 0f
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
fun
|
|
63
|
+
@Suppress("unused") // Used by ObjectAnimator via reflection
|
|
64
|
+
fun getAnimateBorderRadius(): Float = _borderRadius
|
|
65
|
+
@Suppress("unused") // Used by ObjectAnimator via reflection
|
|
66
|
+
fun setAnimateBorderRadius(value: Float) {
|
|
60
67
|
if (_borderRadius != value) {
|
|
61
68
|
_borderRadius = value
|
|
62
69
|
if (value > 0f) {
|
|
@@ -89,6 +96,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
89
96
|
var initialAnimateRotateX: Float = 0.0f
|
|
90
97
|
var initialAnimateRotateY: Float = 0.0f
|
|
91
98
|
var initialAnimateBorderRadius: Float = 0.0f
|
|
99
|
+
var initialAnimateBackgroundColor: Int = Color.TRANSPARENT
|
|
92
100
|
|
|
93
101
|
// --- Pending animate values (buffered per-view, applied in onAfterUpdateTransaction) ---
|
|
94
102
|
var pendingOpacity: Float = 1.0f
|
|
@@ -100,10 +108,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
100
108
|
var pendingRotateX: Float = 0.0f
|
|
101
109
|
var pendingRotateY: Float = 0.0f
|
|
102
110
|
var pendingBorderRadius: Float = 0.0f
|
|
111
|
+
var pendingBackgroundColor: Int = Color.TRANSPARENT
|
|
103
112
|
|
|
104
113
|
// --- Running animations ---
|
|
105
|
-
private val runningAnimators = mutableMapOf<String,
|
|
114
|
+
private val runningAnimators = mutableMapOf<String, Animator>()
|
|
106
115
|
private val runningSpringAnimations = mutableMapOf<DynamicAnimation.ViewProperty, SpringAnimation>()
|
|
116
|
+
private val pendingDelayedRunnables = mutableListOf<Runnable>()
|
|
107
117
|
|
|
108
118
|
// --- Animated properties bitmask (set by ViewManager) ---
|
|
109
119
|
var animatedProperties: Int = 0
|
|
@@ -120,6 +130,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
120
130
|
const val MASK_ROTATE_X = 1 shl 6
|
|
121
131
|
const val MASK_ROTATE_Y = 1 shl 7
|
|
122
132
|
const val MASK_BORDER_RADIUS = 1 shl 8
|
|
133
|
+
const val MASK_BACKGROUND_COLOR = 1 shl 9
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
init {
|
|
@@ -169,7 +180,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
fun applyPendingAnimateValues() {
|
|
172
|
-
applyAnimateValues(pendingOpacity, pendingTranslateX, pendingTranslateY, pendingScaleX, pendingScaleY, pendingRotate, pendingRotateX, pendingRotateY, pendingBorderRadius)
|
|
183
|
+
applyAnimateValues(pendingOpacity, pendingTranslateX, pendingTranslateY, pendingScaleX, pendingScaleY, pendingRotate, pendingRotateX, pendingRotateY, pendingBorderRadius, pendingBackgroundColor)
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
private fun applyAnimateValues(
|
|
@@ -181,7 +192,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
181
192
|
rotate: Float,
|
|
182
193
|
rotateX: Float,
|
|
183
194
|
rotateY: Float,
|
|
184
|
-
borderRadius: Float
|
|
195
|
+
borderRadius: Float,
|
|
196
|
+
backgroundColor: Int
|
|
185
197
|
) {
|
|
186
198
|
if (pendingBatchAnimationCount > 0) {
|
|
187
199
|
onTransitionEnd?.invoke(false)
|
|
@@ -206,7 +218,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
206
218
|
(mask and MASK_ROTATE != 0 && initialAnimateRotate != rotate) ||
|
|
207
219
|
(mask and MASK_ROTATE_X != 0 && initialAnimateRotateX != rotateX) ||
|
|
208
220
|
(mask and MASK_ROTATE_Y != 0 && initialAnimateRotateY != rotateY) ||
|
|
209
|
-
(mask and MASK_BORDER_RADIUS != 0 && initialAnimateBorderRadius != borderRadius)
|
|
221
|
+
(mask and MASK_BORDER_RADIUS != 0 && initialAnimateBorderRadius != borderRadius) ||
|
|
222
|
+
(mask and MASK_BACKGROUND_COLOR != 0 && initialAnimateBackgroundColor != backgroundColor)
|
|
210
223
|
|
|
211
224
|
if (hasInitialAnimation) {
|
|
212
225
|
// Set initial values for animated properties
|
|
@@ -218,7 +231,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
218
231
|
if (mask and MASK_ROTATE != 0) this.rotation = initialAnimateRotate
|
|
219
232
|
if (mask and MASK_ROTATE_X != 0) this.rotationX = initialAnimateRotateX
|
|
220
233
|
if (mask and MASK_ROTATE_Y != 0) this.rotationY = initialAnimateRotateY
|
|
221
|
-
if (mask and MASK_BORDER_RADIUS != 0)
|
|
234
|
+
if (mask and MASK_BORDER_RADIUS != 0) setAnimateBorderRadius(initialAnimateBorderRadius)
|
|
235
|
+
if (mask and MASK_BACKGROUND_COLOR != 0) applyBackgroundColor(initialAnimateBackgroundColor)
|
|
222
236
|
|
|
223
237
|
// Animate properties that differ from initial to target
|
|
224
238
|
if (mask and MASK_OPACITY != 0 && initialAnimateOpacity != opacity) {
|
|
@@ -246,7 +260,10 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
246
260
|
animateProperty("rotationY", DynamicAnimation.ROTATION_Y, initialAnimateRotateY, rotateY, loop = true)
|
|
247
261
|
}
|
|
248
262
|
if (mask and MASK_BORDER_RADIUS != 0 && initialAnimateBorderRadius != borderRadius) {
|
|
249
|
-
animateProperty("
|
|
263
|
+
animateProperty("animateBorderRadius", null, initialAnimateBorderRadius, borderRadius, loop = true)
|
|
264
|
+
}
|
|
265
|
+
if (mask and MASK_BACKGROUND_COLOR != 0 && initialAnimateBackgroundColor != backgroundColor) {
|
|
266
|
+
animateBackgroundColor(initialAnimateBackgroundColor, backgroundColor, loop = true)
|
|
250
267
|
}
|
|
251
268
|
} else {
|
|
252
269
|
// No initial animation — set target values directly (skip non-animated)
|
|
@@ -258,7 +275,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
258
275
|
if (mask and MASK_ROTATE != 0) this.rotation = rotate
|
|
259
276
|
if (mask and MASK_ROTATE_X != 0) this.rotationX = rotateX
|
|
260
277
|
if (mask and MASK_ROTATE_Y != 0) this.rotationY = rotateY
|
|
261
|
-
if (mask and MASK_BORDER_RADIUS != 0)
|
|
278
|
+
if (mask and MASK_BORDER_RADIUS != 0) setAnimateBorderRadius(borderRadius)
|
|
279
|
+
if (mask and MASK_BACKGROUND_COLOR != 0) applyBackgroundColor(backgroundColor)
|
|
262
280
|
}
|
|
263
281
|
} else if (transitionType == "none") {
|
|
264
282
|
// No transition — set values immediately, cancel running animations
|
|
@@ -271,7 +289,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
271
289
|
if (mask and MASK_ROTATE != 0) this.rotation = rotate
|
|
272
290
|
if (mask and MASK_ROTATE_X != 0) this.rotationX = rotateX
|
|
273
291
|
if (mask and MASK_ROTATE_Y != 0) this.rotationY = rotateY
|
|
274
|
-
if (mask and MASK_BORDER_RADIUS != 0)
|
|
292
|
+
if (mask and MASK_BORDER_RADIUS != 0) setAnimateBorderRadius(borderRadius)
|
|
293
|
+
if (mask and MASK_BACKGROUND_COLOR != 0) applyBackgroundColor(backgroundColor)
|
|
275
294
|
onTransitionEnd?.invoke(true)
|
|
276
295
|
} else {
|
|
277
296
|
// Subsequent updates: animate changed properties (skip non-animated)
|
|
@@ -316,8 +335,12 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
316
335
|
}
|
|
317
336
|
|
|
318
337
|
if (prevBorderRadius != null && mask and MASK_BORDER_RADIUS != 0 && prevBorderRadius != borderRadius) {
|
|
319
|
-
val from = getCurrentValue("
|
|
320
|
-
animateProperty("
|
|
338
|
+
val from = getCurrentValue("animateBorderRadius")
|
|
339
|
+
animateProperty("animateBorderRadius", null, from, borderRadius)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (prevBackgroundColor != null && mask and MASK_BACKGROUND_COLOR != 0 && prevBackgroundColor != backgroundColor) {
|
|
343
|
+
animateBackgroundColor(getCurrentBackgroundColor(), backgroundColor)
|
|
321
344
|
}
|
|
322
345
|
}
|
|
323
346
|
|
|
@@ -330,6 +353,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
330
353
|
prevRotateX = rotateX
|
|
331
354
|
prevRotateY = rotateY
|
|
332
355
|
prevBorderRadius = borderRadius
|
|
356
|
+
prevBackgroundColor = backgroundColor
|
|
333
357
|
}
|
|
334
358
|
|
|
335
359
|
private fun getCurrentValue(propertyName: String): Float = when (propertyName) {
|
|
@@ -341,10 +365,64 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
341
365
|
"rotation" -> this.rotation
|
|
342
366
|
"rotationX" -> this.rotationX
|
|
343
367
|
"rotationY" -> this.rotationY
|
|
344
|
-
"
|
|
368
|
+
"animateBorderRadius" -> getAnimateBorderRadius()
|
|
345
369
|
else -> 0f
|
|
346
370
|
}
|
|
347
371
|
|
|
372
|
+
private fun getCurrentBackgroundColor(): Int {
|
|
373
|
+
return currentBackgroundColor
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private fun applyBackgroundColor(color: Int) {
|
|
377
|
+
currentBackgroundColor = color
|
|
378
|
+
setBackgroundColor(color)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private fun animateBackgroundColor(fromColor: Int, toColor: Int, loop: Boolean = false) {
|
|
382
|
+
runningAnimators["backgroundColor"]?.cancel()
|
|
383
|
+
|
|
384
|
+
val batchId = animationBatchId
|
|
385
|
+
pendingBatchAnimationCount++
|
|
386
|
+
|
|
387
|
+
val animator = ValueAnimator.ofArgb(fromColor, toColor).apply {
|
|
388
|
+
duration = transitionDuration.toLong()
|
|
389
|
+
startDelay = transitionDelay
|
|
390
|
+
interpolator = PathInterpolator(
|
|
391
|
+
transitionEasingBezier[0], transitionEasingBezier[1],
|
|
392
|
+
transitionEasingBezier[2], transitionEasingBezier[3]
|
|
393
|
+
)
|
|
394
|
+
if (loop && transitionLoop != "none") {
|
|
395
|
+
repeatCount = ValueAnimator.INFINITE
|
|
396
|
+
repeatMode = if (transitionLoop == "reverse") ValueAnimator.REVERSE else ValueAnimator.RESTART
|
|
397
|
+
}
|
|
398
|
+
addUpdateListener { animation ->
|
|
399
|
+
val color = animation.animatedValue as Int
|
|
400
|
+
this@EaseView.currentBackgroundColor = color
|
|
401
|
+
this@EaseView.setBackgroundColor(color)
|
|
402
|
+
}
|
|
403
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
404
|
+
private var cancelled = false
|
|
405
|
+
override fun onAnimationStart(animation: Animator) {
|
|
406
|
+
this@EaseView.onEaseAnimationStart()
|
|
407
|
+
}
|
|
408
|
+
override fun onAnimationCancel(animation: Animator) { cancelled = true }
|
|
409
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
410
|
+
this@EaseView.onEaseAnimationEnd()
|
|
411
|
+
if (batchId == animationBatchId) {
|
|
412
|
+
if (cancelled) anyInterrupted = true
|
|
413
|
+
pendingBatchAnimationCount--
|
|
414
|
+
if (pendingBatchAnimationCount <= 0) {
|
|
415
|
+
onTransitionEnd?.invoke(!anyInterrupted)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
runningAnimators["backgroundColor"] = animator
|
|
423
|
+
animator.start()
|
|
424
|
+
}
|
|
425
|
+
|
|
348
426
|
private fun animateProperty(
|
|
349
427
|
propertyName: String,
|
|
350
428
|
viewProperty: DynamicAnimation.ViewProperty?,
|
|
@@ -368,6 +446,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
368
446
|
|
|
369
447
|
val animator = ObjectAnimator.ofFloat(this, propertyName, fromValue, toValue).apply {
|
|
370
448
|
duration = transitionDuration.toLong()
|
|
449
|
+
startDelay = transitionDelay
|
|
371
450
|
interpolator = PathInterpolator(
|
|
372
451
|
transitionEasingBezier[0], transitionEasingBezier[1],
|
|
373
452
|
transitionEasingBezier[2], transitionEasingBezier[3]
|
|
@@ -445,10 +524,20 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
445
524
|
|
|
446
525
|
onEaseAnimationStart()
|
|
447
526
|
runningSpringAnimations[viewProperty] = spring
|
|
448
|
-
|
|
527
|
+
if (transitionDelay > 0) {
|
|
528
|
+
val runnable = Runnable { spring.start() }
|
|
529
|
+
pendingDelayedRunnables.add(runnable)
|
|
530
|
+
postDelayed(runnable, transitionDelay)
|
|
531
|
+
} else {
|
|
532
|
+
spring.start()
|
|
533
|
+
}
|
|
449
534
|
}
|
|
450
535
|
|
|
451
536
|
private fun cancelAllAnimations() {
|
|
537
|
+
for (runnable in pendingDelayedRunnables) {
|
|
538
|
+
removeCallbacks(runnable)
|
|
539
|
+
}
|
|
540
|
+
pendingDelayedRunnables.clear()
|
|
452
541
|
for (animator in runningAnimators.values) {
|
|
453
542
|
animator.cancel()
|
|
454
543
|
}
|
|
@@ -498,6 +587,10 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
498
587
|
}
|
|
499
588
|
|
|
500
589
|
fun cleanup() {
|
|
590
|
+
for (runnable in pendingDelayedRunnables) {
|
|
591
|
+
removeCallbacks(runnable)
|
|
592
|
+
}
|
|
593
|
+
pendingDelayedRunnables.clear()
|
|
501
594
|
for (animator in runningAnimators.values) {
|
|
502
595
|
animator.cancel()
|
|
503
596
|
}
|
|
@@ -524,6 +617,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
524
617
|
prevRotateX = null
|
|
525
618
|
prevRotateY = null
|
|
526
619
|
prevBorderRadius = null
|
|
620
|
+
prevBackgroundColor = null
|
|
527
621
|
|
|
528
622
|
this.alpha = 1f
|
|
529
623
|
this.translationX = 0f
|
|
@@ -533,7 +627,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
|
|
|
533
627
|
this.rotation = 0f
|
|
534
628
|
this.rotationX = 0f
|
|
535
629
|
this.rotationY = 0f
|
|
536
|
-
|
|
630
|
+
setAnimateBorderRadius(0f)
|
|
631
|
+
applyBackgroundColor(Color.TRANSPARENT)
|
|
537
632
|
|
|
538
633
|
isFirstMount = true
|
|
539
634
|
transitionLoop = "none"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.ease
|
|
2
2
|
|
|
3
|
+
import android.graphics.Color
|
|
3
4
|
import com.facebook.react.bridge.Arguments
|
|
4
5
|
import com.facebook.react.bridge.ReadableArray
|
|
5
6
|
import com.facebook.react.bridge.WritableMap
|
|
@@ -172,6 +173,11 @@ class EaseViewManager : ReactViewManager() {
|
|
|
172
173
|
view.transitionLoop = value ?: "none"
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
@ReactProp(name = "transitionDelay", defaultInt = 0)
|
|
177
|
+
fun setTransitionDelay(view: EaseView, value: Int) {
|
|
178
|
+
view.transitionDelay = value.toLong()
|
|
179
|
+
}
|
|
180
|
+
|
|
175
181
|
// --- Border radius ---
|
|
176
182
|
|
|
177
183
|
@ReactProp(name = "animateBorderRadius", defaultFloat = 0f)
|
|
@@ -179,6 +185,18 @@ class EaseViewManager : ReactViewManager() {
|
|
|
179
185
|
view.pendingBorderRadius = PixelUtil.toPixelFromDIP(value)
|
|
180
186
|
}
|
|
181
187
|
|
|
188
|
+
// --- Background color ---
|
|
189
|
+
|
|
190
|
+
@ReactProp(name = "animateBackgroundColor", customType = "Color")
|
|
191
|
+
fun setAnimateBackgroundColor(view: EaseView, value: Int?) {
|
|
192
|
+
view.pendingBackgroundColor = value ?: Color.TRANSPARENT
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@ReactProp(name = "initialAnimateBackgroundColor", customType = "Color")
|
|
196
|
+
fun setInitialAnimateBackgroundColor(view: EaseView, value: Int?) {
|
|
197
|
+
view.initialAnimateBackgroundColor = value ?: Color.TRANSPARENT
|
|
198
|
+
}
|
|
199
|
+
|
|
182
200
|
// --- Hardware layer ---
|
|
183
201
|
|
|
184
202
|
@ReactProp(name = "useHardwareLayer", defaultBoolean = false)
|