react-native-directional-toggle 0.1.1 → 0.1.2

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.
@@ -0,0 +1,43 @@
1
+ import { type StyleProp, type ViewStyle, type TextStyle } from 'react-native';
2
+ import { type WithSpringConfig, type WithTimingConfig } from 'react-native-reanimated';
3
+ type Option = {
4
+ label: string;
5
+ value: string | number;
6
+ };
7
+ export type AnimatedSwitchProps = {
8
+ options: Option[];
9
+ value: string | number;
10
+ onChange: (value: string | number) => void;
11
+ /**
12
+ * Direction of the switch.
13
+ * @default false (Horizontal)
14
+ */
15
+ vertical?: boolean;
16
+ /**
17
+ * Container style. Use this to set width/height.
18
+ */
19
+ style?: StyleProp<ViewStyle>;
20
+ /**
21
+ * Style for the moving thumb.
22
+ */
23
+ thumbStyle?: StyleProp<ViewStyle>;
24
+ /**
25
+ * Base style for option text.
26
+ */
27
+ textStyle?: StyleProp<TextStyle>;
28
+ /**
29
+ * Style for the text when active.
30
+ */
31
+ activeTextStyle?: StyleProp<TextStyle>;
32
+ /**
33
+ * Style for the text when inactive.
34
+ */
35
+ inactiveTextStyle?: StyleProp<TextStyle>;
36
+ /**
37
+ * Animation configuration.
38
+ */
39
+ animationConfig?: WithTimingConfig | WithSpringConfig;
40
+ };
41
+ export declare function AnimatedSwitch({ options, value, onChange, vertical, style, thumbStyle, textStyle, activeTextStyle, inactiveTextStyle, animationConfig, }: AnimatedSwitchProps): import("react/jsx-runtime").JSX.Element;
42
+ export {};
43
+ //# sourceMappingURL=AnimatedSwitch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedSwitch.d.ts","sourceRoot":"","sources":["../../../../src/AnimatedSwitch.tsx"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAGtB,OAAiB,EAMf,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAEtB,MAAM,yBAAyB,CAAC;AAEjC,KAAK,MAAM,GAAG;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC;;OAEG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC;;OAEG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACvC;;OAEG;IACH,iBAAiB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACzC;;OAEG;IACH,eAAe,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,CAAC;CACvD,CAAC;AAQF,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAgB,EAChB,KAAK,EACL,UAAU,EACV,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,eAAmC,GACpC,EAAE,mBAAmB,2CAiIrB"}
@@ -0,0 +1,4 @@
1
+ import { AnimatedSwitch, type AnimatedSwitchProps } from './AnimatedSwitch';
2
+ export default AnimatedSwitch;
3
+ export { AnimatedSwitch, type AnimatedSwitchProps };
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5E,eAAe,cAAc,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { type StyleProp, type ViewStyle, type TextStyle } from 'react-native';
2
+ import { type WithSpringConfig, type WithTimingConfig } from 'react-native-reanimated';
3
+ type Option = {
4
+ label: string;
5
+ value: string | number;
6
+ };
7
+ export type AnimatedSwitchProps = {
8
+ options: Option[];
9
+ value: string | number;
10
+ onChange: (value: string | number) => void;
11
+ /**
12
+ * Direction of the switch.
13
+ * @default false (Horizontal)
14
+ */
15
+ vertical?: boolean;
16
+ /**
17
+ * Container style. Use this to set width/height.
18
+ */
19
+ style?: StyleProp<ViewStyle>;
20
+ /**
21
+ * Style for the moving thumb.
22
+ */
23
+ thumbStyle?: StyleProp<ViewStyle>;
24
+ /**
25
+ * Base style for option text.
26
+ */
27
+ textStyle?: StyleProp<TextStyle>;
28
+ /**
29
+ * Style for the text when active.
30
+ */
31
+ activeTextStyle?: StyleProp<TextStyle>;
32
+ /**
33
+ * Style for the text when inactive.
34
+ */
35
+ inactiveTextStyle?: StyleProp<TextStyle>;
36
+ /**
37
+ * Animation configuration.
38
+ */
39
+ animationConfig?: WithTimingConfig | WithSpringConfig;
40
+ };
41
+ export declare function AnimatedSwitch({ options, value, onChange, vertical, style, thumbStyle, textStyle, activeTextStyle, inactiveTextStyle, animationConfig, }: AnimatedSwitchProps): import("react/jsx-runtime").JSX.Element;
42
+ export {};
43
+ //# sourceMappingURL=AnimatedSwitch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedSwitch.d.ts","sourceRoot":"","sources":["../../../../src/AnimatedSwitch.tsx"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAGtB,OAAiB,EAMf,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAEtB,MAAM,yBAAyB,CAAC;AAEjC,KAAK,MAAM,GAAG;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC;;OAEG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC;;OAEG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACvC;;OAEG;IACH,iBAAiB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACzC;;OAEG;IACH,eAAe,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,CAAC;CACvD,CAAC;AAQF,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAgB,EAChB,KAAK,EACL,UAAU,EACV,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,eAAmC,GACpC,EAAE,mBAAmB,2CAiIrB"}
@@ -0,0 +1,4 @@
1
+ import { AnimatedSwitch, type AnimatedSwitchProps } from './AnimatedSwitch';
2
+ export default AnimatedSwitch;
3
+ export { AnimatedSwitch, type AnimatedSwitchProps };
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5E,eAAe,cAAc,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,31 +1,32 @@
1
1
  {
2
2
  "name": "react-native-directional-toggle",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Multi-element toggle component for React Native and Expo with support for vertical and horizontal layouts and animations.",
5
- "main": "./lib/module/index.js",
6
- "types": "./lib/typescript/src/index.d.ts",
5
+ "source": "./src/index.tsx",
6
+ "main": "./lib/commonjs/index.js",
7
+ "module": "./lib/module/index.js",
8
+ "types": "./lib/typescript/module/src/index.d.ts",
9
+ "react-native": "./src/index.tsx",
7
10
  "exports": {
8
11
  ".": {
9
- "source": "./src/index.tsx",
10
- "types": "./lib/typescript/src/index.d.ts",
11
- "default": "./lib/module/index.js"
12
+ "import": {
13
+ "types": "./lib/typescript/module/src/index.d.ts",
14
+ "default": "./lib/module/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./lib/typescript/commonjs/src/index.d.ts",
18
+ "default": "./lib/commonjs/index.js"
19
+ }
12
20
  },
13
21
  "./package.json": "./package.json"
14
22
  },
23
+ "sideEffects": false,
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
15
27
  "files": [
16
28
  "src",
17
29
  "lib",
18
- "android",
19
- "ios",
20
- "cpp",
21
- "*.podspec",
22
- "react-native.config.js",
23
- "!ios/build",
24
- "!android/build",
25
- "!android/gradle",
26
- "!android/gradlew",
27
- "!android/gradlew.bat",
28
- "!android/local.properties",
29
30
  "!**/__tests__",
30
31
  "!**/__fixtures__",
31
32
  "!**/__mocks__",
@@ -33,8 +34,19 @@
33
34
  ],
34
35
  "keywords": [
35
36
  "react-native",
37
+ "expo",
38
+ "toggle",
39
+ "switch",
40
+ "animated",
41
+ "gesture",
36
42
  "ios",
37
- "android"
43
+ "android",
44
+ "segmented-control",
45
+ "multi-switch",
46
+ "animated-switch",
47
+ "react-native-component",
48
+ "ui-component",
49
+ "typescript"
38
50
  ],
39
51
  "repository": {
40
52
  "type": "git",
@@ -69,6 +81,9 @@
69
81
  "prettier": "^2.8.8",
70
82
  "react": "19.1.0",
71
83
  "react-native": "0.81.5",
84
+ "react-native-gesture-handler": "^2.30.0",
85
+ "react-native-reanimated": "^4.2.1",
86
+ "react-native-worklets": "0.7.1",
72
87
  "react-native-builder-bob": "^0.40.17",
73
88
  "release-it": "^19.0.4",
74
89
  "typescript": "^5.9.2"
@@ -87,6 +102,9 @@
87
102
  "source": "src",
88
103
  "output": "lib",
89
104
  "targets": [
105
+ [
106
+ "commonjs"
107
+ ],
90
108
  [
91
109
  "module",
92
110
  {
@@ -113,6 +131,9 @@
113
131
  "modulePathIgnorePatterns": [
114
132
  "<rootDir>/example/node_modules",
115
133
  "<rootDir>/lib/"
134
+ ],
135
+ "transformIgnorePatterns": [
136
+ "node_modules/(?!(\\.pnpm/.*)?(react-native|@react-native|@react-navigation)/)"
116
137
  ]
117
138
  },
118
139
  "commitlint": {
@@ -150,12 +171,15 @@
150
171
  ],
151
172
  "version": "0.56.1"
152
173
  },
174
+ "dependencies": {},
153
175
  "scripts": {
154
176
  "example": "pnpm --filter react-native-directional-toggle-example run start",
155
177
  "clean": "del-cli lib",
156
178
  "typecheck": "tsc",
157
179
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
158
180
  "test": "jest",
159
- "release": "release-it --only-version"
181
+ "release": "release-it --only-version",
182
+ "android": "expo run:android",
183
+ "ios": "expo run:ios"
160
184
  }
161
185
  }
@@ -1,172 +1,181 @@
1
- import { useCallback, useEffect } from "react";
2
- import { type LayoutChangeEvent, Pressable, StyleSheet, View } from "react-native";
3
- import { Gesture, GestureDetector } from "react-native-gesture-handler";
1
+ import { useCallback, useEffect } from 'react';
2
+ import {
3
+ type LayoutChangeEvent,
4
+ Pressable,
5
+ StyleSheet,
6
+ View,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ type TextStyle,
10
+ } from 'react-native';
11
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
12
+ import { scheduleOnRN } from 'react-native-worklets';
4
13
  import Animated, {
5
14
  interpolateColor,
6
- runOnJS,
7
15
  useAnimatedStyle,
8
16
  useSharedValue,
9
17
  withSpring,
10
18
  withTiming,
11
- } from "react-native-reanimated";
19
+ type WithSpringConfig,
20
+ type WithTimingConfig,
21
+ type SharedValue,
22
+ } from 'react-native-reanimated';
12
23
 
13
24
  type Option = {
14
25
  label: string;
15
26
  value: string | number;
16
27
  };
17
28
 
18
- type Props = {
29
+ export type AnimatedSwitchProps = {
19
30
  options: Option[];
20
31
  value: string | number;
21
32
  onChange: (value: string | number) => void;
22
- height?: number;
33
+ /**
34
+ * Direction of the switch.
35
+ * @default false (Horizontal)
36
+ */
23
37
  vertical?: boolean;
24
- colors?: {
25
- activeText: string;
26
- inactiveText: string;
27
- bgFront: string;
28
- bgBack: string;
29
- };
30
- animationConfig?: {
31
- duration?: number;
32
- damping?: number;
33
- stiffness?: number;
34
- };
38
+ /**
39
+ * Container style. Use this to set width/height.
40
+ */
41
+ style?: StyleProp<ViewStyle>;
42
+ /**
43
+ * Style for the moving thumb.
44
+ */
45
+ thumbStyle?: StyleProp<ViewStyle>;
46
+ /**
47
+ * Base style for option text.
48
+ */
49
+ textStyle?: StyleProp<TextStyle>;
50
+ /**
51
+ * Style for the text when active.
52
+ */
53
+ activeTextStyle?: StyleProp<TextStyle>;
54
+ /**
55
+ * Style for the text when inactive.
56
+ */
57
+ inactiveTextStyle?: StyleProp<TextStyle>;
58
+ /**
59
+ * Animation configuration.
60
+ */
61
+ animationConfig?: WithTimingConfig | WithSpringConfig;
35
62
  };
36
63
 
37
- const THUMB_INSET = 4;
38
- const VERTICAL_WIDTH = 128;
64
+ const DEFAULT_ANIMATION = {
65
+ duration: 150,
66
+ damping: 5,
67
+ stiffness: 100,
68
+ };
39
69
 
40
70
  export function AnimatedSwitch({
41
71
  options,
42
72
  value,
43
73
  onChange,
44
- height = 36,
45
74
  vertical = false,
46
- colors = {
47
- activeText: "#373737",
48
- inactiveText: "#dededeff",
49
- bgFront: "#d4d4d4",
50
- bgBack: "#9a9a9a",
51
- },
52
- animationConfig = {
53
- duration: 100,
54
- damping: 50,
55
- stiffness: 200,
56
- },
57
- }: Props) {
58
-
59
- /** ===== SharedValues ===== */
75
+ style,
76
+ thumbStyle,
77
+ textStyle,
78
+ activeTextStyle,
79
+ inactiveTextStyle,
80
+ animationConfig = DEFAULT_ANIMATION,
81
+ }: AnimatedSwitchProps) {
60
82
  const itemSizeSV = useSharedValue(0);
61
83
  const translate = useSharedValue(0);
62
84
  const indexSV = useSharedValue(0);
63
85
 
64
86
  const currentIndex = options.findIndex((o) => o.value === value);
65
87
 
66
- /** * 1. 监听外部 value 变化
67
- * 当外部修改 value 时,滑块应动画移动到新位置
68
- */
88
+ // Measure container layout
89
+ const onLayout = useCallback(
90
+ (e: LayoutChangeEvent) => {
91
+ const { width, height } = e.nativeEvent.layout;
92
+
93
+ const totalSize = vertical ? height - 4 : width - 4;
94
+ const itemSize = totalSize / options.length;
95
+ itemSizeSV.value = itemSize;
96
+
97
+ // Fix initial position on layout
98
+ if (currentIndex >= 0 && itemSize > 0) {
99
+ translate.value = currentIndex * itemSize;
100
+ }
101
+ },
102
+ [vertical, options.length, currentIndex, itemSizeSV, translate]
103
+ );
104
+
105
+ // Sync with external value changes
69
106
  useEffect(() => {
70
- if (currentIndex >= 0) {
107
+ if (currentIndex >= 0 && itemSizeSV.value > 0) {
71
108
  indexSV.value = currentIndex;
72
- // 只有当 itemSizeSV 已经有值(即 Layout 已完成)时才执行动画
73
- if (itemSizeSV.value > 0) {
74
- translate.value = withTiming(currentIndex * itemSizeSV.value, {
75
- duration: animationConfig.duration,
76
- });
77
- }
109
+ translate.value = withTiming(
110
+ currentIndex * itemSizeSV.value,
111
+ animationConfig as WithTimingConfig
112
+ );
78
113
  }
79
- }, [currentIndex, animationConfig.duration]);
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
115
+ }, [currentIndex, animationConfig]);
80
116
 
81
- /** ===== JS 回调 ===== */
82
117
  const emitChange = useCallback(
83
118
  (index: number) => {
84
- if (options.length > 0 && index >= 0 && index < options.length) {
85
- onChange(options[index]?.value || "");
119
+ // Clamp index to safe bounds
120
+ const safeIndex = Math.max(0, Math.min(index, options.length - 1));
121
+ if (options[safeIndex]) {
122
+ onChange(options[safeIndex].value);
86
123
  }
87
124
  },
88
- [onChange, options],
125
+ [onChange, options]
89
126
  );
90
127
 
91
- /** ===== Tap 点击切换 ===== */
92
128
  const handlePress = (index: number) => {
129
+ if (itemSizeSV.value === 0) return;
93
130
  indexSV.value = index;
94
- translate.value = withTiming(index * itemSizeSV.value, {
95
- duration: animationConfig.duration ?? 150
96
- });
131
+ translate.value = withTiming(
132
+ index * itemSizeSV.value,
133
+ animationConfig as WithTimingConfig
134
+ );
97
135
  emitChange(index);
98
136
  };
99
137
 
100
- /** ===== Drag 手势 ===== */
101
138
  const panGesture = Gesture.Pan()
102
139
  .onUpdate((e) => {
103
- const delta = vertical ? e.translationY : e.translationX;
104
- // 基于手势开始时的位置进行偏移计算
105
- const pos = delta + indexSV.value * itemSizeSV.value;
140
+ if (itemSizeSV.value === 0) return;
141
+ const term = vertical ? e.translationY : e.translationX;
142
+ const startPos = indexSV.value * itemSizeSV.value;
143
+ const pos = startPos + term;
106
144
  const max = (options.length - 1) * itemSizeSV.value;
107
-
108
145
  translate.value = Math.min(Math.max(0, pos), max);
109
146
  })
110
147
  .onEnd(() => {
148
+ if (itemSizeSV.value === 0) return;
111
149
  const index = Math.round(translate.value / itemSizeSV.value);
112
150
  indexSV.value = index;
113
-
114
- translate.value = withSpring(index * itemSizeSV.value, {
115
- damping: animationConfig.damping ?? 20,
116
- stiffness: animationConfig.stiffness ?? 200,
117
- });
118
-
119
- runOnJS(emitChange)(index);
151
+ translate.value = withSpring(
152
+ index * itemSizeSV.value,
153
+ animationConfig as WithSpringConfig
154
+ );
155
+ scheduleOnRN(emitChange, index);
120
156
  });
121
157
 
122
- /** ===== Thumb 动画样式 ===== */
123
158
  const animatedThumbStyle = useAnimatedStyle(() => {
124
- if (vertical) {
125
- return {
126
- height: itemSizeSV.value - THUMB_INSET * 2,
127
- top: THUMB_INSET,
128
- left: THUMB_INSET,
129
- right: THUMB_INSET,
130
- transform: [{ translateY: translate.value }],
131
- };
132
- }
133
- return {
134
- width: itemSizeSV.value - THUMB_INSET * 2,
135
- left: THUMB_INSET,
136
- top: THUMB_INSET,
137
- bottom: THUMB_INSET,
138
- transform: [{ translateX: translate.value }],
139
- };
140
- });
159
+ // If layout not ready, hide thumb or show nothing
160
+ if (itemSizeSV.value === 0) return { opacity: 0 };
141
161
 
142
- /** ===== Label 动画样式 ===== */
143
- const getLabelAnimatedStyle = (index: number) =>
144
- useAnimatedStyle(() => {
145
- const center = index * itemSizeSV.value;
146
- const color = interpolateColor(
147
- translate.value,
148
- [center - itemSizeSV.value, center, center + itemSizeSV.value],
149
- [colors.inactiveText, colors.activeText, colors.inactiveText],
150
- );
151
- return { color };
152
- });
162
+ const transform = vertical
163
+ ? [{ translateY: translate.value }]
164
+ : [{ translateX: translate.value }];
153
165
 
154
- /** * 2. Layout 初始化
155
- * 这是修复初始显示问题的关键
156
- */
157
- const onLayout = (e: LayoutChangeEvent) => {
158
- const size = vertical
159
- ? e.nativeEvent.layout.height
160
- : e.nativeEvent.layout.width;
166
+ const sizeStyle = vertical
167
+ ? { height: itemSizeSV.value, width: '100%' }
168
+ : { width: itemSizeSV.value, height: '100%' };
161
169
 
162
- const newItemSize = size / options.length;
163
- itemSizeSV.value = newItemSize;
164
-
165
- // 核心修复:在获取到尺寸的第一时间,根据 currentIndex 强行同步 translate 的值
166
- if (currentIndex >= 0) {
167
- translate.value = currentIndex * newItemSize;
168
- }
169
- };
170
+ return {
171
+ position: 'absolute',
172
+ left: 2,
173
+ top: 2,
174
+ ...sizeStyle,
175
+ transform,
176
+ opacity: 1,
177
+ };
178
+ });
170
179
 
