Commit 4d9424d8 by tungnq

TODO: Bổ sung FAB button

parent d278ae87
import React from 'react';
import {
TouchableOpacity,
Text,
StyleSheet,
Animated,
Pressable,
PanResponder,
Dimensions,
} 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, height: SCREEN_HEIGHT } = Dimensions.get('window');
// COMPONENT: FAB (Floating Action Button) có thể di chuyển
const FAB = ({
icon = '+',
label,
onPress,
position = 'bottom-right',
size = 'medium',
backgroundColor = '#007AFF',
color = '#FFFFFF',
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
const scaleValue = React.useRef(new Animated.Value(1)).current;
const pan = React.useRef(new Animated.ValueXY()).current;
// STATE: Tracking drag và press
const [isDragging, setIsDragging] = React.useState(false);
const [initialPosition, setInitialPosition] = React.useState({ x: 0, y: 0 });
// FUNCTIONALITY: Khởi tạo vị trí ban đầu từ position prop
React.useEffect(() => {
if (!draggable) return;
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)
const handlePressIn = () => {
if (!isDragging && !disabled) {
Animated.spring(scaleValue, {
toValue: 0.95,
useNativeDriver: true,
}).start();
}
};
// FUNCTIONALITY: Xử lý press out effect
const handlePressOut = () => {
if (!isDragging && !disabled) {
Animated.spring(scaleValue, {
toValue: 1,
friction: 3,
tension: 40,
useNativeDriver: true,
}).start();
}
};
// FUNCTIONALITY: Xử lý press - chỉ trigger khi không drag
const handlePress = () => {
if (!isDragging && !disabled && onPress) {
onPress();
}
};
// UI/UX: Tính toán styles
const fabSize = getFABSize(size);
const positionStyle = draggable ? {} : getPositionStyle(position); // Chỉ dùng position khi không draggable
const backgroundColorStyle = disabled ? '#CCCCCC' : backgroundColor;
// RENDER: Draggable version
if (draggable) {
return (
<Animated.View
style={[
styles.container,
{
transform: [
{ translateX: pan.x },
{ translateY: pan.y },
{ scale: scaleValue }
],
},
style,
]}
{...panResponder.panHandlers}
>
<Pressable
onPress={handlePress}
onPressIn={handlePressIn}
onPressOut={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>
);
}
// 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>
);
};
// FUNCTIONALITY: Lấy kích thước FAB theo size
const getFABSize = (size) => {
switch (size) {
case 'small':
return 40;
case 'medium':
return 56;
case 'large':
return 72;
default:
return 56;
}
};
// FUNCTIONALITY: Lấy font size theo size
const getFontSize = (size) => {
switch (size) {
case 'small':
return 16;
case 'medium':
return 20;
case 'large':
return 24;
default:
return 20;
}
};
// FUNCTIONALITY: Lấy style vị trí theo position prop (chỉ dùng khi không draggable)
const getPositionStyle = (position) => {
const baseStyle = {
position: 'absolute',
margin: 16,
};
switch (position) {
case 'bottom-right':
return { ...baseStyle, bottom: 0, right: 0 };
case 'bottom-left':
return { ...baseStyle, bottom: 0, left: 0 };
case 'top-right':
return { ...baseStyle, top: 0, right: 0 };
case 'top-left':
return { ...baseStyle, top: 0, left: 0 };
default:
return { ...baseStyle, bottom: 0, right: 0 };
}
};
// STYLES: Định nghĩa styles cho component
const styles = StyleSheet.create({
// UI/UX: Container chính của FAB
container: {
position: 'absolute',
zIndex: 1000, // Đảm bảo FAB luôn hiển thị trên cùng
},
// UI/UX: Style cho button FAB
fab: {
justifyContent: 'center',
alignItems: 'center',
// PERFORMANCE: Shadow cho iOS
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 4,
},
shadowRadius: 8,
// UI/UX: Active state styling
activeOpacity: 0.8,
},
// UI/UX: Style cho text/icon
text: {
fontWeight: 'bold',
textAlign: 'center',
},
});
export default FAB;
\ No newline at end of file
......@@ -4,6 +4,7 @@ import R from '../../../assets/R';
import styles from './style';
import Header from '../../../components/Header/Header';
import TextMulti from '../../../components/Input/TextMulti';
import FAB from '../../../components/fabButton';
const DetailIncomingDocumentView = props => {
const {icomingDocument} = props;
console.log(props);
......@@ -144,6 +145,7 @@ const DetailIncomingDocumentView = props => {
keyExtractor={(item, index) => index.toString()}
/>
</View>
</ScrollView>
</View>
......
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