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
cf1f778b
Commit
cf1f778b
authored
Aug 27, 2025
by
tungnq
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
TODO: Bổ sung FAB Group
parent
4d9424d8
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
267 additions
and
214 deletions
+267
-214
FabButton.js
src/components/FabButton.js
+245
-212
view.js
src/screens/incoming_document/detail/view.js
+22
-2
No files found.
src/components/FabButton.js
View file @
cf1f778b
import
React
from
'react'
;
import
React
,
{
useState
,
useRef
}
from
'react'
;
import
{
import
{
TouchableOpacity
,
View
,
Text
,
Text
,
StyleSheet
,
StyleSheet
,
Animated
,
Animated
,
Pressable
,
Pressable
,
PanResponder
,
Dimensions
,
Dimensions
,
}
from
'react-native'
;
}
from
'react-native'
;
// SCREEN: Lấy kích thước màn hình để giới hạn vùng di chuyển
const
{
width
:
SCREEN_WIDTH
}
=
Dimensions
.
get
(
'window'
);
const
{
width
:
SCREEN_WIDTH
,
height
:
SCREEN_HEIGHT
}
=
Dimensions
.
get
(
'window'
);
// COMPONENT: FAB (Floating Action Button) có thể di chuyển
// COMPONENT: FAB Group với menu mở rộng
const
FAB
=
({
const
FABGroup
=
({
icon
=
'+'
,
label
,
onPress
,
position
=
'bottom-right'
,
position
=
'bottom-right'
,
mainIcon
=
'+'
,
mainBackgroundColor
=
'#007AFF'
,
mainColor
=
'#FFFFFF'
,
size
=
'medium'
,
size
=
'medium'
,
backgroundColor
=
'#007AFF'
,
actions
=
[],
// Mảng các action buttons
color
=
'#FFFFFF'
,
overlayColor
=
'rgba(0, 0, 0, 0.3)'
,
animationDuration
=
200
,
spacing
=
10
,
style
,
style
,
disabled
=
false
,
elevation
=
8
,
draggable
=
false
,
// FEATURE: Bật/tắt tính năng kéo thả
snapToEdges
=
true
,
// FEATURE: Tự động dính vào cạnh màn hình
})
=>
{
})
=>
{
// STATE: Animation values
// STATE: Quản lý trạng thái mở/đóng
const
scaleValue
=
React
.
useRef
(
new
Animated
.
Value
(
1
)).
current
;
const
[
isOpen
,
setIsOpen
]
=
useState
(
false
);
const
pan
=
React
.
useRef
(
new
Animated
.
ValueXY
()).
current
;
// STATE: Tracking drag và press
// ANIMATION: Animation values
const
[
isDragging
,
setIsDragging
]
=
React
.
useState
(
false
);
const
fadeAnim
=
useRef
(
new
Animated
.
Value
(
0
)).
current
;
const
[
initialPosition
,
setInitialPosition
]
=
React
.
useState
({
x
:
0
,
y
:
0
});
const
rotateAnim
=
useRef
(
new
Animated
.
Value
(
0
)).
current
;
const
scaleAnim
=
useRef
(
new
Animated
.
Value
(
1
)).
current
;
// FUNCTIONALITY: Khởi tạo vị trí ban đầu từ position prop
// FUNCTIONALITY: Toggle FAB Group
React
.
useEffect
(()
=>
{
const
toggleFAB
=
()
=>
{
if
(
!
draggable
)
return
;
const
toValue
=
isOpen
?
0
:
1
;
setIsOpen
(
!
isOpen
);
const
fabSize
=
getFABSize
(
size
);
const
margin
=
16
;
let
x
,
y
;
// SETUP: Tính toán vị trí ban đầu dựa trên position
switch
(
position
)
{
case
'bottom-right'
:
x
=
SCREEN_WIDTH
-
fabSize
-
margin
;
y
=
SCREEN_HEIGHT
-
fabSize
-
margin
;
break
;
case
'bottom-left'
:
x
=
margin
;
y
=
SCREEN_HEIGHT
-
fabSize
-
margin
;
break
;
case
'top-right'
:
x
=
SCREEN_WIDTH
-
fabSize
-
margin
;
y
=
margin
;
break
;
case
'top-left'
:
x
=
margin
;
y
=
margin
;
break
;
default
:
x
=
SCREEN_WIDTH
-
fabSize
-
margin
;
y
=
SCREEN_HEIGHT
-
fabSize
-
margin
;
}
setInitialPosition
({
x
,
y
});
pan
.
setValue
({
x
,
y
});
},
[
position
,
size
,
draggable
]);
// FUNCTIONALITY: PanResponder cho drag functionality
const
panResponder
=
React
.
useRef
(
PanResponder
.
create
({
// DRAG: Cho phép bắt đầu drag
onStartShouldSetPanResponder
:
()
=>
draggable
&&
!
disabled
,
onMoveShouldSetPanResponder
:
()
=>
draggable
&&
!
disabled
,
// DRAG: Xử lý khi bắt đầu drag
onPanResponderGrant
:
()
=>
{
setIsDragging
(
true
);
// ANIMATION: Scale effect khi bắt đầu drag
Animated
.
spring
(
scaleValue
,
{
toValue
:
1.1
,
useNativeDriver
:
true
,
}).
start
();
},
// DRAG: Xử lý khi đang drag
onPanResponderMove
:
Animated
.
event
(
[
null
,
{
dx
:
pan
.
x
,
dy
:
pan
.
y
}],
{
useNativeDriver
:
false
}
),
// DRAG: Xử lý khi kết thúc drag
onPanResponderRelease
:
(
evt
,
gestureState
)
=>
{
setIsDragging
(
false
);
// ANIMATION: Trở về scale bình thường
Animated
.
spring
(
scaleValue
,
{
toValue
:
1
,
useNativeDriver
:
true
,
}).
start
();
const
fabSize
=
getFABSize
(
size
);
const
margin
=
16
;
// BOUNDARY: Tính toán vị trí cuối cùng trong boundaries
let
finalX
=
Math
.
max
(
margin
,
Math
.
min
(
SCREEN_WIDTH
-
fabSize
-
margin
,
pan
.
x
.
_value
));
let
finalY
=
Math
.
max
(
margin
,
Math
.
min
(
SCREEN_HEIGHT
-
fabSize
-
margin
,
pan
.
y
.
_value
));
// FEATURE: Snap to edges nếu enabled
if
(
snapToEdges
)
{
const
centerX
=
SCREEN_WIDTH
/
2
;
if
(
finalX
<
centerX
)
{
finalX
=
margin
;
// Snap to left edge
}
else
{
finalX
=
SCREEN_WIDTH
-
fabSize
-
margin
;
// Snap to right edge
}
}
// ANIMATION: Animate tới vị trí cuối cùng
Animated
.
spring
(
pan
,
{
toValue
:
{
x
:
finalX
,
y
:
finalY
},
useNativeDriver
:
false
,
tension
:
100
,
friction
:
8
,
}).
start
();
},
})
).
current
;
// FUNCTIONALITY: Xử lý press in effect (chỉ khi không drag)
// ANIMATION: Parallel animations cho smooth effect
const
handlePressIn
=
()
=>
{
Animated
.
parallel
([
if
(
!
isDragging
&&
!
disabled
)
{
// Fade overlay
Animated
.
spring
(
scaleValue
,
{
Animated
.
timing
(
fadeAnim
,
{
toValue
:
0.95
,
toValue
,
duration
:
animationDuration
,
useNativeDriver
:
true
,
useNativeDriver
:
true
,
}).
start
();
}),
}
// Rotate main FAB
Animated
.
timing
(
rotateAnim
,
{
toValue
,
duration
:
animationDuration
,
useNativeDriver
:
true
,
}),
]).
start
();
};
};
// FUNCTIONALITY: Xử lý press out effect
// FUNCTIONALITY: Xử lý press action item
const
handlePressOut
=
()
=>
{
const
handleActionPress
=
(
action
)
=>
{
if
(
!
isDragging
&&
!
disabled
)
{
// Close FAB group trước
Animated
.
spring
(
scaleValue
,
{
toggleFAB
();
toValue
:
1
,
friction
:
3
,
// Delay để animation hoàn thành rồi mới trigger action
tension
:
40
,
setTimeout
(()
=>
{
useNativeDriver
:
true
,
if
(
action
.
onPress
)
{
}).
start
();
action
.
onPress
();
}
}
},
animationDuration
/
2
);
};
// FUNCTIONALITY: Press effects cho main FAB
const
handleMainPressIn
=
()
=>
{
Animated
.
spring
(
scaleAnim
,
{
toValue
:
0.95
,
useNativeDriver
:
true
,
}).
start
();
};
};
// FUNCTIONALITY: Xử lý press - chỉ trigger khi không drag
const
handleMainPressOut
=
()
=>
{
const
handlePress
=
()
=>
{
Animated
.
spring
(
scaleAnim
,
{
if
(
!
isDragging
&&
!
disabled
&&
onPress
)
{
toValue
:
1
,
onPress
();
friction
:
3
,
}
tension
:
40
,
useNativeDriver
:
true
,
}).
start
();
};
};
// UI/UX: Tính toán styles
// UI/UX: Tính toán styles
const
fabSize
=
getFABSize
(
size
);
const
fabSize
=
getFABSize
(
size
);
const
positionStyle
=
draggable
?
{}
:
getPositionStyle
(
position
);
// Chỉ dùng position khi không draggable
const
positionStyle
=
getPositionStyle
(
position
);
const
backgroundColorStyle
=
disabled
?
'#CCCCCC'
:
backgroundColor
;
// ANIMATION: Rotation cho main icon
const
rotateInterpolate
=
rotateAnim
.
interpolate
({
inputRange
:
[
0
,
1
],
outputRange
:
[
'0deg'
,
'45deg'
],
// Xoay 45 độ khi mở
});
// RENDER: Draggable version
return
(
if
(
draggable
)
{
<>
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
<
Animated
.
View
style
=
{[
style
=
{[
styles
.
container
,
styles
.
mainFABContainer
,
positionStyle
,
{
{
transform
:
[
transform
:
[
{
translateX
:
pan
.
x
},
{
scale
:
scaleAnim
},
{
translateY
:
pan
.
y
},
{
rotate
:
rotateInterpolate
},
{
scale
:
scaleValue
}
],
],
},
},
style
,
style
,
]}
]}
{...
panResponder
.
panHandlers
}
>
>
<
Pressable
<
Pressable
onPress
=
{
handlePress
}
onPress
=
{
toggleFAB
}
onPressIn
=
{
handlePressIn
}
onPressIn
=
{
handle
Main
PressIn
}
onPressOut
=
{
handlePressOut
}
onPressOut
=
{
handle
Main
PressOut
}
style
=
{[
style
=
{[
styles
.
fab
,
styles
.
mainFAB
,
{
{
width
:
fabSize
,
width
:
fabSize
,
height
:
fabSize
,
height
:
fabSize
,
borderRadius
:
fabSize
/
2
,
borderRadius
:
fabSize
/
2
,
backgroundColor
:
backgroundColorStyle
,
backgroundColor
:
mainBackgroundColor
,
elevation
:
disabled
?
2
:
elevation
,
shadowOpacity
:
disabled
?
0.2
:
0.3
,
},
},
]}
]}
disabled
=
{
disabled
}
>
>
<
Text
style
=
{[
styles
.
text
,
{
color
,
fontSize
:
getFontSize
(
size
)
}]}
>
<
Text
style
=
{[
{
label
||
icon
}
styles
.
mainIcon
,
{
color
:
mainColor
,
fontSize
:
getFontSize
(
size
),
}
]}
>
{
mainIcon
}
<
/Text
>
<
/Text
>
<
/Pressable
>
<
/Pressable
>
<
/Animated.View
>
<
/Animated.View
>
);
<
/
>
}
// RENDER: Static version (original FAB)
return
(
<
Animated
.
View
style
=
{[
styles
.
container
,
positionStyle
,
{
transform
:
[{
scale
:
scaleValue
}],
},
style
,
]}
>
<
Pressable
onPress
=
{
disabled
?
undefined
:
onPress
}
onPressIn
=
{
disabled
?
undefined
:
handlePressIn
}
onPressOut
=
{
disabled
?
undefined
:
handlePressOut
}
style
=
{[
styles
.
fab
,
{
width
:
fabSize
,
height
:
fabSize
,
borderRadius
:
fabSize
/
2
,
backgroundColor
:
backgroundColorStyle
,
elevation
:
disabled
?
2
:
elevation
,
shadowOpacity
:
disabled
?
0.2
:
0.3
,
},
]}
disabled
=
{
disabled
}
>
<
Text
style
=
{[
styles
.
text
,
{
color
,
fontSize
:
getFontSize
(
size
)
}]}
>
{
label
||
icon
}
<
/Text
>
<
/Pressable
>
<
/Animated.View
>
);
);
};
};
...
@@ -272,11 +247,11 @@ const getFontSize = (size) => {
...
@@ -272,11 +247,11 @@ const getFontSize = (size) => {
}
}
};
};
// FUNCTIONALITY: Lấy style vị trí theo position prop
(chỉ dùng khi không draggable)
// FUNCTIONALITY: Lấy style vị trí theo position prop
const
getPositionStyle
=
(
position
)
=>
{
const
getPositionStyle
=
(
position
)
=>
{
const
baseStyle
=
{
const
baseStyle
=
{
position
:
'absolute'
,
position
:
'absolute'
,
margin
:
1
6
,
margin
:
1
5
,
};
};
switch
(
position
)
{
switch
(
position
)
{
...
@@ -295,34 +270,92 @@ const getPositionStyle = (position) => {
...
@@ -295,34 +270,92 @@ const getPositionStyle = (position) => {
// STYLES: Định nghĩa styles cho component
// STYLES: Định nghĩa styles cho component
const
styles
=
StyleSheet
.
create
({
const
styles
=
StyleSheet
.
create
({
//
UI/UX: Container chính của FAB
//
OVERLAY: Background overlay
container
:
{
overlay
:
{
position
:
'absolute'
,
position
:
'absolute'
,
zIndex
:
1000
,
// Đảm bảo FAB luôn hiển thị trên cùng
top
:
0
,
left
:
0
,
right
:
0
,
bottom
:
0
,
zIndex
:
999
,
},
},
// UI/UX: Style cho button FAB
overlayPressable
:
{
fab
:
{
flex
:
1
,
},
// MAIN FAB: Container và button chính
mainFABContainer
:
{
zIndex
:
1001
,
},
mainFAB
:
{
justifyContent
:
'center'
,
justifyContent
:
'center'
,
alignItems
:
'center'
,
alignItems
:
'center'
,
elevation
:
8
,
// PERFORMANCE: Shadow cho iOS
shadowColor
:
'#000'
,
shadowColor
:
'#000'
,
shadowOffset
:
{
shadowOffset
:
{
width
:
0
,
width
:
0
,
height
:
4
,
height
:
4
,
},
},
shadowOpacity
:
0.3
,
shadowRadius
:
8
,
shadowRadius
:
8
,
// UI/UX: Active state styling
activeOpacity
:
0.8
,
},
},
// UI/UX: Style cho text/icon
mainIcon
:
{
text
:
{
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'
,
fontWeight
:
'bold'
,
textAlign
:
'center'
,
textAlign
:
'center'
,
},
},
});
});
export
default
FAB
;
export
default
FABGroup
;
\ No newline at end of file
\ No newline at end of file
src/screens/incoming_document/detail/view.js
View file @
cf1f778b
...
@@ -4,7 +4,8 @@ import R from '../../../assets/R';
...
@@ -4,7 +4,8 @@ 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/FabButton'
;
import
FABGroup
from
'../../../components/FabButton'
;
const
DetailIncomingDocumentView
=
props
=>
{
const
DetailIncomingDocumentView
=
props
=>
{
const
{
icomingDocument
}
=
props
;
const
{
icomingDocument
}
=
props
;
console
.
log
(
props
);
console
.
log
(
props
);
...
@@ -144,7 +145,26 @@ const DetailIncomingDocumentView = props => {
...
@@ -144,7 +145,26 @@ const DetailIncomingDocumentView = props => {
renderItem
=
{
renderItem
}
renderItem
=
{
renderItem
}
keyExtractor
=
{(
item
,
index
)
=>
index
.
toString
()}
keyExtractor
=
{(
item
,
index
)
=>
index
.
toString
()}
/
>
/
>
<
FABGroup
mainIcon
=
"+"
position
=
"bottom-right"
actions
=
{[
{
id
:
'add-pen'
,
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
>
...
...
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