@momo-kits/foundation 0.161.2-beta.8 → 0.161.2-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.
@@ -8,13 +8,22 @@ import React, {
8
8
  useState,
9
9
  } from 'react';
10
10
  import {
11
- Animated,
12
11
  PixelRatio,
13
12
  TextInput,
14
13
  TextInputFocusEvent,
15
14
  TouchableOpacity,
16
15
  View,
17
16
  } from 'react-native';
17
+ import Animated, {
18
+ cancelAnimation,
19
+ Easing,
20
+ useAnimatedStyle,
21
+ useSharedValue,
22
+ withDelay,
23
+ withRepeat,
24
+ withSequence,
25
+ withTiming,
26
+ } from 'react-native-reanimated';
18
27
  import { useComponentId } from '../Application';
19
28
  import { Spacing, Styles } from '../Consts';
20
29
  import { useScaleSize, Text } from '../Text';
@@ -32,37 +41,43 @@ import {
32
41
  const OTPCaret: FC<CaretProps> = ({ index, length }) => {
33
42
  const DURATION = 300;
34
43
  const { theme } = useContext(ApplicationContext);
35
- const opacity = useRef(new Animated.Value(0)).current;
44
+ const opacity = useSharedValue(0);
36
45
 
37
46
  useEffect(() => {
38
- Animated.loop(
39
- Animated.sequence([
40
- Animated.timing(opacity, {
41
- toValue: 1,
42
- duration: DURATION,
43
- useNativeDriver: true,
44
- }),
45
- Animated.delay(DURATION * 2),
46
- Animated.timing(opacity, {
47
- toValue: 0,
48
- duration: DURATION,
49
- useNativeDriver: true,
50
- }),
51
- ]),
52
- ).start();
47
+ opacity.value = withRepeat(
48
+ withSequence(
49
+ withTiming(1, { duration: DURATION, easing: Easing.linear }),
50
+ withDelay(
51
+ DURATION * 2,
52
+ withTiming(0, { duration: DURATION, easing: Easing.linear }),
53
+ ),
54
+ ),
55
+ -1,
56
+ false,
57
+ );
58
+ return () => {
59
+ cancelAnimation(opacity);
60
+ };
53
61
  }, [opacity]);
62
+
63
+ const animatedStyle = useAnimatedStyle(() => ({
64
+ opacity: opacity.value,
65
+ }));
66
+
54
67
  const spacingStyle = !isNaN(Number(length)) &&
55
68
  index !== Number(length) - 1 && { marginRight: Spacing.L };
56
69
 
57
70
  return (
58
71
  <View style={[Styles.rowCenter, spacingStyle]}>
59
72
  <Animated.View
60
- style={{
61
- height: useScaleSize(12),
62
- width: 1,
63
- backgroundColor: theme.colors.primary,
64
- opacity,
65
- }}
73
+ style={[
74
+ {
75
+ height: useScaleSize(12),
76
+ width: 1,
77
+ backgroundColor: theme.colors.primary,
78
+ },
79
+ animatedStyle,
80
+ ]}
66
81
  />
67
82
  <Text color={theme.colors.text.hint} typography={'body_default_regular'}>
68
83
  -
@@ -1,17 +1,18 @@
1
+ import React, { useContext, useEffect, useState } from 'react';
1
2
  import {
2
- default as React,
3
- useContext,
4
- useEffect,
5
- useRef,
6
- useState,
7
- } from 'react';
8
- import {
9
- Animated,
10
3
  LayoutChangeEvent,
11
4
  StyleSheet,
12
5
  TouchableOpacity,
13
6
  View,
14
7
  } from 'react-native';
8
+ import Animated, {
9
+ useAnimatedReaction,
10
+ useAnimatedStyle,
11
+ useSharedValue,
12
+ withTiming,
13
+ runOnJS,
14
+ type SharedValue,
15
+ } from 'react-native-reanimated';
15
16
  import { ApplicationContext } from '../Context';
16
17
  import { Icon } from '../Icon';
17
18
  import { useScaleSize } from '../Text';
@@ -23,7 +24,7 @@ export interface FloatingButtonProps {
23
24
  icon?: string;
24
25
  iconColor?: string;
25
26
  size?: 'small' | 'large';
26
- animatedValue?: Animated.Value;
27
+ animatedValue?: SharedValue<number>;
27
28
  bottom?: number;
28
29
  renderComponent?: () => React.ReactNode;
29
30
  }
@@ -42,68 +43,67 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
42
43
  const { theme } = useContext(ApplicationContext);
43
44
  const scaledFontSize = useScaleSize(16);
44
45
  const scaledLineHeight = useScaleSize(22);
45
- const maxWidth = useRef(0);
46
+ const maxWidth = useSharedValue(0);
46
47
  const minWidth = size === 'small' ? 36 : 48;
47
- const [opacityAnimated] = useState(new Animated.Value(0)); // Initial opacity set to 0
48
- const [widthAnimated, setWidthAnimated] = useState<Animated.Value>();
49
- const lastOffset = useRef(0);
50
- const lastDirection = useRef<string>(null);
51
- const [showText, setShowText] = React.useState(true);
48
+ const opacityAnimated = useSharedValue(0);
49
+ const widthAnimated = useSharedValue<number | null>(null);
50
+ const lastOffset = useSharedValue(0);
51
+ const lastDirection = useSharedValue<string | null>(null);
52
+ const [showText, setShowText] = useState(true);
52
53
 
53
54
  useEffect(() => {
54
55
  if (!label) return;
56
+ opacityAnimated.value = withTiming(1, { duration: 100 });
57
+ }, [label, opacityAnimated]);
55
58
 
56
- Animated.timing(opacityAnimated, {
57
- toValue: 1,
58
- duration: 100,
59
- useNativeDriver: true,
60
- }).start();
61
-
62
- const listener = animatedValue?.addListener(({ value }) => {
63
- if (value !== lastOffset.current && value > 0) {
64
- const direction = value > lastOffset.current ? 'down' : 'up';
65
- lastOffset.current = value;
66
- if (lastDirection.current !== direction) {
67
- lastDirection.current = direction;
59
+ useAnimatedReaction(
60
+ () => animatedValue?.value ?? 0,
61
+ (value) => {
62
+ 'worklet';
63
+ if (!label || !animatedValue) return;
64
+ if (value !== lastOffset.value && value > 0) {
65
+ const direction = value > lastOffset.value ? 'down' : 'up';
66
+ lastOffset.value = value;
67
+ if (lastDirection.value !== direction) {
68
+ lastDirection.value = direction;
68
69
  if (direction === 'down') {
69
- Animated.timing(opacityAnimated, {
70
- toValue: 0,
71
- duration: 100,
72
- useNativeDriver: true,
73
- }).start();
74
- Animated.timing(widthAnimated!, {
75
- toValue: minWidth,
76
- duration: 100,
77
- useNativeDriver: false,
78
- }).start(() => setShowText(false));
70
+ opacityAnimated.value = withTiming(0, { duration: 100 });
71
+ widthAnimated.value = withTiming(
72
+ minWidth,
73
+ { duration: 100 },
74
+ (finished) => {
75
+ if (finished) runOnJS(setShowText)(false);
76
+ },
77
+ );
79
78
  } else {
80
- Animated.timing(opacityAnimated, {
81
- toValue: 1,
82
- duration: 100,
83
- useNativeDriver: true,
84
- }).start();
85
- Animated.timing(widthAnimated!, {
86
- toValue: maxWidth.current,
87
- duration: 100,
88
- useNativeDriver: false,
89
- }).start(() => setShowText(true));
79
+ opacityAnimated.value = withTiming(1, { duration: 100 });
80
+ widthAnimated.value = withTiming(
81
+ maxWidth.value,
82
+ { duration: 100 },
83
+ (finished) => {
84
+ if (finished) runOnJS(setShowText)(true);
85
+ },
86
+ );
90
87
  }
91
88
  }
92
89
  }
93
- });
90
+ },
91
+ [label, animatedValue, minWidth],
92
+ );
94
93
 
95
- return () => {
96
- if (listener) {
97
- animatedValue?.removeListener(listener);
98
- }
99
- };
100
- }, [animatedValue, label, minWidth, opacityAnimated, widthAnimated]);
94
+ const containerStyle = useAnimatedStyle(() => ({
95
+ width: widthAnimated.value ?? undefined,
96
+ }));
97
+
98
+ const labelStyle = useAnimatedStyle(() => ({
99
+ opacity: opacityAnimated.value,
100
+ }));
101
101
 
102
102
  const handleLayout = (event: LayoutChangeEvent) => {
103
103
  const layout = event.nativeEvent.layout;
104
- if (widthAnimated) return;
105
- maxWidth.current = layout.width;
106
- setWidthAnimated(new Animated.Value(layout.width));
104
+ if (widthAnimated.value != null) return;
105
+ maxWidth.value = layout.width;
106
+ widthAnimated.value = layout.width;
107
107
  };
108
108
 
109
109
  if (renderComponent) {
@@ -132,11 +132,11 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
132
132
  {
133
133
  right: position === 'right' ? 12 : undefined,
134
134
  alignSelf: position === 'center' ? 'center' : 'flex-end',
135
- width: widthAnimated,
136
135
  height: size === 'small' ? 36 : 48,
137
136
  backgroundColor: theme.colors.primary,
138
137
  bottom,
139
138
  },
139
+ containerStyle,
140
140
  ]}
141
141
  >
142
142
  <TouchableOpacity
@@ -156,9 +156,9 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
156
156
  {
157
157
  fontSize: scaledFontSize,
158
158
  lineHeight: scaledLineHeight,
159
- opacity: opacityAnimated,
160
159
  color: 'white',
161
160
  },
161
+ labelStyle,
162
162
  ]}
