Skip to content

feat: add per-property transition support#4

Open
EQuimper wants to merge 7 commits intomainfrom
feat/per-property-transition
Open

feat: add per-property transition support#4
EQuimper wants to merge 7 commits intomainfrom
feat/per-property-transition

Conversation

@EQuimper
Copy link
Member

@EQuimper EQuimper commented Mar 14, 2026

Summary

  • Add per-property transition support to EaseView, allowing different animation configs per animatable property (e.g., spring for opacity, timing for translateX, none for scale)
  • Implement TransitionMap type with default fallback and per-property SingleTransition overrides
  • Support type: 'none' to immediately snap a property to its target value without animation
  • Full native implementation on both iOS (Core Animation) and Android (ObjectAnimator/SpringAnimation)

API Example

<EaseView
  animate={{ opacity: 1, translateX: 100, scale: 1.2 }}
  transition={{
    default: { type: 'spring', damping: 15 },
    opacity: { type: 'timing', duration: 200 },
    scale: { type: 'none' },
  }}
/>

Solution

  • src/types.ts — new SingleTransition, TransitionMap, and updated Transition union type
  • src/EaseView.tsx — resolve per-property transition maps into flat native props (transitionOpacityType, transitionOpacityDuration, etc.)
  • src/EaseViewNativeComponent.ts — codegen props for per-property transition overrides
  • ios/EaseView.mm — resolve per-property transition config on the native side, support none type for immediate value application
  • android/EaseView.kt + EaseViewManager.kt — per-property transition resolution and none support
  • src/__tests__/EaseView.test.tsx — tests for per-property resolution, default fallback, and none behavior
  • example/src/App.tsx — demo showcasing per-property transitions
  • README.md — documentation for the new API

Test Plan

Per-Property Transitions demo — opacity fades slowly (1.5s easeInOut) while translateX bounces with a spring. The timing difference between properties is clearly visible.

iOS — iPhone 17 Pro (iOS 26.2)

pr4-ios-trimmed.mp4

Android — Android 16 emulator (arm64)

pr4-android-trimmed.mp4

Web — Chrome, 390px viewport

pr4-web-final.mp4

@EQuimper EQuimper requested a review from janicduplessis March 14, 2026 18:45
@janicduplessis
Copy link
Collaborator

I think I would prefer to generalize to the transition per prop format before passing to native so we always use the same format and don’t end up with duplicate props. I would also just pass array of objects at the point, will be simpler.

So for the codegen I would do something like:

interface TransitionConfig {
… all possible configs
}

interface Transitions {
default: TransitionConfig,
opacity: TransitionConfig,
… all other props
}

transitions: Transitions

then in js we can normalize from the nice transition type we have to this shape. Can also provide a default value there if one wasn’t passed.

in native to get transition for a prop we can do transitions.prop || transitions.default

@AppAndFlow AppAndFlow deleted a comment from clawrence-p Mar 15, 2026
@janicduplessis janicduplessis force-pushed the feat/per-property-transition branch 2 times, most recently from d48c482 to befb0c7 Compare March 16, 2026 02:44
@janicduplessis janicduplessis force-pushed the feat/per-property-transition branch from 8b48ee8 to 772755e Compare March 16, 2026 20:12
/** Resolve the transition prop into a fully-populated NativeTransitions struct. */
function resolveTransitions(transition?: Transition): NativeTransitions {
// Single transition: apply the same config to all 11 slots
if (transition != null && isSingleTransition(transition)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a single transition I think we should just pass {{ default: config }} instead of setting the config to all props.

src/EaseView.tsx Outdated

// No transition: use library defaults per category
if (transition == null) {
const result = { defaultConfig: TIMING_DEFAULT_CONFIG } as Record<
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we should just pass {{ default: DEFAULT_CONFIG }}

src/EaseView.tsx Outdated
defaultConfig: defaultNative ?? TIMING_DEFAULT_CONFIG,
} as Record<string, NativeTransitionConfig>;

for (const key of TRANSITION_KEYS) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I feel like this might not be needed?

The way I see it here is just all we need to do is add the default config if one is not passed.

So if we pass:

  • transition={{}} we send to native transitions={{ default: DEFAULT_TRANSITION }}
  • transition={{alpha: {...}}} send to native transitions={{ default: DEFAULT_TRANSITION, alpha: {...}}}
  • transition={{default: { some values }}} send to native transitions={{default: { some values }}}

}>;

type NativeTransitions = Readonly<{
defaultConfig: NativeTransitionConfig;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make all the props optional / nullable here, except defaultConfig that will always be there.

};

/** Full transitions struct passed to native. */
type NativeTransitions = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Cant we just use the codegen type directly?

if (props.transitionLoop == EaseViewTransitionLoop::Repeat) {
if (config.loop == "repeat") {
timing.repeatCount = HUGE_VALF;
} else if (props.transitionLoop == EaseViewTransitionLoop::Reverse) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this stay an enum in the codegen

| NoneTransition;

/** Per-property transition map. Each key overrides the transition for that animatable property. */
export type TransitionMap = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized it might not make sense to allow having a different config for transforms (scale, translate, roration). On iOS we set all of those together as a matrix so I dont think it is possible to animated with different timings, on Android it seems possible and on web probably not since it is all the transform prop.

What I suggest is instead of having a config for all transforms we just have a transform config that will apply to all transforms.

SO options would become

  • default
  • transform
  • opacity
  • borderRadius
  • backgroundColor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants