@minkyumdev/react-native-switch 1.0.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 +22 -0
- package/README.md +209 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/style.d.ts +16 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +18 -0
- package/dist/style.js.map +1 -0
- package/package.json +69 -0
- package/src/index.tsx +281 -0
- package/src/style.ts +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 minkyumdev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @minkyumdev/react-native-switch
|
|
2
|
+
|
|
3
|
+
A customizable and animated switch component for React Native built with `react-native-reanimated`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎨 Fully customizable colors and sizes
|
|
8
|
+
- ✨ Smooth animations powered by react-native-reanimated
|
|
9
|
+
- 🎯 Customizable thumb scale animation
|
|
10
|
+
- 📳 Haptic feedback support (optional)
|
|
11
|
+
- ♿ Accessible and disabled state support
|
|
12
|
+
- 📱 Works on both iOS and Android
|
|
13
|
+
- 💪 Written in TypeScript with full type definitions
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @minkyumdev/react-native-switch
|
|
19
|
+
# or
|
|
20
|
+
yarn add @minkyumdev/react-native-switch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Peer Dependencies
|
|
24
|
+
|
|
25
|
+
This library requires the following peer dependencies:
|
|
26
|
+
|
|
27
|
+
- `react` (>=16.8.0)
|
|
28
|
+
- `react-native` (>=0.60.0)
|
|
29
|
+
- `react-native-reanimated` (>=2.0.0)
|
|
30
|
+
- `@mhpdev/react-native-haptics` (optional, for haptic feedback)
|
|
31
|
+
|
|
32
|
+
Make sure to install these dependencies in your project:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install react-native-reanimated
|
|
36
|
+
npm install @mhpdev/react-native-haptics
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### iOS Setup
|
|
40
|
+
|
|
41
|
+
For iOS, you need to install pods:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd ios && pod install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Android Setup
|
|
48
|
+
|
|
49
|
+
No additional setup required for Android.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import React, { useState } from 'react';
|
|
55
|
+
import { View } from 'react-native';
|
|
56
|
+
import Switch from '@minkyumdev/react-native-switch';
|
|
57
|
+
|
|
58
|
+
function App() {
|
|
59
|
+
const [isEnabled, setIsEnabled] = useState(false);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
63
|
+
<Switch
|
|
64
|
+
value={isEnabled}
|
|
65
|
+
onValueChange={setIsEnabled}
|
|
66
|
+
activeColor="#34C759"
|
|
67
|
+
inactiveColor="#E5E5EA"
|
|
68
|
+
/>
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Props
|
|
75
|
+
|
|
76
|
+
| Prop | Type | Default | Description |
|
|
77
|
+
| -------------------- | -------------------------------- | ----------- | -------------------------------------------------------------- |
|
|
78
|
+
| `value` | `boolean` | `false` | Current state of the switch (on/off) |
|
|
79
|
+
| `onValueChange` | `(value: boolean) => void` | - | Callback function called when the switch value changes |
|
|
80
|
+
| `disabled` | `boolean` | `false` | Whether the switch is disabled |
|
|
81
|
+
| `activeColor` | `string` | `'#34C759'` | Color of the switch when active (on) |
|
|
82
|
+
| `inactiveColor` | `string` | `'#F6F6F6'` | Color of the switch when inactive (off) |
|
|
83
|
+
| `thumbColor` | `string` | `'#FFFFFF'` | Color of the thumb (the circular element) |
|
|
84
|
+
| `size` | `'small' \| 'medium' \| 'large'` | `'small'` | Predefined size of the switch (used when width/height not set) |
|
|
85
|
+
| `width` | `number` | - | Custom width of the switch (takes priority over size) |
|
|
86
|
+
| `height` | `number` | - | Custom height of the switch (takes priority over size) |
|
|
87
|
+
| `thumbSize` | `number` | - | Custom size of the thumb (auto-calculated if not provided) |
|
|
88
|
+
| `thumbScaleInactive` | `number` | `0.8` | Scale value of the thumb when inactive (off state) |
|
|
89
|
+
| `enableHaptics` | `boolean` | `true` | Whether to enable haptic feedback on toggle |
|
|
90
|
+
| `style` | `StyleProp<ViewStyle>` | - | Additional style for the switch container |
|
|
91
|
+
| `testID` | `string` | - | Test ID for testing purposes |
|
|
92
|
+
|
|
93
|
+
## Examples
|
|
94
|
+
|
|
95
|
+
### Basic Usage
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<Switch value={isEnabled} onValueChange={setIsEnabled} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Custom Colors
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
<Switch
|
|
105
|
+
value={isEnabled}
|
|
106
|
+
onValueChange={setIsEnabled}
|
|
107
|
+
activeColor="#FF3B30"
|
|
108
|
+
inactiveColor="#C7C7CC"
|
|
109
|
+
thumbColor="#FFFFFF"
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Custom Size
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// Using predefined sizes
|
|
117
|
+
<Switch value={isEnabled} onValueChange={setIsEnabled} size="large" />
|
|
118
|
+
|
|
119
|
+
// Using custom width and height
|
|
120
|
+
<Switch
|
|
121
|
+
value={isEnabled}
|
|
122
|
+
onValueChange={setIsEnabled}
|
|
123
|
+
width={80}
|
|
124
|
+
height={40}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
// Custom width, height, and thumbSize
|
|
128
|
+
<Switch
|
|
129
|
+
value={isEnabled}
|
|
130
|
+
onValueChange={setIsEnabled}
|
|
131
|
+
width={100}
|
|
132
|
+
height={50}
|
|
133
|
+
thumbSize={35}
|
|
134
|
+
/>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Disabled State
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<Switch value={isEnabled} onValueChange={setIsEnabled} disabled={true} />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Disable Haptics
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<Switch value={isEnabled} onValueChange={setIsEnabled} enableHaptics={false} />
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Custom Thumb Scale
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
// Custom scale for inactive state (default is 0.8)
|
|
153
|
+
<Switch
|
|
154
|
+
value={isEnabled}
|
|
155
|
+
onValueChange={setIsEnabled}
|
|
156
|
+
thumbScaleInactive={0.7} // Thumb will be smaller when off
|
|
157
|
+
/>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Install dependencies
|
|
164
|
+
npm install
|
|
165
|
+
|
|
166
|
+
# Build the library
|
|
167
|
+
npm run build
|
|
168
|
+
|
|
169
|
+
# Watch mode for development
|
|
170
|
+
npm run build:watch
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## API Reference
|
|
174
|
+
|
|
175
|
+
### Switch Component
|
|
176
|
+
|
|
177
|
+
The main component exported from this library.
|
|
178
|
+
|
|
179
|
+
#### Props
|
|
180
|
+
|
|
181
|
+
See the [Props](#props) table above for detailed information.
|
|
182
|
+
|
|
183
|
+
#### Example
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import Switch from '@minkyumdev/react-native-switch';
|
|
187
|
+
|
|
188
|
+
function MyComponent() {
|
|
189
|
+
const [enabled, setEnabled] = useState(false);
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<Switch
|
|
193
|
+
value={enabled}
|
|
194
|
+
onValueChange={setEnabled}
|
|
195
|
+
size="medium"
|
|
196
|
+
activeColor="#34C759"
|
|
197
|
+
inactiveColor="#E5E5EA"
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
|
206
|
+
|
|
207
|
+
## Contributing
|
|
208
|
+
|
|
209
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
export interface SwitchProps {
|
|
4
|
+
/**
|
|
5
|
+
* Current state of the switch (on/off)
|
|
6
|
+
*/
|
|
7
|
+
value: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Callback function called when the switch value changes
|
|
10
|
+
*/
|
|
11
|
+
onValueChange?: (value: boolean) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the switch is disabled
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Color of the switch when active (on)
|
|
19
|
+
* @default '#34C759'
|
|
20
|
+
*/
|
|
21
|
+
activeColor?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Color of the switch when inactive (off)
|
|
24
|
+
* @default '#F6F6F6'
|
|
25
|
+
*/
|
|
26
|
+
inactiveColor?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Color of the thumb (the circular element)
|
|
29
|
+
* @default '#FFFFFF'
|
|
30
|
+
*/
|
|
31
|
+
thumbColor?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Predefined size of the switch (used when width/height not set)
|
|
34
|
+
* @default 'small'
|
|
35
|
+
*/
|
|
36
|
+
size?: 'small' | 'medium' | 'large';
|
|
37
|
+
/**
|
|
38
|
+
* Custom width of the switch (takes priority over size)
|
|
39
|
+
*/
|
|
40
|
+
width?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Custom height of the switch (takes priority over size)
|
|
43
|
+
*/
|
|
44
|
+
height?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Custom size of the thumb (auto-calculated if not provided)
|
|
47
|
+
*/
|
|
48
|
+
thumbSize?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Additional style for the switch container
|
|
51
|
+
*/
|
|
52
|
+
style?: StyleProp<ViewStyle>;
|
|
53
|
+
/**
|
|
54
|
+
* Test ID for testing purposes
|
|
55
|
+
*/
|
|
56
|
+
testID?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Whether to enable haptic feedback on toggle
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
enableHaptics?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Scale value of the thumb when inactive (off state)
|
|
64
|
+
* @default 0.8
|
|
65
|
+
*/
|
|
66
|
+
thumbScaleInactive?: number;
|
|
67
|
+
}
|
|
68
|
+
export declare const SWITCH_ANIMATION_DURATION = 150;
|
|
69
|
+
/**
|
|
70
|
+
* React Native용 커스텀 Switch 컴포넌트
|
|
71
|
+
* react-native-reanimated를 사용한 부드러운 애니메이션과 햅틱 피드백을 제공합니다.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```tsx
|
|
75
|
+
* import Switch from 'react-native-switch';
|
|
76
|
+
*
|
|
77
|
+
* function App() {
|
|
78
|
+
* const [isEnabled, setIsEnabled] = useState(false);
|
|
79
|
+
*
|
|
80
|
+
* return (
|
|
81
|
+
* <Switch
|
|
82
|
+
* value={isEnabled}
|
|
83
|
+
* onValueChange={setIsEnabled}
|
|
84
|
+
* size="medium"
|
|
85
|
+
* activeColor="#20A2F9"
|
|
86
|
+
* />
|
|
87
|
+
* );
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare const Switch: React.FC<SwitchProps>;
|
|
92
|
+
export default Switch;
|
|
93
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAC/D,OAAO,EAAa,SAAS,EAAE,SAAS,EAAc,MAAM,cAAc,CAAC;AAe3E,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,KAAK,EAAE,OAAO,CAAC;IACf;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAYD,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAY7C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAwJxC,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import { Pressable, PixelRatio } from 'react-native';
|
|
3
|
+
import Animated, { interpolateColor, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
|
|
4
|
+
import Haptics from '@mhpdev/react-native-haptics';
|
|
5
|
+
import { styles } from './style';
|
|
6
|
+
// Helper function for pixel ratio
|
|
7
|
+
var pixelRatio = function (value) {
|
|
8
|
+
return PixelRatio.roundToNearestPixel(value);
|
|
9
|
+
};
|
|
10
|
+
var SWITCH_CONFIG = {
|
|
11
|
+
small: { width: 40, height: 24, thumbSize: 14 },
|
|
12
|
+
medium: { width: 50, height: 32, thumbSize: 20 },
|
|
13
|
+
large: { width: 60, height: 36, thumbSize: 26 },
|
|
14
|
+
};
|
|
15
|
+
// Thumb scale constants
|
|
16
|
+
var THUMB_SCALE_ACTIVE = 1.0;
|
|
17
|
+
// Animation constants
|
|
18
|
+
export var SWITCH_ANIMATION_DURATION = 150;
|
|
19
|
+
var ANIMATION_CONFIG = {
|
|
20
|
+
duration: SWITCH_ANIMATION_DURATION,
|
|
21
|
+
};
|
|
22
|
+
// Track padding
|
|
23
|
+
var TRACK_PADDING = 4;
|
|
24
|
+
// TranslateX values
|
|
25
|
+
var TRANSLATE_X_INACTIVE = 0;
|
|
26
|
+
var TRANSLATE_X_ACTIVE = 1;
|
|
27
|
+
/**
|
|
28
|
+
* React Native용 커스텀 Switch 컴포넌트
|
|
29
|
+
* react-native-reanimated를 사용한 부드러운 애니메이션과 햅틱 피드백을 제공합니다.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* import Switch from 'react-native-switch';
|
|
34
|
+
*
|
|
35
|
+
* function App() {
|
|
36
|
+
* const [isEnabled, setIsEnabled] = useState(false);
|
|
37
|
+
*
|
|
38
|
+
* return (
|
|
39
|
+
* <Switch
|
|
40
|
+
* value={isEnabled}
|
|
41
|
+
* onValueChange={setIsEnabled}
|
|
42
|
+
* size="medium"
|
|
43
|
+
* activeColor="#20A2F9"
|
|
44
|
+
* />
|
|
45
|
+
* );
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export var Switch = React.memo(function (_a) {
|
|
50
|
+
var value = _a.value, onValueChange = _a.onValueChange, _b = _a.disabled, disabled = _b === void 0 ? false : _b, _c = _a.activeColor, activeColor = _c === void 0 ? '#34C759' : _c, _d = _a.inactiveColor, inactiveColor = _d === void 0 ? '#F6F6F6' : _d, _e = _a.thumbColor, thumbColor = _e === void 0 ? '#FFFFFF' : _e, _f = _a.size, size = _f === void 0 ? 'small' : _f, width = _a.width, height = _a.height, thumbSize = _a.thumbSize, style = _a.style, testID = _a.testID, _g = _a.enableHaptics, enableHaptics = _g === void 0 ? true : _g, _h = _a.thumbScaleInactive, thumbScaleInactive = _h === void 0 ? 0.8 : _h;
|
|
51
|
+
var translateX = useSharedValue(value ? TRANSLATE_X_ACTIVE : TRANSLATE_X_INACTIVE);
|
|
52
|
+
var switchConfig = useMemo(function () {
|
|
53
|
+
if (width !== undefined || height !== undefined) {
|
|
54
|
+
var configWidth = width !== null && width !== void 0 ? width : SWITCH_CONFIG[size].width;
|
|
55
|
+
var configHeight = height !== null && height !== void 0 ? height : SWITCH_CONFIG[size].height;
|
|
56
|
+
var calculatedThumbSize_1 = thumbSize !== null && thumbSize !== void 0 ? thumbSize : configHeight * 0.7;
|
|
57
|
+
return {
|
|
58
|
+
width: configWidth,
|
|
59
|
+
height: configHeight,
|
|
60
|
+
thumbSize: calculatedThumbSize_1,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return SWITCH_CONFIG[size];
|
|
64
|
+
}, [size, width, height, thumbSize]);
|
|
65
|
+
var thumbScaleActive = THUMB_SCALE_ACTIVE;
|
|
66
|
+
var thumbScaleDiff = thumbScaleActive - thumbScaleInactive;
|
|
67
|
+
var scaleOffsetPadding = useMemo(function () { return (switchConfig.thumbSize * (1 - thumbScaleInactive)) / 2; }, [switchConfig.thumbSize, thumbScaleInactive]);
|
|
68
|
+
var maxTranslate = useMemo(function () {
|
|
69
|
+
return switchConfig.width - switchConfig.thumbSize - TRACK_PADDING * 2;
|
|
70
|
+
}, [switchConfig.width, switchConfig.thumbSize]);
|
|
71
|
+
useEffect(function () {
|
|
72
|
+
translateX.value = withTiming(value ? TRANSLATE_X_ACTIVE : TRANSLATE_X_INACTIVE, ANIMATION_CONFIG);
|
|
73
|
+
}, [value, translateX]);
|
|
74
|
+
var handlePress = useCallback(function () {
|
|
75
|
+
if (disabled)
|
|
76
|
+
return;
|
|
77
|
+
if (enableHaptics) {
|
|
78
|
+
Haptics.impact('soft');
|
|
79
|
+
}
|
|
80
|
+
var newValue = !value;
|
|
81
|
+
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(newValue);
|
|
82
|
+
}, [disabled, value, onValueChange, enableHaptics]);
|
|
83
|
+
var trackAnimatedStyle = useAnimatedStyle(function () {
|
|
84
|
+
var backgroundColor = interpolateColor(translateX.value, [0, 1], [inactiveColor, activeColor]);
|
|
85
|
+
return {
|
|
86
|
+
backgroundColor: backgroundColor,
|
|
87
|
+
};
|
|
88
|
+
}, [inactiveColor, activeColor]);
|
|
89
|
+
var thumbAnimatedStyle = useAnimatedStyle(function () {
|
|
90
|
+
var scale = thumbScaleInactive + translateX.value * thumbScaleDiff;
|
|
91
|
+
var currentScaleOffset = scaleOffsetPadding * (1 - translateX.value);
|
|
92
|
+
var baseTranslateX = TRACK_PADDING + translateX.value * maxTranslate;
|
|
93
|
+
var translateXValue = baseTranslateX - currentScaleOffset;
|
|
94
|
+
return {
|
|
95
|
+
transform: [
|
|
96
|
+
{
|
|
97
|
+
translateX: translateXValue,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
scale: scale,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}, [maxTranslate, scaleOffsetPadding, thumbScaleInactive, thumbScaleDiff]);
|
|
105
|
+
var trackStyle = useMemo(function () { return ({
|
|
106
|
+
width: switchConfig.width,
|
|
107
|
+
height: switchConfig.height,
|
|
108
|
+
borderRadius: switchConfig.height / 2,
|
|
109
|
+
opacity: disabled ? 0.5 : 1,
|
|
110
|
+
}); }, [switchConfig.width, switchConfig.height, disabled]);
|
|
111
|
+
var calculatedThumbSize = useMemo(function () { return pixelRatio(switchConfig.thumbSize); }, [switchConfig.thumbSize]);
|
|
112
|
+
var thumbStyle = useMemo(function () { return ({
|
|
113
|
+
width: calculatedThumbSize,
|
|
114
|
+
height: calculatedThumbSize,
|
|
115
|
+
borderRadius: pixelRatio(switchConfig.thumbSize / 2),
|
|
116
|
+
backgroundColor: thumbColor,
|
|
117
|
+
}); }, [calculatedThumbSize, switchConfig.thumbSize, thumbColor]);
|
|
118
|
+
return (<Pressable onPress={handlePress} disabled={disabled} style={[style]} testID={testID}>
|
|
119
|
+
<Animated.View style={[styles.track, trackAnimatedStyle, trackStyle]}>
|
|
120
|
+
<Animated.View style={[styles.thumb, thumbAnimatedStyle, thumbStyle]}/>
|
|
121
|
+
</Animated.View>
|
|
122
|
+
</Pressable>);
|
|
123
|
+
}, function (prevProps, nextProps) {
|
|
124
|
+
// Custom comparison for React.memo - prevent unnecessary re-renders
|
|
125
|
+
return (prevProps.value === nextProps.value &&
|
|
126
|
+
prevProps.disabled === nextProps.disabled &&
|
|
127
|
+
prevProps.activeColor === nextProps.activeColor &&
|
|
128
|
+
prevProps.inactiveColor === nextProps.inactiveColor &&
|
|
129
|
+
prevProps.thumbColor === nextProps.thumbColor &&
|
|
130
|
+
prevProps.size === nextProps.size &&
|
|
131
|
+
prevProps.width === nextProps.width &&
|
|
132
|
+
prevProps.height === nextProps.height &&
|
|
133
|
+
prevProps.thumbSize === nextProps.thumbSize &&
|
|
134
|
+
prevProps.enableHaptics === nextProps.enableHaptics &&
|
|
135
|
+
prevProps.onValueChange === nextProps.onValueChange);
|
|
136
|
+
});
|
|
137
|
+
export default Switch;
|
|
138
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const styles: {
|
|
2
|
+
track: {
|
|
3
|
+
justifyContent: "center";
|
|
4
|
+
};
|
|
5
|
+
thumb: {
|
|
6
|
+
shadowColor: string;
|
|
7
|
+
shadowOffset: {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
shadowOpacity: number;
|
|
12
|
+
shadowRadius: number;
|
|
13
|
+
elevation: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=style.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;CAejB,CAAC"}
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
export var styles = StyleSheet.create({
|
|
3
|
+
track: {
|
|
4
|
+
justifyContent: 'center',
|
|
5
|
+
// padding: 2,
|
|
6
|
+
},
|
|
7
|
+
thumb: {
|
|
8
|
+
shadowColor: '#000',
|
|
9
|
+
shadowOffset: {
|
|
10
|
+
width: 0,
|
|
11
|
+
height: 2,
|
|
12
|
+
},
|
|
13
|
+
shadowOpacity: 0.1,
|
|
14
|
+
shadowRadius: 2,
|
|
15
|
+
elevation: 2,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=style.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.js","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,MAAM,CAAC,IAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IACtC,KAAK,EAAE;QACL,cAAc,EAAE,QAAQ;QACxB,cAAc;KACf;IACD,KAAK,EAAE;QACL,WAAW,EAAE,MAAM;QACnB,YAAY,EAAE;YACZ,KAAK,EAAE,CAAC;YACR,MAAM,EAAE,CAAC;SACV;QACD,aAAa,EAAE,GAAG;QAClB,YAAY,EAAE,CAAC;QACf,SAAS,EAAE,CAAC;KACb;CACF,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@minkyumdev/react-native-switch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A customizable switch component for React Native",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react-native",
|
|
15
|
+
"switch",
|
|
16
|
+
"toggle",
|
|
17
|
+
"component",
|
|
18
|
+
"ui"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/minkyumdev/react-native-switch.git"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf dist && tsc",
|
|
28
|
+
"build:watch": "rm -rf dist && tsc -w",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"android": "react-native run-android",
|
|
31
|
+
"ios": "react-native run-ios",
|
|
32
|
+
"lint": "eslint .",
|
|
33
|
+
"start": "react-native start",
|
|
34
|
+
"test": "jest"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@mhpdev/react-native-haptics": "*",
|
|
38
|
+
"react": ">=16.8.0",
|
|
39
|
+
"react-native": ">=0.60.0",
|
|
40
|
+
"react-native-reanimated": ">=2.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@babel/core": "^7.25.2",
|
|
45
|
+
"@babel/preset-env": "^7.25.3",
|
|
46
|
+
"@babel/runtime": "^7.25.0",
|
|
47
|
+
"@mhpdev/react-native-haptics": "1.0.1",
|
|
48
|
+
"@react-native-community/cli": "20.0.0",
|
|
49
|
+
"@react-native-community/cli-platform-android": "20.0.0",
|
|
50
|
+
"@react-native-community/cli-platform-ios": "20.0.0",
|
|
51
|
+
"@react-native/babel-preset": "0.82.1",
|
|
52
|
+
"@react-native/eslint-config": "0.82.1",
|
|
53
|
+
"@react-native/metro-config": "0.82.1",
|
|
54
|
+
"@react-native/typescript-config": "0.82.1",
|
|
55
|
+
"@types/jest": "^29.5.13",
|
|
56
|
+
"@types/react": "^19.1.1",
|
|
57
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
58
|
+
"eslint": "^8.19.0",
|
|
59
|
+
"jest": "^29.6.3",
|
|
60
|
+
"prettier": "2.8.8",
|
|
61
|
+
"react-native-reanimated": "4.1.3",
|
|
62
|
+
"react-native-worklets": "0.6.1",
|
|
63
|
+
"react-test-renderer": "19.1.1",
|
|
64
|
+
"typescript": "^5.8.3"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import { Pressable, StyleProp, ViewStyle, PixelRatio } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
interpolateColor,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withTiming,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
import Haptics from '@mhpdev/react-native-haptics';
|
|
10
|
+
import { styles } from './style';
|
|
11
|
+
|
|
12
|
+
// Helper function for pixel ratio
|
|
13
|
+
const pixelRatio = (value: number): number => {
|
|
14
|
+
return PixelRatio.roundToNearestPixel(value);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface SwitchProps {
|
|
18
|
+
/**
|
|
19
|
+
* Current state of the switch (on/off)
|
|
20
|
+
*/
|
|
21
|
+
value: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Callback function called when the switch value changes
|
|
24
|
+
*/
|
|
25
|
+
onValueChange?: (value: boolean) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the switch is disabled
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Color of the switch when active (on)
|
|
33
|
+
* @default '#34C759'
|
|
34
|
+
*/
|
|
35
|
+
activeColor?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Color of the switch when inactive (off)
|
|
38
|
+
* @default '#F6F6F6'
|
|
39
|
+
*/
|
|
40
|
+
inactiveColor?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Color of the thumb (the circular element)
|
|
43
|
+
* @default '#FFFFFF'
|
|
44
|
+
*/
|
|
45
|
+
thumbColor?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Predefined size of the switch (used when width/height not set)
|
|
48
|
+
* @default 'small'
|
|
49
|
+
*/
|
|
50
|
+
size?: 'small' | 'medium' | 'large';
|
|
51
|
+
/**
|
|
52
|
+
* Custom width of the switch (takes priority over size)
|
|
53
|
+
*/
|
|
54
|
+
width?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Custom height of the switch (takes priority over size)
|
|
57
|
+
*/
|
|
58
|
+
height?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Custom size of the thumb (auto-calculated if not provided)
|
|
61
|
+
*/
|
|
62
|
+
thumbSize?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Additional style for the switch container
|
|
65
|
+
*/
|
|
66
|
+
style?: StyleProp<ViewStyle>;
|
|
67
|
+
/**
|
|
68
|
+
* Test ID for testing purposes
|
|
69
|
+
*/
|
|
70
|
+
testID?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Whether to enable haptic feedback on toggle
|
|
73
|
+
* @default true
|
|
74
|
+
*/
|
|
75
|
+
enableHaptics?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Scale value of the thumb when inactive (off state)
|
|
78
|
+
* @default 0.8
|
|
79
|
+
*/
|
|
80
|
+
thumbScaleInactive?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const SWITCH_CONFIG = {
|
|
84
|
+
small: { width: 40, height: 24, thumbSize: 14 },
|
|
85
|
+
medium: { width: 50, height: 32, thumbSize: 20 },
|
|
86
|
+
large: { width: 60, height: 36, thumbSize: 26 },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Thumb scale constants
|
|
90
|
+
const THUMB_SCALE_ACTIVE = 1.0;
|
|
91
|
+
|
|
92
|
+
// Animation constants
|
|
93
|
+
export const SWITCH_ANIMATION_DURATION = 150;
|
|
94
|
+
const ANIMATION_CONFIG = {
|
|
95
|
+
duration: SWITCH_ANIMATION_DURATION,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Track padding
|
|
99
|
+
const TRACK_PADDING = 4;
|
|
100
|
+
|
|
101
|
+
// TranslateX values
|
|
102
|
+
const TRANSLATE_X_INACTIVE = 0;
|
|
103
|
+
const TRANSLATE_X_ACTIVE = 1;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* React Native용 커스텀 Switch 컴포넌트
|
|
107
|
+
* react-native-reanimated를 사용한 부드러운 애니메이션과 햅틱 피드백을 제공합니다.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```tsx
|
|
111
|
+
* import Switch from 'react-native-switch';
|
|
112
|
+
*
|
|
113
|
+
* function App() {
|
|
114
|
+
* const [isEnabled, setIsEnabled] = useState(false);
|
|
115
|
+
*
|
|
116
|
+
* return (
|
|
117
|
+
* <Switch
|
|
118
|
+
* value={isEnabled}
|
|
119
|
+
* onValueChange={setIsEnabled}
|
|
120
|
+
* size="medium"
|
|
121
|
+
* activeColor="#20A2F9"
|
|
122
|
+
* />
|
|
123
|
+
* );
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export const Switch: React.FC<SwitchProps> = React.memo<SwitchProps>(
|
|
128
|
+
({
|
|
129
|
+
value,
|
|
130
|
+
onValueChange,
|
|
131
|
+
disabled = false,
|
|
132
|
+
activeColor = '#34C759',
|
|
133
|
+
inactiveColor = '#F6F6F6',
|
|
134
|
+
thumbColor = '#FFFFFF',
|
|
135
|
+
size = 'small',
|
|
136
|
+
width,
|
|
137
|
+
height,
|
|
138
|
+
thumbSize,
|
|
139
|
+
style,
|
|
140
|
+
testID,
|
|
141
|
+
enableHaptics = true,
|
|
142
|
+
thumbScaleInactive = 0.8,
|
|
143
|
+
}) => {
|
|
144
|
+
const translateX = useSharedValue(
|
|
145
|
+
value ? TRANSLATE_X_ACTIVE : TRANSLATE_X_INACTIVE,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const switchConfig = useMemo(() => {
|
|
149
|
+
if (width !== undefined || height !== undefined) {
|
|
150
|
+
const configWidth = width ?? SWITCH_CONFIG[size].width;
|
|
151
|
+
const configHeight = height ?? SWITCH_CONFIG[size].height;
|
|
152
|
+
const calculatedThumbSize = thumbSize ?? configHeight * 0.7;
|
|
153
|
+
return {
|
|
154
|
+
width: configWidth,
|
|
155
|
+
height: configHeight,
|
|
156
|
+
thumbSize: calculatedThumbSize,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return SWITCH_CONFIG[size];
|
|
160
|
+
}, [size, width, height, thumbSize]);
|
|
161
|
+
|
|
162
|
+
const thumbScaleActive = THUMB_SCALE_ACTIVE;
|
|
163
|
+
const thumbScaleDiff = thumbScaleActive - thumbScaleInactive;
|
|
164
|
+
|
|
165
|
+
const scaleOffsetPadding = useMemo(
|
|
166
|
+
() => (switchConfig.thumbSize * (1 - thumbScaleInactive)) / 2,
|
|
167
|
+
[switchConfig.thumbSize, thumbScaleInactive],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const maxTranslate = useMemo(() => {
|
|
171
|
+
return switchConfig.width - switchConfig.thumbSize - TRACK_PADDING * 2;
|
|
172
|
+
}, [switchConfig.width, switchConfig.thumbSize]);
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
translateX.value = withTiming(
|
|
176
|
+
value ? TRANSLATE_X_ACTIVE : TRANSLATE_X_INACTIVE,
|
|
177
|
+
ANIMATION_CONFIG,
|
|
178
|
+
);
|
|
179
|
+
}, [value, translateX]);
|
|
180
|
+
|
|
181
|
+
const handlePress = useCallback(() => {
|
|
182
|
+
if (disabled) return;
|
|
183
|
+
if (enableHaptics) {
|
|
184
|
+
Haptics.impact('soft');
|
|
185
|
+
}
|
|
186
|
+
const newValue = !value;
|
|
187
|
+
onValueChange?.(newValue);
|
|
188
|
+
}, [disabled, value, onValueChange, enableHaptics]);
|
|
189
|
+
|
|
190
|
+
const trackAnimatedStyle = useAnimatedStyle(() => {
|
|
191
|
+
const backgroundColor = interpolateColor(
|
|
192
|
+
translateX.value,
|
|
193
|
+
[0, 1],
|
|
194
|
+
[inactiveColor, activeColor],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
backgroundColor,
|
|
199
|
+
};
|
|
200
|
+
}, [inactiveColor, activeColor]);
|
|
201
|
+
|
|
202
|
+
const thumbAnimatedStyle = useAnimatedStyle(() => {
|
|
203
|
+
const scale = thumbScaleInactive + translateX.value * thumbScaleDiff;
|
|
204
|
+
|
|
205
|
+
const currentScaleOffset = scaleOffsetPadding * (1 - translateX.value);
|
|
206
|
+
|
|
207
|
+
const baseTranslateX = TRACK_PADDING + translateX.value * maxTranslate;
|
|
208
|
+
|
|
209
|
+
const translateXValue = baseTranslateX - currentScaleOffset;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
transform: [
|
|
213
|
+
{
|
|
214
|
+
translateX: translateXValue,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
scale,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
}, [maxTranslate, scaleOffsetPadding, thumbScaleInactive, thumbScaleDiff]);
|
|
222
|
+
|
|
223
|
+
const trackStyle = useMemo(
|
|
224
|
+
() => ({
|
|
225
|
+
width: switchConfig.width,
|
|
226
|
+
height: switchConfig.height,
|
|
227
|
+
borderRadius: switchConfig.height / 2,
|
|
228
|
+
opacity: disabled ? 0.5 : 1,
|
|
229
|
+
}),
|
|
230
|
+
[switchConfig.width, switchConfig.height, disabled],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const calculatedThumbSize = useMemo(
|
|
234
|
+
() => pixelRatio(switchConfig.thumbSize),
|
|
235
|
+
[switchConfig.thumbSize],
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const thumbStyle = useMemo(
|
|
239
|
+
() => ({
|
|
240
|
+
width: calculatedThumbSize,
|
|
241
|
+
height: calculatedThumbSize,
|
|
242
|
+
borderRadius: pixelRatio(switchConfig.thumbSize / 2),
|
|
243
|
+
backgroundColor: thumbColor,
|
|
244
|
+
}),
|
|
245
|
+
[calculatedThumbSize, switchConfig.thumbSize, thumbColor],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<Pressable
|
|
250
|
+
onPress={handlePress}
|
|
251
|
+
disabled={disabled}
|
|
252
|
+
style={[style]}
|
|
253
|
+
testID={testID}
|
|
254
|
+
>
|
|
255
|
+
<Animated.View style={[styles.track, trackAnimatedStyle, trackStyle]}>
|
|
256
|
+
<Animated.View
|
|
257
|
+
style={[styles.thumb, thumbAnimatedStyle, thumbStyle]}
|
|
258
|
+
/>
|
|
259
|
+
</Animated.View>
|
|
260
|
+
</Pressable>
|
|
261
|
+
);
|
|
262
|
+
},
|
|
263
|
+
(prevProps, nextProps) => {
|
|
264
|
+
// Custom comparison for React.memo - prevent unnecessary re-renders
|
|
265
|
+
return (
|
|
266
|
+
prevProps.value === nextProps.value &&
|
|
267
|
+
prevProps.disabled === nextProps.disabled &&
|
|
268
|
+
prevProps.activeColor === nextProps.activeColor &&
|
|
269
|
+
prevProps.inactiveColor === nextProps.inactiveColor &&
|
|
270
|
+
prevProps.thumbColor === nextProps.thumbColor &&
|
|
271
|
+
prevProps.size === nextProps.size &&
|
|
272
|
+
prevProps.width === nextProps.width &&
|
|
273
|
+
prevProps.height === nextProps.height &&
|
|
274
|
+
prevProps.thumbSize === nextProps.thumbSize &&
|
|
275
|
+
prevProps.enableHaptics === nextProps.enableHaptics &&
|
|
276
|
+
prevProps.onValueChange === nextProps.onValueChange
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
export default Switch;
|
package/src/style.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export const styles = StyleSheet.create({
|
|
4
|
+
track: {
|
|
5
|
+
justifyContent: 'center',
|
|
6
|
+
// padding: 2,
|
|
7
|
+
},
|
|
8
|
+
thumb: {
|
|
9
|
+
shadowColor: '#000',
|
|
10
|
+
shadowOffset: {
|
|
11
|
+
width: 0,
|
|
12
|
+
height: 2,
|
|
13
|
+
},
|
|
14
|
+
shadowOpacity: 0.1,
|
|
15
|
+
shadowRadius: 2,
|
|
16
|
+
elevation: 2,
|
|
17
|
+
},
|
|
18
|
+
});
|