163
163
  numberOfLines={1}
164
164
  >
package/Layout/Screen.tsx CHANGED
@@ -12,7 +12,6 @@ import React, {
12
12
  useRef,
13
13
  } from 'react';
14
14
  import {
15
- Animated,
16
15
  KeyboardAvoidingView,
17
16
  NativeScrollEvent,
18
17
  NativeSyntheticEvent,
@@ -25,6 +24,13 @@ import {
25
24
  View,
26
25
  ViewProps,
27
26
  } from 'react-native';
27
+ import Animated, {
28
+ runOnJS,
29
+ useAnimatedScrollHandler,
30
+ useSharedValue,
31
+ withTiming,
32
+ type SharedValue,
33
+ } from 'react-native-reanimated';
28
34
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
29
35
  import { ApplicationContext, ScreenContext } from '../Context';
30
36
  import Navigation from '../Application/Navigation';
@@ -138,7 +144,7 @@ export interface ScreenProps extends ViewProps {
138
144
  /**
139
145
  * Optional. Animated value for header.
140
146
  */
141
- animatedValue?: Animated.Value;
147
+ animatedValue?: SharedValue<number>;
142
148
 
143
149
  /**
144
150
  * Optional. If `true`, use shadow header.
@@ -195,18 +201,23 @@ const Screen = forwardRef(
195
201
  const screen: any = useContext(ScreenContext);
196
202
  const insets = useSafeAreaInsets();
197
203
  const heightHeader = useHeaderHeight();
198
- const animatedValue = useRef<Animated.Value>(
199
- customAnimatedValue || new Animated.Value(0),
200
- );
204
+ const internalAnimatedValue = useSharedValue(0);
205
+ const animatedValue = customAnimatedValue ?? internalAnimatedValue;
201
206
  const currentTint = useRef<string | undefined>(undefined);
202
207
  const isTab = navigation?.instance?.getState?.()?.type === 'tab';
203
208
 
204
- let handleScroll;
209
+ let handleScroll: any;
205
210
  let Component: any = View;
206
211
 
207
- let keyboardOffset = heightHeader - Math.min(insets.bottom, 21);
212
+ // AI-GENERATED START
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;
208
219
  if (headerType === 'extended' || animatedHeader || inputSearchProps) {
209
- keyboardOffset = -Math.min(insets.bottom, 21);
220
+ keyboardOffset = -bottomInset;
210
221
  }
211
222
 
212
223
  /**
@@ -248,9 +259,8 @@ const Screen = forwardRef(
248
259
  interpolate={{
249
260
  inputRange: [0, 50],
250
261
  outputRange: [1, 0],
251
- extrapolate: 'clamp',
252
262
  }}
253
- animatedValue={animatedValue.current}
263
+ animatedValue={animatedValue}
254
264
  />
255
265
  ),
256
266
  };
@@ -265,7 +275,7 @@ const Screen = forwardRef(
265
275
  headerBackground: (props: any) => (
266
276
  <HeaderBackground
267
277
  {...props}
268
- animatedValue={animatedValue.current}
278
+ animatedValue={animatedValue}
269
279
  useShadowHeader={useShadowHeader}
270
280
  headerBackground={headerBackground}
271
281
  gradientColor={gradientColor}
@@ -284,9 +294,8 @@ const Screen = forwardRef(
284
294
  interpolate={{
285
295
  inputRange: [0, 50],
286
296
  outputRange: [1, 0],
287
- extrapolate: 'clamp',
288
297
  }}
289
- animatedValue={animatedValue.current}
298
+ animatedValue={animatedValue}
290
299
  />
291
300
  ),
292
301
  };
@@ -321,7 +330,7 @@ const Screen = forwardRef(
321
330
  headerBackground: (props: any) => (
322
331
  <HeaderBackground
323
332
  {...props}
324
- animatedValue={animatedValue.current}
333
+ animatedValue={animatedValue}
325
334
  useGradient={false}
326
335
  useShadowHeader={useShadowHeader}
327
336
  headerBackground={headerBackground}
@@ -359,9 +368,8 @@ const Screen = forwardRef(
359
368
  interpolate={{
360
369
  inputRange: [0, 50],
361
370
  outputRange: [1, 0],
362
- extrapolate: 'clamp',
363
371
  }}
364
- animatedValue={animatedValue.current}
372
+ animatedValue={animatedValue}
365
373
  />
366
374
  ),
367
375
  };
@@ -390,7 +398,7 @@ const Screen = forwardRef(
390
398
  headerLeft: (props: any) =>
391
399
  params?.hiddenBack ? null : <HeaderLeft {...props} />,
392
400
  headerTitle: () => (
393
- <SearchHeader {...params} animatedValue={animatedValue.current} />
401
+ <SearchHeader {...params} animatedValue={animatedValue} />
394
402
  ),
395
403
  };
396
404
 
@@ -441,45 +449,51 @@ const Screen = forwardRef(
441
449
  });
442
450
  });
443
451
 
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
+
444
491
  /**
445
492
  * animated when use scroll && animated value
446
493
  */
447
494
  if (scrollable) {
448
495
  Component = Animated.ScrollView;
449
- handleScroll = Animated.event(
450
- [
451
- {
452
- nativeEvent: {
453
- contentOffset: { y: animatedValue.current as Animated.Value },
454
- },
455
- },
456
- ],
457
- {
458
- useNativeDriver: true,
459
- listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
460
- scrollViewProps?.onScroll?.(e);
461
- if (animatedHeader) {
462
- const offsetY = e.nativeEvent.contentOffset.y;
463
- let color = animatedHeader?.headerTintColor ?? Colors.black_17;
464
- if (offsetY > 50) {
465
- color = Colors.black_17;
466
- }
467
- if (color !== currentTint.current) {
468
- currentTint.current = color;
469
- navigation?.setOptions({
470
- headerTintColor: color,
471
- });
472
-
473
- let barStyle: StatusBarStyle = 'dark-content';
474
- if (currentTint.current === Colors.black_01) {
475
- barStyle = 'light-content';
476
- }
477
- StatusBar.setBarStyle(barStyle, true);
478
- }
479
- }
480
- },
481
- },
482
- );
496
+ handleScroll = scrollHandler;
483
497
  }
484
498
 
485
499
  /**
@@ -489,11 +503,7 @@ const Screen = forwardRef(
489
503
  const handleScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
490
504
  const offsetY = e.nativeEvent.contentOffset.y;
491
505
  if (inputSearchProps && offsetY < 100 && offsetY > 0) {
492
- Animated.timing(animatedValue.current, {
493
- toValue: 0,
494
- useNativeDriver: true,
495
- duration: 300,
496
- }).start();
506
+ animatedValue.value = withTiming(0, { duration: 300 });
497
507
  ref?.scrollTo?.({ y: 0, animated: true });
498
508
  }
499
509
  scrollViewProps?.onScrollEndDrag?.(e);
@@ -509,7 +519,7 @@ const Screen = forwardRef(
509
519
  style={[styles.screenBanner, { maxHeight: 210 + layoutOffset }]}
510
520
  >
511
521
  {animatedHeader?.component({
512
- animatedValue: animatedValue.current,
522
+ animatedValue: animatedValue,
513
523
  })}
514
524
  </View>
515
525
  );
@@ -576,7 +586,7 @@ const Screen = forwardRef(
576
586
  headerType={headerType}
577
587
  heightHeader={heightHeader}
578
588
  headerRightWidth={headerRightWidth}
579
- animatedValue={animatedValue.current}
589
+ animatedValue={animatedValue}
580
590
  inputSearchProps={inputSearchProps}
581
591
  navigation={navigation}
582
592
  inputSearchRef={inputSearchRef}
@@ -614,9 +624,9 @@ const Screen = forwardRef(
614
624
  <View>
615
625
  <FloatingButton
616
626
  {...floatingButtonProps}
617
- animatedValue={animatedValue.current}
627
+ animatedValue={animatedValue}
618
628
  bottom={
619
- Footer || isTab ? 12 : Math.min(insets.bottom, 21) + Spacing.S
629
+ Footer || isTab ? 12 : bottomInset + Spacing.S
620
630
  }
621
631
  />
622
632
  </View>
@@ -627,7 +637,7 @@ const Screen = forwardRef(
627
637
  style={[
628
638
  styles.shadow,
629
639
  {
630
- paddingBottom: Math.min(insets.bottom, 21) + Spacing.S,
640
+ paddingBottom: bottomInset + Spacing.S,
631
641
  backgroundColor: theme.colors.background.surface,
632
642
  },
633
643
  ]}
@@ -1,5 +1,10 @@
1
- import React, { FC, useContext, useEffect, useRef } from 'react';
2
- import { Animated, View } from 'react-native';
1
+ import React, { FC, useContext, useEffect } from 'react';
2
+ import { View } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withTiming,
7
+ } from 'react-native-reanimated';
3
8
  import styles from './styles';
4
9
  import { ProgressBarProps } from './types';
5
10
  import { ApplicationContext } from '../Context';
@@ -7,20 +12,15 @@ import { Radius } from '../Consts';
7
12
 
8
13
  const ProgressBar: FC<ProgressBarProps> = ({ percent = 0, style }) => {
9
14
  const { theme } = useContext(ApplicationContext);
10
- const animation = useRef(new Animated.Value(0)).current;
15
+ const animation = useSharedValue(0);
11
16
 
12
17
  useEffect(() => {
13
- Animated.timing(animation, {
14
- toValue: percent,
15
- duration: 200,
16
- useNativeDriver: false,
17
- }).start();
18
+ animation.value = withTiming(percent, { duration: 200 });
18
19
  }, [percent, animation]);
19
20
 
20
- const width = animation.interpolate({
21
- inputRange: [0, 100],
22
- outputRange: ['0%', '100%'],
23
- });
21
+ const animatedStyle = useAnimatedStyle(() => ({
22
+ width: `${Math.min(Math.max(animation.value, 0), 100)}%`,
23
+ }));
24
24
 
25
25
  return (
26
26
  <View
@@ -31,12 +31,14 @@ const ProgressBar: FC<ProgressBarProps> = ({ percent = 0, style }) => {
31
31
  ]}
32
32
  >
33
33
  <Animated.View
34
- style={{
35
- height: 4,
36
- borderRadius: Radius.XXS,
37
- width,
38
- backgroundColor: theme.colors.primary,
39
- }}
34
+ style={[
35
+ {
36
+ height: 4,
37
+ borderRadius: Radius.XXS,
38
+ backgroundColor: theme.colors.primary,
39
+ },
40
+ animatedStyle,
41
+ ]}
40
42
  />
41
43
  </View>
42
44
  );
@@ -1,5 +1,5 @@
1
1
  import React, { FC, useContext } from 'react';
2
- import { Animated } from 'react-native';
2
+ import { View } from 'react-native';
3
3
  import styles from './styles';
4
4
  import { DotProps } from './types';
5
5
  import { ApplicationContext } from '../Context';
@@ -13,7 +13,7 @@ const Dot: FC<DotProps> = ({ active, style }) => {
13
13
  { backgroundColor: theme.colors.background.pressed },
14
14
  ];
15
15
 
16
- return <Animated.View style={[style, dotStyle]} />;
16
+ return <View style={[style, dotStyle]} />;
17
17
  };
18
18
 
19
19
  export default Dot;
@@ -1,5 +1,12 @@
1
- import React, { FC, useContext, useRef, useState } from 'react';
2
- import { Animated, View } from 'react-native';
1
+ import React, { FC, useContext, useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import Animated, {
4
+ Extrapolation,
5
+ interpolate,
6
+ useAnimatedScrollHandler,
7
+ useAnimatedStyle,
8
+ useSharedValue,
9
+ } from 'react-native-reanimated';
3
10
  import { ScrollIndicatorProps } from './types';
4
11
  import styles from './styles';
5
12
  import { ApplicationContext, MiniAppContext } from '../Context';
@@ -13,40 +20,37 @@ const PaginationScroll: FC<ScrollIndicatorProps> = ({
13
20
  }) => {
14
21
  const { theme } = useContext(ApplicationContext);
15
22
  const context = useContext<any>(MiniAppContext);
16
- const left = useRef(new Animated.Value(0)).current;
23
+ const left = useSharedValue(0);
17
24
  const [scrollViewWidth, setScrollViewWidth] = useState(0);
18
25
  const [scrollContentWidth, setScrollContentWidth] = useState(0);
19
26
 
20
27
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
21
28
 
22
- const translateX = () => {
23
- if (scrollViewWidth && scrollContentWidth) {
24
- const value = left.interpolate({
25
- inputRange: [0, scrollContentWidth - scrollViewWidth],
26
- outputRange: [0, INDICATOR_CONTAINER_WIDTH - INDICATOR_WIDTH],
27
- extrapolate: 'clamp',
28
- });
29
- return { transform: [{ translateX: value }] };
29
+ const onScroll = useAnimatedScrollHandler({
30
+ onScroll: (event) => {
31
+ left.value = event.contentOffset.x;
32
+ },
33
+ });
34
+
35
+ const indicatorStyle = useAnimatedStyle(() => {
36
+ if (!scrollViewWidth || !scrollContentWidth) {
37
+ return {};
30
38
  }
31
- return {};
32
- };
39
+ const value = interpolate(
40
+ left.value,
41
+ [0, scrollContentWidth - scrollViewWidth],
42
+ [0, INDICATOR_CONTAINER_WIDTH - INDICATOR_WIDTH],
43
+ Extrapolation.CLAMP,
44
+ );
45
+ return { transform: [{ translateX: value }] };
46
+ });
33
47
 
34
48
  const renderScrollView = () => {
35
49
  return (
36
50
  <Animated.ScrollView
37
- ref={scrollViewRef}
38
- onScroll={Animated.event(
39
- [
40
- {
41
- nativeEvent: {
42
- contentOffset: {
43
- x: left,
44
- },
45
- },
46
- },
47
- ],
48
- { useNativeDriver: true },
49
- )}
51
+ ref={scrollViewRef as any}
52
+ onScroll={onScroll}
53
+ scrollEventThrottle={16}
50
54
  alwaysBounceHorizontal={false}
51
55
  showsHorizontalScrollIndicator={false}
52
56
  horizontal
@@ -76,7 +80,7 @@ const PaginationScroll: FC<ScrollIndicatorProps> = ({
76
80
  {
77
81
  backgroundColor: theme.colors.primary,
78
82
  },
79
- translateX(),
83
+ indicatorStyle,
80
84
  ]}
81
85
  />
82
86
  </View>