react-native-divkit 1.7.0 → 1.8.1
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/README.md +17 -16
- package/dist/DivKit.d.ts.map +1 -1
- package/dist/DivKit.js +109 -1
- package/dist/DivKit.js.map +1 -1
- package/dist/components/pager/utils.d.ts.map +1 -1
- package/dist/components/pager/utils.js +17 -4
- package/dist/components/pager/utils.js.map +1 -1
- package/dist/components/state/DivState.d.ts +11 -12
- package/dist/components/state/DivState.d.ts.map +1 -1
- package/dist/components/state/DivState.js +263 -35
- package/dist/components/state/DivState.js.map +1 -1
- package/dist/components/utilities/Background.d.ts.map +1 -1
- package/dist/components/utilities/Background.js +4 -3
- package/dist/components/utilities/Background.js.map +1 -1
- package/dist/components/utilities/Outer.d.ts.map +1 -1
- package/dist/components/utilities/Outer.js +172 -76
- package/dist/components/utilities/Outer.js.map +1 -1
- package/dist/context/DivStateScopeContext.d.ts +18 -0
- package/dist/context/DivStateScopeContext.d.ts.map +1 -0
- package/dist/context/DivStateScopeContext.js +7 -0
- package/dist/context/DivStateScopeContext.js.map +1 -0
- package/dist/hooks/useAppearanceTransition.d.ts +86 -0
- package/dist/hooks/useAppearanceTransition.d.ts.map +1 -0
- package/dist/hooks/useAppearanceTransition.js +490 -0
- package/dist/hooks/useAppearanceTransition.js.map +1 -0
- package/dist/hooks/useChangeBoundsTransition.d.ts +46 -0
- package/dist/hooks/useChangeBoundsTransition.d.ts.map +1 -0
- package/dist/hooks/useChangeBoundsTransition.js +151 -0
- package/dist/hooks/useChangeBoundsTransition.js.map +1 -0
- package/dist/utils/configureChangeBoundsLayout.d.ts +11 -0
- package/dist/utils/configureChangeBoundsLayout.d.ts.map +1 -0
- package/dist/utils/configureChangeBoundsLayout.js +65 -0
- package/dist/utils/configureChangeBoundsLayout.js.map +1 -0
- package/dist/utils/flattenTransition.d.ts +5 -0
- package/dist/utils/flattenTransition.d.ts.map +1 -0
- package/dist/utils/flattenTransition.js +27 -0
- package/dist/utils/flattenTransition.js.map +1 -0
- package/package.json +2 -1
- package/src/DivKit.tsx +125 -2
- package/src/components/pager/utils.ts +18 -4
- package/src/components/state/DivState.tsx +308 -39
- package/src/components/utilities/Background.tsx +4 -3
- package/src/components/utilities/Outer.tsx +188 -73
- package/src/context/DivStateScopeContext.tsx +23 -0
- package/src/hooks/useAppearanceTransition.ts +621 -0
- package/src/hooks/useChangeBoundsTransition.ts +193 -0
- package/src/utils/configureChangeBoundsLayout.ts +74 -0
- package/src/utils/flattenTransition.ts +36 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Animated, Dimensions, Easing, EasingFunction } from 'react-native';
|
|
3
|
+
import type { MaybeMissing } from '../expressions/json';
|
|
4
|
+
import type {
|
|
5
|
+
AnyTransition,
|
|
6
|
+
AppearanceTransition,
|
|
7
|
+
FadeTransition,
|
|
8
|
+
ScaleTransition,
|
|
9
|
+
SlideTransition,
|
|
10
|
+
Visibility
|
|
11
|
+
} from '../types/base';
|
|
12
|
+
import type { Interpolation } from '../../typings/common';
|
|
13
|
+
import { flattenAppearanceTransition } from '../utils/flattenTransition';
|
|
14
|
+
|
|
15
|
+
function interpolationToEasing(interpolator: Interpolation | undefined): EasingFunction {
|
|
16
|
+
switch (interpolator) {
|
|
17
|
+
case 'linear': return Easing.linear;
|
|
18
|
+
case 'ease': return Easing.ease;
|
|
19
|
+
case 'ease_in': return Easing.in(Easing.ease);
|
|
20
|
+
case 'ease_out': return Easing.out(Easing.ease);
|
|
21
|
+
case 'ease_in_out': return Easing.inOut(Easing.ease);
|
|
22
|
+
case 'spring': return Easing.inOut(Easing.ease);
|
|
23
|
+
default: return Easing.inOut(Easing.ease);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface NormalizedTransition {
|
|
28
|
+
hasFade: boolean;
|
|
29
|
+
hasScale: boolean;
|
|
30
|
+
hasSlide: boolean;
|
|
31
|
+
|
|
32
|
+
fadeAlpha: number;
|
|
33
|
+
scaleValue: number;
|
|
34
|
+
scalePivotX: number;
|
|
35
|
+
scalePivotY: number;
|
|
36
|
+
slideEdge: 'left' | 'top' | 'right' | 'bottom';
|
|
37
|
+
slideDistance: number | null;
|
|
38
|
+
|
|
39
|
+
fadeDuration: number;
|
|
40
|
+
fadeDelay: number;
|
|
41
|
+
fadeEasing: EasingFunction;
|
|
42
|
+
|
|
43
|
+
scaleDuration: number;
|
|
44
|
+
scaleDelay: number;
|
|
45
|
+
scaleEasing: EasingFunction;
|
|
46
|
+
|
|
47
|
+
slideDuration: number;
|
|
48
|
+
slideDelay: number;
|
|
49
|
+
slideEasing: EasingFunction;
|
|
50
|
+
|
|
51
|
+
totalDuration: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalize(
|
|
55
|
+
transition: MaybeMissing<AppearanceTransition> | undefined
|
|
56
|
+
): NormalizedTransition {
|
|
57
|
+
const res: NormalizedTransition = {
|
|
58
|
+
hasFade: false,
|
|
59
|
+
hasScale: false,
|
|
60
|
+
hasSlide: false,
|
|
61
|
+
fadeAlpha: 0,
|
|
62
|
+
scaleValue: 0,
|
|
63
|
+
scalePivotX: 0.5,
|
|
64
|
+
scalePivotY: 0.5,
|
|
65
|
+
slideEdge: 'bottom',
|
|
66
|
+
slideDistance: null,
|
|
67
|
+
fadeDuration: 0,
|
|
68
|
+
fadeDelay: 0,
|
|
69
|
+
fadeEasing: Easing.inOut(Easing.ease),
|
|
70
|
+
scaleDuration: 0,
|
|
71
|
+
scaleDelay: 0,
|
|
72
|
+
scaleEasing: Easing.inOut(Easing.ease),
|
|
73
|
+
slideDuration: 0,
|
|
74
|
+
slideDelay: 0,
|
|
75
|
+
slideEasing: Easing.inOut(Easing.ease),
|
|
76
|
+
totalDuration: 0
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (!transition) return res;
|
|
80
|
+
|
|
81
|
+
const items = flattenAppearanceTransition(transition);
|
|
82
|
+
for (const itAny of items) {
|
|
83
|
+
const it = itAny as MaybeMissing<AnyTransition>;
|
|
84
|
+
const duration = Math.max(0, (it as any).duration ?? 300);
|
|
85
|
+
const delay = Math.max(0, (it as any).start_delay ?? 0);
|
|
86
|
+
const easing = interpolationToEasing((it as any).interpolator);
|
|
87
|
+
res.totalDuration = Math.max(res.totalDuration, duration + delay);
|
|
88
|
+
|
|
89
|
+
if (it.type === 'fade') {
|
|
90
|
+
res.hasFade = true;
|
|
91
|
+
const fade = it as MaybeMissing<FadeTransition>;
|
|
92
|
+
res.fadeAlpha = typeof fade.alpha === 'number' ? fade.alpha : 0;
|
|
93
|
+
res.fadeDuration = duration;
|
|
94
|
+
res.fadeDelay = delay;
|
|
95
|
+
res.fadeEasing = easing;
|
|
96
|
+
} else if (it.type === 'scale') {
|
|
97
|
+
res.hasScale = true;
|
|
98
|
+
const sc = it as MaybeMissing<ScaleTransition>;
|
|
99
|
+
res.scaleValue = typeof sc.scale === 'number' ? sc.scale : 0;
|
|
100
|
+
res.scalePivotX = typeof sc.pivot_x === 'number' ? sc.pivot_x : 0.5;
|
|
101
|
+
res.scalePivotY = typeof sc.pivot_y === 'number' ? sc.pivot_y : 0.5;
|
|
102
|
+
res.scaleDuration = duration;
|
|
103
|
+
res.scaleDelay = delay;
|
|
104
|
+
res.scaleEasing = easing;
|
|
105
|
+
} else if (it.type === 'slide') {
|
|
106
|
+
res.hasSlide = true;
|
|
107
|
+
const sl = it as MaybeMissing<SlideTransition>;
|
|
108
|
+
res.slideEdge = (sl.edge ?? 'bottom') as 'left' | 'top' | 'right' | 'bottom';
|
|
109
|
+
const dim: any = sl.distance;
|
|
110
|
+
const distVal = dim && typeof dim.value === 'number' ? dim.value : null;
|
|
111
|
+
res.slideDistance = distVal;
|
|
112
|
+
res.slideDuration = duration;
|
|
113
|
+
res.slideDelay = delay;
|
|
114
|
+
res.slideEasing = easing;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return res;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function slideOffsetFor(edge: 'left' | 'top' | 'right' | 'bottom', distance: number | null) {
|
|
122
|
+
const win = Dimensions.get('window');
|
|
123
|
+
const dx = distance ?? win.width;
|
|
124
|
+
const dy = distance ?? win.height;
|
|
125
|
+
switch (edge) {
|
|
126
|
+
case 'left': return { tx: -dx, ty: 0 };
|
|
127
|
+
case 'right': return { tx: dx, ty: 0 };
|
|
128
|
+
case 'top': return { tx: 0, ty: -dy };
|
|
129
|
+
case 'bottom':
|
|
130
|
+
default: return { tx: 0, ty: dy };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface AppearanceTransitionOptions {
|
|
135
|
+
visibility: Visibility;
|
|
136
|
+
transitionIn?: MaybeMissing<AppearanceTransition>;
|
|
137
|
+
transitionOut?: MaybeMissing<AppearanceTransition>;
|
|
138
|
+
/**
|
|
139
|
+
* When false, transitions are skipped (used during initial mount when visibility starts as 'visible'
|
|
140
|
+
* and you don't want a flicker). Default: true.
|
|
141
|
+
*/
|
|
142
|
+
enabled?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* 'visibility' (default) — transitions are driven by the visibility prop changing.
|
|
145
|
+
* 'imperative' — visibility changes do not auto-play; the consumer must call playOut/playIn.
|
|
146
|
+
* 'auto-in' — like 'imperative' but transition_in is played automatically on first mount.
|
|
147
|
+
*/
|
|
148
|
+
mode?: 'visibility' | 'imperative' | 'auto-in';
|
|
149
|
+
/**
|
|
150
|
+
* Called once right before the wrapper collapses layout (target visibility 'gone'
|
|
151
|
+
* and out animation just finished). Use to queue LayoutAnimation for parents.
|
|
152
|
+
*/
|
|
153
|
+
onBeforeCollapse?: () => void;
|
|
154
|
+
/**
|
|
155
|
+
* Called once right before the wrapper mounts after being collapsed
|
|
156
|
+
* (visibility goes back to 'visible'). Use to queue LayoutAnimation for parents.
|
|
157
|
+
*/
|
|
158
|
+
onBeforeExpand?: () => void;
|
|
159
|
+
/**
|
|
160
|
+
* Measured width of the element (from onLayout). Used to emulate off-center pivot for scale
|
|
161
|
+
* via translate-scale-translate. If absent, scale is applied from the view center (pivot 0.5/0.5).
|
|
162
|
+
*/
|
|
163
|
+
layoutWidth?: number;
|
|
164
|
+
/** Measured height of the element. */
|
|
165
|
+
layoutHeight?: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
type AnyTransformValue = number | Animated.AnimatedInterpolation<number> | Animated.Value;
|
|
169
|
+
|
|
170
|
+
export interface AppearanceTransitionResult {
|
|
171
|
+
/**
|
|
172
|
+
* True while the children should be present in the tree.
|
|
173
|
+
* Goes false only AFTER transition_out completed and target visibility is 'gone'/'invisible'.
|
|
174
|
+
*/
|
|
175
|
+
rendered: boolean;
|
|
176
|
+
/** True after transition_out completed; the wrapper should collapse layout (return null). */
|
|
177
|
+
collapsed: boolean;
|
|
178
|
+
/** Animated opacity value (or constant). */
|
|
179
|
+
opacity: Animated.Value | number;
|
|
180
|
+
/**
|
|
181
|
+
* Ready-to-use transform array for Animated.View. Combines slide translate, scale, and
|
|
182
|
+
* pivot translate-scale-translate compensation. Empty array if no transition produces transforms.
|
|
183
|
+
*/
|
|
184
|
+
transform: Array<{ translateX?: AnyTransformValue; translateY?: AnyTransformValue; scale?: AnyTransformValue }>;
|
|
185
|
+
/**
|
|
186
|
+
* Imperatively play transition_out (without affecting rendered/collapsed state).
|
|
187
|
+
* Resolves when animation finishes (or immediately if no transition_out is specified).
|
|
188
|
+
*/
|
|
189
|
+
playOut: () => Promise<void>;
|
|
190
|
+
/**
|
|
191
|
+
* Imperatively reset values to transition_in start, then animate to identity.
|
|
192
|
+
* Resolves when animation finishes (or immediately if no transition_in is specified).
|
|
193
|
+
*/
|
|
194
|
+
playIn: () => Promise<void>;
|
|
195
|
+
/** Whether transition_out is specified (helps consumers decide whether to wait for playOut). */
|
|
196
|
+
hasTransitionOut: boolean;
|
|
197
|
+
/** Whether transition_in is specified. */
|
|
198
|
+
hasTransitionIn: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Hook that drives transition_in / transition_out for an Outer wrapper.
|
|
203
|
+
*
|
|
204
|
+
* - On first mount: NO transition_in is played (matches Web Outer.svelte behavior with
|
|
205
|
+
* isVisibilityInited). Use mode='auto-in' if you do want a first-mount play (DivState
|
|
206
|
+
* uses this for newly-mounted children of a switched state).
|
|
207
|
+
* - On change visible→gone/invisible with a transition_out: animates identity → end values,
|
|
208
|
+
* then collapses.
|
|
209
|
+
* - On change gone/invisible→visible: re-mounts and plays transition_in.
|
|
210
|
+
*
|
|
211
|
+
* Only fade/scale/slide are supported here (AppearanceTransition).
|
|
212
|
+
*/
|
|
213
|
+
export function useAppearanceTransition(
|
|
214
|
+
opts: AppearanceTransitionOptions
|
|
215
|
+
): AppearanceTransitionResult {
|
|
216
|
+
const {
|
|
217
|
+
visibility,
|
|
218
|
+
transitionIn,
|
|
219
|
+
transitionOut,
|
|
220
|
+
enabled = true,
|
|
221
|
+
mode = 'visibility',
|
|
222
|
+
onBeforeCollapse,
|
|
223
|
+
onBeforeExpand,
|
|
224
|
+
layoutWidth,
|
|
225
|
+
layoutHeight
|
|
226
|
+
} = opts;
|
|
227
|
+
const onBeforeCollapseRef = useRef(onBeforeCollapse);
|
|
228
|
+
const onBeforeExpandRef = useRef(onBeforeExpand);
|
|
229
|
+
onBeforeCollapseRef.current = onBeforeCollapse;
|
|
230
|
+
onBeforeExpandRef.current = onBeforeExpand;
|
|
231
|
+
|
|
232
|
+
const inSpec = useMemo(() => normalize(transitionIn), [transitionIn]);
|
|
233
|
+
const outSpec = useMemo(() => normalize(transitionOut), [transitionOut]);
|
|
234
|
+
|
|
235
|
+
// Refs to Animated values — created once
|
|
236
|
+
const opacity = useRef(new Animated.Value(visibility === 'visible' ? 1 : 0)).current;
|
|
237
|
+
const scale = useRef(new Animated.Value(1)).current;
|
|
238
|
+
const slideTx = useRef(new Animated.Value(0)).current;
|
|
239
|
+
const slideTy = useRef(new Animated.Value(0)).current;
|
|
240
|
+
|
|
241
|
+
const prevVisibilityRef = useRef<Visibility>(visibility);
|
|
242
|
+
const isFirstRunRef = useRef(true);
|
|
243
|
+
const inFlightRef = useRef<Animated.CompositeAnimation | null>(null);
|
|
244
|
+
|
|
245
|
+
const [rendered, setRendered] = useState<boolean>(visibility !== 'gone');
|
|
246
|
+
const [collapsed, setCollapsed] = useState<boolean>(visibility === 'gone');
|
|
247
|
+
|
|
248
|
+
// Active scale spec for transform composition (pivot + endpoint).
|
|
249
|
+
// Updated when we start an "in" or "out" scale animation; used to build interpolated transform.
|
|
250
|
+
const [activeScale, setActiveScale] = useState<{
|
|
251
|
+
value: number;
|
|
252
|
+
pivotX: number;
|
|
253
|
+
pivotY: number;
|
|
254
|
+
} | null>(() => {
|
|
255
|
+
if (visibility === 'visible' && (inSpec.hasScale)) {
|
|
256
|
+
return { value: inSpec.scaleValue, pivotX: inSpec.scalePivotX, pivotY: inSpec.scalePivotY };
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
const prev = prevVisibilityRef.current;
|
|
263
|
+
prevVisibilityRef.current = visibility;
|
|
264
|
+
|
|
265
|
+
// Imperative mode: visibility prop is ignored for transitions — consumer drives via playOut/playIn.
|
|
266
|
+
// Only sync rendered/collapsed for static defaults; never auto-play.
|
|
267
|
+
if (mode === 'imperative') {
|
|
268
|
+
if (isFirstRunRef.current) {
|
|
269
|
+
isFirstRunRef.current = false;
|
|
270
|
+
if (visibility === 'visible') {
|
|
271
|
+
opacity.setValue(1);
|
|
272
|
+
scale.setValue(1);
|
|
273
|
+
slideTx.setValue(0);
|
|
274
|
+
slideTy.setValue(0);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// auto-in mode: play transition_in on first mount, then ignore visibility changes
|
|
281
|
+
// (the consumer — e.g. DivState — drives transition_out programmatically).
|
|
282
|
+
if (mode === 'auto-in' && !isFirstRunRef.current) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (isFirstRunRef.current) {
|
|
287
|
+
isFirstRunRef.current = false;
|
|
288
|
+
// Matches Web Outer.svelte: на первом монтаже не играем transition_in —
|
|
289
|
+
// он запускается только при последующем изменении visibility (см. isVisibilityInited).
|
|
290
|
+
if (visibility === 'visible') {
|
|
291
|
+
opacity.setValue(1);
|
|
292
|
+
scale.setValue(1);
|
|
293
|
+
slideTx.setValue(0);
|
|
294
|
+
slideTy.setValue(0);
|
|
295
|
+
} else {
|
|
296
|
+
opacity.setValue(0);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// visible → gone/invisible
|
|
302
|
+
if (prev === 'visible' && visibility !== 'visible') {
|
|
303
|
+
if (enabled && (outSpec.hasFade || outSpec.hasScale || outSpec.hasSlide)) {
|
|
304
|
+
setRendered(true);
|
|
305
|
+
setCollapsed(false);
|
|
306
|
+
if (outSpec.hasScale) {
|
|
307
|
+
setActiveScale({ value: outSpec.scaleValue, pivotX: outSpec.scalePivotX, pivotY: outSpec.scalePivotY });
|
|
308
|
+
}
|
|
309
|
+
runOut(visibility);
|
|
310
|
+
} else {
|
|
311
|
+
opacity.setValue(visibility === 'invisible' ? 0 : 0);
|
|
312
|
+
if (visibility === 'gone') {
|
|
313
|
+
onBeforeCollapseRef.current?.();
|
|
314
|
+
setRendered(false);
|
|
315
|
+
setCollapsed(true);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// gone/invisible → visible
|
|
322
|
+
if (prev !== 'visible' && visibility === 'visible') {
|
|
323
|
+
if (prev === 'gone') {
|
|
324
|
+
onBeforeExpandRef.current?.();
|
|
325
|
+
}
|
|
326
|
+
setRendered(true);
|
|
327
|
+
setCollapsed(false);
|
|
328
|
+
if (enabled && (inSpec.hasFade || inSpec.hasScale || inSpec.hasSlide)) {
|
|
329
|
+
if (inSpec.hasFade) opacity.setValue(inSpec.fadeAlpha);
|
|
330
|
+
else opacity.setValue(1);
|
|
331
|
+
if (inSpec.hasScale) {
|
|
332
|
+
scale.setValue(inSpec.scaleValue);
|
|
333
|
+
setActiveScale({ value: inSpec.scaleValue, pivotX: inSpec.scalePivotX, pivotY: inSpec.scalePivotY });
|
|
334
|
+
} else {
|
|
335
|
+
scale.setValue(1);
|
|
336
|
+
setActiveScale(null);
|
|
337
|
+
}
|
|
338
|
+
if (inSpec.hasSlide) {
|
|
339
|
+
const off = slideOffsetFor(inSpec.slideEdge, inSpec.slideDistance);
|
|
340
|
+
slideTx.setValue(off.tx);
|
|
341
|
+
slideTy.setValue(off.ty);
|
|
342
|
+
} else {
|
|
343
|
+
slideTx.setValue(0);
|
|
344
|
+
slideTy.setValue(0);
|
|
345
|
+
}
|
|
346
|
+
runIn();
|
|
347
|
+
} else {
|
|
348
|
+
opacity.setValue(1);
|
|
349
|
+
scale.setValue(1);
|
|
350
|
+
slideTx.setValue(0);
|
|
351
|
+
slideTy.setValue(0);
|
|
352
|
+
setActiveScale(null);
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (prev !== 'visible' && visibility !== 'visible') {
|
|
358
|
+
setRendered(visibility !== 'gone');
|
|
359
|
+
setCollapsed(visibility === 'gone');
|
|
360
|
+
}
|
|
361
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
362
|
+
}, [visibility, inSpec, outSpec, enabled, mode]);
|
|
363
|
+
|
|
364
|
+
function stopInFlight() {
|
|
365
|
+
if (inFlightRef.current) {
|
|
366
|
+
inFlightRef.current.stop();
|
|
367
|
+
inFlightRef.current = null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Build the list of timings for a transition_in (target = identity). */
|
|
372
|
+
function buildInAnimations(): Animated.CompositeAnimation[] {
|
|
373
|
+
const anims: Animated.CompositeAnimation[] = [];
|
|
374
|
+
if (inSpec.hasFade) {
|
|
375
|
+
anims.push(Animated.timing(opacity, {
|
|
376
|
+
toValue: 1, duration: inSpec.fadeDuration, delay: inSpec.fadeDelay,
|
|
377
|
+
easing: inSpec.fadeEasing, useNativeDriver: true
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
if (inSpec.hasScale) {
|
|
381
|
+
anims.push(Animated.timing(scale, {
|
|
382
|
+
toValue: 1, duration: inSpec.scaleDuration, delay: inSpec.scaleDelay,
|
|
383
|
+
easing: inSpec.scaleEasing, useNativeDriver: true
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
if (inSpec.hasSlide) {
|
|
387
|
+
anims.push(Animated.timing(slideTx, {
|
|
388
|
+
toValue: 0, duration: inSpec.slideDuration, delay: inSpec.slideDelay,
|
|
389
|
+
easing: inSpec.slideEasing, useNativeDriver: true
|
|
390
|
+
}));
|
|
391
|
+
anims.push(Animated.timing(slideTy, {
|
|
392
|
+
toValue: 0, duration: inSpec.slideDuration, delay: inSpec.slideDelay,
|
|
393
|
+
easing: inSpec.slideEasing, useNativeDriver: true
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
return anims;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Build the list of timings for a transition_out (target = end values). */
|
|
400
|
+
function buildOutAnimations(): Animated.CompositeAnimation[] {
|
|
401
|
+
const anims: Animated.CompositeAnimation[] = [];
|
|
402
|
+
if (outSpec.hasFade) {
|
|
403
|
+
anims.push(Animated.timing(opacity, {
|
|
404
|
+
toValue: outSpec.fadeAlpha, duration: outSpec.fadeDuration, delay: outSpec.fadeDelay,
|
|
405
|
+
easing: outSpec.fadeEasing, useNativeDriver: true
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
if (outSpec.hasScale) {
|
|
409
|
+
anims.push(Animated.timing(scale, {
|
|
410
|
+
toValue: outSpec.scaleValue, duration: outSpec.scaleDuration, delay: outSpec.scaleDelay,
|
|
411
|
+
easing: outSpec.scaleEasing, useNativeDriver: true
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
if (outSpec.hasSlide) {
|
|
415
|
+
const off = slideOffsetFor(outSpec.slideEdge, outSpec.slideDistance);
|
|
416
|
+
anims.push(Animated.timing(slideTx, {
|
|
417
|
+
toValue: off.tx, duration: outSpec.slideDuration, delay: outSpec.slideDelay,
|
|
418
|
+
easing: outSpec.slideEasing, useNativeDriver: true
|
|
419
|
+
}));
|
|
420
|
+
anims.push(Animated.timing(slideTy, {
|
|
421
|
+
toValue: off.ty, duration: outSpec.slideDuration, delay: outSpec.slideDelay,
|
|
422
|
+
easing: outSpec.slideEasing, useNativeDriver: true
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
return anims;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function runIn(): Promise<void> {
|
|
429
|
+
stopInFlight();
|
|
430
|
+
const anims = buildInAnimations();
|
|
431
|
+
if (anims.length === 0) return Promise.resolve();
|
|
432
|
+
const comp = Animated.parallel(anims);
|
|
433
|
+
inFlightRef.current = comp;
|
|
434
|
+
return new Promise<void>(resolve => {
|
|
435
|
+
comp.start(({ finished }) => {
|
|
436
|
+
if (inFlightRef.current === comp) inFlightRef.current = null;
|
|
437
|
+
if (finished) {
|
|
438
|
+
if (inSpec.hasFade) opacity.setValue(1);
|
|
439
|
+
if (inSpec.hasScale) scale.setValue(1);
|
|
440
|
+
if (inSpec.hasSlide) {
|
|
441
|
+
slideTx.setValue(0);
|
|
442
|
+
slideTy.setValue(0);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
resolve();
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function runOut(target: Visibility): Promise<void> {
|
|
451
|
+
stopInFlight();
|
|
452
|
+
const anims = buildOutAnimations();
|
|
453
|
+
if (anims.length === 0) {
|
|
454
|
+
if (target === 'gone') {
|
|
455
|
+
onBeforeCollapseRef.current?.();
|
|
456
|
+
setRendered(false);
|
|
457
|
+
setCollapsed(true);
|
|
458
|
+
}
|
|
459
|
+
return Promise.resolve();
|
|
460
|
+
}
|
|
461
|
+
const comp = Animated.parallel(anims);
|
|
462
|
+
inFlightRef.current = comp;
|
|
463
|
+
return new Promise<void>(resolve => {
|
|
464
|
+
comp.start(({ finished }) => {
|
|
465
|
+
if (inFlightRef.current === comp) inFlightRef.current = null;
|
|
466
|
+
if (!finished) {
|
|
467
|
+
resolve();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (target === 'gone') {
|
|
471
|
+
onBeforeCollapseRef.current?.();
|
|
472
|
+
setRendered(false);
|
|
473
|
+
setCollapsed(true);
|
|
474
|
+
}
|
|
475
|
+
resolve();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Imperative API: play transition_out without touching rendered/collapsed state.
|
|
482
|
+
* Caller decides whether to unmount the element after the promise resolves.
|
|
483
|
+
*/
|
|
484
|
+
const playOut = useCallback((): Promise<void> => {
|
|
485
|
+
stopInFlight();
|
|
486
|
+
const anims = buildOutAnimations();
|
|
487
|
+
if (anims.length === 0) return Promise.resolve();
|
|
488
|
+
const comp = Animated.parallel(anims);
|
|
489
|
+
inFlightRef.current = comp;
|
|
490
|
+
if (outSpec.hasScale) {
|
|
491
|
+
setActiveScale({
|
|
492
|
+
value: outSpec.scaleValue,
|
|
493
|
+
pivotX: outSpec.scalePivotX,
|
|
494
|
+
pivotY: outSpec.scalePivotY
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return new Promise<void>(resolve => {
|
|
498
|
+
comp.start(() => {
|
|
499
|
+
if (inFlightRef.current === comp) inFlightRef.current = null;
|
|
500
|
+
resolve();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
504
|
+
}, [outSpec]);
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Imperative API: reset values to transition_in start, then animate to identity.
|
|
508
|
+
*/
|
|
509
|
+
const playIn = useCallback((): Promise<void> => {
|
|
510
|
+
stopInFlight();
|
|
511
|
+
// Reset values to start
|
|
512
|
+
if (inSpec.hasFade) opacity.setValue(inSpec.fadeAlpha);
|
|
513
|
+
if (inSpec.hasScale) {
|
|
514
|
+
scale.setValue(inSpec.scaleValue);
|
|
515
|
+
setActiveScale({
|
|
516
|
+
value: inSpec.scaleValue,
|
|
517
|
+
pivotX: inSpec.scalePivotX,
|
|
518
|
+
pivotY: inSpec.scalePivotY
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (inSpec.hasSlide) {
|
|
522
|
+
const off = slideOffsetFor(inSpec.slideEdge, inSpec.slideDistance);
|
|
523
|
+
slideTx.setValue(off.tx);
|
|
524
|
+
slideTy.setValue(off.ty);
|
|
525
|
+
}
|
|
526
|
+
const anims = buildInAnimations();
|
|
527
|
+
if (anims.length === 0) return Promise.resolve();
|
|
528
|
+
const comp = Animated.parallel(anims);
|
|
529
|
+
inFlightRef.current = comp;
|
|
530
|
+
return new Promise<void>(resolve => {
|
|
531
|
+
comp.start(({ finished }) => {
|
|
532
|
+
if (inFlightRef.current === comp) inFlightRef.current = null;
|
|
533
|
+
if (finished) {
|
|
534
|
+
if (inSpec.hasFade) opacity.setValue(1);
|
|
535
|
+
if (inSpec.hasScale) scale.setValue(1);
|
|
536
|
+
if (inSpec.hasSlide) {
|
|
537
|
+
slideTx.setValue(0);
|
|
538
|
+
slideTy.setValue(0);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
resolve();
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
545
|
+
}, [inSpec]);
|
|
546
|
+
|
|
547
|
+
const hasTransitionIn = inSpec.hasFade || inSpec.hasScale || inSpec.hasSlide;
|
|
548
|
+
const hasTransitionOut = outSpec.hasFade || outSpec.hasScale || outSpec.hasSlide;
|
|
549
|
+
|
|
550
|
+
useEffect(() => {
|
|
551
|
+
return () => {
|
|
552
|
+
if (inFlightRef.current) {
|
|
553
|
+
inFlightRef.current.stop();
|
|
554
|
+
inFlightRef.current = null;
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
}, []);
|
|
558
|
+
|
|
559
|
+
// Build the final transform array, combining slide translate + scale + pivot compensation.
|
|
560
|
+
const transform = useMemo(() => {
|
|
561
|
+
const out: Array<{ translateX?: AnyTransformValue; translateY?: AnyTransformValue; scale?: AnyTransformValue }> = [];
|
|
562
|
+
const hasAnySlide = inSpec.hasSlide || outSpec.hasSlide;
|
|
563
|
+
const hasAnyScale = inSpec.hasScale || outSpec.hasScale;
|
|
564
|
+
if (!hasAnySlide && !hasAnyScale) return out;
|
|
565
|
+
|
|
566
|
+
// Build translateX/Y from slide + pivot interpolation.
|
|
567
|
+
let txExpr: AnyTransformValue | null = hasAnySlide ? slideTx : null;
|
|
568
|
+
let tyExpr: AnyTransformValue | null = hasAnySlide ? slideTy : null;
|
|
569
|
+
|
|
570
|
+
if (
|
|
571
|
+
hasAnyScale &&
|
|
572
|
+
activeScale &&
|
|
573
|
+
(activeScale.pivotX !== 0.5 || activeScale.pivotY !== 0.5) &&
|
|
574
|
+
typeof layoutWidth === 'number' &&
|
|
575
|
+
typeof layoutHeight === 'number' &&
|
|
576
|
+
layoutWidth > 0 &&
|
|
577
|
+
layoutHeight > 0
|
|
578
|
+
) {
|
|
579
|
+
// pivotTx = (pivotX - 0.5) * width * (1 - scale)
|
|
580
|
+
// Interpolate over [min(value,1), max(value,1)] mapped to corresponding endpoints.
|
|
581
|
+
const a = Math.min(activeScale.value, 1);
|
|
582
|
+
const b = Math.max(activeScale.value, 1);
|
|
583
|
+
if (a !== b && activeScale.pivotX !== 0.5) {
|
|
584
|
+
const offsetA = (activeScale.pivotX - 0.5) * layoutWidth * (1 - a);
|
|
585
|
+
const offsetB = (activeScale.pivotX - 0.5) * layoutWidth * (1 - b);
|
|
586
|
+
const pivotTx = scale.interpolate({
|
|
587
|
+
inputRange: [a, b],
|
|
588
|
+
outputRange: [offsetA, offsetB],
|
|
589
|
+
extrapolate: 'clamp'
|
|
590
|
+
});
|
|
591
|
+
txExpr = txExpr !== null ? Animated.add(txExpr as Animated.Animated, pivotTx) as AnyTransformValue : pivotTx;
|
|
592
|
+
}
|
|
593
|
+
if (a !== b && activeScale.pivotY !== 0.5) {
|
|
594
|
+
const offsetA = (activeScale.pivotY - 0.5) * layoutHeight * (1 - a);
|
|
595
|
+
const offsetB = (activeScale.pivotY - 0.5) * layoutHeight * (1 - b);
|
|
596
|
+
const pivotTy = scale.interpolate({
|
|
597
|
+
inputRange: [a, b],
|
|
598
|
+
outputRange: [offsetA, offsetB],
|
|
599
|
+
extrapolate: 'clamp'
|
|
600
|
+
});
|
|
601
|
+
tyExpr = tyExpr !== null ? Animated.add(tyExpr as Animated.Animated, pivotTy) as AnyTransformValue : pivotTy;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (txExpr !== null) out.push({ translateX: txExpr });
|
|
606
|
+
if (tyExpr !== null) out.push({ translateY: tyExpr });
|
|
607
|
+
if (hasAnyScale) out.push({ scale });
|
|
608
|
+
return out;
|
|
609
|
+
}, [inSpec, outSpec, activeScale, layoutWidth, layoutHeight, slideTx, slideTy, scale]);
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
rendered,
|
|
613
|
+
collapsed,
|
|
614
|
+
opacity,
|
|
615
|
+
transform,
|
|
616
|
+
playIn,
|
|
617
|
+
playOut,
|
|
618
|
+
hasTransitionIn,
|
|
619
|
+
hasTransitionOut
|
|
620
|
+
};
|
|
621
|
+
}
|