@momo-kits/foundation 0.162.2-test.9 → 0.162.3-test.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.
@@ -1,7 +1,12 @@
1
- import React, { Ref, useContext, useEffect, useRef } from 'react';
1
+ import React, { Ref, useContext } from 'react';
2
2
  import LinearGradient from 'react-native-linear-gradient';
3
+ import { Dimensions, Platform, StyleSheet, View } from 'react-native';
4
+ import Animated, {
5
+ Extrapolation,
6
+ interpolate,
7
+ useAnimatedStyle,
8
+ } from 'react-native-reanimated';
3
9
  import { ApplicationContext, MiniAppContext } from '../../Context';
4
- import { Animated, Dimensions, Platform, StyleSheet, View } from 'react-native';
5
10
  import { HeaderType } from '../../Layout/types';
6
11
  import { InputRef, InputSearch } from '../../Input';
7
12
  import Navigation from '../Navigation';
@@ -9,20 +14,19 @@ import { Colors, Radius, Spacing } from '../../Consts';
9
14
  import { Image } from '../../Image';
10
15
  import { SearchHeaderProps } from '../types';
11
16
  import BackgroundImageView from './BackgroundImageView';
17
+ import {
18
+ AnimatedCompatValue,
19
+ useNormalizedSharedValue,
20
+ } from '../utils';
12
21
 
13
22
  const SCREEN_PADDING = 12;
14
23
  const BACK_WIDTH = 28;
15
24
 
16
25
  const { width: screenWidth } = Dimensions.get('window');
17
- const LinearGradientAnimated = Animated.createAnimatedComponent(LinearGradient);
18
26
 
