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,193 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing, EasingFunction, LayoutChangeEvent } from 'react-native';
|
|
3
|
+
import type { MaybeMissing } from '../expressions/json';
|
|
4
|
+
import type { ChangeBoundsTransition, TransitionChange } from '../types/base';
|
|
5
|
+
import type { Interpolation } from '../../typings/common';
|
|
6
|
+
import { flattenChangeTransition } from '../utils/flattenTransition';
|
|
7
|
+
|
|
8
|
+
function interpolationToEasing(interpolator: Interpolation | undefined): EasingFunction {
|
|
9
|
+
switch (interpolator) {
|
|
10
|
+
case 'linear': return Easing.linear;
|
|
11
|
+
case 'ease': return Easing.ease;
|
|
12
|
+
case 'ease_in': return Easing.in(Easing.ease);
|
|
13
|
+
case 'ease_out': return Easing.out(Easing.ease);
|
|
14
|
+
case 'ease_in_out': return Easing.inOut(Easing.ease);
|
|
15
|
+
case 'spring': return Easing.elastic(1);
|
|
16
|
+
default: return Easing.inOut(Easing.ease);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface NormalizedChange {
|
|
21
|
+
duration: number;
|
|
22
|
+
delay: number;
|
|
23
|
+
easing: EasingFunction;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalize(transition: MaybeMissing<TransitionChange> | undefined): NormalizedChange | null {
|
|
27
|
+
if (!transition) return null;
|
|
28
|
+
const items = flattenChangeTransition(transition);
|
|
29
|
+
let longest: MaybeMissing<ChangeBoundsTransition> | null = null;
|
|
30
|
+
let longestTotal = 0;
|
|
31
|
+
for (const it of items) {
|
|
32
|
+
const dur = Math.max(0, (it as any).duration ?? 300);
|
|
33
|
+
const delay = Math.max(0, (it as any).start_delay ?? 0);
|
|
34
|
+
if (dur + delay > longestTotal) {
|
|
35
|
+
longestTotal = dur + delay;
|
|
36
|
+
longest = it;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!longest || longestTotal === 0) return null;
|
|
40
|
+
return {
|
|
41
|
+
duration: Math.max(0, (longest as any).duration ?? 300),
|
|
42
|
+
delay: Math.max(0, (longest as any).start_delay ?? 0),
|
|
43
|
+
easing: interpolationToEasing((longest as any).interpolator)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ChangeBoundsTransitionOptions {
|
|
48
|
+
transitionChange?: MaybeMissing<TransitionChange>;
|
|
49
|
+
/**
|
|
50
|
+
* When true, the element is being unmounted/collapsed and we should NOT play a FLIP transition
|
|
51
|
+
* from previous bounds (the element is going away — that's handled by transition_out).
|
|
52
|
+
*/
|
|
53
|
+
suspended?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ChangeBoundsTransitionResult {
|
|
57
|
+
/** Plug onto Animated.View. Computes delta vs prev layout and starts FLIP timing. */
|
|
58
|
+
onLayout: (e: LayoutChangeEvent) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Transform array to merge into Animated.View style. Empty if no change_bounds spec.
|
|
61
|
+
* Returns translateX/translateY/scaleX/scaleY animated values that ride to identity.
|
|
62
|
+
*/
|
|
63
|
+
transform: Array<{ translateX?: any; translateY?: any; scaleX?: any; scaleY?: any }>;
|
|
64
|
+
/** Width measured by onLayout (handy for pivot calc in appearance hook). */
|
|
65
|
+
layoutWidth: number | undefined;
|
|
66
|
+
layoutHeight: number | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* FLIP (First-Last-Invert-Play) hook for transition_change with custom cubic easing.
|
|
71
|
+
*
|
|
72
|
+
* On each layout change:
|
|
73
|
+
* 1. Capture previous (First) and new (Last) bounds via onLayout.
|
|
74
|
+
* 2. Set transform to translate(-dx, -dy) * scale(prevW/newW, prevH/newH) so the element
|
|
75
|
+
* visually stays at its old position/size (Invert).
|
|
76
|
+
* 3. Animate transform to identity over the spec duration (Play).
|
|
77
|
+
*
|
|
78
|
+
* Limitations:
|
|
79
|
+
* - onLayout reports coords relative to the parent. If the parent itself moves, we will
|
|
80
|
+
* see a position change but interpret it as our own movement — usually fine for items
|
|
81
|
+
* inside a stable container.
|
|
82
|
+
* - useNativeDriver is enabled (transform-only props), so the animation runs on the UI thread.
|
|
83
|
+
* - First layout is treated as the baseline and is not animated.
|
|
84
|
+
*/
|
|
85
|
+
export function useChangeBoundsTransition(
|
|
86
|
+
opts: ChangeBoundsTransitionOptions
|
|
87
|
+
): ChangeBoundsTransitionResult {
|
|
88
|
+
const { transitionChange, suspended } = opts;
|
|
89
|
+
|
|
90
|
+
const spec = useMemo(() => normalize(transitionChange), [transitionChange]);
|
|
91
|
+
|
|
92
|
+
const translateX = useRef(new Animated.Value(0)).current;
|
|
93
|
+
const translateY = useRef(new Animated.Value(0)).current;
|
|
94
|
+
const scaleX = useRef(new Animated.Value(1)).current;
|
|
95
|
+
const scaleY = useRef(new Animated.Value(1)).current;
|
|
96
|
+
|
|
97
|
+
const prevLayoutRef = useRef<{ x: number; y: number; width: number; height: number } | null>(null);
|
|
98
|
+
const sizeRef = useRef<{ width: number; height: number } | undefined>(undefined);
|
|
99
|
+
const inFlightRef = useRef<Animated.CompositeAnimation | null>(null);
|
|
100
|
+
|
|
101
|
+
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
|
102
|
+
const { x, y, width, height } = e.nativeEvent.layout;
|
|
103
|
+
const prev = prevLayoutRef.current;
|
|
104
|
+
prevLayoutRef.current = { x, y, width, height };
|
|
105
|
+
sizeRef.current = { width, height };
|
|
106
|
+
|
|
107
|
+
if (!spec || suspended) return;
|
|
108
|
+
if (!prev) return; // baseline — nothing to invert from
|
|
109
|
+
if (prev.x === x && prev.y === y && prev.width === width && prev.height === height) {
|
|
110
|
+
return; // identical layout
|
|
111
|
+
}
|
|
112
|
+
if (width === 0 || height === 0) return;
|
|
113
|
+
|
|
114
|
+
const sx = width > 0 ? prev.width / width : 1;
|
|
115
|
+
const sy = height > 0 ? prev.height / height : 1;
|
|
116
|
+
const prevCenterX = prev.x + prev.width / 2;
|
|
117
|
+
const prevCenterY = prev.y + prev.height / 2;
|
|
118
|
+
const nextCenterX = x + width / 2;
|
|
119
|
+
const nextCenterY = y + height / 2;
|
|
120
|
+
const dx = prevCenterX - nextCenterX;
|
|
121
|
+
const dy = prevCenterY - nextCenterY;
|
|
122
|
+
|
|
123
|
+
// Skip imperceptible movements
|
|
124
|
+
const SIGNIFICANT = 0.5;
|
|
125
|
+
const SCALE_EPS = 0.01;
|
|
126
|
+
if (
|
|
127
|
+
Math.abs(dx) < SIGNIFICANT &&
|
|
128
|
+
Math.abs(dy) < SIGNIFICANT &&
|
|
129
|
+
Math.abs(sx - 1) < SCALE_EPS &&
|
|
130
|
+
Math.abs(sy - 1) < SCALE_EPS
|
|
131
|
+
) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (inFlightRef.current) {
|
|
136
|
+
inFlightRef.current.stop();
|
|
137
|
+
inFlightRef.current = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Invert: snap to old position/size in transform space
|
|
141
|
+
translateX.setValue(dx);
|
|
142
|
+
translateY.setValue(dy);
|
|
143
|
+
scaleX.setValue(sx);
|
|
144
|
+
scaleY.setValue(sy);
|
|
145
|
+
|
|
146
|
+
// Play to identity
|
|
147
|
+
const comp = Animated.parallel([
|
|
148
|
+
Animated.timing(translateX, {
|
|
149
|
+
toValue: 0, duration: spec.duration, delay: spec.delay,
|
|
150
|
+
easing: spec.easing, useNativeDriver: true
|
|
151
|
+
}),
|
|
152
|
+
Animated.timing(translateY, {
|
|
153
|
+
toValue: 0, duration: spec.duration, delay: spec.delay,
|
|
154
|
+
easing: spec.easing, useNativeDriver: true
|
|
155
|
+
}),
|
|
156
|
+
Animated.timing(scaleX, {
|
|
157
|
+
toValue: 1, duration: spec.duration, delay: spec.delay,
|
|
158
|
+
easing: spec.easing, useNativeDriver: true
|
|
159
|
+
}),
|
|
160
|
+
Animated.timing(scaleY, {
|
|
161
|
+
toValue: 1, duration: spec.duration, delay: spec.delay,
|
|
162
|
+
easing: spec.easing, useNativeDriver: true
|
|
163
|
+
})
|
|
164
|
+
]);
|
|
165
|
+
inFlightRef.current = comp;
|
|
166
|
+
comp.start(({ finished }) => {
|
|
167
|
+
if (inFlightRef.current === comp) inFlightRef.current = null;
|
|
168
|
+
if (finished) {
|
|
169
|
+
translateX.setValue(0);
|
|
170
|
+
translateY.setValue(0);
|
|
171
|
+
scaleX.setValue(1);
|
|
172
|
+
scaleY.setValue(1);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}, [spec, suspended, translateX, translateY, scaleX, scaleY]);
|
|
176
|
+
|
|
177
|
+
const transform = useMemo(() => {
|
|
178
|
+
if (!spec) return [];
|
|
179
|
+
return [
|
|
180
|
+
{ translateX },
|
|
181
|
+
{ translateY },
|
|
182
|
+
{ scaleX },
|
|
183
|
+
{ scaleY }
|
|
184
|
+
];
|
|
185
|
+
}, [spec, translateX, translateY, scaleX, scaleY]);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
onLayout,
|
|
189
|
+
transform,
|
|
190
|
+
layoutWidth: sizeRef.current?.width,
|
|
191
|
+
layoutHeight: sizeRef.current?.height
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { LayoutAnimation, LayoutAnimationConfig, Platform, UIManager } from 'react-native';
|
|
2
|
+
import type { MaybeMissing } from '../expressions/json';
|
|
3
|
+
import type { TransitionChange } from '../types/base';
|
|
4
|
+
import type { Interpolation } from '../../typings/common';
|
|
5
|
+
import { flattenChangeTransition } from './flattenTransition';
|
|
6
|
+
|
|
7
|
+
type LATypeKey = 'linear' | 'easeInEaseOut' | 'easeIn' | 'easeOut' | 'spring';
|
|
8
|
+
|
|
9
|
+
let layoutAnimationEnabled = false;
|
|
10
|
+
|
|
11
|
+
function enableLayoutAnimationIfNeeded(): void {
|
|
12
|
+
if (layoutAnimationEnabled) return;
|
|
13
|
+
layoutAnimationEnabled = true;
|
|
14
|
+
|
|
15
|
+
if (Platform?.OS === 'android') {
|
|
16
|
+
UIManager.setLayoutAnimationEnabledExperimental?.(true);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function interpolatorToLAType(interp: Interpolation | undefined): LATypeKey {
|
|
21
|
+
switch (interp) {
|
|
22
|
+
case 'linear': return 'linear';
|
|
23
|
+
case 'ease': return 'easeInEaseOut';
|
|
24
|
+
case 'ease_in': return 'easeIn';
|
|
25
|
+
case 'ease_out': return 'easeOut';
|
|
26
|
+
case 'ease_in_out': return 'easeInEaseOut';
|
|
27
|
+
case 'spring': return 'spring';
|
|
28
|
+
default: return 'easeInEaseOut';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Triggers a smooth layout transition for the next render, based on a DivKit transition_change spec.
|
|
34
|
+
* Uses React Native's LayoutAnimation API (which respects duration and a coarse easing type,
|
|
35
|
+
* but not arbitrary cubic-bezier curves).
|
|
36
|
+
*
|
|
37
|
+
* Returns true if a transition was queued, false if there is no spec or duration is zero.
|
|
38
|
+
*/
|
|
39
|
+
export function configureChangeBoundsLayout(
|
|
40
|
+
transition: MaybeMissing<TransitionChange> | undefined
|
|
41
|
+
): boolean {
|
|
42
|
+
if (!transition) return false;
|
|
43
|
+
const items = flattenChangeTransition(transition);
|
|
44
|
+
if (items.length === 0) return false;
|
|
45
|
+
|
|
46
|
+
// Pick the longest duration (parallel composition); use the interpolator of that one.
|
|
47
|
+
let duration = 0;
|
|
48
|
+
let delayMax = 0;
|
|
49
|
+
let chosenInterp: Interpolation | undefined;
|
|
50
|
+
for (const it of items) {
|
|
51
|
+
const d = Math.max(0, (it as any).duration ?? 300);
|
|
52
|
+
const delay = Math.max(0, (it as any).start_delay ?? 0);
|
|
53
|
+
if (d > duration) {
|
|
54
|
+
duration = d;
|
|
55
|
+
chosenInterp = (it as any).interpolator as Interpolation | undefined;
|
|
56
|
+
}
|
|
57
|
+
if (delay > delayMax) delayMax = delay;
|
|
58
|
+
}
|
|
59
|
+
if (duration === 0) return false;
|
|
60
|
+
|
|
61
|
+
const typeKey = interpolatorToLAType(chosenInterp);
|
|
62
|
+
const type = LayoutAnimation.Types[typeKey];
|
|
63
|
+
const property = LayoutAnimation.Properties.opacity;
|
|
64
|
+
|
|
65
|
+
const config: LayoutAnimationConfig = {
|
|
66
|
+
duration: duration + delayMax,
|
|
67
|
+
create: { type, property },
|
|
68
|
+
update: { type },
|
|
69
|
+
delete: { type, property }
|
|
70
|
+
};
|
|
71
|
+
enableLayoutAnimationIfNeeded();
|
|
72
|
+
LayoutAnimation.configureNext(config);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MaybeMissing } from '../expressions/json';
|
|
2
|
+
import type { AnyTransition, AppearanceTransition, ChangeBoundsTransition, TransitionChange } from '../types/base';
|
|
3
|
+
|
|
4
|
+
export function flattenAppearanceTransition(
|
|
5
|
+
transition: MaybeMissing<AppearanceTransition>
|
|
6
|
+
): MaybeMissing<AnyTransition>[] {
|
|
7
|
+
const res: MaybeMissing<AnyTransition>[] = [];
|
|
8
|
+
|
|
9
|
+
if ((transition as any).type === 'set') {
|
|
10
|
+
const items = (transition as any).items as MaybeMissing<AppearanceTransition>[] | undefined;
|
|
11
|
+
(items || []).forEach(item => {
|
|
12
|
+
res.push(...flattenAppearanceTransition(item));
|
|
13
|
+
});
|
|
14
|
+
} else {
|
|
15
|
+
res.push(transition as MaybeMissing<AnyTransition>);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return res;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function flattenChangeTransition(
|
|
22
|
+
transition: MaybeMissing<TransitionChange>
|
|
23
|
+
): MaybeMissing<ChangeBoundsTransition>[] {
|
|
24
|
+
const res: MaybeMissing<ChangeBoundsTransition>[] = [];
|
|
25
|
+
|
|
26
|
+
if ((transition as any).type === 'set') {
|
|
27
|
+
const items = (transition as any).items as MaybeMissing<TransitionChange>[] | undefined;
|
|
28
|
+
(items || []).forEach(item => {
|
|
29
|
+
res.push(...flattenChangeTransition(item));
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
res.push(transition as MaybeMissing<ChangeBoundsTransition>);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return res;
|
|
36
|
+
}
|