Skip to content

Commit 2d56f18

Browse files
feat: support custom cubic bezier easing curves
Resolve all easing (presets + custom) to a [x1, y1, x2, y2] array in JS. Replace the transitionEasing string enum with a single transitionEasingBezier array prop. Native always receives 4 floats and constructs the timing function directly.
1 parent 5e28c1d commit 2d56f18

11 files changed

Lines changed: 1078 additions & 279 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ transition={{ type: 'spring', damping: 10 }} → transitionType="spring", tran
5050
3. Add default to `IDENTITY` in `src/EaseView.tsx` and pass the flat props to `NativeEaseView`
5151
4. **iOS:** Handle the new property in `updateProps:` — diff old/new, read presentation value, create animation
5252
5. **Android:** Add `pending<Prop>` field, `@ReactProp` setter in `EaseViewManager.kt`, and handle in `applyAnimateValues()`
53-
6. Add tests and update README
53+
6. **Recycle:** Reset the new property to its identity value in `prepareForRecycle` (iOS) and `cleanup()` (Android). Fabric recycles views — any property not reset will leak stale values to the next user of the view.
54+
7. Add tests and update README
5455

5556
## Development Commands
5657

@@ -99,7 +100,8 @@ Use conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, etc.
99100

100101
## Key Gotchas
101102

102-
- **`style` must not contain `opacity` or `transform`** — these are controlled by the `animate` prop. The component warns in dev mode if you do this.
103+
- **Style/animate conflict:** `style` can contain any `ViewStyle` property. If a property appears in both `style` and `animate`, the animated value wins and the style value is stripped. A dev warning is logged. Properties like `opacity` in `style` work fine when not in `animate` — the bitmask tells native which properties are animated vs style-managed.
104+
- **View recycling:** Fabric recycles native views. `prepareForRecycle` (iOS) and `cleanup()` (Android) must reset ALL mutable view properties (opacity, translation, scale, rotation) to identity. Missing a reset causes stale values to leak across hot reloads or view reuse.
103105
- **translateX/translateY are in DIPs (density-independent pixels) on the JS side.** Android `EaseViewManager` converts to pixels via `PixelUtil.toPixelFromDIP()`. iOS codegen handles this automatically.
104106
- **iOS uses `CALayer` key-path animations** (`transform.scale.x`, `transform.translation.x`, etc.), not the `transform` property directly. This means `anchorPoint` controls the pivot for scale/rotation.
105107
- **iOS `anchorPoint` changes shift visual position.** The `updateLayoutMetrics:oldLayoutMetrics:` override compensates for this — don't change it without understanding the position math.

README.md

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Timing animations transition from one value to another over a fixed duration wit
7070
| Parameter | Type | Default | Description |
7171
|---|---|---|---|
7272
| `duration` | `number` | `300` | Duration in milliseconds |
73-
| `easing` | `string` | `'easeInOut'` | Easing curve |
73+
| `easing` | `EasingType` | `'easeInOut'` | Easing curve (preset name or `[x1, y1, x2, y2]` cubic bezier) |
7474
| `loop` | `string` || `'repeat'` restarts from the beginning, `'reverse'` alternates direction |
7575

7676
Available easing curves:
@@ -79,6 +79,27 @@ Available easing curves:
7979
- `'easeIn'` — starts slow, accelerates
8080
- `'easeOut'` — starts fast, decelerates
8181
- `'easeInOut'` — slow start and end, fast middle
82+
- `[x1, y1, x2, y2]` — custom cubic bezier (same as CSS `cubic-bezier()`)
83+
84+
### Custom Easing
85+
86+
Pass a `[x1, y1, x2, y2]` tuple for custom cubic bezier curves. The values correspond to the two control points of the bezier curve, matching the CSS `cubic-bezier()` function.
87+
88+
```tsx
89+
// Standard Material Design easing
90+
<EaseView
91+
animate={{ opacity: isVisible ? 1 : 0 }}
92+
transition={{ type: 'timing', duration: 300, easing: [0.4, 0, 0.2, 1] }}
93+
/>
94+
95+
// Overshoot (y-values can exceed 0–1)
96+
<EaseView
97+
animate={{ scale: active ? 1.2 : 1 }}
98+
transition={{ type: 'timing', duration: 500, easing: [0.68, -0.55, 0.265, 1.55] }}
99+
/>
100+
```
101+
102+
x-values (x1, x2) must be between 0 and 1. y-values can exceed this range to create overshoot effects.
82103

83104
### Spring Animations
84105

@@ -113,6 +134,19 @@ Spring presets for common feels:
113134
{ type: 'spring', damping: 20, stiffness: 60, mass: 2 }
114135
```
115136

137+
### Disabling Animations
138+
139+
Use `{ type: 'none' }` to apply values immediately without animation. Useful for skipping animations in reduced-motion modes or when you need an instant state change.
140+
141+
```tsx
142+
<EaseView
143+
animate={{ opacity: isVisible ? 1 : 0 }}
144+
transition={{ type: 'none' }}
145+
/>
146+
```
147+
148+
`onTransitionEnd` fires immediately with `{ finished: true }`.
149+
116150
### Animatable Properties
117151

118152
All properties are set in the `animate` prop as flat values (no transform array).
@@ -123,12 +157,18 @@ All properties are set in the `animate` prop as flat values (no transform array)
123157
opacity: 1, // 0 to 1
124158
translateX: 0, // pixels
125159
translateY: 0, // pixels
126-
scale: 1, // 1 = normal size
127-
rotate: 0, // degrees
160+
scale: 1, // 1 = normal size (shorthand for scaleX + scaleY)
161+
scaleX: 1, // horizontal scale
162+
scaleY: 1, // vertical scale
163+
rotate: 0, // Z-axis rotation in degrees
164+
rotateX: 0, // X-axis rotation in degrees (3D)
165+
rotateY: 0, // Y-axis rotation in degrees (3D)
128166
}}
129167
/>
130168
```
131169

