Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
A
AppUms_Lecturer
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
tungnq
AppUms_Lecturer
Commits
9052caa2
Commit
9052caa2
authored
Aug 27, 2025
by
tungnq
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
IMPORTANT: FAB di chuyển được
parent
faec694d
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
45 additions
and
186 deletions
+45
-186
constants.js
src/components/FAB/constants.js
+2
-0
fab.js
src/components/FAB/fab.js
+42
-186
view.js
src/screens/incoming_document/detail/view.js
+1
-0
No files found.
src/components/FAB/constants.js
View file @
9052caa2
...
@@ -16,6 +16,8 @@ export const FAB_BACKGROUND_COLOR = '#2F6BFF';
...
@@ -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
// Khoảng cách margin của FAB với viền màn hình
export
const
FAB_MARGIN
=
10
;
export
const
FAB_MARGIN
=
10
;
export
const
LEFT_EDGE_SPACING
=
110
;
// -------- Trạng thái khi mở FAB --------
// -------- Trạng thái khi mở FAB --------
...
...
src/components/FAB/fab.js
View file @
9052caa2
import
React
,
{
useState
,
useEffect
}
from
'react'
;
import
React
from
'react'
;
import
{
import
{
StyleSheet
,
useWindowDimensions
}
from
'react-native'
;
StyleSheet
,
import
{
PanGestureHandler
}
from
'react-native-gesture-handler'
;
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
,
{
import
Animated
,
{
useAnimatedGestureHandler
,
useAnimatedGestureHandler
,
useAnimatedStyle
,
useAnimatedStyle
,
useSharedValue
,
useSharedValue
,
withSpring
,
withSpring
,
withTiming
,
}
from
'react-native-reanimated'
;
}
from
'react-native-reanimated'
;
const
FAB
=
props
=>
{
import
{
// State: quản lý trạng thái mở/đóng của FAB
FAB_BORDER_RADIUS
,
const
[
opened
,
setOpened
]
=
useState
(
false
);
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
();
const
{
width
}
=
useWindowDimensions
();
// Nhận children (SubButton) truyền vào component
const
{
children
}
=
props
;
const
{
children
}
=
props
;
/**
// Vị trí drag
* 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
fabPositionX
=
useSharedValue
(
0
);
const
fabPositionY
=
useSharedValue
(
0
);
const
fabPositionY
=
useSharedValue
(
0
);
const
fabRotation
=
useSharedValue
(
FAB_ROTATION_CLOSE
);
const
fabPlusTranslateY
=
useSharedValue
(
FAB_PLUS_TRANSLATE_Y_CLOSE
);
/**
// Kéo & snap trái/phải (giữ nguyên logic bạn đang dùng)
* 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
*/
const
_onPanHandlerStateChange
=
useAnimatedGestureHandler
({
const
_onPanHandlerStateChange
=
useAnimatedGestureHandler
({
onStart
:
(
_
,
ctx
)
=>
{
onStart
:
(
_
,
ctx
)
=>
{
ctx
.
startX
=
fabPositionX
.
value
;
ctx
.
startX
=
fabPositionX
.
value
;
ctx
.
startY
=
fabPositionY
.
value
;
ctx
.
startY
=
fabPositionY
.
value
;
},
},
onActive
:
(
event
,
ctx
)
=>
{
onActive
:
(
event
,
ctx
)
=>
{
fabPositionX
.
value
=
ctx
.
startX
+
event
.
translationX
;
fabPositionX
.
value
=
ctx
.
startX
+
event
.
translationX
;
fabPositionY
.
value
=
ctx
.
startY
+
event
.
translationY
;
fabPositionY
.
value
=
ctx
.
startY
+
event
.
translationY
;
},
},
onEnd
:
_
=>
{
onEnd
:
_
=>
{
if
(
fabPositionX
.
value
>
-
width
/
2
)
{
const
SNAP_RIGHT
=
FAB_STARTING_POSITION
;
// 0
fabPositionX
.
value
=
withSpring
(
FAB_STARTING_POSITION
);
const
SNAP_LEFT
=
-
width
+
FAB_WIDTH
+
FAB_MARGIN
*
2
+
LEFT_EDGE_SPACING
;
fabPositionY
.
value
=
withSpring
(
FAB_STARTING_POSITION
);
}
else
{
fabPositionX
.
value
=
withSpring
(
-
width
+
FAB_WIDTH
+
FAB_MARGIN
*
2
);
fabPositionY
.
value
=
withSpring
(
FAB_STARTING_POSITION
);
}
},
});
// Style động cho FAB khi drag
fabPositionX
.
value
=
const
animatedRootStyles
=
useAnimatedStyle
(()
=>
{
Math
.
abs
(
fabPositionX
.
value
-
SNAP_LEFT
)
<
Math
.
abs
(
fabPositionX
.
value
-
SNAP_RIGHT
)
return
{
?
withSpring
(
SNAP_LEFT
)
transform
:
[
:
withSpring
(
SNAP_RIGHT
);
{
translateX
:
fabPositionX
.
value
},
{
translateY
:
fabPositionY
.
value
},
],
};
});
// Style động cho children (ẩn/hiện + trượt lên/xuống)
// nếu muốn luôn bám dưới, có thể reset Y về 0:
const
animatedChildrenStyles
=
useAnimatedStyle
(()
=>
{
// fabPositionY.value = withSpring(FAB_STARTING_POSITION);
return
{
},
opacity
:
childrenOpacity
.
value
,
transform
:
[{
translateY
:
childrenYPosition
.
value
}],
};
});
// Style động cho FAB (xoay dấu +)
const
animatedFABStyles
=
useAnimatedStyle
(()
=>
{
return
{
transform
:
[{
rotate
:
`
${
fabRotation
.
value
}
deg`
}],
};
});
});
// Style động cho dấu "+" (chỉnh vị trí Y)
const
animatedRootStyles
=
useAnimatedStyle
(()
=>
({
const
animatedPlusText
=
useAnimatedStyle
(()
=>
{
transform
:
[
return
{
{
translateX
:
fabPositionX
.
value
},
transform
:
[{
translateY
:
fabPlusTranslateY
.
value
}]
,
{
translateY
:
fabPositionY
.
value
}
,
};
],
});
})
)
;
return
(
return
(
<
PanGestureHandler
onHandlerStateChange
=
{
_onPanHandlerStateChange
}
>
<
PanGestureHandler
onHandlerStateChange
=
{
_onPanHandlerStateChange
}
>
<
Animated
.
View
style
=
{[
styles
.
rootStyles
,
animatedRootStyles
]}
>
<
Animated
.
View
style
=
{[
styles
.
rootStyles
,
animatedRootStyles
]}
>
{
/* Hiện sub button khi opened = true */
}
{
/* Luôn hiển thị SubButton */
}
{
opened
&&
(
<
Animated
.
View
style
=
{
styles
.
childrenStyles
}
>
<
Animated
.
View
{
children
}
style
=
{[
styles
.
childrenStyles
,
animatedChildrenStyles
]}
>
<
/Animated.View
>
{
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
>
<
/Animated.View
>
<
/Animated.View
>
<
/PanGestureHandler
>
<
/PanGestureHandler
>
);
);
...
@@ -198,33 +69,18 @@ const FAB = props => {
...
@@ -198,33 +69,18 @@ const FAB = props => {
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)
//
Neo dưới-phải
rootStyles
:
{
rootStyles
:
{
borderRadius
:
FAB_BORDER_RADIUS
,
position
:
'absolute'
,
position
:
'absolute'
,
bottom
:
FAB_MARGIN
,
bottom
:
30
,
right
:
FAB_MARGIN
,
right
:
0
,
},
// Style cho nút FAB chính
fabButtonStyles
:
{
alignItems
:
'center'
,
justifyContent
:
'center'
,
backgroundColor
:
FAB_BACKGROUND_COLOR
,
width
:
FAB_WIDTH
,
height
:
FAB_HEIGHT
,
borderRadius
:
FAB_BORDER_RADIUS
,
borderRadius
:
FAB_BORDER_RADIUS
,
},
},
//
Style cho container của SubButton
//
Container SubButton (xếp dọc, lệch phải)
childrenStyles
:
{
childrenStyles
:
{
width
:
FAB_WIDTH
,
width
:
FAB_WIDTH
,
alignItems
:
'center'
,
alignItems
:
'flex-end'
,
marginBottom
:
20
,
marginBottom
:
10
,
},
// Style cho dấu "+"
plus
:
{
fontSize
:
20
,
color
:
'#EFFBFA'
,
},
},
});
});
src/screens/incoming_document/detail/view.js
View file @
9052caa2
...
@@ -149,6 +149,7 @@ const DetailIncomingDocumentView = props => {
...
@@ -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 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
}
/
>
<
SubButton
onPress
=
{()
=>
Alert
.
alert
(
'Pressed 2!'
)}
label
=
"Tạo công việc"
images
=
{
R
.
images
.
icMenuEdit
}
backgroundColor
=
{
R
.
colors
.
blue
}
/
>
<
/FAB
>
<
/FAB
>
<
/View
>
<
/View
>
<
/ScrollView
>
<
/ScrollView
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment