Commit faec694d by tungnq

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

parent 379de4f5
...@@ -8,6 +8,7 @@ const images = { ...@@ -8,6 +8,7 @@ const images = {
bg_cannot_connect: require('./images/bg_cannot_connect.png'), bg_cannot_connect: require('./images/bg_cannot_connect.png'),
icGallery: require('./images/icGallery.png'), icGallery: require('./images/icGallery.png'),
icNoData: require('./icon/icon_png/icon_no_data.png'), icNoData: require('./icon/icon_png/icon_no_data.png'),
icMenuEdit: require('./icon/icon_png/menuEdit.png'),
//HomeScreen //HomeScreen
icLichDay:require('./icon/lich_day.png'), icLichDay:require('./icon/lich_day.png'),
icBaoBu: require('./icon/bao_bu.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_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_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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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_BORDER_RADIUS = SUBBTN_WIDTH / 2;
export const SUBBTN_BACKGROUND_COLOR = '#1B746B';
export const SUBBTN_TAP_EVENT = 'SUBBTN_TAP_EVENT'; // Màu nền mặc định của nút con
\ No newline at end of file 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';
import React, {useState, useEffect} from 'react'; import React, {useState, useEffect} from 'react';
import { import {
StyleSheet, StyleSheet,
useWindowDimensions, useWindowDimensions,
...@@ -38,23 +38,20 @@ import Animated, { ...@@ -38,23 +38,20 @@ import Animated, {
} from 'react-native-reanimated'; } from 'react-native-reanimated';
const FAB = props => { const FAB = props => {
// State: quản lý trạng thái mở/đóng của FAB
const [opened, setOpened] = useState(false); 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(); const { width } = useWindowDimensions();
/** // Nhận children (SubButton) truyền vào component
* Destructure the children prop for the SubButton(s)
*/
const { children } = props; const { children } = props;
/** /**
* (X,Y) position of the FAB. We use these * Các biến dùng chung để animate:
* for keeping track of the button when dragging it. * - fabPositionX, fabPositionY: vị trí FAB khi kéo (drag)
* * - fabRotation: góc xoay của FAB (+ thành x)
* We also rotate the button to change the + to a x * - fabPlusTranslateY: chỉnh lại vị trí của dấu "+"
* 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 fabPositionX = useSharedValue(0);
const fabPositionY = useSharedValue(0); const fabPositionY = useSharedValue(0);
...@@ -62,18 +59,21 @@ const FAB = props => { ...@@ -62,18 +59,21 @@ const FAB = props => {
const fabPlusTranslateY = useSharedValue(FAB_PLUS_TRANSLATE_Y_CLOSE); const fabPlusTranslateY = useSharedValue(FAB_PLUS_TRANSLATE_Y_CLOSE);
/** /**
* The opacity and Y position of the children container for the * Quản lý animation cho view chứa SubButton(s)
* SubButton(s). We use this to show a sliding fade in/out animation when * - childrenYPosition: vị trí Y khi show/hide
* the user taps the FAB button * - childrenOpacity: độ mờ khi show/hide
*/ */
const childrenYPosition = useSharedValue(FAB_CHILDREN_POSITION_Y_CLOSE); const childrenYPosition = useSharedValue(FAB_CHILDREN_POSITION_Y_CLOSE);
const childrenOpacity = useSharedValue(FAB_CHILDREN_OPACITY_CLOSE); const childrenOpacity = useSharedValue(FAB_CHILDREN_OPACITY_CLOSE);
// Khi tap vào FAB thì toggle mở/đóng
const _onTapHandlerStateChange = ({nativeEvent}) => { const _onTapHandlerStateChange = ({nativeEvent}) => {
if (nativeEvent.state === State.END) { if (nativeEvent.state === State.END) {
opened ? _close() : _open(); opened ? _close() : _open();
} }
}; };
// Hàm mở FAB -> show sub button với animation
function _open() { function _open() {
setOpened(true); setOpened(true);
childrenOpacity.value = withTiming(FAB_CHILDREN_OPACITY_OPEN, { childrenOpacity.value = withTiming(FAB_CHILDREN_OPACITY_OPEN, {
...@@ -86,14 +86,7 @@ const FAB = props => { ...@@ -86,14 +86,7 @@ const FAB = props => {
fabPlusTranslateY.value = withSpring(FAB_PLUS_TRANSLATE_Y_OPEN); fabPlusTranslateY.value = withSpring(FAB_PLUS_TRANSLATE_Y_OPEN);
} }
/** // Hàm đóng FAB -> ẩn sub button với animation
* 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() { function _close() {
childrenOpacity.value = withTiming(FAB_CHILDREN_OPACITY_CLOSE, { childrenOpacity.value = withTiming(FAB_CHILDREN_OPACITY_CLOSE, {
duration: 300, duration: 300,
...@@ -103,17 +96,16 @@ const FAB = props => { ...@@ -103,17 +96,16 @@ const FAB = props => {
}); });
fabRotation.value = withSpring(FAB_ROTATION_CLOSE); fabRotation.value = withSpring(FAB_ROTATION_CLOSE);
fabPlusTranslateY.value = withSpring(FAB_PLUS_TRANSLATE_Y_CLOSE); fabPlusTranslateY.value = withSpring(FAB_PLUS_TRANSLATE_Y_CLOSE);
// delay để chờ animation chạy xong rồi mới setOpened(false)
setTimeout(() => { setTimeout(() => {
setOpened(false); setOpened(false);
}, 300); }, 300);
} }
/** /**
* A useEffect (componentDidMount) that adds an * useEffect: đăng ký listener SUBBTN_TAP_EVENT
* event listener to the FAB so when we tap any SubButton we close * Khi nhấn SubButton -> tự động đóng FAB
* the SubButton container. * cleanup: gỡ listener khi unmount
*
* The return statement (componentWillUnmount) removes the listener.
*/ */
useEffect(() => { useEffect(() => {
let listener = DeviceEventEmitter.addListener(SUBBTN_TAP_EVENT, () => { let listener = DeviceEventEmitter.addListener(SUBBTN_TAP_EVENT, () => {
...@@ -122,6 +114,12 @@ const FAB = props => { ...@@ -122,6 +114,12 @@ const FAB = props => {
return () => listener.remove(); 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({ const _onPanHandlerStateChange = useAnimatedGestureHandler({
onStart: (_, ctx) => { onStart: (_, ctx) => {
ctx.startX = fabPositionX.value; ctx.startX = fabPositionX.value;
...@@ -142,6 +140,7 @@ const FAB = props => { ...@@ -142,6 +140,7 @@ const FAB = props => {
}, },
}); });
// Style động cho FAB khi drag
const animatedRootStyles = useAnimatedStyle(() => { const animatedRootStyles = useAnimatedStyle(() => {
return { return {
transform: [ transform: [
...@@ -151,11 +150,7 @@ const FAB = props => { ...@@ -151,11 +150,7 @@ const FAB = props => {
}; };
}); });
/** // Style động cho children (ẩn/hiện + trượt lên/xuống)
* 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(() => { const animatedChildrenStyles = useAnimatedStyle(() => {
return { return {
opacity: childrenOpacity.value, opacity: childrenOpacity.value,
...@@ -163,23 +158,14 @@ const FAB = props => { ...@@ -163,23 +158,14 @@ const FAB = props => {
}; };
}); });
/** // Style động cho FAB (xoay dấu +)
* 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(() => { const animatedFABStyles = useAnimatedStyle(() => {
return { return {
transform: [{ rotate: `${fabRotation.value}deg` }], transform: [{ rotate: `${fabRotation.value}deg` }],
}; };
}); });
/** // Style động cho dấu "+" (chỉnh vị trí Y)
* 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(() => { const animatedPlusText = useAnimatedStyle(() => {
return { return {
transform: [{ translateY: fabPlusTranslateY.value }], transform: [{ translateY: fabPlusTranslateY.value }],
...@@ -188,34 +174,40 @@ const FAB = props => { ...@@ -188,34 +174,40 @@ const FAB = props => {
return ( return (
<PanGestureHandler onHandlerStateChange={_onPanHandlerStateChange}> <PanGestureHandler onHandlerStateChange={_onPanHandlerStateChange}>
<Animated.View style={[styles.rootStyles, animatedRootStyles]}> <Animated.View style={[styles.rootStyles, animatedRootStyles]}>
{opened && ( {/* Hiện sub button khi opened = true */}
<Animated.View {opened && (
style={[styles.childrenStyles, animatedChildrenStyles]}> <Animated.View
{children} style={[styles.childrenStyles, animatedChildrenStyles]}>
</Animated.View> {children}
)} </Animated.View>
<TapGestureHandler onHandlerStateChange={_onTapHandlerStateChange}> )}
<Animated.View style={[styles.fabButtonStyles, animatedFABStyles]}>
<Animated.Text style={[styles.plus, animatedPlusText]}> {/* FAB chính (nút tròn màu xanh + dấu cộng) */}
+ <TapGestureHandler onHandlerStateChange={_onTapHandlerStateChange}>
</Animated.Text> <Animated.View style={[styles.fabButtonStyles, animatedFABStyles]}>
</Animated.View> <Animated.Text style={[styles.plus, animatedPlusText]}>
</TapGestureHandler> +
</Animated.View> </Animated.Text>
</PanGestureHandler> </Animated.View>
</TapGestureHandler>
</Animated.View>
</PanGestureHandler>
); );
}; };
export default FAB; export default FAB;
// Style cho các phần tử trong FAB
const styles = StyleSheet.create({ const styles = StyleSheet.create({
// Vị trí gốc của FAB (bottom-right)
rootStyles: { rootStyles: {
borderRadius: FAB_BORDER_RADIUS, borderRadius: FAB_BORDER_RADIUS,
position: 'absolute', position: 'absolute',
bottom: FAB_MARGIN, bottom: FAB_MARGIN,
right: FAB_MARGIN, right: FAB_MARGIN,
}, },
// Style cho nút FAB chính
fabButtonStyles: { fabButtonStyles: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
...@@ -224,13 +216,15 @@ const styles = StyleSheet.create({ ...@@ -224,13 +216,15 @@ const styles = StyleSheet.create({
height: FAB_HEIGHT, height: FAB_HEIGHT,
borderRadius: FAB_BORDER_RADIUS, borderRadius: FAB_BORDER_RADIUS,
}, },
// Style cho container của SubButton
childrenStyles: { childrenStyles: {
width: FAB_WIDTH, width: FAB_WIDTH,
alignItems: 'center', alignItems: 'center',
marginBottom: 20, marginBottom: 20,
}, },
// Style cho dấu "+"
plus: { plus: {
fontSize: 36, fontSize: 20,
color: '#EFFBFA', color: '#EFFBFA',
}, },
}); });
import React from 'react'; 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 { TapGestureHandler , State} from 'react-native-gesture-handler';
import Animated , { import Animated , {
...@@ -15,29 +15,41 @@ import { ...@@ -15,29 +15,41 @@ import {
SUBBTN_WIDTH, SUBBTN_WIDTH,
SUBBTN_TAP_EVENT, SUBBTN_TAP_EVENT,
} from './constants'; } from './constants';
import R from '../../assets/R';
// Component SubButton (nút con của FAB)
const SubButton = props => { 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); const buttonOpacity = useSharedValue(1);
// Style động: cập nhật opacity theo buttonOpacity
const animatedStyles = useAnimatedStyle(() => { const animatedStyles = useAnimatedStyle(() => {
return { return {
opacity: buttonOpacity.value, opacity: buttonOpacity.value,
}; };
}); });
// Hàm xử lý sự kiện khi người dùng tap SubButton
function _onTapHandlerStateChange({ nativeEvent }) { function _onTapHandlerStateChange({ nativeEvent }) {
switch (nativeEvent.state) { switch (nativeEvent.state) {
case State.BEGAN: { case State.BEGAN: {
// Khi bắt đầu nhấn -> giảm opacity để tạo hiệu ứng feedback
buttonOpacity.value = 0.5; buttonOpacity.value = 0.5;
break; break;
} }
case State.END: { case State.END: {
// Khi nhấn xong -> phát sự kiện đóng FAB
DeviceEventEmitter.emit(SUBBTN_TAP_EVENT); DeviceEventEmitter.emit(SUBBTN_TAP_EVENT);
// Trả opacity về lại bình thường
buttonOpacity.value = 1.0; buttonOpacity.value = 1.0;
// Gọi hàm onPress được truyền từ props
onPress && onPress(); onPress && onPress();
break; break;
} }
case State.CANCELLED: { case State.CANCELLED: {
// Nếu hủy tap -> trả opacity về 1
buttonOpacity.value = 1.0; buttonOpacity.value = 1.0;
break; break;
} }
...@@ -50,32 +62,36 @@ const SubButton = props => { ...@@ -50,32 +62,36 @@ const SubButton = props => {
break; break;
} }
} }
} }
return ( return (
// Bọc SubButton trong TapGestureHandler để xử lý cử chỉ chạm
<TapGestureHandler onHandlerStateChange={_onTapHandlerStateChange}> <TapGestureHandler onHandlerStateChange={_onTapHandlerStateChange}>
<Animated.View style={[styles.subButton, animatedStyles]}> <Animated.View style={[styles.subButton,{backgroundColor:backgroundColor} ,animatedStyles]}>
<Text style={styles.label}>{label}</Text> <Text style={styles.label}>{label}</Text>
</Animated.View> <Image source={images} style={{width: 20, height: 20}} resizeMode="contain" tintColor={R.colors.white}/>
</TapGestureHandler> </Animated.View>
</TapGestureHandler>
); );
}; };
export default SubButton; export default SubButton;
// Style cho SubButton
const styles = StyleSheet.create({ const styles = StyleSheet.create({
subButton: { subButton: {
width: SUBBTN_WIDTH, width: 150, // Chiều rộng nút con
height: SUBBTN_HEIGHT, height: 35, // Chiều cao nút con
borderRadius: SUBBTN_BORDER_RADIUS, borderRadius: SUBBTN_BORDER_RADIUS, // Bo tròn (tròn hoàn hảo)
backgroundColor: SUBBTN_BACKGROUND_COLOR, 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', alignItems: 'center',
justifyContent: 'center',
marginTop: 10,
}, },
label: { label: {
color: '#EFFBFA', marginRight:3,
fontSize: 24, color: '#FFFFFF', // Màu chữ (trắng xanh nhạt)
fontSize: 12, // Kích thước chữ
}, },
}); });
\ No newline at end of file
...@@ -145,11 +145,9 @@ const DetailIncomingDocumentView = props => { ...@@ -145,11 +145,9 @@ const DetailIncomingDocumentView = props => {
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item, index) => index.toString()} keyExtractor={(item, index) => index.toString()}
/> />
<FAB> <FAB>
<SubButton onPress={() => Alert.alert('Pressed 1!')} label="1" /> <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="2" /> <SubButton onPress={() => Alert.alert('Pressed 2!')} label="Tạo công việc" images={R.images.icMenuEdit} backgroundColor={R.colors.blue}/>
<SubButton onPress={() => Alert.alert('Pressed 3!')} label="3" />
<SubButton onPress={() => Alert.alert('Pressed 4!')} label="4" />
</FAB> </FAB>
</View> </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