Commit cf1f778b by tungnq

TODO: Bổ sung FAB Group

parent 4d9424d8
import React from 'react'; import React, { useState, useRef } from 'react';
import { import {
TouchableOpacity, View,
Text, Text,
StyleSheet, StyleSheet,
Animated, Animated,
Pressable, Pressable,
PanResponder,
Dimensions, Dimensions,
} from 'react-native'; } from 'react-native';
// SCREEN: Lấy kích thước màn hình để giới hạn vùng di chuyển const { width: SCREEN_WIDTH } = Dimensions.get('window');
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
// COMPONENT: FAB (Floating Action Button) có thể di chuyển // COMPONENT: FAB Group với menu mở rộng
const FAB = ({ const FABGroup = ({
icon = '+',
label,
onPress,
position = 'bottom-right', position = 'bottom-right',
mainIcon = '+',
mainBackgroundColor = '#007AFF',
mainColor = '#FFFFFF',
size = 'medium', size = 'medium',
backgroundColor = '#007AFF', actions = [], // Mảng các action buttons
color = '#FFFFFF', overlayColor = 'rgba(0, 0, 0, 0.3)',
animationDuration = 200,
spacing = 10,
style, style,
disabled = false,
elevation = 8,
draggable = false, // FEATURE: Bật/tắt tính năng kéo thả
snapToEdges = true, // FEATURE: Tự động dính vào cạnh màn hình
}) => { }) => {
// STATE: Animation values // STATE: Quản lý trạng thái mở/đóng
const scaleValue = React.useRef(new Animated.Value(1)).current; const [isOpen, setIsOpen] = useState(false);
const pan = React.useRef(new Animated.ValueXY()).current;
// STATE: Tracking drag và press // ANIMATION: Animation values
const [isDragging, setIsDragging] = React.useState(false); const fadeAnim = useRef(new Animated.Value(0)).current;
const [initialPosition, setInitialPosition] = React.useState({ x: 0, y: 0 }); const rotateAnim = useRef(new Animated.Value(0)).current;
const scaleAnim = useRef(new Animated.Value(1)).current;
// FUNCTIONALITY: Khởi tạo vị trí ban đầu từ position prop // FUNCTIONALITY: Toggle FAB Group
React.useEffect(() => { const toggleFAB = () => {
if (!draggable) return; const toValue = isOpen ? 0 : 1;
setIsOpen(!isOpen);
const fabSize = getFABSize(size);
const margin = 16;
let x, y;
// SETUP: Tính toán vị trí ban đầu dựa trên position
switch (position) {
case 'bottom-right':
x = SCREEN_WIDTH - fabSize - margin;
y = SCREEN_HEIGHT - fabSize - margin;
break;
case 'bottom-left':
x = margin;
y = SCREEN_HEIGHT - fabSize - margin;
break;
case 'top-right':
x = SCREEN_WIDTH - fabSize - margin;
y = margin;
break;
case 'top-left':
x = margin;
y = margin;
break;
default:
x = SCREEN_WIDTH - fabSize - margin;
y = SCREEN_HEIGHT - fabSize - margin;
}
setInitialPosition({ x, y });
pan.setValue({ x, y });
}, [position, size, draggable]);
// FUNCTIONALITY: PanResponder cho drag functionality
const panResponder = React.useRef(
PanResponder.create({
// DRAG: Cho phép bắt đầu drag
onStartShouldSetPanResponder: () => draggable && !disabled,
onMoveShouldSetPanResponder: () => draggable && !disabled,
// DRAG: Xử lý khi bắt đầu drag
onPanResponderGrant: () => {
setIsDragging(true);
// ANIMATION: Scale effect khi bắt đầu drag
Animated.spring(scaleValue, {
toValue: 1.1,
useNativeDriver: true,
}).start();
},
// DRAG: Xử lý khi đang drag
onPanResponderMove: Animated.event(
[null, { dx: pan.x, dy: pan.y }],
{ useNativeDriver: false }
),
// DRAG: Xử lý khi kết thúc drag
onPanResponderRelease: (evt, gestureState) => {
setIsDragging(false);
// ANIMATION: Trở về scale bình thường
Animated.spring(scaleValue, {
toValue: 1,
useNativeDriver: true,
}).start();
const fabSize = getFABSize(size);
const margin = 16;
// BOUNDARY: Tính toán vị trí cuối cùng trong boundaries
let finalX = Math.max(margin, Math.min(SCREEN_WIDTH - fabSize - margin, pan.x._value));
let finalY = Math.max(margin, Math.min(SCREEN_HEIGHT - fabSize - margin, pan.y._value));
// FEATURE: Snap to edges nếu enabled
if (snapToEdges) {
const centerX = SCREEN_WIDTH / 2;
if (finalX < centerX) {
finalX = margin; // Snap to left edge
} else {
finalX = SCREEN_WIDTH - fabSize - margin; // Snap to right edge
}
}
// ANIMATION: Animate tới vị trí cuối cùng
Animated.spring(pan, {
toValue: { x: finalX, y: finalY },
useNativeDriver: false,
tension: 100,
friction: 8,
}).start();
},
})
).current;
// FUNCTIONALITY: Xử lý press in effect (chỉ khi không drag) // ANIMATION: Parallel animations cho smooth effect
const handlePressIn = () => { Animated.parallel([
if (!isDragging && !disabled) { // Fade overlay
Animated.spring(scaleValue, { Animated.timing(fadeAnim, {
toValue: 0.95, toValue,
duration: animationDuration,
useNativeDriver: true, useNativeDriver: true,
}).start(); }),
} // Rotate main FAB
Animated.timing(rotateAnim, {
toValue,
duration: animationDuration,
useNativeDriver: true,
}),
]).start();
}; };
// FUNCTIONALITY: Xử lý press out effect // FUNCTIONALITY: Xử lý press action item
const handlePressOut = () => { const handleActionPress = (action) => {
if (!isDragging && !disabled) { // Close FAB group trước
Animated.spring(scaleValue, { toggleFAB();
toValue: 1,
friction: 3, // Delay để animation hoàn thành rồi mới trigger action
tension: 40, setTimeout(() => {
useNativeDriver: true, if (action.onPress) {
}).start(); action.onPress();
} }
}, animationDuration / 2);
};
// FUNCTIONALITY: Press effects cho main FAB
const handleMainPressIn = () => {
Animated.spring(scaleAnim, {
toValue: 0.95,
useNativeDriver: true,
}).start();
}; };
// FUNCTIONALITY: Xử lý press - chỉ trigger khi không drag const handleMainPressOut = () => {
const handlePress = () => { Animated.spring(scaleAnim, {
if (!isDragging && !disabled && onPress) { toValue: 1,
onPress(); friction: 3,
} tension: 40,
useNativeDriver: true,
}).start();
}; };
// UI/UX: Tính toán styles // UI/UX: Tính toán styles
const fabSize = getFABSize(size); const fabSize = getFABSize(size);
const positionStyle = draggable ? {} : getPositionStyle(position); // Chỉ dùng position khi không draggable const positionStyle = getPositionStyle(position);
const backgroundColorStyle = disabled ? '#CCCCCC' : backgroundColor;
// ANIMATION: Rotation cho main icon
const rotateInterpolate = rotateAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '45deg'], // Xoay 45 độ khi mở
});
// RENDER: Draggable version return (
if (draggable) { <>
return ( {/* OVERLAY: Background overlay khi mở */}
{isOpen && (
<Animated.View
style={[
styles.overlay,
{
opacity: fadeAnim,
},
]}
>
<Pressable
style={styles.overlayPressable}
onPress={toggleFAB}
/>
</Animated.View>
)}
{/* ACTIONS: Render action buttons */}
{isOpen && actions.map((action, index) => {
// ANIMATION: Stagger animation cho từng action
const actionScale = fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
});
const actionTranslateY = fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, -(fabSize + spacing) * (index + 1)],
});
return (
<Animated.View
key={action.id || index}
style={[
styles.actionContainer,
positionStyle,
{
transform: [
{ scale: actionScale },
{ translateY: actionTranslateY },
],
marginBottom: 30, // Offset để không đè lên main FAB
},
]}
>
{/* ACTION: Label text */}
<View style={styles.actionLabelContainer}>
<View style={styles.actionLabel}>
<Text style={styles.actionLabelText}>
{action.label}
</Text>
</View>
</View>
{/* ACTION: Button */}
<Pressable
style={[
styles.actionButton,
{
width: fabSize * 0.8, // Nhỏ hơn main FAB một chút
height: fabSize * 0.8,
borderRadius: (fabSize * 0.8) / 2,
backgroundColor: action.backgroundColor || '#FFFFFF',
},
]}
onPress={() => handleActionPress(action)}
>
<Text style={[
styles.actionIcon,
{
color: action.color || '#333333',
fontSize: getFontSize(size) * 0.8,
}
]}>
{action.icon}
</Text>
</Pressable>
</Animated.View>
);
})}
{/* MAIN FAB: Button chính */}
<Animated.View <Animated.View
style={[ style={[
styles.container, styles.mainFABContainer,
positionStyle,
{ {
transform: [ transform: [
{ translateX: pan.x }, { scale: scaleAnim },
{ translateY: pan.y }, { rotate: rotateInterpolate },
{ scale: scaleValue }
], ],
}, },
style, style,
]} ]}
{...panResponder.panHandlers}
> >
<Pressable <Pressable
onPress={handlePress} onPress={toggleFAB}
onPressIn={handlePressIn} onPressIn={handleMainPressIn}
onPressOut={handlePressOut} onPressOut={handleMainPressOut}
style={[ style={[
styles.fab, styles.mainFAB,
{ {
width: fabSize, width: fabSize,
height: fabSize, height: fabSize,
borderRadius: fabSize / 2, borderRadius: fabSize / 2,
backgroundColor: backgroundColorStyle, backgroundColor: mainBackgroundColor,
elevation: disabled ? 2 : elevation,
shadowOpacity: disabled ? 0.2 : 0.3,
}, },
]} ]}
disabled={disabled}
> >
<Text style={[styles.text, { color, fontSize: getFontSize(size) }]}> <Text style={[
{label || icon} styles.mainIcon,
{
color: mainColor,
fontSize: getFontSize(size),
}
]}>
{mainIcon}
</Text> </Text>
</Pressable> </Pressable>
</Animated.View> </Animated.View>
); </>
}
// RENDER: Static version (original FAB)
return (
<Animated.View
style={[
styles.container,
positionStyle,
{
transform: [{ scale: scaleValue }],
},
style,
]}
>
<Pressable
onPress={disabled ? undefined : onPress}
onPressIn={disabled ? undefined : handlePressIn}
onPressOut={disabled ? undefined : handlePressOut}
style={[
styles.fab,
{
width: fabSize,
height: fabSize,
borderRadius: fabSize / 2,
backgroundColor: backgroundColorStyle,
elevation: disabled ? 2 : elevation,
shadowOpacity: disabled ? 0.2 : 0.3,
},
]}
disabled={disabled}
>
<Text style={[styles.text, { color, fontSize: getFontSize(size) }]}>
{label || icon}
</Text>
</Pressable>
</Animated.View>
); );
}; };
...@@ -272,11 +247,11 @@ const getFontSize = (size) => { ...@@ -272,11 +247,11 @@ const getFontSize = (size) => {
} }
}; };
// FUNCTIONALITY: Lấy style vị trí theo position prop (chỉ dùng khi không draggable) // FUNCTIONALITY: Lấy style vị trí theo position prop
const getPositionStyle = (position) => { const getPositionStyle = (position) => {
const baseStyle = { const baseStyle = {
position: 'absolute', position: 'absolute',
margin: 16, margin: 15,
}; };
switch (position) { switch (position) {
...@@ -295,34 +270,92 @@ const getPositionStyle = (position) => { ...@@ -295,34 +270,92 @@ const getPositionStyle = (position) => {
// STYLES: Định nghĩa styles cho component // STYLES: Định nghĩa styles cho component
const styles = StyleSheet.create({ const styles = StyleSheet.create({
// UI/UX: Container chính của FAB // OVERLAY: Background overlay
container: { overlay: {
position: 'absolute', position: 'absolute',
zIndex: 1000, // Đảm bảo FAB luôn hiển thị trên cùng top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 999,
}, },
// UI/UX: Style cho button FAB overlayPressable: {
fab: { flex: 1,
},
// MAIN FAB: Container và button chính
mainFABContainer: {
zIndex: 1001,
},
mainFAB: {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
elevation: 8,
// PERFORMANCE: Shadow cho iOS
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 4, height: 4,
}, },
shadowOpacity: 0.3,
shadowRadius: 8, shadowRadius: 8,
// UI/UX: Active state styling
activeOpacity: 0.8,
}, },
// UI/UX: Style cho text/icon mainIcon: {
text: { fontWeight: 'bold',
textAlign: 'center',
},
// ACTION: Container và styles cho action buttons
actionContainer: {
zIndex: 1000,
flexDirection: 'row',
alignItems: 'center',
},
actionLabelContainer: {
marginRight: 12,
},
actionLabel: {
backgroundColor: '#FFFFFF',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
},
actionLabelText: {
color: '#333333',
fontSize: 14,
fontWeight: '500',
},
actionButton: {
justifyContent: 'center',
alignItems: 'center',
elevation: 4,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.2,
shadowRadius: 4,
},
actionIcon: {
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
}, },
}); });
export default FAB; export default FABGroup;
\ No newline at end of file \ No newline at end of file
...@@ -4,7 +4,8 @@ import R from '../../../assets/R'; ...@@ -4,7 +4,8 @@ import R from '../../../assets/R';
import styles from './style'; import styles from './style';
import Header from '../../../components/Header/Header'; import Header from '../../../components/Header/Header';
import TextMulti from '../../../components/Input/TextMulti'; import TextMulti from '../../../components/Input/TextMulti';
import FAB from '../../../components/fabButton'; import FAB from '../../../components/FabButton';
import FABGroup from '../../../components/FabButton';
const DetailIncomingDocumentView = props => { const DetailIncomingDocumentView = props => {
const {icomingDocument} = props; const {icomingDocument} = props;
console.log(props); console.log(props);
...@@ -144,7 +145,26 @@ const DetailIncomingDocumentView = props => { ...@@ -144,7 +145,26 @@ const DetailIncomingDocumentView = props => {
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item, index) => index.toString()} keyExtractor={(item, index) => index.toString()}
/> />
<FABGroup
mainIcon="+"
position="bottom-right"
actions={[
{
id: 'add-pen',
icon: '✏️',
label: 'Thêm bút phê',
backgroundColor: R.colors.orange,
onPress: () => console.log('Thêm bút phê'),
},
{
id: 'create-task',
icon: '📝',
label: 'Tạo công việc',
backgroundColor: R.colors.blue,
onPress: () => console.log('Tạo công việc'),
},
]}
/>
</View> </View>
</ScrollView> </ScrollView>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment