Commit 9052caa2 by tungnq

IMPORTANT: FAB di chuyển được

parent faec694d
......@@ -16,6 +16,8 @@ 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;
export const LEFT_EDGE_SPACING = 110;
// -------- Trạng thái khi mở FAB --------
......
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 React from 'react';
import { StyleSheet, useWindowDimensions } from 'react-native';
import { PanGestureHandler } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} 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);
import {
FAB_BORDER_RADIUS,
FAB_WIDTH,
FAB_MARGIN,
FAB_STARTING_POSITION,
LEFT_EDGE_SPACING,
} from './constants';
// Lấy kích thước màn hình hiện tại (để xử lý drag sang trái/phải)
const FAB = (props) => {
const { width } = useWindowDimensions();
// Nhận children (SubButton) truyền vào component
const { children } = props;
/**
* 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 "+"
*/
// Vị trí drag
const fabPositionX = useSharedValue(0);
const fabPositionY = useSharedValue(0);
const fabRotation = useSharedValue(FAB_ROTATION_CLOSE);
const fabPlusTranslateY = useSharedValue(FAB_PLUS_TRANSLATE_Y_CLOSE);
/**
* 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, {
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);
}
// Hàm đóng FAB -> ẩn sub button với animation
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);
// delay để chờ animation chạy xong rồi mới setOpened(false)
setTimeout(() => {
setOpened(false);
}, 300);
}
/**
* 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, () => {
_close();
});
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
*/
// Kéo & snap trái/phải (giữ nguyên logic bạn đang dùng)
const _onPanHandlerStateChange = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = fabPositionX.value;
ctx.startY = fabPositionY.value;
ctx.startX = fabPositionX.value;
ctx.startY = fabPositionY.value;
},
onActive: (event, ctx) => {
fabPositionX.value = ctx.startX + event.translationX;
fabPositionY.value = ctx.startY + event.translationY;
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 SNAP_RIGHT = FAB_STARTING_POSITION; // 0
const SNAP_LEFT = -width + FAB_WIDTH + FAB_MARGIN * 2 + LEFT_EDGE_SPACING;
// Style động cho FAB khi drag
const animatedRootStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateX: fabPositionX.value },
{ translateY: fabPositionY.value },
],
};
});
fabPositionX.value =
Math.abs(fabPositionX.value - SNAP_LEFT) < Math.abs(fabPositionX.value - SNAP_RIGHT)
? withSpring(SNAP_LEFT)
: withSpring(SNAP_RIGHT);
// Style động cho children (ẩn/hiện + trượt lên/xuống)
const animatedChildrenStyles = useAnimatedStyle(() => {
return {
opacity: childrenOpacity.value,
transform: [{ translateY: childrenYPosition.value }],
};
});
// Style động cho FAB (xoay dấu +)
const animatedFABStyles = useAnimatedStyle(() => {
return {
transform: [{ rotate: `${fabRotation.value}deg` }],
};
// nếu muốn luôn bám dưới, có thể reset Y về 0:
// fabPositionY.value = withSpring(FAB_STARTING_POSITION);
},
});
// Style động cho dấu "+" (chỉnh vị trí Y)
const animatedPlusText = useAnimatedStyle(() => {
return {
transform: [{ translateY: fabPlusTranslateY.value }],
};
});
const animatedRootStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: fabPositionX.value },
{ translateY: fabPositionY.value },
],
}));
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]}>
+
</Animated.Text>
</Animated.View>
</TapGestureHandler>
{/* Luôn hiển thị SubButton */}
<Animated.View style={styles.childrenStyles}>
{children}
</Animated.View>
</Animated.View>
</PanGestureHandler>
);
......@@ -198,33 +69,18 @@ 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)
// Neo dưới-phải
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',
backgroundColor: FAB_BACKGROUND_COLOR,
width: FAB_WIDTH,
height: FAB_HEIGHT,
bottom: 30,
right: 0,
borderRadius: FAB_BORDER_RADIUS,
},
// Style cho container của SubButton
// Container SubButton (xếp dọc, lệch phải)
childrenStyles: {
width: FAB_WIDTH,
alignItems: 'center',
marginBottom: 20,
},
// Style cho dấu "+"
plus: {
fontSize: 20,
color: '#EFFBFA',
alignItems: 'flex-end',
marginBottom: 10,
},
});
......@@ -149,6 +149,7 @@ const DetailIncomingDocumentView = props => {
<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>
</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