Commit faec694d by tungnq

TODOL Điều chỉnh giao diện FAB

parent 379de4f5
......@@ -8,6 +8,7 @@ const images = {
bg_cannot_connect: require('./images/bg_cannot_connect.png'),
icGallery: require('./images/icGallery.png'),
icNoData: require('./icon/icon_png/icon_no_data.png'),
icMenuEdit: require('./icon/icon_png/menuEdit.png'),
//HomeScreen
icLichDay:require('./icon/lich_day.png'),
icBaoBu: require('./icon/bao_bu.png'),
......
// Vị trí bắt đầu của FAB (Floating Action Button)
export const FAB_STARTING_POSITION = 0;
export const FAB_WIDTH = 60;
// Kích thước chiều rộng của nút FAB
export const FAB_WIDTH = 50;
// Kích thước chiều cao của nút FAB (bằng với chiều rộng để tạo nút tròn)
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;
// Bán kính bo tròn (FAB tròn hoàn hảo)
export const FAB_BORDER_RADIUS = FAB_WIDTH / 2;
// Màu nền mặc định của FAB
export const FAB_BACKGROUND_COLOR = '#2F6BFF';
// Khoảng cách margin của FAB với viền màn hình
export const FAB_MARGIN = 10;
// -------- Trạng thái khi mở FAB --------
// Góc xoay của biểu tượng FAB khi mở (xoay 225 độ, thường là dấu "+" xoay thành "x")
export const FAB_ROTATION_OPEN = 225;
// Độ mờ của các nút con khi FAB mở (1 = hiện rõ)
export const FAB_CHILDREN_OPACITY_OPEN = 1;
// Vị trí Y của các nút con khi mở (0 = sát với FAB chính)
export const FAB_CHILDREN_POSITION_Y_OPEN = 0;
// Độ dịch chuyển Y của dấu "+" khi FAB mở (để canh giữa khi xoay)
export const FAB_PLUS_TRANSLATE_Y_OPEN = -3;
// -------- Trạng thái khi đóng FAB --------
// Độ mờ của các nút con khi FAB đóng (0 = ẩn hoàn toàn)
export const FAB_CHILDREN_OPACITY_CLOSE = 0;
// Vị trí Y của các nút con khi đóng (cách FAB chính 30px)
export const FAB_CHILDREN_POSITION_Y_CLOSE = 30;
// Góc xoay của FAB khi đóng (0 độ = trạng thái ban đầu)
export const FAB_ROTATION_CLOSE = 0;
// Độ dịch chuyển Y của dấu "+" khi FAB đóng
export const FAB_PLUS_TRANSLATE_Y_CLOSE = -2;
// -------- Cấu hình nút con (sub button) --------
// Chiều rộng nút con
export const SUBBTN_WIDTH = FAB_WIDTH;
// Chiều cao nút con (bằng chiều rộng để tạo hình tròn)
export const SUBBTN_HEIGHT = SUBBTN_WIDTH;
// Bán kính bo tròn (nút con hình tròn)
export const SUBBTN_BORDER_RADIUS = SUBBTN_WIDTH / 2;
export const SUBBTN_BACKGROUND_COLOR = '#1B746B';
// Màu nền mặc định của nút con
export const SUBBTN_BACKGROUND_COLOR = '#2F6BFF';
// Tên sự kiện khi nhấn vào nút con
export const SUBBTN_TAP_EVENT = 'SUBBTN_TAP_EVENT';
......@@ -38,23 +38,20 @@ import Animated, {
} from 'react-native-reanimated';
const FAB = props => {
// State: quản lý trạng thái mở/đóng của FAB
const [opened, setOpened] = useState(false);
// Lấy kích thước màn hình hiện tại (để xử lý drag sang trái/phải)
const { width } = useWindowDimensions();
/**
* Destructure the children prop for the SubButton(s)
*/
// Nhận children (SubButton) truyền vào component
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 +
* Các biến dùng chung để animate:
* - fabPositionX, fabPositionY: vị trí FAB khi kéo (drag)
* - fabRotation: góc xoay của FAB (+ thành x)
* - fabPlusTranslateY: chỉnh lại vị trí của dấu "+"
*/
const fabPositionX = useSharedValue(0);
const fabPositionY = useSharedValue(0);
......@@ -62,18 +59,21 @@ const FAB = props => {
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
* Quản lý animation cho view chứa SubButton(s)
* - childrenYPosition: vị trí Y khi show/hide
* - childrenOpacity: độ mờ khi show/hide
*/
const childrenYPosition = useSharedValue(FAB_CHILDREN_POSITION_Y_CLOSE);
const childrenOpacity = useSharedValue(FAB_CHILDREN_OPACITY_CLOSE);
// Khi tap vào FAB thì toggle mở/đóng
const _onTapHandlerStateChange = ({nativeEvent}) => {
if (nativeEvent.state === State.END) {
opened ? _close() : _open();
}
};
// Hàm mở FAB -> show sub button với animation
function _open() {
setOpened(true);
childrenOpacity.value = withTiming(FAB_CHILDREN_OPACITY_OPEN, {
......@@ -86,14 +86,7 @@ const FAB = props => {
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.
*/
// Hàm đóng FAB -> ẩn sub button với animation
function _close() {
childrenOpacity.value = withTiming(FAB_CHILDREN_OPACITY_CLOSE, {
duration: 300,
......@@ -103,17 +96,16 @@ const FAB = props => {
});
fabRotation.value = withSpring(FAB_ROTATION_CLOSE);
fabPlusTranslateY.value = withSpring(FAB_PLUS_TRANSLATE_Y_CLOSE);
// delay để chờ animation chạy xong rồi mới setOpened(false)
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: đăng ký listener SUBBTN_TAP_EVENT
* Khi nhấn SubButton -> tự động đóng FAB
* cleanup: gỡ listener khi unmount
*/
useEffect(() => {
let listener = DeviceEventEmitter.addListener(SUBBTN_TAP_EVENT, () => {
......@@ -122,6 +114,12 @@ const FAB = props => {
return () => listener.remove();
}, []);
/**
* Xử lý gesture kéo thả FAB
* - onStart: lưu vị trí ban đầu
* - onActive: update vị trí khi kéo
* - onEnd: snap FAB về trái/phải màn hình
*/
const _onPanHandlerStateChange = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = fabPositionX.value;
......@@ -142,6 +140,7 @@ const FAB = props => {
},
});
// Style động cho FAB khi drag
const animatedRootStyles = useAnimatedStyle(() => {
return {
transform: [
......@@ -151,11 +150,7 @@ const FAB = props => {
};
});
/**
* 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.
*/
// Style động cho children (ẩn/hiện + trượt lên/xuống)
const animatedChildrenStyles = useAnimatedStyle(() => {
return {
opacity: childrenOpacity.value,
......@@ -163,23 +158,14 @@ const FAB = props => {
};
});
/**
* 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.
*/
// Style động cho FAB (xoay dấu +)
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.
*/
// Style động cho dấu "+" (chỉnh vị trí Y)
const animatedPlusText = useAnimatedStyle(() => {
return {
transform: [{ translateY: fabPlusTranslateY.value }],
......@@ -189,12 +175,15 @@ const FAB = props => {
return (
<PanGestureHandler onHandlerStateChange={_onPanHandlerStateChange}>
<Animated.View style={[styles.rootStyles, animatedRootStyles]}>
{/* Hiện sub button khi opened = true */}
{opened && (
<Animated.View
style={[styles.childrenStyles, animatedChildrenStyles]}>
{children}
</Animated.View>
)}
{/* FAB chính (nút tròn màu xanh + dấu cộng) */}
<TapGestureHandler onHandlerStateChange={_onTapHandlerStateChange}>
<Animated.View style={[styles.fabButtonStyles, animatedFABStyles]}>
<Animated.Text style={[styles.plus, animatedPlusText]}>
......@@ -209,13 +198,16 @@ const FAB = props => {
export default FAB;
// Style cho các phần tử trong FAB
const styles = StyleSheet.create({
// Vị trí gốc của FAB (bottom-right)
rootStyles: {
borderRadius: FAB_BORDER_RADIUS,
position: 'absolute',
bottom: FAB_MARGIN,
right: FAB_MARGIN,
},
// Style cho nút FAB chính
fabButtonStyles: {
alignItems: 'center',
justifyContent: 'center',
......@@ -224,13 +216,15 @@ const styles = StyleSheet.create({
height: FAB_HEIGHT,
borderRadius: FAB_BORDER_RADIUS,
},
// Style cho container của SubButton
childrenStyles: {
width: FAB_WIDTH,
alignItems: 'center',
marginBottom: 20,
},
// Style cho dấu "+"
plus: {
fontSize: 36,
fontSize: 20,
color: '#EFFBFA',
},
});
import React from 'react';
import { DeviceEventEmitter, StyleSheet, Text } from 'react-native';
import { DeviceEventEmitter, Image, StyleSheet, Text } from 'react-native';
import { TapGestureHandler , State} from 'react-native-gesture-handler';
import Animated , {
......@@ -15,29 +15,41 @@ import {
SUBBTN_WIDTH,
SUBBTN_TAP_EVENT,
} from './constants';
import R from '../../assets/R';
// Component SubButton (nút con của FAB)
const SubButton = props => {
const { label, onPress } = props;
const { label, onPress , images, backgroundColor } = props;
// Biến sharedValue để quản lý độ mờ (opacity) khi nhấn giữ
const buttonOpacity = useSharedValue(1);
// Style động: cập nhật opacity theo buttonOpacity
const animatedStyles = useAnimatedStyle(() => {
return {
opacity: buttonOpacity.value,
};
});
// Hàm xử lý sự kiện khi người dùng tap SubButton
function _onTapHandlerStateChange({ nativeEvent }) {
switch (nativeEvent.state) {
case State.BEGAN: {
// Khi bắt đầu nhấn -> giảm opacity để tạo hiệu ứng feedback
buttonOpacity.value = 0.5;
break;
}
case State.END: {
// Khi nhấn xong -> phát sự kiện đóng FAB
DeviceEventEmitter.emit(SUBBTN_TAP_EVENT);
// Trả opacity về lại bình thường
buttonOpacity.value = 1.0;
// Gọi hàm onPress được truyền từ props
onPress && onPress();
break;
}
case State.CANCELLED: {
// Nếu hủy tap -> trả opacity về 1
buttonOpacity.value = 1.0;
break;
}
......@@ -50,13 +62,14 @@ const SubButton = props => {
break;
}
}
}
return (
// Bọc SubButton trong TapGestureHandler để xử lý cử chỉ chạm
<TapGestureHandler onHandlerStateChange={_onTapHandlerStateChange}>
<Animated.View style={[styles.subButton, animatedStyles]}>
<Animated.View style={[styles.subButton,{backgroundColor:backgroundColor} ,animatedStyles]}>
<Text style={styles.label}>{label}</Text>
<Image source={images} style={{width: 20, height: 20}} resizeMode="contain" tintColor={R.colors.white}/>
</Animated.View>
</TapGestureHandler>
);
......@@ -64,18 +77,21 @@ const SubButton = props => {
export default SubButton;
// Style cho SubButton
const styles = StyleSheet.create({
subButton: {
width: SUBBTN_WIDTH,
height: SUBBTN_HEIGHT,
borderRadius: SUBBTN_BORDER_RADIUS,
backgroundColor: SUBBTN_BACKGROUND_COLOR,
width: 150, // Chiều rộng nút con
height: 35, // Chiều cao nút con
borderRadius: SUBBTN_BORDER_RADIUS, // Bo tròn (tròn hoàn hảo)
backgroundColor: SUBBTN_BACKGROUND_COLOR, // Màu nền nút
justifyContent: 'center', // Canh giữa dọc
marginTop: 10, // Khoảng cách giữa các SubButton4
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 10,
},
label: {
color: '#EFFBFA',
fontSize: 24,
marginRight:3,
color: '#FFFFFF', // Màu chữ (trắng xanh nhạt)
fontSize: 12, // Kích thước chữ
},
});
......@@ -146,10 +146,8 @@ const DetailIncomingDocumentView = props => {
keyExtractor={(item, index) => index.toString()}
/>
<FAB>
<SubButton onPress={() => Alert.alert('Pressed 1!')} label="1" />
<SubButton onPress={() => Alert.alert('Pressed 2!')} label="2" />
<SubButton onPress={() => Alert.alert('Pressed 3!')} label="3" />
<SubButton onPress={() => Alert.alert('Pressed 4!')} label="4" />
<SubButton onPress={() => Alert.alert('Pressed 1!')} label="Thêm bút phê" images={R.images.icEdit} backgroundColor={R.colors.orange}/>
<SubButton onPress={() => Alert.alert('Pressed 2!')} label="Tạo công việc" images={R.images.icMenuEdit} backgroundColor={R.colors.blue}/>
</FAB>
</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