170+
`scale` is a shorthand that sets both `scaleX` and `scaleY`. When `scaleX` or `scaleY` is also specified, it overrides the `scale` value for that axis.
171+
132172
You can animate any combination of properties simultaneously. All properties share the same transition config.
133173

134174
### Looping Animations
@@ -211,18 +251,26 @@ By default, scale and rotation animate from the view's center. Use `transformOri
211251

212252
### Style Handling
213253

214-
Use `animate` for animated properties and `style` for everything else. If you accidentally put `opacity` or `transform` in `style`, they will be ignored and you'll get a dev warning.
254+
`EaseView` accepts all standard `ViewStyle` properties. If a property appears in both `style` and `animate`, the animated value takes priority and the style value is stripped. A dev warning is logged when this happens.
215255

216256
```tsx
217-
// ✅ Correct
257+
// opacity in style works because only translateY is animated
218258
<EaseView
219-
animate={{ opacity: 1, translateY: 0 }}
220-
style={{ backgroundColor: 'white', borderRadius: 12, padding: 16 }}
221-
/>
259+
animate={{ translateY: moved ? -10 : 0 }}
260+
transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
261+
style={{
262+
opacity: 0.9,
263+
backgroundColor: 'white',
264+
borderRadius: 16,
265+
padding: 16,
266+
}}
267+
>
268+
<Text>Notification card</Text>
269+
</EaseView>
222270

223-
// ⚠️ opacity in style will be ignored with a warning
271+
// ⚠️ opacity is in both — animate wins, style opacity is stripped, dev warning logged
224272
<EaseView
225-
animate={{ translateY: 0 }}
273+
animate={{ opacity: 1 }}
226274
style={{ opacity: 0.5, backgroundColor: 'white' }}
227275
/>
228276
```
@@ -237,7 +285,7 @@ A `View` that animates property changes using native platform APIs.
237285
|---|---|---|
238286
| `animate` | `AnimateProps` | Target values for animated properties |
239287
| `initialAnimate` | `AnimateProps` | Starting values for enter animations (animates to `animate` on mount) |
240-
| `transition` | `Transition` | Animation configuration (timing or spring) |
288+
| `transition` | `Transition` | Animation configuration (timing, spring, or none) |
241289
| `onTransitionEnd` | `(event) => void` | Called when all animations complete with `{ finished: boolean }` |
242290
| `transformOrigin` | `{ x?: number; y?: number }` | Pivot point for scale/rotation as 0–1 fractions. Default: `{ x: 0.5, y: 0.5 }` (center) |
243291
| `useHardwareLayer` | `boolean` | Android only — rasterize to GPU texture during animations. See [Hardware Layers](#hardware-layers-android). Default: `false` |
@@ -252,8 +300,12 @@ A `View` that animates property changes using native platform APIs.
252300
| `opacity` | `number` | `1` | View opacity (0–1) |
253301
| `translateX` | `number` | `0` | Horizontal translation in pixels |
254302
| `translateY` | `number` | `0` | Vertical translation in pixels |
255-
| `scale` | `number` | `1` | Uniform scale factor |
256-
| `rotate` | `number` | `0` | Rotation in degrees |
303+
| `scale` | `number` | `1` | Uniform scale factor (shorthand for `scaleX` + `scaleY`) |
304+
| `scaleX` | `number` | `1` | Horizontal scale factor (overrides `scale` for X axis) |
305+
| `scaleY` | `number` | `1` | Vertical scale factor (overrides `scale` for Y axis) |
306+
| `rotate` | `number` | `0` | Z-axis rotation in degrees |
307+
| `rotateX` | `number` | `0` | X-axis rotation in degrees (3D) |
308+
| `rotateY` | `number` | `0` | Y-axis rotation in degrees (3D) |
257309

258310
Properties not specified in `animate` default to their identity values.
259311

@@ -263,7 +315,7 @@ Properties not specified in `animate` default to their identity values.
263315
{
264316
type: 'timing';
265317
duration?: number; // default: 300 (ms)
266-
easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut'; // default: 'easeInOut'
318+
easing?: EasingType; // default: 'easeInOut' — preset name or [x1, y1, x2, y2]
267319
loop?: 'repeat' | 'reverse'; // default: none
268320
}
269321
```
@@ -279,6 +331,16 @@ Properties not specified in `animate` default to their identity values.
279331
}
280332
```
281333

334+
### `NoneTransition`
335+
336+
```tsx
337+
{
338+
type: 'none';
339+
}
340+
```
341+
342+
Applies values instantly with no animation. `onTransitionEnd` fires immediately with `{ finished: true }`.
343+
282344
## Hardware Layers (Android)
283345

284346
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.

0 commit comments

Comments
 (0)