react-native-directional-toggle 0.1.0
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/LICENSE +20 -0
- package/README.en.md +65 -0
- package/README.md +65 -0
- package/lib/module/AnimatedSwitch.js +190 -0
- package/lib/module/AnimatedSwitch.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AnimatedSwitch.d.ts +25 -0
- package/lib/typescript/src/AnimatedSwitch.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +161 -0
- package/src/AnimatedSwitch.tsx +238 -0
- package/src/index.tsx +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alan Suhe
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Description
|
|
2
|
+
|
|
3
|
+
This is a React Native / Expo component library.
|
|
4
|
+
|
|
5
|
+
For Chinese version, please click: [中文](README.md).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add react-native-directional-toggle
|
|
11
|
+
|
|
12
|
+
yarn add react-native-directional-toggle
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Peer Dependencies
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add react-native-gesture-handler react-native-reanimated react-native-worklets --save-peer
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Dependencies required:
|
|
22
|
+
- react-native-gesture-handler
|
|
23
|
+
- react-native-reanimated
|
|
24
|
+
- react-native-worklets
|
|
25
|
+
|
|
26
|
+
Note: Only works with `expo run` after prebuild, not supported by `expo go`.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
> Refer to the [example project](example/).
|
|
31
|
+
|
|
32
|
+
### Import and use
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
36
|
+
import Switcher from 'react-native-directional-toggle';
|
|
37
|
+
|
|
38
|
+
const options = [
|
|
39
|
+
{
|
|
40
|
+
label: 'Option 1',
|
|
41
|
+
value: 'Option 1',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: 'Option 2',
|
|
45
|
+
value: 'Option 2',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Option 3',
|
|
49
|
+
value: 'Option 3',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
<GestureHandlerRootView>
|
|
56
|
+
<Switcher
|
|
57
|
+
options={options}
|
|
58
|
+
value={'Option 2'}
|
|
59
|
+
height={36}
|
|
60
|
+
onChange={value => console.log(value)}
|
|
61
|
+
/>
|
|
62
|
+
</GestureHandlerRootView>
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Note: You need to wrap the outer layer of your App (such as _layout.tsx, etc.) with `GestureHandlerRootView`.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 说明
|
|
2
|
+
|
|
3
|
+
这是一个 React Native / Expo 组件库。
|
|
4
|
+
|
|
5
|
+
For English version, please click: [English](README.en.md)。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add react-native-directional-toggle
|
|
11
|
+
|
|
12
|
+
yarn add react-native-directional-toggle
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 依赖包
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add react-native-gesture-handler react-native-reanimated react-native-worklets --save-peer
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
需要安装的依赖包:
|
|
22
|
+
- react-native-gesture-handler
|
|
23
|
+
- react-native-reanimated
|
|
24
|
+
- react-native-worklets
|
|
25
|
+
|
|
26
|
+
注意:只能在prebuild后正常用`expo run`的方式运行,不支持expo go。
|
|
27
|
+
|
|
28
|
+
## 使用
|
|
29
|
+
|
|
30
|
+
> 参考[示例项目](example/)。
|
|
31
|
+
|
|
32
|
+
### 导入组件使用
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
36
|
+
import Switcher from 'react-native-directional-toggle';
|
|
37
|
+
|
|
38
|
+
const options = [
|
|
39
|
+
{
|
|
40
|
+
label: 'Option 1',
|
|
41
|
+
value: 'Option 1',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: 'Option 2',
|
|
45
|
+
value: 'Option 2',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Option 3',
|
|
49
|
+
value: 'Option 3',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
<GestureHandlerRootView>
|
|
56
|
+
<Switcher
|
|
57
|
+
options={options}
|
|
58
|
+
value={'Option 2'}
|
|
59
|
+
height={36}
|
|
60
|
+
onChange={value => console.log(value)}
|
|
61
|
+
/>
|
|
62
|
+
</GestureHandlerRootView>
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
注意: App中需要在外层(如_layout.tsx等)包裹GestureHandlerRootView。
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect } from "react";
|
|
4
|
+
import { Pressable, StyleSheet, View } from "react-native";
|
|
5
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
6
|
+
import Animated, { interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming } from "react-native-reanimated";
|
|
7
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
const THUMB_INSET = 4;
|
|
9
|
+
const VERTICAL_WIDTH = 128;
|
|
10
|
+
export function AnimatedSwitch({
|
|
11
|
+
options,
|
|
12
|
+
value,
|
|
13
|
+
onChange,
|
|
14
|
+
height = 40,
|
|
15
|
+
vertical = false,
|
|
16
|
+
colors = {
|
|
17
|
+
activeText: "#373737",
|
|
18
|
+
inactiveText: "#dededeff",
|
|
19
|
+
bgFront: "#d4d4d4",
|
|
20
|
+
bgBack: "#798393ff"
|
|
21
|
+
},
|
|
22
|
+
animationConfig = {
|
|
23
|
+
duration: 100,
|
|
24
|
+
damping: 50,
|
|
25
|
+
stiffness: 200
|
|
26
|
+
}
|
|
27
|
+
}) {
|
|
28
|
+
/** ===== SharedValues ===== */
|
|
29
|
+
const itemSizeSV = useSharedValue(0);
|
|
30
|
+
const translate = useSharedValue(0);
|
|
31
|
+
const indexSV = useSharedValue(0);
|
|
32
|
+
const currentIndex = options.findIndex(o => o.value === value);
|
|
33
|
+
|
|
34
|
+
/** * 1. 监听外部 value 变化
|
|
35
|
+
* 当外部修改 value 时,滑块应动画移动到新位置
|
|
36
|
+
*/
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (currentIndex >= 0) {
|
|
39
|
+
indexSV.value = currentIndex;
|
|
40
|
+
// 只有当 itemSizeSV 已经有值(即 Layout 已完成)时才执行动画
|
|
41
|
+
if (itemSizeSV.value > 0) {
|
|
42
|
+
translate.value = withTiming(currentIndex * itemSizeSV.value, {
|
|
43
|
+
duration: animationConfig.duration
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, [currentIndex, animationConfig.duration]);
|
|
48
|
+
|
|
49
|
+
/** ===== JS 回调 ===== */
|
|
50
|
+
const emitChange = useCallback(index => {
|
|
51
|
+
if (options.length > 0 && index >= 0 && index < options.length) {
|
|
52
|
+
onChange(options[index]?.value || "");
|
|
53
|
+
}
|
|
54
|
+
}, [onChange, options]);
|
|
55
|
+
|
|
56
|
+
/** ===== Tap 点击切换 ===== */
|
|
57
|
+
const handlePress = index => {
|
|
58
|
+
indexSV.value = index;
|
|
59
|
+
translate.value = withTiming(index * itemSizeSV.value, {
|
|
60
|
+
duration: animationConfig.duration ?? 150
|
|
61
|
+
});
|
|
62
|
+
emitChange(index);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** ===== Drag 手势 ===== */
|
|
66
|
+
const panGesture = Gesture.Pan().onUpdate(e => {
|
|
67
|
+
const delta = vertical ? e.translationY : e.translationX;
|
|
68
|
+
// 基于手势开始时的位置进行偏移计算
|
|
69
|
+
const pos = delta + indexSV.value * itemSizeSV.value;
|
|
70
|
+
const max = (options.length - 1) * itemSizeSV.value;
|
|
71
|
+
translate.value = Math.min(Math.max(0, pos), max);
|
|
72
|
+
}).onEnd(() => {
|
|
73
|
+
const index = Math.round(translate.value / itemSizeSV.value);
|
|
74
|
+
indexSV.value = index;
|
|
75
|
+
translate.value = withSpring(index * itemSizeSV.value, {
|
|
76
|
+
damping: animationConfig.damping ?? 20,
|
|
77
|
+
stiffness: animationConfig.stiffness ?? 200
|
|
78
|
+
});
|
|
79
|
+
runOnJS(emitChange)(index);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/** ===== Thumb 动画样式 ===== */
|
|
83
|
+
const animatedThumbStyle = useAnimatedStyle(() => {
|
|
84
|
+
if (vertical) {
|
|
85
|
+
return {
|
|
86
|
+
height: itemSizeSV.value - THUMB_INSET * 2,
|
|
87
|
+
top: THUMB_INSET,
|
|
88
|
+
left: THUMB_INSET,
|
|
89
|
+
right: THUMB_INSET,
|
|
90
|
+
transform: [{
|
|
91
|
+
translateY: translate.value
|
|
92
|
+
}]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
width: itemSizeSV.value - THUMB_INSET * 2,
|
|
97
|
+
left: THUMB_INSET,
|
|
98
|
+
top: THUMB_INSET,
|
|
99
|
+
bottom: THUMB_INSET,
|
|
100
|
+
transform: [{
|
|
101
|
+
translateX: translate.value
|
|
102
|
+
}]
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** ===== Label 动画样式 ===== */
|
|
107
|
+
const getLabelAnimatedStyle = index => useAnimatedStyle(() => {
|
|
108
|
+
const center = index * itemSizeSV.value;
|
|
109
|
+
const color = interpolateColor(translate.value, [center - itemSizeSV.value, center, center + itemSizeSV.value], [colors.inactiveText, colors.activeText, colors.inactiveText]);
|
|
110
|
+
return {
|
|
111
|
+
color
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** * 2. Layout 初始化
|
|
116
|
+
* 这是修复初始显示问题的关键
|
|
117
|
+
*/
|
|
118
|
+
const onLayout = e => {
|
|
119
|
+
const size = vertical ? e.nativeEvent.layout.height : e.nativeEvent.layout.width;
|
|
120
|
+
const newItemSize = size / options.length;
|
|
121
|
+
itemSizeSV.value = newItemSize;
|
|
122
|
+
|
|
123
|
+
// 核心修复:在获取到尺寸的第一时间,根据 currentIndex 强行同步 translate 的值
|
|
124
|
+
if (currentIndex >= 0) {
|
|
125
|
+
translate.value = currentIndex * newItemSize;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
return /*#__PURE__*/_jsx(GestureDetector, {
|
|
129
|
+
gesture: panGesture,
|
|
130
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
131
|
+
onLayout: onLayout,
|
|
132
|
+
style: [styles.container, {
|
|
133
|
+
backgroundColor: colors.bgBack
|
|
134
|
+
}, vertical ? {
|
|
135
|
+
height: height * options.length,
|
|
136
|
+
width: VERTICAL_WIDTH,
|
|
137
|
+
flexDirection: "column"
|
|
138
|
+
} : {
|
|
139
|
+
height,
|
|
140
|
+
flexDirection: "row"
|
|
141
|
+
}],
|
|
142
|
+
children: [/*#__PURE__*/_jsx(Animated.View, {
|
|
143
|
+
pointerEvents: "none",
|
|
144
|
+
style: [styles.thumbBase, {
|
|
145
|
+
backgroundColor: colors.bgFront
|
|
146
|
+
}, animatedThumbStyle]
|
|
147
|
+
}), options.map((opt, index) => {
|
|
148
|
+
const labelStyle = getLabelAnimatedStyle(index);
|
|
149
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
150
|
+
style: styles.item,
|
|
151
|
+
onPress: () => handlePress(index),
|
|
152
|
+
children: /*#__PURE__*/_jsx(Animated.Text, {
|
|
153
|
+
style: [styles.label, labelStyle],
|
|
154
|
+
children: opt.label
|
|
155
|
+
})
|
|
156
|
+
}, opt.value);
|
|
157
|
+
})]
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const styles = StyleSheet.create({
|
|
162
|
+
container: {
|
|
163
|
+
borderRadius: 16,
|
|
164
|
+
overflow: "hidden",
|
|
165
|
+
flex: 1
|
|
166
|
+
},
|
|
167
|
+
thumbBase: {
|
|
168
|
+
position: "absolute",
|
|
169
|
+
borderRadius: 12,
|
|
170
|
+
shadowColor: '#000',
|
|
171
|
+
shadowOpacity: 0.3,
|
|
172
|
+
shadowOffset: {
|
|
173
|
+
width: 0,
|
|
174
|
+
height: 1
|
|
175
|
+
},
|
|
176
|
+
shadowRadius: 2,
|
|
177
|
+
elevation: 2
|
|
178
|
+
},
|
|
179
|
+
item: {
|
|
180
|
+
flex: 1,
|
|
181
|
+
alignItems: "center",
|
|
182
|
+
justifyContent: "center",
|
|
183
|
+
zIndex: 1 // 确保文字在滑块上方
|
|
184
|
+
},
|
|
185
|
+
label: {
|
|
186
|
+
fontSize: 14,
|
|
187
|
+
fontWeight: "600"
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
//# sourceMappingURL=AnimatedSwitch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useCallback","useEffect","Pressable","StyleSheet","View","Gesture","GestureDetector","Animated","interpolateColor","runOnJS","useAnimatedStyle","useSharedValue","withSpring","withTiming","jsx","_jsx","jsxs","_jsxs","THUMB_INSET","VERTICAL_WIDTH","AnimatedSwitch","options","value","onChange","height","vertical","colors","activeText","inactiveText","bgFront","bgBack","animationConfig","duration","damping","stiffness","itemSizeSV","translate","indexSV","currentIndex","findIndex","o","emitChange","index","length","handlePress","panGesture","Pan","onUpdate","e","delta","translationY","translationX","pos","max","Math","min","onEnd","round","animatedThumbStyle","top","left","right","transform","translateY","width","bottom","translateX","getLabelAnimatedStyle","center","color","onLayout","size","nativeEvent","layout","newItemSize","gesture","children","style","styles","container","backgroundColor","flexDirection","pointerEvents","thumbBase","map","opt","labelStyle","item","onPress","Text","label","create","borderRadius","overflow","flex","position","shadowColor","shadowOpacity","shadowOffset","shadowRadius","elevation","alignItems","justifyContent","zIndex","fontSize","fontWeight"],"sourceRoot":"../../src","sources":["AnimatedSwitch.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["AnimatedSwitch"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,cAAc,QAAQ,qBAAkB;AAEjD,eAAeA,cAAc","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type Option = {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string | number;
|
|
4
|
+
};
|
|
5
|
+
type Props = {
|
|
6
|
+
options: Option[];
|
|
7
|
+
value: string | number;
|
|
8
|
+
onChange: (value: string | number) => void;
|
|
9
|
+
height?: number;
|
|
10
|
+
vertical?: boolean;
|
|
11
|
+
colors?: {
|
|
12
|
+
activeText: string;
|
|
13
|
+
inactiveText: string;
|
|
14
|
+
bgFront: string;
|
|
15
|
+
bgBack: string;
|
|
16
|
+
};
|
|
17
|
+
animationConfig?: {
|
|
18
|
+
duration?: number;
|
|
19
|
+
damping?: number;
|
|
20
|
+
stiffness?: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare function AnimatedSwitch({ options, value, onChange, height, vertical, colors, animationConfig, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=AnimatedSwitch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnimatedSwitch.d.ts","sourceRoot":"","sources":["../../../src/AnimatedSwitch.tsx"],"names":[],"mappings":"AAYA,KAAK,MAAM,GAAG;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,KAAK,GAAG;IACX,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,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE;QACP,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,eAAe,CAAC,EAAE;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH,CAAC;AAKF,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,KAAK,EACL,QAAQ,EACR,MAAW,EACX,QAAgB,EAChB,MAKC,EACD,eAIC,GACF,EAAE,KAAK,2CA0JP"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,eAAe,cAAc,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-directional-toggle",
|
|
3
|
+
"version": "0.1.0",
|
|
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",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"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
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"react-native",
|
|
36
|
+
"ios",
|
|
37
|
+
"android"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/alansuhe/react-native-directional-toggle.git"
|
|
42
|
+
},
|
|
43
|
+
"author": "Alan Suhe <tosuhe@gmail.com> (https://github.com/alansuhe)",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/alansuhe/react-native-directional-toggle/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/alansuhe/react-native-directional-toggle#readme",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"registry": "https://registry.npmjs.org/"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
54
|
+
"@eslint/compat": "^1.3.2",
|
|
55
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
56
|
+
"@eslint/js": "^9.35.0",
|
|
57
|
+
"@react-native/babel-preset": "0.83.0",
|
|
58
|
+
"@react-native/eslint-config": "0.83.0",
|
|
59
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
60
|
+
"@types/jest": "^29.5.14",
|
|
61
|
+
"@types/react": "^19.1.12",
|
|
62
|
+
"commitlint": "^19.8.1",
|
|
63
|
+
"del-cli": "^6.0.0",
|
|
64
|
+
"eslint": "^9.35.0",
|
|
65
|
+
"eslint-config-prettier": "^10.1.8",
|
|
66
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
67
|
+
"jest": "^29.7.0",
|
|
68
|
+
"lefthook": "^2.0.3",
|
|
69
|
+
"prettier": "^2.8.8",
|
|
70
|
+
"react": "19.1.0",
|
|
71
|
+
"react-native": "0.81.5",
|
|
72
|
+
"react-native-builder-bob": "^0.40.17",
|
|
73
|
+
"release-it": "^19.0.4",
|
|
74
|
+
"typescript": "^5.9.2"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"react": "*",
|
|
78
|
+
"react-native": "*",
|
|
79
|
+
"react-native-gesture-handler": "^2.30.0",
|
|
80
|
+
"react-native-reanimated": "^4.2.1",
|
|
81
|
+
"react-native-worklets": "0.7.1"
|
|
82
|
+
},
|
|
83
|
+
"workspaces": [
|
|
84
|
+
"example"
|
|
85
|
+
],
|
|
86
|
+
"react-native-builder-bob": {
|
|
87
|
+
"source": "src",
|
|
88
|
+
"output": "lib",
|
|
89
|
+
"targets": [
|
|
90
|
+
[
|
|
91
|
+
"module",
|
|
92
|
+
{
|
|
93
|
+
"esm": true
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"typescript",
|
|
98
|
+
{
|
|
99
|
+
"project": "tsconfig.build.json"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"prettier": {
|
|
105
|
+
"quoteProps": "consistent",
|
|
106
|
+
"singleQuote": true,
|
|
107
|
+
"tabWidth": 2,
|
|
108
|
+
"trailingComma": "es5",
|
|
109
|
+
"useTabs": false
|
|
110
|
+
},
|
|
111
|
+
"jest": {
|
|
112
|
+
"preset": "react-native",
|
|
113
|
+
"modulePathIgnorePatterns": [
|
|
114
|
+
"<rootDir>/example/node_modules",
|
|
115
|
+
"<rootDir>/lib/"
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
"commitlint": {
|
|
119
|
+
"extends": [
|
|
120
|
+
"@commitlint/config-conventional"
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"release-it": {
|
|
124
|
+
"git": {
|
|
125
|
+
"commitMessage": "chore: release ${version}",
|
|
126
|
+
"tagName": "v${version}"
|
|
127
|
+
},
|
|
128
|
+
"npm": {
|
|
129
|
+
"publish": true
|
|
130
|
+
},
|
|
131
|
+
"github": {
|
|
132
|
+
"release": true
|
|
133
|
+
},
|
|
134
|
+
"plugins": {
|
|
135
|
+
"@release-it/conventional-changelog": {
|
|
136
|
+
"preset": {
|
|
137
|
+
"name": "angular"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"create-react-native-library": {
|
|
143
|
+
"type": "library",
|
|
144
|
+
"languages": "js",
|
|
145
|
+
"tools": [
|
|
146
|
+
"eslint",
|
|
147
|
+
"jest",
|
|
148
|
+
"lefthook",
|
|
149
|
+
"release-it"
|
|
150
|
+
],
|
|
151
|
+
"version": "0.56.1"
|
|
152
|
+
},
|
|
153
|
+
"scripts": {
|
|
154
|
+
"example": "pnpm --filter react-native-directional-toggle-example run start",
|
|
155
|
+
"clean": "del-cli lib",
|
|
156
|
+
"typecheck": "tsc",
|
|
157
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
158
|
+
"test": "jest",
|
|
159
|
+
"release": "release-it --only-version"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
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";
|
|
4
|
+
import Animated, {
|
|
5
|
+
interpolateColor,
|
|
6
|
+
runOnJS,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
useSharedValue,
|
|
9
|
+
withSpring,
|
|
10
|
+
withTiming,
|
|
11
|
+
} from "react-native-reanimated";
|
|
12
|
+
|
|
13
|
+
type Option = {
|
|
14
|
+
label: string;
|
|
15
|
+
value: string | number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
options: Option[];
|
|
20
|
+
value: string | number;
|
|
21
|
+
onChange: (value: string | number) => void;
|
|
22
|
+
height?: number;
|
|
23
|
+
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
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const THUMB_INSET = 4;
|
|
38
|
+
const VERTICAL_WIDTH = 128;
|
|
39
|
+
|
|
40
|
+
export function AnimatedSwitch({
|
|
41
|
+
options,
|
|
42
|
+
value,
|
|
43
|
+
onChange,
|
|
44
|
+
height = 40,
|
|
45
|
+
vertical = false,
|
|
46
|
+
colors = {
|
|
47
|
+
activeText: "#373737",
|
|
48
|
+
inactiveText: "#dededeff",
|
|
49
|
+
bgFront: "#d4d4d4",
|
|
50
|
+
bgBack: "#798393ff",
|
|
51
|
+
},
|
|
52
|
+
animationConfig = {
|
|
53
|
+
duration: 100,
|
|
54
|
+
damping: 50,
|
|
55
|
+
stiffness: 200,
|
|
56
|
+
},
|
|
57
|
+
}: Props) {
|
|
58
|
+
|
|
59
|
+
/** ===== SharedValues ===== */
|
|
60
|
+
const itemSizeSV = useSharedValue(0);
|
|
61
|
+
const translate = useSharedValue(0);
|
|
62
|
+
const indexSV = useSharedValue(0);
|
|
63
|
+
|
|
64
|
+
const currentIndex = options.findIndex((o) => o.value === value);
|
|
65
|
+
|
|
66
|
+
/** * 1. 监听外部 value 变化
|
|
67
|
+
* 当外部修改 value 时,滑块应动画移动到新位置
|
|
68
|
+
*/
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (currentIndex >= 0) {
|
|
71
|
+
indexSV.value = currentIndex;
|
|
72
|
+
// 只有当 itemSizeSV 已经有值(即 Layout 已完成)时才执行动画
|
|
73
|
+
if (itemSizeSV.value > 0) {
|
|
74
|
+
translate.value = withTiming(currentIndex * itemSizeSV.value, {
|
|
75
|
+
duration: animationConfig.duration,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, [currentIndex, animationConfig.duration]);
|
|
80
|
+
|
|
81
|
+
/** ===== JS 回调 ===== */
|
|
82
|
+
const emitChange = useCallback(
|
|
83
|
+
(index: number) => {
|
|
84
|
+
if (options.length > 0 && index >= 0 && index < options.length) {
|
|
85
|
+
onChange(options[index]?.value || "");
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
[onChange, options],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
/** ===== Tap 点击切换 ===== */
|
|
92
|
+
const handlePress = (index: number) => {
|
|
93
|
+
indexSV.value = index;
|
|
94
|
+
translate.value = withTiming(index * itemSizeSV.value, {
|
|
95
|
+
duration: animationConfig.duration ?? 150
|
|
96
|
+
});
|
|
97
|
+
emitChange(index);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** ===== Drag 手势 ===== */
|
|
101
|
+
const panGesture = Gesture.Pan()
|
|
102
|
+
.onUpdate((e) => {
|
|
103
|
+
const delta = vertical ? e.translationY : e.translationX;
|
|
104
|
+
// 基于手势开始时的位置进行偏移计算
|
|
105
|
+
const pos = delta + indexSV.value * itemSizeSV.value;
|
|
106
|
+
const max = (options.length - 1) * itemSizeSV.value;
|
|
107
|
+
|
|
108
|
+
translate.value = Math.min(Math.max(0, pos), max);
|
|
109
|
+
})
|
|
110
|
+
.onEnd(() => {
|
|
111
|
+
const index = Math.round(translate.value / itemSizeSV.value);
|
|
112
|
+
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);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** ===== Thumb 动画样式 ===== */
|
|
123
|
+
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
|
+
});
|
|
141
|
+
|
|
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
|
+
});
|
|
153
|
+
|
|
154
|
+
/** * 2. Layout 初始化
|
|
155
|
+
* 这是修复初始显示问题的关键
|
|
156
|
+
*/
|
|
157
|
+
const onLayout = (e: LayoutChangeEvent) => {
|
|
158
|
+
const size = vertical
|
|
159
|
+
? e.nativeEvent.layout.height
|
|
160
|
+
: e.nativeEvent.layout.width;
|
|
161
|
+
|
|
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
|
+
|
|
171
|
+
return (
|
|
172
|
+
<GestureDetector gesture={panGesture}>
|
|
173
|
+
<View
|
|
174
|
+
onLayout={onLayout}
|
|
175
|
+
style={[
|
|
176
|
+
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" },
|
|
185
|
+
]}
|
|
186
|
+
>
|
|
187
|
+
{/* 滑块背景 */}
|
|
188
|
+
<Animated.View
|
|
189
|
+
pointerEvents="none"
|
|
190
|
+
style={[styles.thumbBase, { backgroundColor: colors.bgFront }, animatedThumbStyle]}
|
|
191
|
+
/>
|
|
192
|
+
|
|
193
|
+
{/* 选项列表 */}
|
|
194
|
+
{options.map((opt, index) => {
|
|
195
|
+
const labelStyle = getLabelAnimatedStyle(index);
|
|
196
|
+
return (
|
|
197
|
+
<Pressable
|
|
198
|
+
key={opt.value}
|
|
199
|
+
style={styles.item}
|
|
200
|
+
onPress={() => handlePress(index)}
|
|
201
|
+
>
|
|
202
|
+
<Animated.Text style={[styles.label, labelStyle]}>
|
|
203
|
+
{opt.label}
|
|
204
|
+
</Animated.Text>
|
|
205
|
+
</Pressable>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
</View>
|
|
209
|
+
</GestureDetector>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const styles = StyleSheet.create({
|
|
214
|
+
container: {
|
|
215
|
+
borderRadius: 16,
|
|
216
|
+
overflow: "hidden",
|
|
217
|
+
flex: 1,
|
|
218
|
+
},
|
|
219
|
+
thumbBase: {
|
|
220
|
+
position: "absolute",
|
|
221
|
+
borderRadius: 12,
|
|
222
|
+
shadowColor: '#000',
|
|
223
|
+
shadowOpacity: 0.3,
|
|
224
|
+
shadowOffset: { width: 0, height: 1 },
|
|
225
|
+
shadowRadius: 2,
|
|
226
|
+
elevation: 2,
|
|
227
|
+
},
|
|
228
|
+
item: {
|
|
229
|
+
flex: 1,
|
|
230
|
+
alignItems: "center",
|
|
231
|
+
justifyContent: "center",
|
|
232
|
+
zIndex: 1, // 确保文字在滑块上方
|
|
233
|
+
},
|
|
234
|
+
label: {
|
|
235
|
+
fontSize: 14,
|
|
236
|
+
fontWeight: "600",
|
|
237
|
+
},
|
|
238
|
+
});
|
package/src/index.tsx
ADDED