@rootnative/inertia 0.0.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/gestureLayer/index.d.mts +119 -0
- package/dist/gestureLayer/index.d.ts +119 -0
- package/dist/gestureLayer/index.js +346 -0
- package/dist/gestureLayer/index.js.map +1 -0
- package/dist/gestureLayer/index.mjs +344 -0
- package/dist/gestureLayer/index.mjs.map +1 -0
- package/dist/index.d.mts +507 -0
- package/dist/index.d.ts +507 -0
- package/dist/index.js +1546 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1515 -0
- package/dist/index.mjs.map +1 -0
- package/dist/motion/Image.d.mts +12 -0
- package/dist/motion/Image.d.ts +12 -0
- package/dist/motion/Image.js +1138 -0
- package/dist/motion/Image.js.map +1 -0
- package/dist/motion/Image.mjs +1132 -0
- package/dist/motion/Image.mjs.map +1 -0
- package/dist/motion/Pressable.d.mts +15 -0
- package/dist/motion/Pressable.d.ts +15 -0
- package/dist/motion/Pressable.js +1138 -0
- package/dist/motion/Pressable.js.map +1 -0
- package/dist/motion/Pressable.mjs +1132 -0
- package/dist/motion/Pressable.mjs.map +1 -0
- package/dist/motion/ScrollView.d.mts +12 -0
- package/dist/motion/ScrollView.d.ts +12 -0
- package/dist/motion/ScrollView.js +1138 -0
- package/dist/motion/ScrollView.js.map +1 -0
- package/dist/motion/ScrollView.mjs +1132 -0
- package/dist/motion/ScrollView.mjs.map +1 -0
- package/dist/motion/Text.d.mts +11 -0
- package/dist/motion/Text.d.ts +11 -0
- package/dist/motion/Text.js +1138 -0
- package/dist/motion/Text.js.map +1 -0
- package/dist/motion/Text.mjs +1132 -0
- package/dist/motion/Text.mjs.map +1 -0
- package/dist/motion/View.d.mts +11 -0
- package/dist/motion/View.d.ts +11 -0
- package/dist/motion/View.js +1138 -0
- package/dist/motion/View.js.map +1 -0
- package/dist/motion/View.mjs +1132 -0
- package/dist/motion/View.mjs.map +1 -0
- package/dist/testing/index.d.mts +57 -0
- package/dist/testing/index.d.ts +57 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +16 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/touch/index.d.mts +146 -0
- package/dist/touch/index.d.ts +146 -0
- package/dist/touch/index.js +166 -0
- package/dist/touch/index.js.map +1 -0
- package/dist/touch/index.mjs +164 -0
- package/dist/touch/index.mjs.map +1 -0
- package/dist/types-cU43dEmH.d.mts +339 -0
- package/dist/types-cU43dEmH.d.ts +339 -0
- package/dist/useGesture-BnY65PlQ.d.ts +84 -0
- package/dist/useGesture-DxtXdz-K.d.mts +84 -0
- package/jest-preset.js +33 -0
- package/jest-setup.js +213 -0
- package/llms.txt +142 -0
- package/package.json +151 -0
- package/src/__type-tests__/animate.test-d.tsx +88 -0
- package/src/__type-tests__/variants.test-d.tsx +67 -0
- package/src/config/MotionConfig.tsx +30 -0
- package/src/config/MotionConfigContext.ts +53 -0
- package/src/config/index.ts +9 -0
- package/src/gestureLayer/index.ts +21 -0
- package/src/gestureLayer/useGestureLayer.ts +285 -0
- package/src/gestures/focusVisibility.ts +61 -0
- package/src/gestures/index.ts +1 -0
- package/src/index.ts +72 -0
- package/src/layout/index.ts +16 -0
- package/src/layout/resolveLayout.ts +54 -0
- package/src/layout/sharedRegistry.ts +111 -0
- package/src/layout/useSharedLayout.ts +289 -0
- package/src/motion/Image.tsx +9 -0
- package/src/motion/Pressable.tsx +12 -0
- package/src/motion/ScrollView.tsx +9 -0
- package/src/motion/Text.tsx +8 -0
- package/src/motion/View.tsx +8 -0
- package/src/motion/createMotionComponent.tsx +1306 -0
- package/src/motion/index.ts +26 -0
- package/src/motion/installCheck.ts +65 -0
- package/src/presence/Presence.tsx +165 -0
- package/src/presence/PresenceContext.ts +28 -0
- package/src/presence/index.ts +6 -0
- package/src/testing/index.ts +78 -0
- package/src/touch/index.ts +18 -0
- package/src/touch/useTouchDrag.ts +289 -0
- package/src/transitions/easing.ts +53 -0
- package/src/transitions/index.ts +7 -0
- package/src/transitions/keys.ts +32 -0
- package/src/transitions/resolve.ts +169 -0
- package/src/transitions/resolveSequence.ts +87 -0
- package/src/transitions/runtime.ts +63 -0
- package/src/transitions/sig.ts +40 -0
- package/src/transitions/spring.ts +42 -0
- package/src/types.ts +383 -0
- package/src/values/index.ts +26 -0
- package/src/values/useAnimation.ts +69 -0
- package/src/values/useBooleanSpring.ts +33 -0
- package/src/values/useColorTransition.ts +72 -0
- package/src/values/useGesture.ts +144 -0
- package/src/values/useMotionValue.ts +33 -0
- package/src/values/useScroll.ts +72 -0
- package/src/values/useShadow.ts +116 -0
- package/src/values/useSpring.ts +98 -0
- package/src/values/useTransform.ts +132 -0
- package/src/values/useVariants.ts +60 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level registry of last-known on-screen rects for shared-layout
|
|
3
|
+
* elements, indexed by `layoutId`. Backs `<Motion.* layoutId="..." />` —
|
|
4
|
+
* Reanimated 4 dropped the `sharedTransitionTag` API the previous design
|
|
5
|
+
* relied on, so the cross-screen shared-element transition lives in
|
|
6
|
+
* userland now.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle, per id:
|
|
9
|
+
* 1. While a Motion primitive with `layoutId={id}` is mounted, every
|
|
10
|
+
* `onLayout` updates the rect via `registerLayout`.
|
|
11
|
+
* 2. When that primitive unmounts, the same rect is left behind under a
|
|
12
|
+
* TTL via `releaseLayout` so a subsequent mount can consume it.
|
|
13
|
+
* 3. The next mount calls `consumeLayout(id)` — if a fresh entry exists
|
|
14
|
+
* it becomes the FLIP source rect; the entry is removed so a third
|
|
15
|
+
* mount with the same id doesn't re-animate from a stale snapshot.
|
|
16
|
+
*
|
|
17
|
+
* Rects are stored in **parent-relative coordinates** (what `onLayout`'s
|
|
18
|
+
* `nativeEvent.layout` reports). This composes for the common case where the
|
|
19
|
+
* source and target share an outer content container — e.g. a typical stack
|
|
20
|
+
* navigator. Nested-parent setups, where the two parents sit at different
|
|
21
|
+
* window offsets, need a window-coordinate path (`measureInWindow`); that is
|
|
22
|
+
* punted to v2 per the roadmap.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Parent-relative rect of a measured element (from `onLayout`). */
|
|
26
|
+
export interface SharedRect {
|
|
27
|
+
x: number
|
|
28
|
+
y: number
|
|
29
|
+
width: number
|
|
30
|
+
height: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Entry {
|
|
34
|
+
rect: SharedRect
|
|
35
|
+
expiresAt: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const REGISTRY = new Map<string, Entry>()
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How long (ms) a released rect remains consumable. Sized to comfortably
|
|
42
|
+
* cover a typical screen transition (slide animation, gesture-driven
|
|
43
|
+
* dismiss) without leaving stale entries lying around if no incoming
|
|
44
|
+
* mount picks them up.
|
|
45
|
+
*/
|
|
46
|
+
export const SHARED_LAYOUT_TTL_MS = 1000
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Provide the current monotonic-ish timestamp. Indirected so tests can
|
|
50
|
+
* stub it via `__setSharedLayoutClock` without touching `Date.now` globally.
|
|
51
|
+
*/
|
|
52
|
+
let now = (): number => Date.now()
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update the latest known rect for `id`. Called on every `onLayout` of a
|
|
56
|
+
* Motion primitive with `layoutId` set so the registry always holds a
|
|
57
|
+
* current measurement if that primitive becomes the source of a future
|
|
58
|
+
* transition. Resets the TTL each call.
|
|
59
|
+
*/
|
|
60
|
+
export function registerLayout(id: string, rect: SharedRect): void {
|
|
61
|
+
REGISTRY.set(id, { rect, expiresAt: now() + SHARED_LAYOUT_TTL_MS })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Record the rect for `id` on unmount so the next mount can consume it as
|
|
66
|
+
* the FLIP source. Functionally identical to `registerLayout` — the split
|
|
67
|
+
* is purely intent-documenting at the call site.
|
|
68
|
+
*/
|
|
69
|
+
export function releaseLayout(id: string, rect: SharedRect): void {
|
|
70
|
+
REGISTRY.set(id, { rect, expiresAt: now() + SHARED_LAYOUT_TTL_MS })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Take the recorded rect for `id` if it exists and hasn't expired. The
|
|
75
|
+
* entry is removed in either case — at most one incoming mount consumes
|
|
76
|
+
* a given release, and an expired entry is dropped so it can't poison a
|
|
77
|
+
* later transition. Returns `undefined` when no fresh source is available,
|
|
78
|
+
* in which case the caller should mount without a layout animation.
|
|
79
|
+
*/
|
|
80
|
+
export function consumeLayout(id: string): SharedRect | undefined {
|
|
81
|
+
const entry = REGISTRY.get(id)
|
|
82
|
+
if (!entry) return undefined
|
|
83
|
+
REGISTRY.delete(id)
|
|
84
|
+
if (entry.expiresAt < now()) return undefined
|
|
85
|
+
return entry.rect
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Drop all entries. Tests use this to isolate between cases. */
|
|
89
|
+
export function clearSharedRegistry(): void {
|
|
90
|
+
REGISTRY.clear()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Inspect a registry entry without consuming it. Intended for tests and
|
|
95
|
+
* dev tooling; production code should go through `consumeLayout`.
|
|
96
|
+
*/
|
|
97
|
+
export function peekSharedLayout(id: string): SharedRect | undefined {
|
|
98
|
+
const entry = REGISTRY.get(id)
|
|
99
|
+
if (!entry) return undefined
|
|
100
|
+
if (entry.expiresAt < now()) return undefined
|
|
101
|
+
return entry.rect
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Test hook: swap the clock used for TTL calculations. Pass `undefined` to
|
|
106
|
+
* restore `Date.now`. Not exported from the package root — reachable only
|
|
107
|
+
* from inside the workspace.
|
|
108
|
+
*/
|
|
109
|
+
export function __setSharedLayoutClock(fn: (() => number) | undefined): void {
|
|
110
|
+
now = fn ?? Date.now
|
|
111
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MutableRefObject,
|
|
3
|
+
type Ref,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { type LayoutChangeEvent } from 'react-native'
|
|
10
|
+
import {
|
|
11
|
+
type SharedValue,
|
|
12
|
+
useSharedValue,
|
|
13
|
+
withSequence,
|
|
14
|
+
withSpring,
|
|
15
|
+
withTiming,
|
|
16
|
+
} from 'react-native-reanimated'
|
|
17
|
+
import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
|
|
18
|
+
import { type SpringTransition, type TransitionConfig } from '../types'
|
|
19
|
+
import {
|
|
20
|
+
consumeLayout,
|
|
21
|
+
registerLayout,
|
|
22
|
+
releaseLayout,
|
|
23
|
+
type SharedRect,
|
|
24
|
+
} from './sharedRegistry'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shared values produced by `useSharedLayout`. The worklet inside
|
|
28
|
+
* `createMotionComponent` appends `translateX/Y` and `scaleX/Y` transforms
|
|
29
|
+
* built from these so a shared-layout source rect maps onto the new
|
|
30
|
+
* element's transform stack without conflicting with the user's `animate`
|
|
31
|
+
* transforms — multiple transform entries of the same key compose
|
|
32
|
+
* additively (for translates) and multiplicatively (for scales), which is
|
|
33
|
+
* exactly the FLIP semantic.
|
|
34
|
+
*
|
|
35
|
+
* At rest the values are `(0, 0, 1, 1)` — the identity transform — so when
|
|
36
|
+
* no shared-layout transition is active the worklet's contribution is a
|
|
37
|
+
* no-op.
|
|
38
|
+
*/
|
|
39
|
+
export interface SharedLayoutValues {
|
|
40
|
+
dx: SharedValue<number>
|
|
41
|
+
dy: SharedValue<number>
|
|
42
|
+
sx: SharedValue<number>
|
|
43
|
+
sy: SharedValue<number>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What the host component needs to wire into its rendered tree. */
|
|
47
|
+
export interface SharedLayoutBindings {
|
|
48
|
+
flip: SharedLayoutValues
|
|
49
|
+
/**
|
|
50
|
+
* Composite ref the consumer attaches to the rendered animated
|
|
51
|
+
* component. Forwards the underlying ref to the user-supplied `ref`
|
|
52
|
+
* (when present); kept as a stable callback so a `<Motion.*>` with no
|
|
53
|
+
* `layoutId` doesn't pay anything extra.
|
|
54
|
+
*/
|
|
55
|
+
setRef: (node: unknown) => void
|
|
56
|
+
/** onLayout handler the consumer must attach to the animated component. */
|
|
57
|
+
onLayout: (event: LayoutChangeEvent) => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook backing `<Motion.* layoutId="..." />`.
|
|
62
|
+
*
|
|
63
|
+
* Responsibilities, in order:
|
|
64
|
+
* 1. Allocate FLIP shared values (identity at rest).
|
|
65
|
+
* 2. Track the latest layout rect via the `onLayout` event, and push it
|
|
66
|
+
* into the registry under `layoutId` so this primitive can serve as
|
|
67
|
+
* the source for a future transition.
|
|
68
|
+
* 3. On unmount, hand the last measured rect to `releaseLayout` so the
|
|
69
|
+
* next mount with the same id can consume it.
|
|
70
|
+
* 4. On the first layout commit, consume any pending source rect and
|
|
71
|
+
* drive the FLIP shared values: snap them to the (delta, scale) that
|
|
72
|
+
* visually places the new element at the source position, then
|
|
73
|
+
* animate back to identity. `withSequence(snap, animate)` keeps the
|
|
74
|
+
* animation starting from the snapped delta rather than from zero.
|
|
75
|
+
*
|
|
76
|
+
* Coordinate space note: rects are in parent-relative coordinates (what
|
|
77
|
+
* `onLayout` reports). For the common cross-screen navigator pattern —
|
|
78
|
+
* both screens share an outer content container — parent-relative deltas
|
|
79
|
+
* match what the user perceives. Nested-parent setups where the source
|
|
80
|
+
* and target screens sit under containers at different screen offsets
|
|
81
|
+
* will be off by that offset; v1 documents this and leaves a precise
|
|
82
|
+
* window-coords path for v2.
|
|
83
|
+
*
|
|
84
|
+
* When `layoutId` is `undefined`, every callback is a no-op and the FLIP
|
|
85
|
+
* shared values stay at identity — the host's worklet then skips the
|
|
86
|
+
* transform contribution entirely.
|
|
87
|
+
*/
|
|
88
|
+
export function useSharedLayout(options: {
|
|
89
|
+
layoutId: string | undefined
|
|
90
|
+
userRef: Ref<unknown> | undefined
|
|
91
|
+
transition: TransitionConfig | undefined
|
|
92
|
+
shouldReduceMotion: boolean
|
|
93
|
+
userOnLayout: ((event: LayoutChangeEvent) => void) | undefined
|
|
94
|
+
}): SharedLayoutBindings {
|
|
95
|
+
const { layoutId, userRef, transition, shouldReduceMotion, userOnLayout } =
|
|
96
|
+
options
|
|
97
|
+
|
|
98
|
+
const dx = useSharedValue(0)
|
|
99
|
+
const dy = useSharedValue(0)
|
|
100
|
+
const sx = useSharedValue(1)
|
|
101
|
+
const sy = useSharedValue(1)
|
|
102
|
+
|
|
103
|
+
// Most-recent rect for this primitive. Updated on every layout commit;
|
|
104
|
+
// read on unmount to populate the registry as the FLIP source for the
|
|
105
|
+
// next mount with the same id.
|
|
106
|
+
const lastRectRef = useRef<SharedRect | null>(null)
|
|
107
|
+
|
|
108
|
+
// First-layout latch — only the first measurement after a fresh mount
|
|
109
|
+
// can consume a source rect. Subsequent layouts (resizes, prop changes)
|
|
110
|
+
// refresh the registry but never re-trigger a FLIP from an old source.
|
|
111
|
+
const consumedRef = useRef(false)
|
|
112
|
+
|
|
113
|
+
const transitionRef = useRef(transition)
|
|
114
|
+
transitionRef.current = transition
|
|
115
|
+
const reducedMotionRef = useRef(shouldReduceMotion)
|
|
116
|
+
reducedMotionRef.current = shouldReduceMotion
|
|
117
|
+
|
|
118
|
+
const setRef = useCallback(
|
|
119
|
+
(node: unknown) => {
|
|
120
|
+
if (typeof userRef === 'function') userRef(node)
|
|
121
|
+
else if (userRef) (userRef as MutableRefObject<unknown>).current = node
|
|
122
|
+
},
|
|
123
|
+
[userRef],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const onLayout = useCallback(
|
|
127
|
+
(event: LayoutChangeEvent) => {
|
|
128
|
+
userOnLayout?.(event)
|
|
129
|
+
if (!layoutId) return
|
|
130
|
+
|
|
131
|
+
const { x, y, width, height } = event.nativeEvent.layout
|
|
132
|
+
const rect: SharedRect = { x, y, width, height }
|
|
133
|
+
lastRectRef.current = rect
|
|
134
|
+
|
|
135
|
+
// First-layout-only: read the registry BEFORE writing our own rect
|
|
136
|
+
// so a previously-released source rect can be consumed cleanly
|
|
137
|
+
// without being overwritten by the current rect first.
|
|
138
|
+
let source: SharedRect | undefined
|
|
139
|
+
if (!consumedRef.current) {
|
|
140
|
+
consumedRef.current = true
|
|
141
|
+
source = consumeLayout(layoutId)
|
|
142
|
+
}
|
|
143
|
+
registerLayout(layoutId, rect)
|
|
144
|
+
|
|
145
|
+
if (source) {
|
|
146
|
+
applyFlip({
|
|
147
|
+
source,
|
|
148
|
+
target: rect,
|
|
149
|
+
dx,
|
|
150
|
+
dy,
|
|
151
|
+
sx,
|
|
152
|
+
sy,
|
|
153
|
+
transition: transitionRef.current,
|
|
154
|
+
shouldReduceMotion: reducedMotionRef.current,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
// dx/dy/sx/sy are stable refs from useSharedValue, but eslint's
|
|
159
|
+
// exhaustive-deps would flag them — including them is harmless and
|
|
160
|
+
// silences the warning.
|
|
161
|
+
[layoutId, userOnLayout, dx, dy, sx, sy],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Reset the first-layout latch when the id changes — a new id is logically
|
|
165
|
+
// a new shared-element identity and should be allowed to consume a source.
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
consumedRef.current = false
|
|
168
|
+
}, [layoutId])
|
|
169
|
+
|
|
170
|
+
// On unmount, hand the latest rect to the registry under this id so the
|
|
171
|
+
// next mount can consume it as a FLIP source.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
return () => {
|
|
174
|
+
if (!layoutId) return
|
|
175
|
+
const rect = lastRectRef.current
|
|
176
|
+
if (!rect) return
|
|
177
|
+
releaseLayout(layoutId, rect)
|
|
178
|
+
}
|
|
179
|
+
}, [layoutId])
|
|
180
|
+
|
|
181
|
+
return useMemo<SharedLayoutBindings>(
|
|
182
|
+
() => ({
|
|
183
|
+
flip: { dx, dy, sx, sy },
|
|
184
|
+
setRef,
|
|
185
|
+
onLayout,
|
|
186
|
+
}),
|
|
187
|
+
[dx, dy, sx, sy, setRef, onLayout],
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Snap the FLIP shared values so the new element visually overlays its
|
|
193
|
+
* source rect, then animate back to identity. The `withSequence(snap,
|
|
194
|
+
* animate)` shape is what makes the spring start from the snapped delta —
|
|
195
|
+
* a plain `withSpring(0)` from a zero base would animate from-zero, not
|
|
196
|
+
* from-source.
|
|
197
|
+
*/
|
|
198
|
+
function applyFlip(args: {
|
|
199
|
+
source: SharedRect
|
|
200
|
+
target: SharedRect
|
|
201
|
+
dx: SharedValue<number>
|
|
202
|
+
dy: SharedValue<number>
|
|
203
|
+
sx: SharedValue<number>
|
|
204
|
+
sy: SharedValue<number>
|
|
205
|
+
transition: TransitionConfig | undefined
|
|
206
|
+
shouldReduceMotion: boolean
|
|
207
|
+
}): void {
|
|
208
|
+
const { source, target, dx, dy, sx, sy, transition, shouldReduceMotion } =
|
|
209
|
+
args
|
|
210
|
+
|
|
211
|
+
// Compute the delta that would visually place the new element at the
|
|
212
|
+
// source rect. The transform origin matters: RN scales around the
|
|
213
|
+
// element's center, so the translation needs to account for the
|
|
214
|
+
// center-of-source vs center-of-target offset, not the top-left offset.
|
|
215
|
+
const sourceCenterX = source.x + source.width / 2
|
|
216
|
+
const sourceCenterY = source.y + source.height / 2
|
|
217
|
+
const targetCenterX = target.x + target.width / 2
|
|
218
|
+
const targetCenterY = target.y + target.height / 2
|
|
219
|
+
const deltaX = sourceCenterX - targetCenterX
|
|
220
|
+
const deltaY = sourceCenterY - targetCenterY
|
|
221
|
+
// Guard against zero-sized targets (degenerate layout) — keep scale at 1
|
|
222
|
+
// so the element at least renders even if the FLIP isn't a perfect match.
|
|
223
|
+
const scaleX = target.width > 0 ? source.width / target.width : 1
|
|
224
|
+
const scaleY = target.height > 0 ? source.height / target.height : 1
|
|
225
|
+
|
|
226
|
+
if (shouldReduceMotion) {
|
|
227
|
+
// Reduced-motion: skip the visual transition entirely. The element
|
|
228
|
+
// appears at its natural position; the source rect is discarded.
|
|
229
|
+
dx.value = 0
|
|
230
|
+
dy.value = 0
|
|
231
|
+
sx.value = 1
|
|
232
|
+
sy.value = 1
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (transition?.type === 'no-animation') {
|
|
237
|
+
dx.value = 0
|
|
238
|
+
dy.value = 0
|
|
239
|
+
sx.value = 1
|
|
240
|
+
sy.value = 1
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (transition?.type === 'timing') {
|
|
245
|
+
const duration = transition.duration ?? 300
|
|
246
|
+
dx.value = withSequence(
|
|
247
|
+
withTiming(deltaX, { duration: 0 }),
|
|
248
|
+
withTiming(0, { duration }),
|
|
249
|
+
)
|
|
250
|
+
dy.value = withSequence(
|
|
251
|
+
withTiming(deltaY, { duration: 0 }),
|
|
252
|
+
withTiming(0, { duration }),
|
|
253
|
+
)
|
|
254
|
+
sx.value = withSequence(
|
|
255
|
+
withTiming(scaleX, { duration: 0 }),
|
|
256
|
+
withTiming(1, { duration }),
|
|
257
|
+
)
|
|
258
|
+
sy.value = withSequence(
|
|
259
|
+
withTiming(scaleY, { duration: 0 }),
|
|
260
|
+
withTiming(1, { duration }),
|
|
261
|
+
)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Spring path (default, and the fallback for `'decay'` which doesn't
|
|
266
|
+
// have a meaningful target value for a FLIP transition).
|
|
267
|
+
const springCfg: SpringTransition =
|
|
268
|
+
transition?.type === 'spring'
|
|
269
|
+
? { ...DEFAULT_SPRING, ...transition }
|
|
270
|
+
: { type: 'spring', ...DEFAULT_SPRING }
|
|
271
|
+
const springParams = springToReanimated(springCfg)
|
|
272
|
+
|
|
273
|
+
dx.value = withSequence(
|
|
274
|
+
withTiming(deltaX, { duration: 0 }),
|
|
275
|
+
withSpring(0, springParams),
|
|
276
|
+
)
|
|
277
|
+
dy.value = withSequence(
|
|
278
|
+
withTiming(deltaY, { duration: 0 }),
|
|
279
|
+
withSpring(0, springParams),
|
|
280
|
+
)
|
|
281
|
+
sx.value = withSequence(
|
|
282
|
+
withTiming(scaleX, { duration: 0 }),
|
|
283
|
+
withSpring(1, springParams),
|
|
284
|
+
)
|
|
285
|
+
sy.value = withSequence(
|
|
286
|
+
withTiming(scaleY, { duration: 0 }),
|
|
287
|
+
withSpring(1, springParams),
|
|
288
|
+
)
|
|
289
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Image } from 'react-native'
|
|
2
|
+
import { createMotionComponent } from './createMotionComponent'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Animatable `Image`. Inherits `Image`'s prop surface, with `animate` /
|
|
6
|
+
* `initial` / `exit` / `transition` typed against `ImageStyle` (so
|
|
7
|
+
* `tintColor` is accepted on `animate` here, but rejected on `Motion.View`).
|
|
8
|
+
*/
|
|
9
|
+
export const MotionImage = createMotionComponent(Image)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Pressable } from 'react-native'
|
|
2
|
+
import { createMotionComponent } from './createMotionComponent'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Animatable `Pressable`. The `gesture` prop's `pressed` sub-state hooks into
|
|
6
|
+
* Pressable's native `onPressIn` / `onPressOut` via the factory's handler
|
|
7
|
+
* composition, picking up touch slop and accessibility semantics for free.
|
|
8
|
+
*
|
|
9
|
+
* Note: the function-style `style={(state) => ...}` Pressable form is not
|
|
10
|
+
* supported. Drive press-state styling through `gesture.pressed` instead.
|
|
11
|
+
*/
|
|
12
|
+
export const MotionPressable = createMotionComponent(Pressable)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ScrollView } from 'react-native'
|
|
2
|
+
import { createMotionComponent } from './createMotionComponent'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Animatable `ScrollView`. Animations apply to the scroll container itself —
|
|
6
|
+
* useful for entrance transforms or fades on the whole list. Scroll-position
|
|
7
|
+
* driven animation (`useScroll`) lands in the values layer.
|
|
8
|
+
*/
|
|
9
|
+
export const MotionScrollView = createMotionComponent(ScrollView)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Text } from 'react-native'
|
|
2
|
+
import { createMotionComponent } from './createMotionComponent'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Animatable `Text`. Inherits `Text`'s prop surface, with `animate` /
|
|
6
|
+
* `initial` / `exit` / `transition` typed against `TextStyle`.
|
|
7
|
+
*/
|
|
8
|
+
export const MotionText = createMotionComponent(Text)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { View } from 'react-native'
|
|
2
|
+
import { createMotionComponent } from './createMotionComponent'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Animatable `View`. Inherits `View`'s prop surface, with `animate` /
|
|
6
|
+
* `initial` / `exit` / `transition` typed against `ViewStyle`.
|
|
7
|
+
*/
|
|
8
|
+
export const MotionView = createMotionComponent(View)
|