react-native-divkit 1.7.0 → 1.8.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/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/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/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
|
@@ -1,77 +1,189 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
-
import { View } from 'react-native';
|
|
1
|
+
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing, View, ViewStyle, LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import type { ComponentContext } from '../../types/componentContext';
|
|
4
4
|
import type { DivStateData, State } from '../../types/state';
|
|
5
|
+
import type { TransitionChange } from '../../types/base';
|
|
6
|
+
import type { MaybeMissing } from '../../expressions/json';
|
|
5
7
|
import { Outer } from '../utilities/Outer';
|
|
6
8
|
import { useStateContext } from '../../context/StateContext';
|
|
7
9
|
import { useDivKitContext } from '../../context/DivKitContext';
|
|
10
|
+
import { DivStateScopeContext, type DivStateScopeValue } from '../../context/DivStateScopeContext';
|
|
11
|
+
import { LayoutParamsContext } from '../../context/LayoutParamsContext';
|
|
8
12
|
import { wrapError } from '../../utils/wrapError';
|
|
13
|
+
import { flattenChangeTransition } from '../../utils/flattenTransition';
|
|
9
14
|
|
|
10
15
|
export interface DivStateProps {
|
|
11
16
|
componentContext: ComponentContext<DivStateData>;
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
interface StagedStateChange {
|
|
20
|
+
targetStateId: string | undefined;
|
|
21
|
+
div: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface BoundsFrame {
|
|
25
|
+
left: number;
|
|
26
|
+
top: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
/**
|
|
15
32
|
* DivState component - renders different content based on state
|
|
16
|
-
*
|
|
33
|
+
*
|
|
34
|
+
* Supports:
|
|
17
35
|
* - State selection by state_id
|
|
18
36
|
* - Default state
|
|
19
|
-
* - State switching via actions (set_state)
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
37
|
+
* - State switching via actions (set_state) and via state_id_variable two-way binding
|
|
38
|
+
* - transition_change on the state container (smooth layout transitions for neighbours via
|
|
39
|
+
* configureChangeBoundsLayout)
|
|
40
|
+
* - Per-element transition_out for children declaring it in the OUTGOING state JSON
|
|
41
|
+
* (children register a playOut via DivStateScopeContext; DivState awaits them in parallel
|
|
42
|
+
* before mounting the new state). Transition_in for the INCOMING children plays automatically
|
|
43
|
+
* on mount via Outer's mode='auto-in'.
|
|
22
44
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* - Animation timing and interpolation
|
|
26
|
-
* - Clip to bounds
|
|
27
|
-
* - Advanced state management
|
|
28
|
-
* - Multiple concurrent state transitions
|
|
29
|
-
*
|
|
30
|
-
* Based on Web State.svelte
|
|
45
|
+
* Based on Web State.svelte (simplified — no per-element bbox tracking for transition_change
|
|
46
|
+
* within state subtree).
|
|
31
47
|
*/
|
|
32
48
|
export function DivState({ componentContext }: DivStateProps) {
|
|
33
49
|
const { json } = componentContext;
|
|
34
50
|
const { getVariable } = useDivKitContext();
|
|
35
51
|
const { registerState } = useStateContext();
|
|
36
52
|
|
|
37
|
-
// Get state ID for registration
|
|
38
53
|
const stateId = json.div_id || json.id;
|
|
39
54
|
|
|
40
|
-
// Find default state
|
|
41
55
|
const defaultStateId = useMemo(() => {
|
|
42
56
|
if (json.default_state_id) {
|
|
43
57
|
return json.default_state_id;
|
|
44
58
|
}
|
|
45
|
-
// If no default, use first state
|
|
46
59
|
if (json.states && json.states.length > 0) {
|
|
47
60
|
return json.states[0].state_id;
|
|
48
61
|
}
|
|
49
62
|
return undefined;
|
|
50
63
|
}, [json.default_state_id, json.states]);
|
|
51
64
|
|
|
52
|
-
// State management
|
|
53
65
|
const [currentStateId, setCurrentStateId] = useState<string | undefined>(defaultStateId);
|
|
66
|
+
const [stagedStateChange, setStagedStateChange] = useState<StagedStateChange | null>(null);
|
|
67
|
+
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
|
|
68
|
+
// True while we're awaiting transition_out of the previous state — we keep rendering the
|
|
69
|
+
// outgoing children during this window so their out-animations remain visible.
|
|
70
|
+
const [pendingStateId, setPendingStateId] = useState<string | undefined>(undefined);
|
|
71
|
+
|
|
72
|
+
// Registry of transition_out players from children inside this state's scope.
|
|
73
|
+
// The set is REPLACED each time the state swaps (because children unmount), so we don't need
|
|
74
|
+
// explicit clearing — old entries are pruned naturally by Outer's cleanup effect.
|
|
75
|
+
const outPlayersRef = useRef<Set<() => Promise<void>>>(new Set());
|
|
76
|
+
const scopeValue: DivStateScopeValue = useMemo(() => ({
|
|
77
|
+
registerTransitionOutPlayer(play: () => Promise<void>) {
|
|
78
|
+
outPlayersRef.current.add(play);
|
|
79
|
+
return () => {
|
|
80
|
+
outPlayersRef.current.delete(play);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}), []);
|
|
84
|
+
|
|
85
|
+
const transitionChange = (json as DivStateData).transition_change as MaybeMissing<TransitionChange> | undefined;
|
|
86
|
+
const stageTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
87
|
+
const animatedFrame = useRef({
|
|
88
|
+
left: new Animated.Value(0),
|
|
89
|
+
top: new Animated.Value(0),
|
|
90
|
+
width: new Animated.Value(0),
|
|
91
|
+
height: new Animated.Value(0),
|
|
92
|
+
}).current;
|
|
93
|
+
|
|
94
|
+
const applyStateChange = useCallback(async (newStateId: string | undefined) => {
|
|
95
|
+
if (newStateId === currentStateId || newStateId === pendingStateId) return;
|
|
96
|
+
|
|
97
|
+
const outPlayers = Array.from(outPlayersRef.current);
|
|
98
|
+
if (outPlayers.length > 0) {
|
|
99
|
+
setPendingStateId(newStateId);
|
|
100
|
+
try {
|
|
101
|
+
await Promise.all(outPlayers.map(p => p()));
|
|
102
|
+
} catch (err) {
|
|
103
|
+
componentContext.logError(wrapError(err as Error, {
|
|
104
|
+
additional: { phase: 'state_transition_out' }
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const nextState = json.states?.find(state => state.state_id === newStateId);
|
|
110
|
+
const previousState = json.states?.find(state => state.state_id === currentStateId);
|
|
111
|
+
const nextTransitionChange = (nextState?.div as any)?.transition_change as MaybeMissing<TransitionChange> | undefined;
|
|
112
|
+
const currentTransitionChange = (previousState?.div as any)?.transition_change as MaybeMissing<TransitionChange> | undefined;
|
|
113
|
+
const effectiveTransitionChange = nextTransitionChange || currentTransitionChange || transitionChange;
|
|
114
|
+
const duration = getChangeBoundsDuration(effectiveTransitionChange);
|
|
115
|
+
|
|
116
|
+
if (previousState?.div && nextState?.div && contentSize && duration > 0) {
|
|
117
|
+
const fromFrame = getChildFrame(previousState.div as any, contentSize);
|
|
118
|
+
const toFrame = getChildFrame(nextState.div as any, contentSize);
|
|
119
|
+
|
|
120
|
+
animatedFrame.left.setValue(fromFrame.left);
|
|
121
|
+
animatedFrame.top.setValue(fromFrame.top);
|
|
122
|
+
animatedFrame.width.setValue(fromFrame.width);
|
|
123
|
+
animatedFrame.height.setValue(fromFrame.height);
|
|
124
|
+
setPendingStateId(newStateId);
|
|
125
|
+
setStagedStateChange({
|
|
126
|
+
targetStateId: newStateId,
|
|
127
|
+
div: createOverlayDiv(previousState.div)
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await new Promise<void>(resolve => {
|
|
131
|
+
if (stageTimerRef.current) {
|
|
132
|
+
clearTimeout(stageTimerRef.current);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Animated.parallel([
|
|
136
|
+
Animated.timing(animatedFrame.left, {
|
|
137
|
+
toValue: toFrame.left,
|
|
138
|
+
duration,
|
|
139
|
+
easing: Easing.inOut(Easing.ease),
|
|
140
|
+
useNativeDriver: false,
|
|
141
|
+
}),
|
|
142
|
+
Animated.timing(animatedFrame.top, {
|
|
143
|
+
toValue: toFrame.top,
|
|
144
|
+
duration,
|
|
145
|
+
easing: Easing.inOut(Easing.ease),
|
|
146
|
+
useNativeDriver: false,
|
|
147
|
+
}),
|
|
148
|
+
Animated.timing(animatedFrame.width, {
|
|
149
|
+
toValue: toFrame.width,
|
|
150
|
+
duration,
|
|
151
|
+
easing: Easing.inOut(Easing.ease),
|
|
152
|
+
useNativeDriver: false,
|
|
153
|
+
}),
|
|
154
|
+
Animated.timing(animatedFrame.height, {
|
|
155
|
+
toValue: toFrame.height,
|
|
156
|
+
duration,
|
|
157
|
+
easing: Easing.inOut(Easing.ease),
|
|
158
|
+
useNativeDriver: false,
|
|
159
|
+
}),
|
|
160
|
+
]).start(() => {
|
|
161
|
+
resolve();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setCurrentStateId(newStateId);
|
|
167
|
+
setStagedStateChange(null);
|
|
168
|
+
setPendingStateId(undefined);
|
|
169
|
+
}, [currentStateId, pendingStateId, json.states, transitionChange, contentSize, animatedFrame, componentContext]);
|
|
54
170
|
|
|
55
171
|
// Handle state_id_variable (two-way binding)
|
|
56
172
|
const stateVariableName = json.state_id_variable;
|
|
57
173
|
const stateVariable = stateVariableName ? getVariable(stateVariableName) : undefined;
|
|
58
174
|
|
|
59
|
-
// Sync with state variable
|
|
60
175
|
useEffect(() => {
|
|
61
176
|
if (stateVariable) {
|
|
62
|
-
// Subscribe to variable changes
|
|
63
177
|
const unsubscribe = stateVariable.subscribe((value: unknown) => {
|
|
64
178
|
if (typeof value === 'string' && value !== currentStateId) {
|
|
65
|
-
|
|
179
|
+
void applyStateChange(value);
|
|
66
180
|
}
|
|
67
181
|
});
|
|
68
|
-
|
|
69
182
|
return unsubscribe;
|
|
70
183
|
}
|
|
71
184
|
return undefined;
|
|
72
|
-
}, [stateVariable, currentStateId]);
|
|
185
|
+
}, [stateVariable, currentStateId, applyStateChange]);
|
|
73
186
|
|
|
74
|
-
// Update variable when state changes
|
|
75
187
|
useEffect(() => {
|
|
76
188
|
if (stateVariable && currentStateId) {
|
|
77
189
|
const currentValue = stateVariable.getValue();
|
|
@@ -81,19 +193,17 @@ export function DivState({ componentContext }: DivStateProps) {
|
|
|
81
193
|
}
|
|
82
194
|
}, [stateVariable, currentStateId]);
|
|
83
195
|
|
|
84
|
-
// Register state in context for set_state action
|
|
85
196
|
useEffect(() => {
|
|
86
197
|
if (stateId) {
|
|
87
198
|
const unregister = registerState(stateId, async (newStateId: string) => {
|
|
88
|
-
|
|
199
|
+
await applyStateChange(newStateId);
|
|
89
200
|
return undefined;
|
|
90
201
|
});
|
|
91
202
|
return unregister;
|
|
92
203
|
}
|
|
93
204
|
return undefined;
|
|
94
|
-
}, [stateId, registerState]);
|
|
205
|
+
}, [stateId, registerState, applyStateChange]);
|
|
95
206
|
|
|
96
|
-
// Validate states
|
|
97
207
|
useEffect(() => {
|
|
98
208
|
if (!json.states || json.states.length === 0) {
|
|
99
209
|
componentContext.logError(wrapError(new Error('Empty "states" prop for div "state"')));
|
|
@@ -103,7 +213,15 @@ export function DivState({ componentContext }: DivStateProps) {
|
|
|
103
213
|
}
|
|
104
214
|
}, [json.states, stateId, componentContext]);
|
|
105
215
|
|
|
106
|
-
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
return () => {
|
|
218
|
+
if (stageTimerRef.current) {
|
|
219
|
+
clearTimeout(stageTimerRef.current);
|
|
220
|
+
stageTimerRef.current = null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
107
225
|
const currentState = useMemo((): State | undefined => {
|
|
108
226
|
if (!json.states) return undefined;
|
|
109
227
|
const found = json.states.find(s => s.state_id === currentStateId);
|
|
@@ -111,31 +229,182 @@ export function DivState({ componentContext }: DivStateProps) {
|
|
|
111
229
|
return found as State;
|
|
112
230
|
}, [json.states, currentStateId]);
|
|
113
231
|
|
|
114
|
-
|
|
115
|
-
const childContext = useMemo(() => {
|
|
116
|
-
if (!currentState?.div) return undefined;
|
|
232
|
+
const renderedDiv = currentState?.div;
|
|
117
233
|
|
|
118
|
-
|
|
234
|
+
const childContext = useMemo(() => {
|
|
235
|
+
if (!renderedDiv) return undefined;
|
|
236
|
+
return componentContext.produceChildContext(renderedDiv, {
|
|
119
237
|
path: currentStateId
|
|
120
238
|
});
|
|
121
|
-
}, [
|
|
239
|
+
}, [renderedDiv, currentStateId, componentContext]);
|
|
240
|
+
|
|
241
|
+
const contentStyle = useMemo((): ViewStyle => {
|
|
242
|
+
const child = renderedDiv as any;
|
|
243
|
+
const style: ViewStyle = {
|
|
244
|
+
width: '100%',
|
|
245
|
+
alignItems: mapAlignmentToFlex(child?.alignment_horizontal),
|
|
246
|
+
justifyContent: mapAlignmentToFlex(child?.alignment_vertical),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const heightType = (json.height as any)?.type;
|
|
250
|
+
if (heightType === 'fixed' || heightType === 'match_parent') {
|
|
251
|
+
style.flex = 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return style;
|
|
255
|
+
}, [renderedDiv, json.height]);
|
|
122
256
|
|
|
123
|
-
// Render current state
|
|
124
257
|
const renderContent = () => {
|
|
125
|
-
if (!
|
|
258
|
+
if (!renderedDiv || !childContext) {
|
|
126
259
|
return null;
|
|
127
260
|
}
|
|
128
|
-
|
|
129
261
|
// Import DivComponent dynamically to avoid circular dependency
|
|
130
262
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
131
263
|
const DivComponent = require('../DivComponent').DivComponent;
|
|
132
|
-
|
|
133
264
|
return <DivComponent componentContext={childContext} />;
|
|
134
265
|
};
|
|
135
266
|
|
|
267
|
+
const overlayContext = useMemo(() => {
|
|
268
|
+
if (!stagedStateChange?.div) return undefined;
|
|
269
|
+
return componentContext.produceChildContext(stagedStateChange.div, {
|
|
270
|
+
path: currentStateId
|
|
271
|
+
});
|
|
272
|
+
}, [stagedStateChange, currentStateId, componentContext]);
|
|
273
|
+
|
|
274
|
+
const renderOverlay = () => {
|
|
275
|
+
if (!stagedStateChange?.div || !overlayContext) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
279
|
+
const DivComponent = require('../DivComponent').DivComponent;
|
|
280
|
+
return (
|
|
281
|
+
<Animated.View
|
|
282
|
+
pointerEvents="none"
|
|
283
|
+
style={{
|
|
284
|
+
position: 'absolute',
|
|
285
|
+
left: animatedFrame.left,
|
|
286
|
+
top: animatedFrame.top,
|
|
287
|
+
width: animatedFrame.width,
|
|
288
|
+
height: animatedFrame.height,
|
|
289
|
+
overflow: 'hidden',
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
<LayoutParamsContext.Provider value={{ parentContainerOrientation: 'vertical' }}>
|
|
293
|
+
<DivComponent componentContext={overlayContext} />
|
|
294
|
+
</LayoutParamsContext.Provider>
|
|
295
|
+
</Animated.View>
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const handleContentLayout = useCallback((event: LayoutChangeEvent) => {
|
|
300
|
+
const { width, height } = event.nativeEvent.layout;
|
|
301
|
+
setContentSize(prev => {
|
|
302
|
+
if (prev && prev.width === width && prev.height === height) return prev;
|
|
303
|
+
return { width, height };
|
|
304
|
+
});
|
|
305
|
+
}, []);
|
|
306
|
+
|
|
136
307
|
return (
|
|
137
308
|
<Outer componentContext={componentContext}>
|
|
138
|
-
<
|
|
309
|
+
<DivStateScopeContext.Provider value={scopeValue}>
|
|
310
|
+
<View style={[contentStyle, { position: 'relative' }]} onLayout={handleContentLayout}>
|
|
311
|
+
{stagedStateChange ? (
|
|
312
|
+
<View style={{ opacity: 0 }}>
|
|
313
|
+
<LayoutParamsContext.Provider value={{ parentContainerOrientation: 'vertical' }}>
|
|
314
|
+
{renderContent()}
|
|
315
|
+
</LayoutParamsContext.Provider>
|
|
316
|
+
</View>
|
|
317
|
+
) : (
|
|
318
|
+
<LayoutParamsContext.Provider value={{ parentContainerOrientation: 'vertical' }}>
|
|
319
|
+
{renderContent()}
|
|
320
|
+
</LayoutParamsContext.Provider>
|
|
321
|
+
)}
|
|
322
|
+
{renderOverlay()}
|
|
323
|
+
</View>
|
|
324
|
+
</DivStateScopeContext.Provider>
|
|
139
325
|
</Outer>
|
|
140
326
|
);
|
|
141
327
|
}
|
|
328
|
+
|
|
329
|
+
type FlexAlignment = 'flex-start' | 'center' | 'flex-end';
|
|
330
|
+
|
|
331
|
+
function mapAlignmentToFlex(alignment: string | undefined): FlexAlignment {
|
|
332
|
+
switch (alignment) {
|
|
333
|
+
case 'center':
|
|
334
|
+
return 'center';
|
|
335
|
+
case 'right':
|
|
336
|
+
case 'bottom':
|
|
337
|
+
case 'end':
|
|
338
|
+
return 'flex-end';
|
|
339
|
+
case 'left':
|
|
340
|
+
case 'top':
|
|
341
|
+
case 'start':
|
|
342
|
+
default:
|
|
343
|
+
return 'flex-start';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function getChangeBoundsDuration(transition: MaybeMissing<TransitionChange> | undefined): number {
|
|
348
|
+
if (!transition) return 0;
|
|
349
|
+
return flattenChangeTransition(transition).reduce((max, item) => {
|
|
350
|
+
const duration = Math.max(0, (item as any).duration ?? 300);
|
|
351
|
+
const delay = Math.max(0, (item as any).start_delay ?? 0);
|
|
352
|
+
return Math.max(max, duration + delay);
|
|
353
|
+
}, 0);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function createOverlayDiv(previousDiv: any): any {
|
|
357
|
+
return {
|
|
358
|
+
...previousDiv,
|
|
359
|
+
alignment_horizontal: 'left',
|
|
360
|
+
alignment_vertical: 'top',
|
|
361
|
+
width: { type: 'match_parent' },
|
|
362
|
+
height: { type: 'match_parent' },
|
|
363
|
+
margins: undefined,
|
|
364
|
+
transition_change: undefined,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function getChildFrame(div: any, container: { width: number; height: number }): BoundsFrame {
|
|
369
|
+
const margins = div?.margins || {};
|
|
370
|
+
const leftMargin = numberOrZero(margins.left ?? margins.start);
|
|
371
|
+
const rightMargin = numberOrZero(margins.right ?? margins.end);
|
|
372
|
+
const topMargin = numberOrZero(margins.top);
|
|
373
|
+
const bottomMargin = numberOrZero(margins.bottom);
|
|
374
|
+
const availableWidth = Math.max(0, container.width - leftMargin - rightMargin);
|
|
375
|
+
const availableHeight = Math.max(0, container.height - topMargin - bottomMargin);
|
|
376
|
+
const width = resolveSize(div?.width, availableWidth);
|
|
377
|
+
const height = resolveSize(div?.height, availableHeight);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
left: resolvePosition(div?.alignment_horizontal, leftMargin, availableWidth, width),
|
|
381
|
+
top: resolvePosition(div?.alignment_vertical, topMargin, availableHeight, height),
|
|
382
|
+
width,
|
|
383
|
+
height,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function resolveSize(size: any, available: number): number {
|
|
388
|
+
if (size?.type === 'fixed') return Math.max(0, numberOrZero(size.value));
|
|
389
|
+
return available;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function resolvePosition(alignment: string | undefined, start: number, available: number, size: number): number {
|
|
393
|
+
switch (alignment) {
|
|
394
|
+
case 'center':
|
|
395
|
+
return start + (available - size) / 2;
|
|
396
|
+
case 'right':
|
|
397
|
+
case 'bottom':
|
|
398
|
+
case 'end':
|
|
399
|
+
return start + available - size;
|
|
400
|
+
case 'left':
|
|
401
|
+
case 'top':
|
|
402
|
+
case 'start':
|
|
403
|
+
default:
|
|
404
|
+
return start;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function numberOrZero(value: unknown): number {
|
|
409
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
410
|
+
}
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { StyleSheet, View, ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Defs, RadialGradient, Stop, Rect } from 'react-native-svg';
|
|
4
4
|
import type { Background as BackgroundType, RadialBackground } from '../../types/background';
|
|
5
|
+
import { correctColor } from '../../utils/correctColor';
|
|
5
6
|
|
|
6
7
|
export interface BackgroundProps {
|
|
7
8
|
layers?: BackgroundType[];
|
|
@@ -41,7 +42,7 @@ const RadialGradientLayer = ({ layer }: { layer: RadialBackground }) => {
|
|
|
41
42
|
<Stop
|
|
42
43
|
key={index}
|
|
43
44
|
offset={index / (layer.colors!.length - 1)}
|
|
44
|
-
stopColor={color}
|
|
45
|
+
stopColor={correctColor(color)}
|
|
45
46
|
stopOpacity={1}
|
|
46
47
|
/>
|
|
47
48
|
));
|
|
@@ -50,7 +51,7 @@ const RadialGradientLayer = ({ layer }: { layer: RadialBackground }) => {
|
|
|
50
51
|
<Stop
|
|
51
52
|
key={index}
|
|
52
53
|
offset={point.position}
|
|
53
|
-
stopColor={point.color}
|
|
54
|
+
stopColor={correctColor(point.color)}
|
|
54
55
|
stopOpacity={1}
|
|
55
56
|
/>
|
|
56
57
|
));
|
|
@@ -102,7 +103,7 @@ export const Background = ({ layers, style }: BackgroundProps) => {
|
|
|
102
103
|
return (
|
|
103
104
|
<View
|
|
104
105
|
key={index}
|
|
105
|
-
style={[StyleSheet.absoluteFill, { backgroundColor: layer.color }]}
|
|
106
|
+
style={[StyleSheet.absoluteFill, { backgroundColor: correctColor(layer.color) }]}
|
|
106
107
|
/>
|
|
107
108
|
);
|
|
108
109
|
}
|