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
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import React, { ReactNode, useMemo, useRef, useCallback } from 'react';
|
|
2
|
-
import { View, Pressable, Animated, ViewStyle, StyleSheet, Easing, EasingFunction } from 'react-native';
|
|
1
|
+
import React, { ReactNode, useMemo, useRef, useCallback, useState } from 'react';
|
|
2
|
+
import { View, Pressable, Animated, ViewStyle, StyleSheet, Easing, EasingFunction, LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import type { ComponentContext } from '../../types/componentContext';
|
|
4
4
|
import type { DivBaseData } from '../../types/base';
|
|
5
|
-
import type { Visibility } from '../../types/base';
|
|
5
|
+
import type { Visibility, AppearanceTransition, TransitionChange } from '../../types/base';
|
|
6
6
|
import type { FixedSize, MatchParentSize, WrapContentSize } from '../../types/sizes';
|
|
7
7
|
import type { MaybeMissing } from '../../expressions/json';
|
|
8
8
|
import type { Animation } from '../../types/animation';
|
|
9
9
|
import type { Interpolation } from '../../../typings/common';
|
|
10
10
|
import { useDerivedFromVarsSimple } from '../../hooks/useDerivedFromVars';
|
|
11
11
|
import { useActionHandler, useHasActions } from '../../hooks/useAction';
|
|
12
|
+
import { useAppearanceTransition } from '../../hooks/useAppearanceTransition';
|
|
13
|
+
import { useChangeBoundsTransition } from '../../hooks/useChangeBoundsTransition';
|
|
12
14
|
import { useDivKitContext } from '../../context/DivKitContext';
|
|
13
15
|
import { useLayoutParams } from '../../context/LayoutParamsContext';
|
|
16
|
+
import { useDivStateScopeOptional } from '../../context/DivStateScopeContext';
|
|
14
17
|
import { Background } from './Background';
|
|
15
18
|
import { flattenAnimation } from '../../utils/flattenAnimation';
|
|
19
|
+
import { configureChangeBoundsLayout } from '../../utils/configureChangeBoundsLayout';
|
|
16
20
|
|
|
17
21
|
function resolveAlignSelf(
|
|
18
22
|
alignment: string | undefined,
|
|
@@ -195,20 +199,72 @@ export function Outer<T extends DivBaseData = DivBaseData>({
|
|
|
195
199
|
Animated.parallel(anims).start();
|
|
196
200
|
}, [parsedAnimations, animOpacity, animScale]);
|
|
197
201
|
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
// Appearance transitions (transition_in / transition_out) and change_bounds (transition_change)
|
|
203
|
+
const transitionIn = (json as any).transition_in as MaybeMissing<AppearanceTransition> | undefined;
|
|
204
|
+
const transitionOut = (json as any).transition_out as MaybeMissing<AppearanceTransition> | undefined;
|
|
205
|
+
const transitionChange = (json as any).transition_change as MaybeMissing<TransitionChange> | undefined;
|
|
206
|
+
const hasAppearanceTransitions = Boolean(transitionIn || transitionOut);
|
|
207
|
+
const stateScope = useDivStateScopeOptional();
|
|
208
|
+
const insideDivState = Boolean(stateScope);
|
|
209
|
+
|
|
210
|
+
// For neighbors: when WE appear/collapse, queue a coarse LayoutAnimation so they reflow smoothly.
|
|
211
|
+
const triggerChangeBoundsForNeighbors = useCallback(() => {
|
|
212
|
+
configureChangeBoundsLayout(transitionChange);
|
|
213
|
+
}, [transitionChange]);
|
|
214
|
+
|
|
215
|
+
// FLIP-based bounds animation for THIS element: tracks layout deltas and animates via
|
|
216
|
+
// transform (translate+scale) so the spec's interpolator/cubic-bezier actually applies.
|
|
217
|
+
const bounds = useChangeBoundsTransition({
|
|
218
|
+
transitionChange
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Measured size for off-center pivot scale (translate-scale-translate emulation).
|
|
222
|
+
// We reuse the same onLayout for both FLIP and pivot calculations.
|
|
223
|
+
const [layoutSize, setLayoutSize] = useState<{ width: number; height: number } | null>(null);
|
|
224
|
+
const onLayoutMeasure = useCallback((e: LayoutChangeEvent) => {
|
|
225
|
+
const { width, height } = e.nativeEvent.layout;
|
|
226
|
+
setLayoutSize(prev => {
|
|
227
|
+
if (prev && prev.width === width && prev.height === height) return prev;
|
|
228
|
+
return { width, height };
|
|
229
|
+
});
|
|
230
|
+
bounds.onLayout(e);
|
|
231
|
+
}, [bounds]);
|
|
232
|
+
|
|
233
|
+
// Whether this element lives inside a DivState. If so, on state-change DivState will call
|
|
234
|
+
// our playOut (via the scope context) and await it before swapping children. In that mode we
|
|
235
|
+
// disable the visibility-driven first-mount transition_in (DivState will trigger playIn on
|
|
236
|
+
// the new children automatically through their mount-effect with auto-in mode).
|
|
237
|
+
const appearance = useAppearanceTransition({
|
|
238
|
+
visibility: visibility as Visibility,
|
|
239
|
+
transitionIn,
|
|
240
|
+
transitionOut,
|
|
241
|
+
enabled: hasAppearanceTransitions,
|
|
242
|
+
mode: insideDivState ? 'auto-in' : 'visibility',
|
|
243
|
+
onBeforeCollapse: triggerChangeBoundsForNeighbors,
|
|
244
|
+
onBeforeExpand: triggerChangeBoundsForNeighbors,
|
|
245
|
+
layoutWidth: layoutSize?.width,
|
|
246
|
+
layoutHeight: layoutSize?.height,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Register transition_out player with the enclosing DivState scope so it can play before unmount.
|
|
250
|
+
React.useEffect(() => {
|
|
251
|
+
if (!stateScope || !appearance.hasTransitionOut) return;
|
|
252
|
+
return stateScope.registerTransitionOutPlayer(appearance.playOut);
|
|
253
|
+
}, [stateScope, appearance.hasTransitionOut, appearance.playOut]);
|
|
202
254
|
|
|
203
255
|
// Build styles
|
|
204
256
|
const containerStyle = useMemo(() => {
|
|
205
257
|
const styles: ViewStyle = {};
|
|
206
258
|
|
|
207
259
|
// Visibility (invisible = opacity 0, but still takes space)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
260
|
+
// When appearance transitions are present, opacity is driven by Animated values below,
|
|
261
|
+
// not by static styles — otherwise the static value would override animation.
|
|
262
|
+
if (!hasAppearanceTransitions) {
|
|
263
|
+
if (visibility === 'invisible') {
|
|
264
|
+
styles.opacity = 0;
|
|
265
|
+
} else if (typeof alpha === 'number' && alpha !== 1) {
|
|
266
|
+
styles.opacity = Math.max(0, Math.min(1, alpha));
|
|
267
|
+
}
|
|
212
268
|
}
|
|
213
269
|
|
|
214
270
|
// Resolve effective alignment (explicit > parent fallback > 'start')
|
|
@@ -436,7 +492,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
|
|
|
436
492
|
}
|
|
437
493
|
|
|
438
494
|
return styles;
|
|
439
|
-
}, [visibility, alpha, width, height, paddings, margins, background, border, direction, layoutParams, alignmentHorizontal, alignmentVertical]);
|
|
495
|
+
}, [visibility, alpha, width, height, paddings, margins, background, border, direction, layoutParams, alignmentHorizontal, alignmentVertical, hasAppearanceTransitions]);
|
|
440
496
|
|
|
441
497
|
const finalStyle = useMemo(() => {
|
|
442
498
|
return StyleSheet.flatten([containerStyle, customStyle]);
|
|
@@ -453,82 +509,141 @@ export function Outer<T extends DivBaseData = DivBaseData>({
|
|
|
453
509
|
return res;
|
|
454
510
|
}, [finalStyle]);
|
|
455
511
|
|
|
456
|
-
//
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
512
|
+
// Collapsed via appearance transitions (or static 'gone' without transitions) — render nothing
|
|
513
|
+
if (appearance.collapsed) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
if (!hasAppearanceTransitions && visibility === 'gone') {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const hasActionAnim = parsedAnimations.length > 0;
|
|
521
|
+
const hasTransitionChange = Boolean(transitionChange);
|
|
522
|
+
const needsAnimatedWrapper = hasActionAnim || hasAppearanceTransitions || hasTransitionChange;
|
|
523
|
+
|
|
524
|
+
if (needsAnimatedWrapper) {
|
|
525
|
+
// Split finalStyle into outer (layout) and inner (visual) styles.
|
|
526
|
+
// Margins/sizing/alignSelf live on the wrapper that participates in parent layout (Pressable or outer View),
|
|
527
|
+
// visual styles go inside Animated.View.
|
|
528
|
+
const {
|
|
529
|
+
alignSelf, flexGrow, flexShrink, flexBasis,
|
|
530
|
+
width: w, height: h, minWidth, maxWidth, minHeight, maxHeight,
|
|
531
|
+
marginTop, marginBottom, marginLeft, marginRight,
|
|
532
|
+
...innerStyle
|
|
533
|
+
} = (finalStyle || {}) as any;
|
|
534
|
+
|
|
535
|
+
const outerStyle: ViewStyle = {};
|
|
536
|
+
if (alignSelf !== undefined) outerStyle.alignSelf = alignSelf;
|
|
537
|
+
if (flexGrow !== undefined) outerStyle.flexGrow = flexGrow;
|
|
538
|
+
if (flexShrink !== undefined) outerStyle.flexShrink = flexShrink;
|
|
539
|
+
if (flexBasis !== undefined) outerStyle.flexBasis = flexBasis;
|
|
540
|
+
if (w !== undefined) outerStyle.width = w;
|
|
541
|
+
if (h !== undefined) outerStyle.height = h;
|
|
542
|
+
if (minWidth !== undefined) outerStyle.minWidth = minWidth;
|
|
543
|
+
if (maxWidth !== undefined) outerStyle.maxWidth = maxWidth;
|
|
544
|
+
if (minHeight !== undefined) outerStyle.minHeight = minHeight;
|
|
545
|
+
if (maxHeight !== undefined) outerStyle.maxHeight = maxHeight;
|
|
546
|
+
if (marginTop !== undefined) outerStyle.marginTop = marginTop;
|
|
547
|
+
if (marginBottom !== undefined) outerStyle.marginBottom = marginBottom;
|
|
548
|
+
if (marginLeft !== undefined) outerStyle.marginLeft = marginLeft;
|
|
549
|
+
if (marginRight !== undefined) outerStyle.marginRight = marginRight;
|
|
550
|
+
|
|
551
|
+
const shouldFillInner =
|
|
552
|
+
w !== undefined ||
|
|
553
|
+
h !== undefined ||
|
|
554
|
+
minWidth !== undefined ||
|
|
555
|
+
maxWidth !== undefined ||
|
|
556
|
+
minHeight !== undefined ||
|
|
557
|
+
maxHeight !== undefined ||
|
|
558
|
+
flexGrow !== undefined ||
|
|
559
|
+
flexShrink !== undefined ||
|
|
560
|
+
flexBasis !== undefined;
|
|
561
|
+
|
|
562
|
+
const animatedStyle: any = shouldFillInner
|
|
563
|
+
? { ...innerStyle, flex: 1 }
|
|
564
|
+
: { ...innerStyle };
|
|
565
|
+
|
|
566
|
+
// Compose opacity chain: static * action_animation * appearance_transition
|
|
567
|
+
let opacityChain: any = undefined;
|
|
568
|
+
const staticOpacity = animatedStyle.opacity;
|
|
569
|
+
if (staticOpacity !== undefined && staticOpacity !== 1) {
|
|
570
|
+
opacityChain = staticOpacity;
|
|
571
|
+
}
|
|
572
|
+
if (hasActionAnim && hasFadeAnimation) {
|
|
573
|
+
opacityChain = opacityChain !== undefined
|
|
574
|
+
? Animated.multiply(animOpacity, opacityChain)
|
|
575
|
+
: animOpacity;
|
|
576
|
+
}
|
|
577
|
+
if (hasAppearanceTransitions) {
|
|
578
|
+
opacityChain = opacityChain !== undefined
|
|
579
|
+
? Animated.multiply(appearance.opacity as Animated.Value, opacityChain)
|
|
580
|
+
: appearance.opacity;
|
|
581
|
+
}
|
|
582
|
+
if (opacityChain !== undefined) {
|
|
583
|
+
animatedStyle.opacity = opacityChain;
|
|
584
|
+
}
|
|
510
585
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
586
|
+
// Compose transform array. Order matters: in RN transforms apply right-to-left
|
|
587
|
+
// (the LAST element is applied first to the point, then earlier ones).
|
|
588
|
+
// We want FLIP (bounds) to wrap everything → put it FIRST (so it applies last on top
|
|
589
|
+
// of action_animation/appearance). Static transforms from style come first too.
|
|
590
|
+
const transforms: any[] = animatedStyle.transform ? [...animatedStyle.transform] : [];
|
|
591
|
+
if (hasTransitionChange && bounds.transform.length > 0) {
|
|
592
|
+
for (const t of bounds.transform) {
|
|
593
|
+
transforms.push(t);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (hasActionAnim && hasScaleAnimation) {
|
|
597
|
+
transforms.push({ scale: animScale });
|
|
598
|
+
}
|
|
599
|
+
if (hasAppearanceTransitions && appearance.transform.length > 0) {
|
|
600
|
+
for (const t of appearance.transform) {
|
|
601
|
+
transforms.push(t);
|
|
514
602
|
}
|
|
603
|
+
}
|
|
604
|
+
if (transforms.length > 0) {
|
|
605
|
+
animatedStyle.transform = transforms;
|
|
606
|
+
}
|
|
515
607
|
|
|
608
|
+
const innerNeedsOnLayout = hasAppearanceTransitions && !hasTransitionChange;
|
|
609
|
+
const outerNeedsOnLayout = hasTransitionChange;
|
|
610
|
+
const content = (
|
|
611
|
+
<Animated.View
|
|
612
|
+
style={animatedStyle}
|
|
613
|
+
onLayout={innerNeedsOnLayout ? onLayoutMeasure : undefined}
|
|
614
|
+
>
|
|
615
|
+
<Background layers={background as any} style={borderStyle} />
|
|
616
|
+
{children}
|
|
617
|
+
</Animated.View>
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
if (hasActions) {
|
|
516
621
|
return (
|
|
517
622
|
<Pressable
|
|
518
623
|
onPress={handlePress}
|
|
519
624
|
onPressIn={onPressIn}
|
|
520
625
|
onPressOut={onPressOut}
|
|
521
626
|
style={outerStyle}
|
|
627
|
+
onLayout={outerNeedsOnLayout ? onLayoutMeasure : undefined}
|
|
522
628
|
testID={testID}
|
|
523
629
|
>
|
|
524
|
-
|
|
525
|
-
<Background layers={background as any} style={borderStyle} />
|
|
526
|
-
{children}
|
|
527
|
-
</Animated.View>
|
|
630
|
+
{content}
|
|
528
631
|
</Pressable>
|
|
529
632
|
);
|
|
530
633
|
}
|
|
531
634
|
|
|
635
|
+
return (
|
|
636
|
+
<View
|
|
637
|
+
style={outerStyle}
|
|
638
|
+
onLayout={outerNeedsOnLayout ? onLayoutMeasure : undefined}
|
|
639
|
+
testID={testID}
|
|
640
|
+
>
|
|
641
|
+
{content}
|
|
642
|
+
</View>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (hasActions) {
|
|
532
647
|
return (
|
|
533
648
|
<Pressable onPress={handlePress} style={finalStyle} testID={testID}>
|
|
534
649
|
<Background layers={background as any} style={borderStyle} />
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scoped context provided by a single DivState wrapping its currently-rendered children.
|
|
5
|
+
*
|
|
6
|
+
* Allows children that declare `transition_out` (via Outer + useAppearanceTransition) to
|
|
7
|
+
* register a playOut callback so that DivState can await all of them in parallel
|
|
8
|
+
* before swapping to a new state. Analogous to Web's stateCtx.registerChildWithTransitionOut.
|
|
9
|
+
*/
|
|
10
|
+
export interface DivStateScopeValue {
|
|
11
|
+
/**
|
|
12
|
+
* Register a child's transition_out player.
|
|
13
|
+
* Returns an unregister callback (call from cleanup).
|
|
14
|
+
*/
|
|
15
|
+
registerTransitionOutPlayer(play: () => Promise<void>): () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DivStateScopeContext = createContext<DivStateScopeValue | null>(null);
|
|
19
|
+
|
|
20
|
+
/** Get the nearest DivState scope, or null if outside any DivState. */
|
|
21
|
+
export function useDivStateScopeOptional(): DivStateScopeValue | null {
|
|
22
|
+
return useContext(DivStateScopeContext);
|
|
23
|
+
}
|