19
- /**
20
- * Header extended with background image
21
- * @constructor
22
- */
23
27
  const HeaderExtendHeader: React.FC<{
24
28
  headerType?: HeaderType;
25
- animatedValue: Animated.Value;
29
+ animatedValue: AnimatedCompatValue;
26
30
  heightHeader: number;
27
31
  headerRightWidth: number;
28
32
  inputSearchProps?: SearchHeaderProps;
@@ -38,118 +42,131 @@ const HeaderExtendHeader: React.FC<{
38
42
  headerRightWidth = 73,
39
43
  inputSearchProps,
40
44
  inputSearchRef,
41
- useShadowHeader = true,
45
+ useShadowHeader: useShadowHeaderProp = true,
42
46
  gradientColor: customGradientColor,
43
47
  headerBackground: customBackground,
44
48
  }) => {
45
49
  const { theme } = useContext(ApplicationContext);
46
50
  const context = useContext<any>(MiniAppContext);
47
- const animated = useRef(new Animated.Value(0));
51
+ const sharedValue = useNormalizedSharedValue(animatedValue);
48
52
  const gradientColor = customGradientColor ?? theme.colors.gradient;
49
53
  const headerBackground = customBackground ?? theme.assets?.headerBackground;
50
54
  const leftPosition = inputSearchProps?.leftPosition || BACK_WIDTH + 20;
51
55
 
52
- const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
56
+ let useShadowHeader = useShadowHeaderProp;
57
+ if (inputSearchProps && Platform.OS === 'android') {
58
+ useShadowHeader = false;
59
+ }
53
60
 
54
- const opacityBackground = animatedValue?.interpolate({
55
- inputRange: [0, 52],
56
- outputRange: [0, 1],
57
- extrapolate: 'clamp',
58
- });
59
- const opacityGradient = animatedValue?.interpolate({
60
- inputRange: [0, 52],
61
- outputRange: [1, 0],
62
- extrapolate: 'clamp',
63
- });
61
+ const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
64
62
 
65
- useEffect(() => {
66
- const listener = animatedValue.addListener(({ value }) => {
67
- animated.current.setValue(value);
68
- });
69
- return () => {
70
- animatedValue?.removeListener(listener);
71
- };
72
- }, [animatedValue]);
63
+ const heightStyle = useAnimatedStyle(() => ({
64
+ height: interpolate(
65
+ sharedValue.value,
66
+ [0, 100],
67
+ [heightHeader + 52, heightHeader],
68
+ Extrapolation.CLAMP,
69
+ ),
70
+ }));
73
71
 
74
- const height = animated.current.interpolate({
75
- inputRange: [0, 100],
76
- outputRange: [heightHeader + 52, heightHeader],
77
- extrapolate: 'clamp',
78
- });
72
+ const gradientOpacityStyle = useAnimatedStyle(() => ({
73
+ opacity: interpolate(sharedValue.value, [0, 52], [1, 0], Extrapolation.CLAMP),
74
+ }));
79
75
 
80
- const translateX = animated.current.interpolate({
81
- inputRange: [0, 100],
82
- outputRange: [SCREEN_PADDING, leftPosition],
83
- extrapolate: 'clamp',
84
- });
85
-
86
- const backgroundColor = animated.current.interpolate({
87
- inputRange: [0, 100],
88
- outputRange: [
89
- theme.colors.background.surface,
90
- theme.colors.background.default,
76
+ const searchTranslateStyle = useAnimatedStyle(() => ({
77
+ transform: [
78
+ {
79
+ translateX: interpolate(
80
+ sharedValue.value,
81
+ [0, 100],
82
+ [SCREEN_PADDING, leftPosition],
83
+ Extrapolation.CLAMP,
84
+ ),
85
+ },
91
86
  ],
92
- extrapolate: 'clamp',
87
+ width: interpolate(
88
+ sharedValue.value,
89
+ [0, 100],
90
+ [
91
+ screenWidth - SCREEN_PADDING * 2,
92
+ screenWidth - leftPosition - 12 - headerRightWidth,
93
+ ],
94
+ Extrapolation.CLAMP,
95
+ ),
96
+ }));
97
+
98
+ const searchBackgroundStyle = useAnimatedStyle(() => {
99
+ const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
100
+ return { backgroundColor: t === 0
101
+ ? theme.colors.background.surface
102
+ : theme.colors.background.default };
93
103
  });
94
104
 
95
- if (inputSearchProps && Platform.OS === 'android') {
96
- useShadowHeader = false;
97
- }
105
+ const extendedHeightStyle = useAnimatedStyle(() => ({
106
+ height: interpolate(
107
+ sharedValue.value,
108
+ [0, 100],
109
+ [heightHeader + 52, heightHeader],
110
+ Extrapolation.CLAMP,
111
+ ),
112
+ }));
98
113
 
99
114
  if (inputSearchProps) {
100
115
  return (
101
116
  <View style={[{ zIndex: 0 }, showBaseLineDebug && styles.debugBaseLine]}>
102
- <Animated.View style={{ height: height }} />
117
+ <Animated.View style={heightStyle} />
103
118
  <BackgroundImageView
104
119
  useShadowHeader={useShadowHeader}
105
120
  heightHeader={heightHeader}
106
- opacityBackground={opacityBackground}
121
+ animatedValue={sharedValue}
107
122
  headerBackground={headerBackground}
108
123
  />
109
124
  <Animated.View
110
125
  style={[
111
126
  styles.headerBox,
112
- { height: headerType === 'extended' ? height : heightHeader },
127
+ headerType === 'extended'
128
+ ? heightStyle
129
+ : { height: heightHeader },
113
130
  ]}
114
131
  >
115
132
  {!!gradientColor && (
116
- <LinearGradientAnimated
117
- colors={[gradientColor, gradientColor + '00']}
118
- style={[styles.extendedHeader, { opacity: opacityGradient }]}
133
+ <Animated.View
134
+ style={[styles.extendedHeader, gradientOpacityStyle]}
119
135
  >
120
- {!!theme.assets?.headerBackground && (
121
- <Image
122
- style={styles.headerBackground}
123
- source={{ uri: theme.assets?.headerBackground }}
124
- loading={false}
125
- />
126
- )}
127
- </LinearGradientAnimated>
136
+ <LinearGradient
137
+ colors={[gradientColor, gradientColor + '00']}
138
+ style={StyleSheet.absoluteFill}
139
+ >
140
+ {!!theme.assets?.headerBackground && (
141
+ <Image
142
+ style={styles.headerBackground}
143
+ source={{ uri: theme.assets?.headerBackground }}
144
+ loading={false}
145
+ />
146
+ )}
147
+ </LinearGradient>
148
+ </Animated.View>
128
149
  )}
129
150
  </Animated.View>
130
151
  <Animated.View
131
- style={{
132
- justifyContent: 'flex-end',
133
- height,
134
- position: 'absolute',
135
- zIndex: 2,
136
- }}
152
+ style={[
153
+ {
154
+ justifyContent: 'flex-end',
155
+ position: 'absolute',
156
+ zIndex: 2,
157
+ },
158
+ heightStyle,
159
+ ]}
137
160
  >
138
161
  <Animated.View
139
- style={{
140
- transform: [{ translateX }],
141
- marginVertical: Spacing.S,
142
- width: animated.current.interpolate({
143
- inputRange: [0, 100],
144
- outputRange: [
145
- screenWidth - SCREEN_PADDING * 2,
146
- screenWidth - leftPosition - 12 - headerRightWidth,
147
- ],
148
- extrapolate: 'clamp',
149
- }),
150
- }}
162
+ style={[{ marginVertical: Spacing.S }, searchTranslateStyle]}
151
163
  >
152
- <Animated.View style={{ backgroundColor, borderRadius: Radius.XL }}>
164
+ <Animated.View
165
+ style={[
166
+ { borderRadius: Radius.XL },
167
+ searchBackgroundStyle,
168
+ ]}
169
+ >
153
170
  <InputSearch
154
171
  {...inputSearchProps}
155
172
  ref={inputSearchRef}
@@ -169,22 +186,26 @@ const HeaderExtendHeader: React.FC<{
169
186
  <BackgroundImageView
170
187
  useShadowHeader={useShadowHeader}
171
188
  heightHeader={heightHeader}
172
- opacityBackground={opacityBackground}
189
+ animatedValue={sharedValue}
173
190
  headerBackground={headerBackground}
174
191
  />
175
192
  {!!gradientColor && (
176
- <LinearGradientAnimated
177
- colors={[gradientColor, gradientColor + '00']}
178
- style={[styles.extendedHeader, { opacity: opacityGradient }]}
193
+ <Animated.View
194
+ style={[styles.extendedHeader, gradientOpacityStyle, extendedHeightStyle]}
179
195
  >
180
- {!!headerBackground && (
181
- <Image
182
- style={styles.headerBackground}
183
- source={{ uri: headerBackground }}
184
- loading={false}
185
- />
186
- )}
187
- </LinearGradientAnimated>
196
+ <LinearGradient
197
+ colors={[gradientColor, gradientColor + '00']}
198
+ style={StyleSheet.absoluteFill}
199
+ >
200
+ {!!headerBackground && (
201
+ <Image
202
+ style={styles.headerBackground}
203
+ source={{ uri: headerBackground }}
204
+ loading={false}
205
+ />
206
+ )}
207
+ </LinearGradient>
208
+ </Animated.View>
188
209
  )}
189
210
  <View style={{ height: heightHeader }} />
190
211
  </View>
@@ -86,7 +86,7 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
86
86
 
87
87
  const styles = StyleSheet.create({
88
88
  headerLeft: {
89
- marginLeft: 8,
89
+ marginLeft: 12,
90
90
  },
91
91
  });
92
92
 
@@ -1,11 +1,15 @@
1
1
  import React, { useContext } from 'react';
2
2
  import {
3
- Animated,
4
3
  Dimensions,
5
4
  StyleSheet,
6
5
  TouchableOpacity,
7
6
  View,
8
7
  } from 'react-native';
8
+ import Animated, {
9
+ Extrapolation,
10
+ interpolate,
11
+ useAnimatedStyle,
12
+ } from 'react-native-reanimated';
9
13
  import { ApplicationContext, MiniAppContext } from '../../Context';
10
14
  import { exportFontFamily, Text, useScaleSize } from '../../Text';
11
15
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
@@ -17,22 +21,51 @@ import {
17
21
  import { Image } from '../../Image';
18
22
  import { Icon } from '../../Icon';
19
23
  import { Skeleton } from '../../Skeleton';
24
+ import {
25
+ AnimatedCompatValue,
26
+ useNormalizedSharedValue,
27
+ } from '../utils';
28
+
29
+ type HeaderTitleInterpolate = {
30
+ inputRange: number[];
31
+ outputRange: number[];
32
+ };
33
+
34
+ type HeaderTitleExtraProps = {
35
+ animatedValue?: AnimatedCompatValue;
36
+ interpolate?: HeaderTitleInterpolate;
37
+ tintColor?: string;
38
+ children?: React.ReactNode;
39
+ };
20
40
 
21
41
  /**
22
42
  * default header title used for nav
23
43
  */
24
- const HeaderTitle: React.FC<any> = props => {
44
+ const HeaderTitle: React.FC<HeaderTitleExtraProps & { [key: string]: any }> = props => {
25
45
  const context = useContext<any>(MiniAppContext);
26
46
 
27
47
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
28
48
 
29
- const opacity = props.animatedValue?.interpolate(
30
- props.interpolate ?? {
31
- inputRange: [0, 200],
32
- outputRange: [0, 1],
33
- extrapolate: 'clamp',
34
- },
35
- );
49
+ const interpolateConfig = props.interpolate ?? {
50
+ inputRange: [0, 200],
51
+ outputRange: [0, 1],
52
+ };
53
+ const hasAnimated = props.animatedValue != null;
54
+ const sharedValue = useNormalizedSharedValue(props.animatedValue);
55
+
56
+ const animatedStyle = useAnimatedStyle(() => {
57
+ if (!hasAnimated) {
58
+ return { opacity: 1 };
59
+ }
60
+ return {
61
+ opacity: interpolate(
62
+ sharedValue.value,
63
+ interpolateConfig.inputRange,
64
+ interpolateConfig.outputRange,
65
+ Extrapolation.CLAMP,
66
+ ),
67
+ };
68
+ });
36
69
 
37
70
  return (
38
71
  <View
@@ -57,9 +90,9 @@ const HeaderTitle: React.FC<any> = props => {
57
90
  },
58
91
  {
59
92
  fontFamily: exportFontFamily('bold', 'action_xs_bold'),
60
- opacity,
61
93
  color: props.tintColor,
62
94
  },
95
+ animatedStyle,
63
96
  ]}
64
97
  numberOfLines={1}
65
98
  />
@@ -1,16 +1,17 @@
1
1
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
2
2
  import { InputRef, InputSearch } from '../../Input';
3
3
  import {
4
- Animated,
5
4
  Dimensions,
6
5
  StyleSheet,
7
6
  TouchableOpacity,
8
7
  View,
9
8
  } from 'react-native';
10
- import React, { useContext, useEffect, useRef } from 'react';
9
+ import Animated, { useAnimatedStyle } from 'react-native-reanimated';
10
+ import React, { useContext } from 'react';
11
11
  import { SearchHeaderProps } from '../types';
12
12
  import { ApplicationContext, MiniAppContext } from '../../Context';
13
13
  import { Text } from '../../Text';
14
+ import { useNormalizedSharedValue } from '../utils';
14
15
 
15
16
  const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
16
17
  (
@@ -28,28 +29,17 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
28
29
  const BACK_WIDTH = 28;
29
30
  const { width: screenWidth } = Dimensions.get('window');
30
31
 
31
- const animated = useRef(new Animated.Value(0));
32
+ const sharedValue = useNormalizedSharedValue(animatedValue);
32
33
  const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
33
34
  const headerRightWidth = props?.headerRightWidth ?? 73;
34
35
 
35
- useEffect(() => {
36
- const listener = animatedValue?.addListener(({ value }) => {
37
- animated.current.setValue(value);
38
- });
39
- return () => {
40
- if (listener) {
41
- animatedValue?.removeListener(listener);
42
- }
36
+ const backgroundStyle = useAnimatedStyle(() => {
37
+ const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
38
+ return {
39
+ backgroundColor: t === 0
40
+ ? theme.colors.background.surface
41
+ : theme.colors.background.default,
43
42
  };
44
- }, [animatedValue]);
45
-
46
- const backgroundColor = animated.current?.interpolate({
47
- inputRange: [0, 100],
48
- outputRange: [
49
- theme.colors.background.surface,
50
- theme.colors.background.default,
51
- ],
52
- extrapolate: 'clamp',
53
43
  });
54
44
 
55
45
  const goBack = () => {
@@ -91,7 +81,9 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
91
81
  },
92
82
  ]}
93
83
  >
94
- <Animated.View style={{ backgroundColor, borderRadius: Radius.XL }}>
84
+ <Animated.View
85
+ style={[{ borderRadius: Radius.XL }, backgroundStyle]}
86
+ >
95
87
  <InputSearch
96
88
  ref={ref}
97
89
  {...props}
@@ -29,7 +29,6 @@ import { HeaderBackground } from './Components/HeaderBackground';
29
29
 
30
30
  const Stack = createStackNavigator();
31
31
 
32
- // Observer storage key the host app writes FontScaleConfig to. Lowercased natively.
33
32
  const FONT_SCALE_OBSERVER_KEY = 'font_scale_config';
34
33
 
35
34
  const NavigationContainer: React.FC<NavigationContainerProps> = ({
@@ -48,8 +47,6 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
48
47
  }) => {
49
48
  const context = useContext<any>(MiniAppContext);
50
49
  const [currentContext, setCurrentContext] = useState<any>({});
51
- // Default = the host-seeded fontScaleConfig from MiniAppContext, so the first frame is already
52
- // scaled (no flash). The observer below overwrites it on later host writes (live rate changes).
53
50
  const [observerFontScaleConfig, setObserverFontScaleConfig] = useState<
54
51
  FontScaleConfig | undefined
55
52
  >(context?.fontScaleConfig);
@@ -64,9 +61,6 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
64
61
  };
65
62
 
66
63
  useEffect(() => {
67
- // The initial value is seeded by the host into MiniAppContext and used as this state's default
68
- // above, so the first frame is already scaled (no flash). Here we only subscribe to later host
69
- // writes so a rate change applies live while the miniapp is open.
70
64
  const sub = maxApi?.observer?.(
71
65
  FONT_SCALE_OBSERVER_KEY,
72
66
  (data: FontScaleConfig) => {
@@ -7,6 +7,7 @@ import {
7
7
  ViewProps,
8
8
  ViewStyle,
9
9
  } from 'react-native';
10
+ import type { SharedValue } from 'react-native-reanimated';
10
11
  import { PopupNotifyProps } from '../Popup/types';
11
12
  import { InputRef, InputSearchProps } from '../Input';
12
13
  import { FontScaleConfig } from '../Context';
@@ -259,7 +260,7 @@ export interface HeaderBackProps extends NavigationButtonProps {
259
260
  }
260
261
 
261
262
  export type HeaderBackgroundProps = {
262
- animatedValue?: Animated.Value;
263
+ animatedValue?: Animated.Value | SharedValue<number>;
263
264
  useGradient?: boolean;
264
265
  useShadowHeader?: boolean;
265
266
  backgroundColor?: string;
@@ -299,7 +300,7 @@ export type TitleJourneyProps = {
299
300
  };
300
301
 
301
302
  export interface HeaderAnimatedProps extends ViewProps {
302
- animatedValue: Animated.Value;
303
+ animatedValue: Animated.Value | SharedValue<number>;
303
304
  image: string;
304
305
  useScale?: boolean;
305
306
  }
@@ -343,7 +344,7 @@ export type AnimatedHeader = {
343
344
 
344
345
  export interface SearchHeaderProps extends InputSearchProps {
345
346
  ref?: React.RefObject<InputRef>;
346
- animatedValue?: Animated.Value;
347
+ animatedValue?: Animated.Value | SharedValue<number>;
347
348
  headerRightWidth?: 0 | 74 | 110 | number;
348
349
  leftPosition?: 12 | 48 | number;
349
350
  renderButtons?: () => ReactNode;
@@ -6,6 +6,8 @@ import {
6
6
  import type { HeaderTitleProps, NavigationOptions } from './types';
7
7
  import { Colors, Spacing } from '../Consts';
8
8
  import { Animated, AppState, Platform } from 'react-native';
9
+ import type { SharedValue } from 'react-native-reanimated';
10
+ import { useSharedValue } from 'react-native-reanimated';
9
11
  import {
10
12
  MiniAppContext,
11
13
  ScreenContext,
@@ -60,7 +62,7 @@ const getModalOptions = (): StackNavigationOptions => {
60
62
  */
61
63
  const getOptions = (
62
64
  params: NavigationOptions,
63
- animatedValue?: Animated.Value,
65
+ animatedValue?: SharedValue<number>,
64
66
  ) => {
65
67
  let options: StackNavigationOptions = {};
66
68
 
@@ -131,7 +133,7 @@ const getOptions = (
131
133
 
132
134
  const exportHeaderTitle = (
133
135
  params: NavigationOptions,
134
- animatedValue?: Animated.Value,
136
+ animatedValue?: SharedValue<number>,
135
137
  ): StackNavigationOptions => {
136
138
  if (typeof params.headerTitle === 'object') {
137
139
  return {
@@ -228,6 +230,43 @@ const useAppState = () => {
228
230
  };
229
231
  };
230
232
 
233
+ /**
234
+ * Union of the two animation runtimes an internal foundation component might
235
+ * receive. Public Screen props still accept React Native Animated.Value only;
236
+ * this type keeps lower-level header/floating components compatible while they
237
+ * render with reanimated.
238
+ */
239
+ export type AnimatedCompatValue = Animated.Value | SharedValue<number>;
240
+
241
+ const isRNAnimatedValue = (v: unknown): v is Animated.Value =>
242
+ !!v &&
243
+ typeof (v as Animated.Value).setValue === 'function' &&
244
+ typeof (v as Animated.Value).interpolate === 'function';
245
+
246
+ const isSharedValue = (v: unknown): v is SharedValue<number> =>
247
+ !!v && !isRNAnimatedValue(v) && 'value' in (v as object);
248
+
249
+ const useNormalizedSharedValue = (
250
+ input?: AnimatedCompatValue,
251
+ ): SharedValue<number> => {
252
+ const fallback = useSharedValue(0);
253
+
254
+ useEffect(() => {
255
+ if (!isRNAnimatedValue(input)) {
256
+ return;
257
+ }
258
+ const id = input.addListener(({ value }) => {
259
+ fallback.value = value;
260
+ });
261
+ return () => input.removeListener(id);
262
+ }, [input, fallback]);
263
+
264
+ if (isSharedValue(input)) {
265
+ return input;
266
+ }
267
+ return fallback;
268
+ };
269
+
231
270
  export {
232
271
  getStackOptions,
233
272
  getModalOptions,
@@ -236,4 +275,5 @@ export {
236
275
  setAutomationID,
237
276
  useComponentId,
238
277
  useAppState,
278
+ useNormalizedSharedValue,
239
279
  };