171
180
  return (
172
181
  <GestureDetector gesture={panGesture}>
@@ -174,35 +183,25 @@ export function AnimatedSwitch({
174
183
  onLayout={onLayout}
175
184
  style={[
176
185
  styles.container,
177
- { backgroundColor: colors.bgBack },
178
- vertical
179
- ? {
180
- height: height * options.length,
181
- width: VERTICAL_WIDTH,
182
- flexDirection: "column",
183
- }
184
- : { height, flexDirection: "row" },
186
+ vertical ? styles.vertical : styles.horizontal,
187
+ style, // User override
185
188
  ]}
186
189
  >
187
- {/* 滑块背景 */}
188
- <Animated.View
189
- pointerEvents="none"
190
- style={[styles.thumbBase, { backgroundColor: colors.bgFront }, animatedThumbStyle]}
191
- />
190
+ <Animated.View style={[styles.thumb, thumbStyle, animatedThumbStyle]} />
192
191
 
193
- {/* 选项列表 */}
194
192
  {options.map((opt, index) => {
195
- const labelStyle = getLabelAnimatedStyle(index);
196
193
  return (
197
- <Pressable
194
+ <OptionItem
198
195
  key={opt.value}
199
- style={styles.item}
196
+ label={opt.label}
200
197
  onPress={() => handlePress(index)}
201
- >
202
- <Animated.Text style={[styles.label, labelStyle]}>
203
- {opt.label}
204
- </Animated.Text>
205
- </Pressable>
198
+ index={index}
199
+ translate={translate}
200
+ itemSizeSV={itemSizeSV}
201
+ textStyle={textStyle}
202
+ activeTextStyle={activeTextStyle}
203
+ inactiveTextStyle={inactiveTextStyle}
204
+ />
206
205
  );
207
206
  })}
208
207
  </View>
@@ -210,29 +209,84 @@ export function AnimatedSwitch({
210
209
  );
211
210
  }
212
211
 
212
+ // Sub-component for individual options to isolate animated styles
213
+ const OptionItem = ({
214
+ label,
215
+ onPress,
216
+ index,
217
+ translate,
218
+ itemSizeSV,
219
+ textStyle,
220
+ activeTextStyle,
221
+ inactiveTextStyle,
222
+ }: {
223
+ label: string;
224
+ onPress: () => void;
225
+ index: number;
226
+ translate: SharedValue<number>;
227
+ itemSizeSV: SharedValue<number>;
228
+ textStyle?: StyleProp<TextStyle>;
229
+ activeTextStyle?: StyleProp<TextStyle>;
230
+ inactiveTextStyle?: StyleProp<TextStyle>;
231
+ }) => {
232
+ const activeColor =
233
+ (StyleSheet.flatten(activeTextStyle)?.color as string) ?? '#000';
234
+ const inactiveColor =
235
+ (StyleSheet.flatten(inactiveTextStyle)?.color as string) ?? '#999';
236
+
237
+ const textAnimatedStyle = useAnimatedStyle(() => {
238
+ const center = index * itemSizeSV.value;
239
+ const color = interpolateColor(
240
+ translate.value,
241
+ [center - itemSizeSV.value, center, center + itemSizeSV.value],
242
+ [inactiveColor, activeColor, inactiveColor]
243
+ );
244
+ return { color };
245
+ });
246
+
247
+ return (
248
+ <Pressable style={styles.option} onPress={onPress}>
249
+ <Animated.Text
250
+ style={[styles.text, textStyle, textAnimatedStyle]}
251
+ numberOfLines={1}
252
+ >
253
+ {label}
254
+ </Animated.Text>
255
+ </Pressable>
256
+ );
257
+ };
258
+
213
259
  const styles = StyleSheet.create({
214
260
  container: {
261
+ // backgroundColor: '#f0f0f0',
215
262
  borderRadius: 16,
216
- overflow: "hidden",
217
- flex: 1,
263
+ overflow: 'hidden',
264
+ padding: 2,
265
+ // position: 'relative',
218
266
  },
219
- thumbBase: {
220
- position: "absolute",
221
- borderRadius: 12,
267
+ horizontal: {
268
+ flexDirection: 'row',
269
+ },
270
+ vertical: {
271
+ flexDirection: 'column',
272
+ },
273
+ thumb: {
274
+ // backgroundColor: '#fff',
275
+ borderRadius: 16,
222
276
  shadowColor: '#000',
223
- shadowOpacity: 0.3,
224
277
  shadowOffset: { width: 0, height: 1 },
278
+ shadowOpacity: 0.1,
225
279
  shadowRadius: 1,
226
- elevation: 2,
280
+ elevation: 1,
227
281
  },
228
- item: {
282
+ option: {
229
283
  flex: 1,
230
- alignItems: "center",
231
- justifyContent: "center",
232
- zIndex: 1, // 确保文字在滑块上方
284
+ alignItems: 'center',
285
+ justifyContent: 'center',
286
+ // zIndex: 1
233
287
  },
234
- label: {
235
- fontSize: 13,
236
- fontWeight: "600",
288
+ text: {
289
+ fontSize: 14,
290
+ fontWeight: '500',
237
291
  },
238
292
  });
package/src/index.tsx CHANGED
@@ -1,3 +1,4 @@
1
- import { AnimatedSwitch } from "./AnimatedSwitch";
1
+ import { AnimatedSwitch, type AnimatedSwitchProps } from './AnimatedSwitch';
2
2
 
3
3
  export default AnimatedSwitch;
4
+ export { AnimatedSwitch, type AnimatedSwitchProps };