Skip to content

Commit 2b88ef9

Browse files
committed
feat: add FloatingButton component with draggable functionality
1 parent f31b3c9 commit 2b88ef9

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed

src/FloatingButton.tsx

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import {
2+
View,
3+
Text,
4+
StyleSheet,
5+
TouchableOpacity,
6+
Dimensions,
7+
} from 'react-native';
8+
import React from 'react';
9+
import Animated, {
10+
useSharedValue,
11+
useAnimatedStyle,
12+
withSpring,
13+
} from 'react-native-reanimated';
14+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
15+
import Icon from './Icon';
16+
17+
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
18+
19+
const BUTTON_SIZE = 58;
20+
const CLOSE_BUTTON_SIZE = 58;
21+
const PADDING = 20;
22+
23+
interface FloatingButtonProps {
24+
openModal: () => void;
25+
hideIcon: () => void;
26+
logsLength: number;
27+
enableDeviceShake?: boolean;
28+
}
29+
30+
const FloatingButton: React.FunctionComponent<FloatingButtonProps> = ({
31+
hideIcon,
32+
logsLength,
33+
openModal,
34+
enableDeviceShake,
35+
}) => {
36+
const positionX = useSharedValue(screenWidth - BUTTON_SIZE - PADDING);
37+
const positionY = useSharedValue(screenHeight - 150);
38+
39+
const startPositionX = useSharedValue(0);
40+
const startPositionY = useSharedValue(0);
41+
42+
const isDragging = useSharedValue(false);
43+
44+
const panGesture = Gesture.Pan()
45+
.onStart(() => {
46+
isDragging.value = true;
47+
startPositionX.value = positionX.value;
48+
startPositionY.value = positionY.value;
49+
})
50+
.onUpdate((event) => {
51+
const newX = startPositionX.value + event.translationX;
52+
const newY = startPositionY.value + event.translationY;
53+
54+
const minX = PADDING;
55+
const maxX = screenWidth - BUTTON_SIZE - PADDING;
56+
const minY = 50;
57+
const maxY = screenHeight - 150;
58+
59+
positionX.value = Math.max(minX, Math.min(maxX, newX));
60+
positionY.value = Math.max(minY, Math.min(maxY, newY));
61+
})
62+
.onEnd(() => {
63+
isDragging.value = false;
64+
65+
const centerX = screenWidth / 2;
66+
67+
if (positionX.value < centerX) {
68+
positionX.value = withSpring(PADDING);
69+
} else {
70+
positionX.value = withSpring(screenWidth - BUTTON_SIZE - PADDING);
71+
}
72+
});
73+
74+
const animatedStyle = useAnimatedStyle(() => {
75+
return {
76+
left: positionX.value,
77+
top: positionY.value,
78+
opacity: isDragging.value ? 0.8 : 1,
79+
};
80+
});
81+
82+
const animatedCloseStyle = useAnimatedStyle(() => {
83+
return {
84+
left: positionX.value + 4,
85+
top: positionY.value - 50,
86+
opacity: isDragging.value ? 0.8 : 1,
87+
};
88+
});
89+
90+
return (
91+
<View style={styles.container}>
92+
{enableDeviceShake && (
93+
<Animated.View style={[styles.floatingCloseIcon, animatedCloseStyle]}>
94+
<TouchableOpacity onPress={hideIcon} style={styles.closeButton}>
95+
<Icon type="close" />
96+
</TouchableOpacity>
97+
</Animated.View>
98+
)}
99+
<GestureDetector gesture={panGesture}>
100+
<Animated.View style={[styles.floatingButton, animatedStyle]}>
101+
<TouchableOpacity
102+
onPress={openModal}
103+
activeOpacity={0.9}
104+
style={styles.buttonTouchable}
105+
>
106+
<Text style={styles.floatingButtonText}>📊 {logsLength}</Text>
107+
</TouchableOpacity>
108+
</Animated.View>
109+
</GestureDetector>
110+
</View>
111+
);
112+
};
113+
114+
const styles = StyleSheet.create({
115+
gestureRoot: {
116+
flex: 1,
117+
top: 0,
118+
left: 0,
119+
right: 0,
120+
bottom: 0,
121+
position: 'absolute',
122+
pointerEvents: 'box-none',
123+
},
124+
container: {
125+
flex: 1,
126+
top: 0,
127+
left: 0,
128+
right: 0,
129+
bottom: 0,
130+
position: 'absolute',
131+
pointerEvents: 'box-none',
132+
},
133+
floatingButton: {
134+
position: 'absolute',
135+
zIndex: 1000,
136+
},
137+
buttonTouchable: {
138+
backgroundColor: '#007AFF',
139+
padding: 16,
140+
borderRadius: 25,
141+
elevation: 5,
142+
shadowOffset: { width: 0, height: 2 },
143+
shadowOpacity: 0.3,
144+
shadowRadius: 4,
145+
justifyContent: 'center',
146+
alignItems: 'center',
147+
},
148+
floatingCloseIcon: {
149+
position: 'absolute',
150+
zIndex: 1001,
151+
},
152+
closeButton: {
153+
padding: 16,
154+
borderRadius: 70,
155+
width: CLOSE_BUTTON_SIZE,
156+
height: CLOSE_BUTTON_SIZE,
157+
justifyContent: 'center',
158+
alignItems: 'center',
159+
fontWeight: '900',
160+
},
161+
floatingButtonText: {
162+
color: 'white',
163+
fontSize: 14,
164+
fontWeight: 'bold',
165+
},
166+
});
167+
168+
export default FloatingButton;

0 commit comments

Comments
 (0)