Commit 379de4f5 by tungnq

TODO: Đã sửa giao diên fab có thể di chuyển được

parent cf1f778b
export const FAB_STARTING_POSITION = 0;
export const FAB_WIDTH = 60;
export const FAB_HEIGHT = FAB_WIDTH;
export const FAB_BORDER_RADIUS = FAB_WIDTH/2;
export const FAB_BACKGROUND_COLOR = '#2EC4B6';
export const FAB_MARGIN = 30;
export const FAB_ROTATION_OPEN = 225;
export const FAB_CHILDREN_OPACITY_OPEN = 1;
export const FAB_CHILDREN_POSITION_Y_OPEN = 0;
export const FAB_PLUS_TRANSLATE_Y_OPEN = -3;
export const FAB_CHILDREN_OPACITY_CLOSE = 0;
export const FAB_CHILDREN_POSITION_Y_CLOSE = 30;
export const FAB_ROTATION_CLOSE = 0;
export const FAB_PLUS_TRANSLATE_Y_CLOSE = -2;
export const SUBBTN_WIDTH = FAB_WIDTH;
export const SUBBTN_HEIGHT = SUBBTN_WIDTH;
export const SUBBTN_BORDER_RADIUS = SUBBTN_WIDTH / 2;
export const SUBBTN_BACKGROUND_COLOR = '#1B746B';
export const SUBBTN_TAP_EVENT = 'SUBBTN_TAP_EVENT';
\ No newline at end of file
import React, {useState, useEffect} from 'react';
import {
StyleSheet,
useWindowDimensions,
DeviceEventEmitter,
} from 'react-native';
import {
FAB_BACKGROUND_COLOR,
FAB_BORDER_RADIUS,
FAB_HEIGHT,
FAB_WIDTH,
FAB_MARGIN,
FAB_CHILDREN_OPACITY_OPEN,
FAB_CHILDREN_POSITION_Y_OPEN,
FAB_CHILDREN_OPACITY_CLOSE,
FAB_CHILDREN_POSITION_Y_CLOSE,
FAB_ROTATION_CLOSE,
FAB_ROTATION_OPEN,
FAB_STARTING_POSITION,
FAB_PLUS_TRANSLATE_Y_OPEN,
FAB_PLUS_TRANSLATE_Y_CLOSE,
SUBBTN_TAP_EVENT,
} from './constants';
import {
PanGestureHandler,
State,
TapGestureHandler,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from 'react-native-reanimated';
const FAB = props => {
const [opened, setOpened] = useState(false);
const { width } = useWindowDimensions();
/**
* Destructure the children prop for the SubButton(s)
*/
const { children } = props;
/**
* (X,Y) position of the FAB. We use these
* for keeping track of the button when dragging it.
*
* We also rotate the button to change the + to a x
* when the children view is visible. The plus text is
* also offset to accomodate for the anchor point of the
* rotation not being in the center of the +
*/
const fabPositionX = useSharedValue(0);
const fabPositionY = useSharedValue(0);
const fabRotation = useSharedValue(FAB_ROTATION_CLOSE);
const fabPlusTranslateY = useSharedValue(FAB_PLUS_TRANSLATE_Y_CLOSE);
/**
* The opacity and Y position of the children container for the
* SubButton(s). We use this to show a sliding fade in/out animation when
* the user taps the FAB button
*/
const childrenYPosition = useSharedValue(FAB_CHILDREN_POSITION_Y_CLOSE);
const childrenOpacity = useSharedValue(FAB_CHILDREN_OPACITY_CLOSE);
const _onTapHandlerStateChange = ({nativeEvent}) => {
if (nativeEvent.state === State.END) {
opened ? _close() : _open();
}
};
function _open() {
setOpened(true);
childrenOpacity.value = withTiming(FAB_CHILDREN_OPACITY_OPEN, {
duration: 300,
});
childrenYPosition.value = withTiming(FAB_CHILDREN_POSITION_Y_OPEN, {
duration: 200,
});
fabRotation.value = withSpring(FAB_ROTATION_OPEN);
fabPlusTranslateY.value = withSpring(FAB_PLUS_TRANSLATE_Y_OPEN);
}
/**
* Method called when we want to hide the SubButton(s)
*
* This is essentially the same function as _open(), but in reverse.
* However, we need to delay setting the opened state to false to let
* the closing animation play out because as soon as we set that state
* to false, the component will unmount.
*/
function _close() {
childrenOpacity.value = withTiming(FAB_CHILDREN_OPACITY_CLOSE, {
duration: 300,
});
childrenYPosition.value = withTiming(FAB_CHILDREN_POSITION_Y_CLOSE, {
duration: 300,
});
fabRotation.value = withSpring(FAB_ROTATION_CLOSE);
fabPlusTranslateY.value = withSpring(FAB_PLUS_TRANSLATE_Y_CLOSE);
setTimeout(() => {
setOpened(false);
}, 300);
}
/**
* A useEffect (componentDidMount) that adds an
* event listener to the FAB so when we tap any SubButton we close
* the SubButton container.
*
* The return statement (componentWillUnmount) removes the listener.
*/
useEffect(() => {
let listener = DeviceEventEmitter.addListener(SUBBTN_TAP_EVENT, () => {
_close();
});
return () => listener.remove();
}, []);
const _onPanHandlerStateChange = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = fabPositionX.value;
ctx.startY = fabPositionY.value;
},
onActive: (event, ctx) => {
fabPositionX.value = ctx.startX + event.translationX;
fabPositionY.value = ctx.startY + event.translationY;
},
onEnd: _ => {
if (fabPositionX.value > -width / 2) {
fabPositionX.value = withSpring(FAB_STARTING_POSITION);
fabPositionY.value = withSpring(FAB_STARTING_POSITION);
} else {
fabPositionX.value = withSpring(-width + FAB_WIDTH + FAB_MARGIN * 2);
fabPositionY.value = withSpring(FAB_STARTING_POSITION);
}
},
});
const animatedRootStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateX: fabPositionX.value },
{ translateY: fabPositionY.value },
],
};
});
/**
* The animated styles hook that is used in the
* style prop for the children view. The opacity of the children
* and the y-position update depending on the shared values used.
*/
const animatedChildrenStyles = useAnimatedStyle(() => {
return {
opacity: childrenOpacity.value,
transform: [{ translateY: childrenYPosition.value }],
};
});
/**
* The animated styles hook that is used in the
* style prop for the FAB. It updates the rotation value
* when the fabRotation shared value is changed.
*/
const animatedFABStyles = useAnimatedStyle(() => {
return {
transform: [{ rotate: `${fabRotation.value}deg` }],
};
});
/**
* The animated styles hook that is used in the
* style prop for the plus text in the FAB. It update
* the y-position for the text when the fabPlusTranslatey shared value
* is changed.
*/
const animatedPlusText = useAnimatedStyle(() => {
return {
transform: [{ translateY: fabPlusTranslateY.value }],
};
});
return (
<PanGestureHandler onHandlerStateChange={_onPanHandlerStateChange}>
<Animated.View style={[styles.rootStyles, animatedRootStyles]}>
{opened && (
<Animated.View
style={[styles.childrenStyles, animatedChildrenStyles]}>
{children}
</Animated.View>
)}
<TapGestureHandler onHandlerStateChange={_onTapHandlerStateChange}>
<Animated.View style={[styles.fabButtonStyles, animatedFABStyles]}>
<Animated.Text style={[styles.plus, animatedPlusText]}>
+
</Animated.Text>
</Animated.View>
</TapGestureHandler>
</Animated.View>
</PanGestureHandler>
);
};
export default FAB;
const styles = StyleSheet.create({
rootStyles: {
borderRadius: FAB_BORDER_RADIUS,
position: 'absolute',
bottom: FAB_MARGIN,
right: FAB_MARGIN,
},
fabButtonStyles: {
alignItems: 'center',
justifyContent: 'center',
backgroundColor: FAB_BACKGROUND_COLOR,
width: FAB_WIDTH,
height: FAB_HEIGHT,
borderRadius: FAB_BORDER_RADIUS,
},
childrenStyles: {
width: FAB_WIDTH,
alignItems: 'center',
marginBottom: 20,
},
plus: {
fontSize: 36,
color: '#EFFBFA',
},
});
import React from 'react';
import { DeviceEventEmitter, StyleSheet, Text } from 'react-native';
import { TapGestureHandler , State} from 'react-native-gesture-handler';
import Animated , {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
import {
SUBBTN_BACKGROUND_COLOR,
SUBBTN_BORDER_RADIUS,
SUBBTN_HEIGHT,
SUBBTN_WIDTH,
SUBBTN_TAP_EVENT,
} from './constants';
const SubButton = props => {
const { label, onPress } = props;
const buttonOpacity = useSharedValue(1);
const animatedStyles = useAnimatedStyle(() => {
return {
opacity: buttonOpacity.value,
};
});
function _onTapHandlerStateChange({ nativeEvent }) {
switch (nativeEvent.state) {
case State.BEGAN: {
buttonOpacity.value = 0.5;
break;
}
case State.END: {
DeviceEventEmitter.emit(SUBBTN_TAP_EVENT);
buttonOpacity.value = 1.0;
onPress && onPress();
break;
}
case State.CANCELLED: {
buttonOpacity.value = 1.0;
break;
}
case State.FAILED: {
buttonOpacity.value = 1.0;
break;
}
case State.UNDETERMINED: {
buttonOpacity.value = 1.0;
break;
}
}
}
return (
<TapGestureHandler onHandlerStateChange={_onTapHandlerStateChange}>
<Animated.View style={[styles.subButton, animatedStyles]}>
<Text style={styles.label}>{label}</Text>
</Animated.View>
</TapGestureHandler>
);
};
export default SubButton;
const styles = StyleSheet.create({
subButton: {
width: SUBBTN_WIDTH,
height: SUBBTN_HEIGHT,
borderRadius: SUBBTN_BORDER_RADIUS,
backgroundColor: SUBBTN_BACKGROUND_COLOR,
alignItems: 'center',
justifyContent: 'center',
marginTop: 10,
},
label: {
color: '#EFFBFA',
fontSize: 24,
},
});
\ No newline at end of file
import React, { useState, useRef } from 'react';
import {
View,
Text,
StyleSheet,
Animated,
Pressable,
Dimensions,
} from 'react-native';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
// COMPONENT: FAB Group với menu mở rộng
const FABGroup = ({
position = 'bottom-right',
mainIcon = '+',
mainBackgroundColor = '#007AFF',
mainColor = '#FFFFFF',
size = 'medium',
actions = [], // Mảng các action buttons
overlayColor = 'rgba(0, 0, 0, 0.3)',
animationDuration = 200,
spacing = 10,
style,
}) => {
// STATE: Quản lý trạng thái mở/đóng
const [isOpen, setIsOpen] = useState(false);
// ANIMATION: Animation values
const fadeAnim = useRef(new Animated.Value(0)).current;
const rotateAnim = useRef(new Animated.Value(0)).current;
const scaleAnim = useRef(new Animated.Value(1)).current;
// FUNCTIONALITY: Toggle FAB Group
const toggleFAB = () => {
const toValue = isOpen ? 0 : 1;
setIsOpen(!isOpen);
// ANIMATION: Parallel animations cho smooth effect
Animated.parallel([
// Fade overlay
Animated.timing(fadeAnim, {
toValue,
duration: animationDuration,
useNativeDriver: true,
}),
// Rotate main FAB
Animated.timing(rotateAnim, {
toValue,
duration: animationDuration,
useNativeDriver: true,
}),
]).start();
};
// FUNCTIONALITY: Xử lý press action item
const handleActionPress = (action) => {
// Close FAB group trước
toggleFAB();
// Delay để animation hoàn thành rồi mới trigger action
setTimeout(() => {
if (action.onPress) {
action.onPress();
}
}, animationDuration / 2);
};
// FUNCTIONALITY: Press effects cho main FAB
const handleMainPressIn = () => {
Animated.spring(scaleAnim, {
toValue: 0.95,
useNativeDriver: true,
}).start();
};
const handleMainPressOut = () => {
Animated.spring(scaleAnim, {
toValue: 1,
friction: 3,
tension: 40,
useNativeDriver: true,
}).start();
};
// UI/UX: Tính toán styles
const fabSize = getFABSize(size);
const positionStyle = getPositionStyle(position);
// ANIMATION: Rotation cho main icon
const rotateInterpolate = rotateAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '45deg'], // Xoay 45 độ khi mở
});
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
style={[
styles.mainFABContainer,
positionStyle,
{
transform: [
{ scale: scaleAnim },
{ rotate: rotateInterpolate },
],
},
style,
]}
>
<Pressable
onPress={toggleFAB}
onPressIn={handleMainPressIn}
onPressOut={handleMainPressOut}
style={[
styles.mainFAB,
{
width: fabSize,
height: fabSize,
borderRadius: fabSize / 2,
backgroundColor: mainBackgroundColor,
},
]}
>
<Text style={[
styles.mainIcon,
{
color: mainColor,
fontSize: getFontSize(size),
}
]}>
{mainIcon}
</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
const getPositionStyle = (position) => {
const baseStyle = {
position: 'absolute',
margin: 15,
};
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({
// OVERLAY: Background overlay
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 999,
},
overlayPressable: {
flex: 1,
},
// MAIN FAB: Container và button chính
mainFABContainer: {
zIndex: 1001,
},
mainFAB: {
justifyContent: 'center',
alignItems: 'center',
elevation: 8,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 8,
},
mainIcon: {
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',
textAlign: 'center',
},
});
export default FABGroup;
\ No newline at end of file
import React from 'react'; import React, {useEffect} from 'react';
import {Text, View, TouchableOpacity, StyleSheet, Image, ScrollView, FlatList} from 'react-native'; import {Text, View, TouchableOpacity, StyleSheet, Image, ScrollView, FlatList, Alert} from 'react-native';
import R from '../../../assets/R'; 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/FAB/fab';
import FABGroup from '../../../components/FabButton'; import SubButton from '../../../components/FAB/sub_button';
const DetailIncomingDocumentView = props => { const DetailIncomingDocumentView = props => {
const {icomingDocument} = props; const {icomingDocument} = props;
console.log(props); console.log(props);
...@@ -145,26 +145,12 @@ const DetailIncomingDocumentView = props => { ...@@ -145,26 +145,12 @@ const DetailIncomingDocumentView = props => {
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item, index) => index.toString()} keyExtractor={(item, index) => index.toString()}
/> />
<FABGroup <FAB>
mainIcon="+" <SubButton onPress={() => Alert.alert('Pressed 1!')} label="1" />
position="bottom-right" <SubButton onPress={() => Alert.alert('Pressed 2!')} label="2" />
actions={[ <SubButton onPress={() => Alert.alert('Pressed 3!')} label="3" />
{ <SubButton onPress={() => Alert.alert('Pressed 4!')} label="4" />
id: 'add-pen', </FAB>
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