@momo-kits/foundation 0.161.2-test.1 → 0.162.2-sp.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/Application/BottomSheet.tsx +2 -5
- package/Application/BottomTab/index.tsx +1 -3
- package/Application/Components/HeaderAnimated.tsx +5 -8
- package/Application/Components/HeaderBackground.tsx +4 -4
- package/Application/Components/HeaderExtendHeader.tsx +14 -13
- package/Application/Components/HeaderTitle.tsx +9 -5
- package/Application/Components/SearchHeader.tsx +4 -7
- package/Application/Components/useAnimatedCompat.ts +63 -0
- package/Application/types.ts +4 -3
- package/Layout/FloatingButton.tsx +10 -5
- package/Layout/Screen.tsx +107 -71
- package/package.json +1 -1
|
@@ -37,10 +37,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
37
37
|
const action = useRef<undefined | string>(undefined);
|
|
38
38
|
const insets = useSafeAreaInsets();
|
|
39
39
|
const heightHeader = useHeaderHeight();
|
|
40
|
-
|
|
41
|
-
const bottomInset = Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
42
|
-
const keyboardOffset = heightHeader - bottomInset;
|
|
43
|
-
// AI-GENERATED END
|
|
40
|
+
const keyboardOffset = heightHeader - Math.min(insets.bottom, 21);
|
|
44
41
|
|
|
45
42
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
46
43
|
|
|
@@ -314,7 +311,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
314
311
|
{useBottomInset && (
|
|
315
312
|
<View
|
|
316
313
|
style={{
|
|
317
|
-
height:
|
|
314
|
+
height: Math.min(insets.bottom, 21),
|
|
318
315
|
backgroundColor: backgroundColor,
|
|
319
316
|
}}
|
|
320
317
|
/>
|
|
@@ -296,9 +296,7 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
296
296
|
activeTintColor={theme.colors.primary}
|
|
297
297
|
style={[
|
|
298
298
|
{
|
|
299
|
-
|
|
300
|
-
height: 64 + (Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom),
|
|
301
|
-
// AI-GENERATED END
|
|
299
|
+
height: 64 + Math.min(insets.bottom, 21),
|
|
302
300
|
},
|
|
303
301
|
]}
|
|
304
302
|
/>
|
|
@@ -8,6 +8,7 @@ import { type HeaderAnimatedProps } from '../types';
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { Image } from '../../Image';
|
|
10
10
|
import { Colors } from '../../Consts';
|
|
11
|
+
import { useNormalizedSharedValue } from './useAnimatedCompat';
|
|
11
12
|
|
|
12
13
|
const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
|
|
13
14
|
animatedValue,
|
|
@@ -15,23 +16,19 @@ const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
|
|
|
15
16
|
useScale = true,
|
|
16
17
|
children,
|
|
17
18
|
}) => {
|
|
19
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
18
20
|
const animatedStyle = useAnimatedStyle(() => {
|
|
19
21
|
const scale = useScale
|
|
20
|
-
? interpolate(
|
|
21
|
-
animatedValue.value,
|
|
22
|
-
[-300, 0, 300],
|
|
23
|
-
[4, 1, 1],
|
|
24
|
-
Extrapolation.CLAMP,
|
|
25
|
-
)
|
|
22
|
+
? interpolate(sharedValue.value, [-300, 0, 300], [4, 1, 1], Extrapolation.CLAMP)
|
|
26
23
|
: 1;
|
|
27
24
|
const opacity = interpolate(
|
|
28
|
-
|
|
25
|
+
sharedValue.value,
|
|
29
26
|
[0, 150, 300],
|
|
30
27
|
[1, 0.5, 0],
|
|
31
28
|
Extrapolation.CLAMP,
|
|
32
29
|
);
|
|
33
30
|
const translateY = interpolate(
|
|
34
|
-
|
|
31
|
+
sharedValue.value,
|
|
35
32
|
[-300, 0],
|
|
36
33
|
[-80, -2],
|
|
37
34
|
Extrapolation.CLAMP,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import LinearGradient from 'react-native-linear-gradient';
|
|
2
2
|
import { StyleSheet, View } from 'react-native';
|
|
3
3
|
import React, { useContext } from 'react';
|
|
4
|
-
import Animated
|
|
4
|
+
import Animated from 'react-native-reanimated';
|
|
5
5
|
import { HeaderBackgroundProps } from '../types';
|
|
6
6
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
7
7
|
import { Colors, Styles } from '../../Consts';
|
|
8
8
|
import { Image } from '../../Image';
|
|
9
9
|
import BackgroundImageView from './BackgroundImageView';
|
|
10
|
+
import { useNormalizedSharedValue } from './useAnimatedCompat';
|
|
10
11
|
|
|
11
12
|
const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
|
|
12
13
|
animatedValue,
|
|
@@ -19,8 +20,7 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
|
|
|
19
20
|
const context = useContext<any>(MiniAppContext);
|
|
20
21
|
const gradientColor = customGradientColor ?? theme.colors.gradient;
|
|
21
22
|
const headerImage = headerBackground ?? theme.assets?.headerBackground;
|
|
22
|
-
const
|
|
23
|
-
const sv = animatedValue ?? fallback;
|
|
23
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
24
24
|
|
|
25
25
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
26
26
|
|
|
@@ -29,7 +29,7 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
|
|
|
29
29
|
<BackgroundImageView
|
|
30
30
|
useShadowHeader={useShadowHeader}
|
|
31
31
|
heightHeader={'100%'}
|
|
32
|
-
animatedValue={
|
|
32
|
+
animatedValue={sharedValue}
|
|
33
33
|
headerBackground={headerImage}
|
|
34
34
|
/>
|
|
35
35
|
<View style={styles.gradientContainer}>
|
|
@@ -5,8 +5,6 @@ import Animated, {
|
|
|
5
5
|
Extrapolation,
|
|
6
6
|
interpolate,
|
|
7
7
|
useAnimatedStyle,
|
|
8
|
-
useSharedValue,
|
|
9
|
-
type SharedValue,
|
|
10
8
|
} from 'react-native-reanimated';
|
|
11
9
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
12
10
|
import { HeaderType } from '../../Layout/types';
|
|
@@ -16,6 +14,10 @@ import { Colors, Radius, Spacing } from '../../Consts';
|
|
|
16
14
|
import { Image } from '../../Image';
|
|
17
15
|
import { SearchHeaderProps } from '../types';
|
|
18
16
|
import BackgroundImageView from './BackgroundImageView';
|
|
17
|
+
import {
|
|
18
|
+
AnimatedCompatValue,
|
|
19
|
+
useNormalizedSharedValue,
|
|
20
|
+
} from './useAnimatedCompat';
|
|
19
21
|
|
|
20
22
|
const SCREEN_PADDING = 12;
|
|
21
23
|
const BACK_WIDTH = 28;
|
|
@@ -24,7 +26,7 @@ const { width: screenWidth } = Dimensions.get('window');
|
|
|
24
26
|
|
|
25
27
|
const HeaderExtendHeader: React.FC<{
|
|
26
28
|
headerType?: HeaderType;
|
|
27
|
-
animatedValue:
|
|
29
|
+
animatedValue: AnimatedCompatValue;
|
|
28
30
|
heightHeader: number;
|
|
29
31
|
headerRightWidth: number;
|
|
30
32
|
inputSearchProps?: SearchHeaderProps;
|
|
@@ -46,8 +48,7 @@ const HeaderExtendHeader: React.FC<{
|
|
|
46
48
|
}) => {
|
|
47
49
|
const { theme } = useContext(ApplicationContext);
|
|
48
50
|
const context = useContext<any>(MiniAppContext);
|
|
49
|
-
const
|
|
50
|
-
const sv = animatedValue ?? fallback;
|
|
51
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
51
52
|
const gradientColor = customGradientColor ?? theme.colors.gradient;
|
|
52
53
|
const headerBackground = customBackground ?? theme.assets?.headerBackground;
|
|
53
54
|
const leftPosition = inputSearchProps?.leftPosition || BACK_WIDTH + 20;
|
|
@@ -61,7 +62,7 @@ const HeaderExtendHeader: React.FC<{
|
|
|
61
62
|
|
|
62
63
|
const heightStyle = useAnimatedStyle(() => ({
|
|
63
64
|
height: interpolate(
|
|
64
|
-
|
|
65
|
+
sharedValue.value,
|
|
65
66
|
[0, 100],
|
|
66
67
|
[heightHeader + 52, heightHeader],
|
|
67
68
|
Extrapolation.CLAMP,
|
|
@@ -69,14 +70,14 @@ const HeaderExtendHeader: React.FC<{
|
|
|
69
70
|
}));
|
|
70
71
|
|
|
71
72
|
const gradientOpacityStyle = useAnimatedStyle(() => ({
|
|
72
|
-
opacity: interpolate(
|
|
73
|
+
opacity: interpolate(sharedValue.value, [0, 52], [1, 0], Extrapolation.CLAMP),
|
|
73
74
|
}));
|
|
74
75
|
|
|
75
76
|
const searchTranslateStyle = useAnimatedStyle(() => ({
|
|
76
77
|
transform: [
|
|
77
78
|
{
|
|
78
79
|
translateX: interpolate(
|
|
79
|
-
|
|
80
|
+
sharedValue.value,
|
|
80
81
|
[0, 100],
|
|
81
82
|
[SCREEN_PADDING, leftPosition],
|
|
82
83
|
Extrapolation.CLAMP,
|
|
@@ -84,7 +85,7 @@ const HeaderExtendHeader: React.FC<{
|
|
|
84
85
|
},
|
|
85
86
|
],
|
|
86
87
|
width: interpolate(
|
|
87
|
-
|
|
88
|
+
sharedValue.value,
|
|
88
89
|
[0, 100],
|
|
89
90
|
[
|
|
90
91
|
screenWidth - SCREEN_PADDING * 2,
|
|
@@ -95,7 +96,7 @@ const HeaderExtendHeader: React.FC<{
|
|
|
95
96
|
}));
|
|
96
97
|
|
|
97
98
|
const searchBackgroundStyle = useAnimatedStyle(() => {
|
|
98
|
-
const t = Math.min(Math.max(
|
|
99
|
+
const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
|
|
99
100
|
return { backgroundColor: t === 0
|
|
100
101
|
? theme.colors.background.surface
|
|
101
102
|
: theme.colors.background.default };
|
|
@@ -103,7 +104,7 @@ const HeaderExtendHeader: React.FC<{
|
|
|
103
104
|
|
|
104
105
|
const extendedHeightStyle = useAnimatedStyle(() => ({
|
|
105
106
|
height: interpolate(
|
|
106
|
-
|
|
107
|
+
sharedValue.value,
|
|
107
108
|
[0, 100],
|
|
108
109
|
[heightHeader + 52, heightHeader],
|
|
109
110
|
Extrapolation.CLAMP,
|
|
@@ -117,7 +118,7 @@ const HeaderExtendHeader: React.FC<{
|
|
|
117
118
|
<BackgroundImageView
|
|
118
119
|
useShadowHeader={useShadowHeader}
|
|
119
120
|
heightHeader={heightHeader}
|
|
120
|
-
animatedValue={
|
|
121
|
+
animatedValue={sharedValue}
|
|
121
122
|
headerBackground={headerBackground}
|
|
122
123
|
/>
|
|
123
124
|
<Animated.View
|
|
@@ -185,7 +186,7 @@ const HeaderExtendHeader: React.FC<{
|
|
|
185
186
|
<BackgroundImageView
|
|
186
187
|
useShadowHeader={useShadowHeader}
|
|
187
188
|
heightHeader={heightHeader}
|
|
188
|
-
animatedValue={
|
|
189
|
+
animatedValue={sharedValue}
|
|
189
190
|
headerBackground={headerBackground}
|
|
190
191
|
/>
|
|
191
192
|
{!!gradientColor && (
|
|
@@ -9,7 +9,6 @@ import Animated, {
|
|
|
9
9
|
Extrapolation,
|
|
10
10
|
interpolate,
|
|
11
11
|
useAnimatedStyle,
|
|
12
|
-
type SharedValue,
|
|
13
12
|
} from 'react-native-reanimated';
|
|
14
13
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
15
14
|
import { exportFontFamily, Text, useScaleSize } from '../../Text';
|
|
@@ -22,6 +21,10 @@ import {
|
|
|
22
21
|
import { Image } from '../../Image';
|
|
23
22
|
import { Icon } from '../../Icon';
|
|
24
23
|
import { Skeleton } from '../../Skeleton';
|
|
24
|
+
import {
|
|
25
|
+
AnimatedCompatValue,
|
|
26
|
+
useNormalizedSharedValue,
|
|
27
|
+
} from './useAnimatedCompat';
|
|
25
28
|
|
|
26
29
|
type HeaderTitleInterpolate = {
|
|
27
30
|
inputRange: number[];
|
|
@@ -29,7 +32,7 @@ type HeaderTitleInterpolate = {
|
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
type HeaderTitleExtraProps = {
|
|
32
|
-
animatedValue?:
|
|
35
|
+
animatedValue?: AnimatedCompatValue;
|
|
33
36
|
interpolate?: HeaderTitleInterpolate;
|
|
34
37
|
tintColor?: string;
|
|
35
38
|
children?: React.ReactNode;
|
|
@@ -47,15 +50,16 @@ const HeaderTitle: React.FC<HeaderTitleExtraProps & { [key: string]: any }> = pr
|
|
|
47
50
|
inputRange: [0, 200],
|
|
48
51
|
outputRange: [0, 1],
|
|
49
52
|
};
|
|
50
|
-
const
|
|
53
|
+
const hasAnimated = props.animatedValue != null;
|
|
54
|
+
const sharedValue = useNormalizedSharedValue(props.animatedValue);
|
|
51
55
|
|
|
52
56
|
const animatedStyle = useAnimatedStyle(() => {
|
|
53
|
-
if (!
|
|
57
|
+
if (!hasAnimated) {
|
|
54
58
|
return { opacity: 1 };
|
|
55
59
|
}
|
|
56
60
|
return {
|
|
57
61
|
opacity: interpolate(
|
|
58
|
-
|
|
62
|
+
sharedValue.value,
|
|
59
63
|
interpolateConfig.inputRange,
|
|
60
64
|
interpolateConfig.outputRange,
|
|
61
65
|
Extrapolation.CLAMP,
|
|
@@ -6,14 +6,12 @@ import {
|
|
|
6
6
|
TouchableOpacity,
|
|
7
7
|
View,
|
|
8
8
|
} from 'react-native';
|
|
9
|
-
import Animated, {
|
|
10
|
-
useAnimatedStyle,
|
|
11
|
-
useSharedValue,
|
|
12
|
-
} from 'react-native-reanimated';
|
|
9
|
+
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
|
|
13
10
|
import React, { useContext } from 'react';
|
|
14
11
|
import { SearchHeaderProps } from '../types';
|
|
15
12
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
16
13
|
import { Text } from '../../Text';
|
|
14
|
+
import { useNormalizedSharedValue } from './useAnimatedCompat';
|
|
17
15
|
|
|
18
16
|
const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
|
|
19
17
|
(
|
|
@@ -31,13 +29,12 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
|
|
|
31
29
|
const BACK_WIDTH = 28;
|
|
32
30
|
const { width: screenWidth } = Dimensions.get('window');
|
|
33
31
|
|
|
34
|
-
const
|
|
35
|
-
const sv = animatedValue ?? fallback;
|
|
32
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
36
33
|
const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
|
|
37
34
|
const headerRightWidth = props?.headerRightWidth ?? 73;
|
|
38
35
|
|
|
39
36
|
const backgroundStyle = useAnimatedStyle(() => {
|
|
40
|
-
const t = Math.min(Math.max(
|
|
37
|
+
const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
|
|
41
38
|
return {
|
|
42
39
|
backgroundColor: t === 0
|
|
43
40
|
? theme.colors.background.surface
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Animated } from 'react-native';
|
|
3
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Union of the two animation runtimes a consumer might hand to a foundation
|
|
7
|
+
* component. react-native `Animated.Value` (legacy) and reanimated
|
|
8
|
+
* `SharedValue` are different objects and cannot be used interchangeably, so
|
|
9
|
+
* foundation components normalize whatever they receive into a `SharedValue`
|
|
10
|
+
* via {@link useNormalizedSharedValue} before rendering with reanimated.
|
|
11
|
+
*/
|
|
12
|
+
export type AnimatedCompatValue = Animated.Value | SharedValue<number>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A react-native `Animated.Value` exposes `setValue`/`interpolate` methods; a
|
|
16
|
+
* reanimated `SharedValue` does not.
|
|
17
|
+
*/
|
|
18
|
+
export const isRNAnimatedValue = (v: unknown): v is Animated.Value =>
|
|
19
|
+
!!v &&
|
|
20
|
+
typeof (v as Animated.Value).setValue === 'function' &&
|
|
21
|
+
typeof (v as Animated.Value).interpolate === 'function';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Heuristic: a reanimated `SharedValue` carries a `value` accessor but none of
|
|
25
|
+
* the `Animated.Value` methods.
|
|
26
|
+
*/
|
|
27
|
+
export const isSharedValue = (v: unknown): v is SharedValue<number> =>
|
|
28
|
+
!!v && !isRNAnimatedValue(v) && 'value' in (v as object);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize an incoming animated value (either runtime, or nothing) into a
|
|
32
|
+
* reanimated `SharedValue<number>` that foundation components can drive with
|
|
33
|
+
* `useAnimatedStyle`/`interpolate`.
|
|
34
|
+
*
|
|
35
|
+
* - SharedValue → returned as-is (zero overhead, shares the same UI-thread cell)
|
|
36
|
+
* - Animated.Value → bridged: a JS-thread `addListener` mirrors its value into
|
|
37
|
+
* an internal SharedValue. NOTE: when the source `Animated.Value` is driven
|
|
38
|
+
* by `Animated.event(..., { useNativeDriver: true })`, `addListener` does NOT
|
|
39
|
+
* fire per frame, so the bridge stays flat. For the Screen-driven case the
|
|
40
|
+
* bridge is fed from the scroll `listener` instead (see Screen.tsx); this
|
|
41
|
+
* hook's listener is the safety net for standalone / JS-driven usage.
|
|
42
|
+
* - undefined → a fresh internal SharedValue stuck at 0
|
|
43
|
+
*/
|
|
44
|
+
export function useNormalizedSharedValue(
|
|
45
|
+
input?: AnimatedCompatValue,
|
|
46
|
+
): SharedValue<number> {
|
|
47
|
+
const fallback = useSharedValue(0);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!isRNAnimatedValue(input)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const id = input.addListener(({ value }) => {
|
|
54
|
+
fallback.value = value;
|
|
55
|
+
});
|
|
56
|
+
return () => input.removeListener(id);
|
|
57
|
+
}, [input, fallback]);
|
|
58
|
+
|
|
59
|
+
if (isSharedValue(input)) {
|
|
60
|
+
return input;
|
|
61
|
+
}
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
package/Application/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { EventArg } from '@react-navigation/core';
|
|
|
2
2
|
import { StackNavigationOptions } from '@react-navigation/stack';
|
|
3
3
|
import React, { ReactNode } from 'react';
|
|
4
4
|
import {
|
|
5
|
+
Animated,
|
|
5
6
|
TouchableOpacityProps,
|
|
6
7
|
ViewProps,
|
|
7
8
|
ViewStyle,
|
|
@@ -258,7 +259,7 @@ export interface HeaderBackProps extends NavigationButtonProps {
|
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
export type HeaderBackgroundProps = {
|
|
261
|
-
animatedValue?: SharedValue<number>;
|
|
262
|
+
animatedValue?: Animated.Value | SharedValue<number>;
|
|
262
263
|
useGradient?: boolean;
|
|
263
264
|
useShadowHeader?: boolean;
|
|
264
265
|
backgroundColor?: string;
|
|
@@ -298,7 +299,7 @@ export type TitleJourneyProps = {
|
|
|
298
299
|
};
|
|
299
300
|
|
|
300
301
|
export interface HeaderAnimatedProps extends ViewProps {
|
|
301
|
-
animatedValue: SharedValue<number>;
|
|
302
|
+
animatedValue: Animated.Value | SharedValue<number>;
|
|
302
303
|
image: string;
|
|
303
304
|
useScale?: boolean;
|
|
304
305
|
}
|
|
@@ -342,7 +343,7 @@ export type AnimatedHeader = {
|
|
|
342
343
|
|
|
343
344
|
export interface SearchHeaderProps extends InputSearchProps {
|
|
344
345
|
ref?: React.RefObject<InputRef>;
|
|
345
|
-
animatedValue?: SharedValue<number>;
|
|
346
|
+
animatedValue?: Animated.Value | SharedValue<number>;
|
|
346
347
|
headerRightWidth?: 0 | 74 | 110 | number;
|
|
347
348
|
leftPosition?: 12 | 48 | number;
|
|
348
349
|
renderButtons?: () => ReactNode;
|
|
@@ -11,11 +11,14 @@ import Animated, {
|
|
|
11
11
|
useSharedValue,
|
|
12
12
|
withTiming,
|
|
13
13
|
runOnJS,
|
|
14
|
-
type SharedValue,
|
|
15
14
|
} from 'react-native-reanimated';
|
|
16
15
|
import { ApplicationContext } from '../Context';
|
|
17
16
|
import { Icon } from '../Icon';
|
|
18
17
|
import { useScaleSize } from '../Text';
|
|
18
|
+
import {
|
|
19
|
+
AnimatedCompatValue,
|
|
20
|
+
useNormalizedSharedValue,
|
|
21
|
+
} from '../Application/Components/useAnimatedCompat';
|
|
19
22
|
|
|
20
23
|
export interface FloatingButtonProps {
|
|
21
24
|
label?: string;
|
|
@@ -24,7 +27,7 @@ export interface FloatingButtonProps {
|
|
|
24
27
|
icon?: string;
|
|
25
28
|
iconColor?: string;
|
|
26
29
|
size?: 'small' | 'large';
|
|
27
|
-
animatedValue?:
|
|
30
|
+
animatedValue?: AnimatedCompatValue;
|
|
28
31
|
bottom?: number;
|
|
29
32
|
renderComponent?: () => React.ReactNode;
|
|
30
33
|
}
|
|
@@ -41,6 +44,8 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
41
44
|
bottom = 12,
|
|
42
45
|
}: FloatingButtonProps) => {
|
|
43
46
|
const { theme } = useContext(ApplicationContext);
|
|
47
|
+
const enabled = animatedValue != null;
|
|
48
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
44
49
|
const scaledFontSize = useScaleSize(16);
|
|
45
50
|
const scaledLineHeight = useScaleSize(22);
|
|
46
51
|
const maxWidth = useSharedValue(0);
|
|
@@ -57,10 +62,10 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
57
62
|
}, [label, opacityAnimated]);
|
|
58
63
|
|
|
59
64
|
useAnimatedReaction(
|
|
60
|
-
() =>
|
|
65
|
+
() => sharedValue.value,
|
|
61
66
|
(value) => {
|
|
62
67
|
'worklet';
|
|
63
|
-
if (!label || !
|
|
68
|
+
if (!label || !enabled) return;
|
|
64
69
|
if (value !== lastOffset.value && value > 0) {
|
|
65
70
|
const direction = value > lastOffset.value ? 'down' : 'up';
|
|
66
71
|
lastOffset.value = value;
|
|
@@ -88,7 +93,7 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
},
|
|
91
|
-
[label,
|
|
96
|
+
[label, enabled, minWidth],
|
|
92
97
|
);
|
|
93
98
|
|
|
94
99
|
const containerStyle = useAnimatedStyle(() => ({
|
package/Layout/Screen.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import React, {
|
|
|
12
12
|
useRef,
|
|
13
13
|
} from 'react';
|
|
14
14
|
import {
|
|
15
|
+
Animated,
|
|
15
16
|
KeyboardAvoidingView,
|
|
16
17
|
NativeScrollEvent,
|
|
17
18
|
NativeSyntheticEvent,
|
|
@@ -24,12 +25,9 @@ import {
|
|
|
24
25
|
View,
|
|
25
26
|
ViewProps,
|
|
26
27
|
} from 'react-native';
|
|
27
|
-
import
|
|
28
|
-
runOnJS,
|
|
29
|
-
useAnimatedScrollHandler,
|
|
28
|
+
import {
|
|
30
29
|
useSharedValue,
|
|
31
30
|
withTiming,
|
|
32
|
-
type SharedValue,
|
|
33
31
|
} from 'react-native-reanimated';
|
|
34
32
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
35
33
|
import { ApplicationContext, ScreenContext } from '../Context';
|
|
@@ -142,9 +140,10 @@ export interface ScreenProps extends ViewProps {
|
|
|
142
140
|
inputSearchRef?: Ref<InputRef>;
|
|
143
141
|
|
|
144
142
|
/**
|
|
145
|
-
* Optional.
|
|
143
|
+
* Optional. React Native animated value for header.
|
|
144
|
+
* Internally, Screen mirrors it into a reanimated shared value.
|
|
146
145
|
*/
|
|
147
|
-
animatedValue?:
|
|
146
|
+
animatedValue?: Animated.Value;
|
|
148
147
|
|
|
149
148
|
/**
|
|
150
149
|
* Optional. If `true`, use shadow header.
|
|
@@ -201,23 +200,49 @@ const Screen = forwardRef(
|
|
|
201
200
|
const screen: any = useContext(ScreenContext);
|
|
202
201
|
const insets = useSafeAreaInsets();
|
|
203
202
|
const heightHeader = useHeaderHeight();
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Dual animation source:
|
|
206
|
+
* - `rnValue` is a react-native `Animated.Value` driven natively by the
|
|
207
|
+
* scroll event. It is what legacy consumers receive (`animatedValue`).
|
|
208
|
+
* - `sharedValue` is a reanimated `SharedValue` consumed by every internal
|
|
209
|
+
* foundation component (and offered to modern consumers). It is kept in
|
|
210
|
+
* sync from the scroll `listener` (see below), which fires on the JS
|
|
211
|
+
* thread even when the scroll uses the native driver.
|
|
212
|
+
* If a legacy value is driven outside of Screen, its listener also
|
|
213
|
+
* mirrors JS-driven updates into the shared value.
|
|
214
|
+
* Consumers pass only `Animated.Value`; the reanimated shared value stays
|
|
215
|
+
* an internal implementation detail.
|
|
216
|
+
*/
|
|
217
|
+
const rnValue = useRef<Animated.Value>(
|
|
218
|
+
customAnimatedValue ?? new Animated.Value(0),
|
|
219
|
+
);
|
|
220
|
+
const internalShared = useSharedValue(0);
|
|
221
|
+
const sharedValue = internalShared;
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!customAnimatedValue) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const listenerId = customAnimatedValue.addListener(({ value }) => {
|
|
229
|
+
internalShared.value = value;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return () => {
|
|
233
|
+
customAnimatedValue.removeListener(listenerId);
|
|
234
|
+
};
|
|
235
|
+
}, [customAnimatedValue, internalShared]);
|
|
236
|
+
|
|
206
237
|
const currentTint = useRef<string | undefined>(undefined);
|
|
207
238
|
const isTab = navigation?.instance?.getState?.()?.type === 'tab';
|
|
208
239
|
|
|
209
|
-
let handleScroll
|
|
240
|
+
let handleScroll;
|
|
210
241
|
let Component: any = View;
|
|
211
242
|
|
|
212
|
-
|
|
213
|
-
// On iOS the home indicator is ~34pt but visual safe area needs only ~21pt cap.
|
|
214
|
-
// On Android 15+ edge-to-edge the nav bar can be 48dp — no cap needed.
|
|
215
|
-
const bottomInset = Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
216
|
-
// AI-GENERATED END
|
|
217
|
-
|
|
218
|
-
let keyboardOffset = heightHeader - bottomInset;
|
|
243
|
+
let keyboardOffset = heightHeader - Math.min(insets.bottom, 21);
|
|
219
244
|
if (headerType === 'extended' || animatedHeader || inputSearchProps) {
|
|
220
|
-
keyboardOffset = -
|
|
245
|
+
keyboardOffset = -Math.min(insets.bottom, 21);
|
|
221
246
|
}
|
|
222
247
|
|
|
223
248
|
/**
|
|
@@ -260,7 +285,7 @@ const Screen = forwardRef(
|
|
|
260
285
|
inputRange: [0, 50],
|
|
261
286
|
outputRange: [1, 0],
|
|
262
287
|
}}
|
|
263
|
-
animatedValue={
|
|
288
|
+
animatedValue={sharedValue}
|
|
264
289
|
/>
|
|
265
290
|
),
|
|
266
291
|
};
|
|
@@ -275,7 +300,7 @@ const Screen = forwardRef(
|
|
|
275
300
|
headerBackground: (props: any) => (
|
|
276
301
|
<HeaderBackground
|
|
277
302
|
{...props}
|
|
278
|
-
animatedValue={
|
|
303
|
+
animatedValue={sharedValue}
|
|
279
304
|
useShadowHeader={useShadowHeader}
|
|
280
305
|
headerBackground={headerBackground}
|
|
281
306
|
gradientColor={gradientColor}
|
|
@@ -295,7 +320,7 @@ const Screen = forwardRef(
|
|
|
295
320
|
inputRange: [0, 50],
|
|
296
321
|
outputRange: [1, 0],
|
|
297
322
|
}}
|
|
298
|
-
animatedValue={
|
|
323
|
+
animatedValue={sharedValue}
|
|
299
324
|
/>
|
|
300
325
|
),
|
|
301
326
|
};
|
|
@@ -309,6 +334,7 @@ const Screen = forwardRef(
|
|
|
309
334
|
headerBackground,
|
|
310
335
|
inputSearchProps,
|
|
311
336
|
navigation?.instance,
|
|
337
|
+
sharedValue,
|
|
312
338
|
useShadowHeader,
|
|
313
339
|
],
|
|
314
340
|
);
|
|
@@ -330,7 +356,7 @@ const Screen = forwardRef(
|
|
|
330
356
|
headerBackground: (props: any) => (
|
|
331
357
|
<HeaderBackground
|
|
332
358
|
{...props}
|
|
333
|
-
animatedValue={
|
|
359
|
+
animatedValue={sharedValue}
|
|
334
360
|
useGradient={false}
|
|
335
361
|
useShadowHeader={useShadowHeader}
|
|
336
362
|
headerBackground={headerBackground}
|
|
@@ -351,7 +377,13 @@ const Screen = forwardRef(
|
|
|
351
377
|
|
|
352
378
|
navigation?.instance?.setOptions(options);
|
|
353
379
|
},
|
|
354
|
-
[
|
|
380
|
+
[
|
|
381
|
+
gradientColor,
|
|
382
|
+
headerBackground,
|
|
383
|
+
navigation?.instance,
|
|
384
|
+
sharedValue,
|
|
385
|
+
useShadowHeader,
|
|
386
|
+
],
|
|
355
387
|
);
|
|
356
388
|
|
|
357
389
|
/**
|
|
@@ -369,13 +401,13 @@ const Screen = forwardRef(
|
|
|
369
401
|
inputRange: [0, 50],
|
|
370
402
|
outputRange: [1, 0],
|
|
371
403
|
}}
|
|
372
|
-
animatedValue={
|
|
404
|
+
animatedValue={sharedValue}
|
|
373
405
|
/>
|
|
374
406
|
),
|
|
375
407
|
};
|
|
376
408
|
|
|
377
409
|
navigation?.instance?.setOptions(options);
|
|
378
|
-
}, [navigation?.instance]);
|
|
410
|
+
}, [navigation?.instance, sharedValue]);
|
|
379
411
|
|
|
380
412
|
/**
|
|
381
413
|
* export search header
|
|
@@ -398,7 +430,7 @@ const Screen = forwardRef(
|
|
|
398
430
|
headerLeft: (props: any) =>
|
|
399
431
|
params?.hiddenBack ? null : <HeaderLeft {...props} />,
|
|
400
432
|
headerTitle: () => (
|
|
401
|
-
<SearchHeader {...params} animatedValue={
|
|
433
|
+
<SearchHeader {...params} animatedValue={sharedValue} />
|
|
402
434
|
),
|
|
403
435
|
};
|
|
404
436
|
|
|
@@ -449,51 +481,47 @@ const Screen = forwardRef(
|
|
|
449
481
|
});
|
|
450
482
|
});
|
|
451
483
|
|
|
452
|
-
const onTintColorChange = useCallback(
|
|
453
|
-
(offsetY: number) => {
|
|
454
|
-
if (!animatedHeader) return;
|
|
455
|
-
let color = animatedHeader?.headerTintColor ?? Colors.black_17;
|
|
456
|
-
if (offsetY > 50) {
|
|
457
|
-
color = Colors.black_17;
|
|
458
|
-
}
|
|
459
|
-
if (color !== currentTint.current) {
|
|
460
|
-
currentTint.current = color;
|
|
461
|
-
navigation?.setOptions({
|
|
462
|
-
headerTintColor: color,
|
|
463
|
-
});
|
|
464
|
-
let barStyle: StatusBarStyle = 'dark-content';
|
|
465
|
-
if (currentTint.current === Colors.black_01) {
|
|
466
|
-
barStyle = 'light-content';
|
|
467
|
-
}
|
|
468
|
-
StatusBar.setBarStyle(barStyle, true);
|
|
469
|
-
}
|
|
470
|
-
},
|
|
471
|
-
[animatedHeader, navigation],
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
const emitOnScroll = useCallback(
|
|
475
|
-
(offsetY: number) => {
|
|
476
|
-
scrollViewProps?.onScroll?.({
|
|
477
|
-
nativeEvent: { contentOffset: { x: 0, y: offsetY } },
|
|
478
|
-
} as NativeSyntheticEvent<NativeScrollEvent>);
|
|
479
|
-
},
|
|
480
|
-
[scrollViewProps],
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
const scrollHandler = useAnimatedScrollHandler({
|
|
484
|
-
onScroll: (event) => {
|
|
485
|
-
animatedValue.value = event.contentOffset.y;
|
|
486
|
-
runOnJS(emitOnScroll)(event.contentOffset.y);
|
|
487
|
-
runOnJS(onTintColorChange)(event.contentOffset.y);
|
|
488
|
-
},
|
|
489
|
-
});
|
|
490
|
-
|
|
491
484
|
/**
|
|
492
485
|
* animated when use scroll && animated value
|
|
493
486
|
*/
|
|
494
487
|
if (scrollable) {
|
|
495
488
|
Component = Animated.ScrollView;
|
|
496
|
-
handleScroll =
|
|
489
|
+
handleScroll = Animated.event(
|
|
490
|
+
[
|
|
491
|
+
{
|
|
492
|
+
nativeEvent: {
|
|
493
|
+
contentOffset: { y: rnValue.current as Animated.Value },
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
{
|
|
498
|
+
useNativeDriver: true,
|
|
499
|
+
listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
500
|
+
const offsetY = e.nativeEvent.contentOffset.y;
|
|
501
|
+
// keep the reanimated source in sync for the internal components.
|
|
502
|
+
sharedValue.value = offsetY;
|
|
503
|
+
scrollViewProps?.onScroll?.(e);
|
|
504
|
+
if (animatedHeader) {
|
|
505
|
+
let color = animatedHeader?.headerTintColor ?? Colors.black_17;
|
|
506
|
+
if (offsetY > 50) {
|
|
507
|
+
color = Colors.black_17;
|
|
508
|
+
}
|
|
509
|
+
if (color !== currentTint.current) {
|
|
510
|
+
currentTint.current = color;
|
|
511
|
+
navigation?.setOptions({
|
|
512
|
+
headerTintColor: color,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
let barStyle: StatusBarStyle = 'dark-content';
|
|
516
|
+
if (currentTint.current === Colors.black_01) {
|
|
517
|
+
barStyle = 'light-content';
|
|
518
|
+
}
|
|
519
|
+
StatusBar.setBarStyle(barStyle, true);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
);
|
|
497
525
|
}
|
|
498
526
|
|
|
499
527
|
/**
|
|
@@ -503,7 +531,12 @@ const Screen = forwardRef(
|
|
|
503
531
|
const handleScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
504
532
|
const offsetY = e.nativeEvent.contentOffset.y;
|
|
505
533
|
if (inputSearchProps && offsetY < 100 && offsetY > 0) {
|
|
506
|
-
|
|
534
|
+
Animated.timing(rnValue.current, {
|
|
535
|
+
toValue: 0,
|
|
536
|
+
useNativeDriver: true,
|
|
537
|
+
duration: 300,
|
|
538
|
+
}).start();
|
|
539
|
+
sharedValue.value = withTiming(0, { duration: 300 });
|
|
507
540
|
ref?.scrollTo?.({ y: 0, animated: true });
|
|
508
541
|
}
|
|
509
542
|
scrollViewProps?.onScrollEndDrag?.(e);
|
|
@@ -519,7 +552,10 @@ const Screen = forwardRef(
|
|
|
519
552
|
style={[styles.screenBanner, { maxHeight: 210 + layoutOffset }]}
|
|
520
553
|
>
|
|
521
554
|
{animatedHeader?.component({
|
|
522
|
-
animatedValue
|
|
555
|
+
// legacy consumers read `animatedValue` (Animated.Value),
|
|
556
|
+
// modern consumers read `sharedValue` (SharedValue).
|
|
557
|
+
animatedValue: rnValue.current,
|
|
558
|
+
sharedValue: sharedValue,
|
|
523
559
|
})}
|
|
524
560
|
</View>
|
|
525
561
|
);
|
|
@@ -586,7 +622,7 @@ const Screen = forwardRef(
|
|
|
586
622
|
headerType={headerType}
|
|
587
623
|
heightHeader={heightHeader}
|
|
588
624
|
headerRightWidth={headerRightWidth}
|
|
589
|
-
animatedValue={
|
|
625
|
+
animatedValue={sharedValue}
|
|
590
626
|
inputSearchProps={inputSearchProps}
|
|
591
627
|
navigation={navigation}
|
|
592
628
|
inputSearchRef={inputSearchRef}
|
|
@@ -624,9 +660,9 @@ const Screen = forwardRef(
|
|
|
624
660
|
<View>
|
|
625
661
|
<FloatingButton
|
|
626
662
|
{...floatingButtonProps}
|
|
627
|
-
animatedValue={
|
|
663
|
+
animatedValue={sharedValue}
|
|
628
664
|
bottom={
|
|
629
|
-
Footer || isTab ? 12 :
|
|
665
|
+
Footer || isTab ? 12 : Math.min(insets.bottom, 21) + Spacing.S
|
|
630
666
|
}
|
|
631
667
|
/>
|
|
632
668
|
</View>
|
|
@@ -637,7 +673,7 @@ const Screen = forwardRef(
|
|
|
637
673
|
style={[
|
|
638
674
|
styles.shadow,
|
|
639
675
|
{
|
|
640
|
-
paddingBottom:
|
|
676
|
+
paddingBottom: Math.min(insets.bottom, 21) + Spacing.S,
|
|
641
677
|
backgroundColor: theme.colors.background.surface,
|
|
642
678
|
},
|
|
643
679
|
]}
|