@momo-kits/native-kits 0.167.1-duo.6 → 0.167.1-duo.6-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.gradle.kts +11 -0
- package/compose/build.gradle.kts +204 -0
- package/compose/build.gradle.kts.backup +204 -0
- package/compose/compose.podspec +47 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +266 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +264 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +552 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +187 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +648 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +194 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +281 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +408 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +105 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +354 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
- package/gradle/libs.versions.toml +69 -0
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/gradle.properties +26 -0
- package/gradlew +252 -0
- package/gradlew.bat +94 -0
- package/ios-demo/README.md +120 -0
- package/package.json +1 -1
- package/settings.gradle.kts +64 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
package vn.momo.kits.navigation.bottomtab
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.Spring
|
|
4
|
+
import androidx.compose.animation.core.animateDpAsState
|
|
5
|
+
import androidx.compose.animation.core.spring
|
|
6
|
+
import androidx.compose.foundation.background
|
|
7
|
+
import androidx.compose.foundation.border
|
|
8
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
9
|
+
import androidx.compose.foundation.layout.Box
|
|
10
|
+
import androidx.compose.foundation.layout.BoxScope
|
|
11
|
+
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
12
|
+
import androidx.compose.foundation.layout.Column
|
|
13
|
+
import androidx.compose.foundation.layout.Row
|
|
14
|
+
import androidx.compose.foundation.layout.RowScope
|
|
15
|
+
import androidx.compose.foundation.layout.absolutePadding
|
|
16
|
+
import androidx.compose.foundation.layout.fillMaxHeight
|
|
17
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
18
|
+
import androidx.compose.foundation.layout.height
|
|
19
|
+
import androidx.compose.foundation.layout.offset
|
|
20
|
+
import androidx.compose.foundation.layout.padding
|
|
21
|
+
import androidx.compose.foundation.layout.size
|
|
22
|
+
import androidx.compose.foundation.layout.width
|
|
23
|
+
import androidx.compose.foundation.shape.CircleShape
|
|
24
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
25
|
+
import androidx.compose.runtime.Composable
|
|
26
|
+
import androidx.compose.runtime.compositionLocalOf
|
|
27
|
+
import androidx.compose.runtime.getValue
|
|
28
|
+
import androidx.compose.ui.AbsoluteAlignment
|
|
29
|
+
import androidx.compose.ui.Alignment
|
|
30
|
+
import androidx.compose.ui.Modifier
|
|
31
|
+
import androidx.compose.ui.draw.clip
|
|
32
|
+
import androidx.compose.ui.draw.shadow
|
|
33
|
+
import androidx.compose.ui.graphics.Color
|
|
34
|
+
import androidx.compose.ui.graphics.Shape
|
|
35
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
36
|
+
import androidx.compose.ui.unit.Dp
|
|
37
|
+
import androidx.compose.ui.unit.dp
|
|
38
|
+
import vn.momo.kits.components.Badge
|
|
39
|
+
import vn.momo.kits.components.BadgeDot
|
|
40
|
+
import vn.momo.kits.components.DotSize
|
|
41
|
+
import vn.momo.kits.components.Icon
|
|
42
|
+
import vn.momo.kits.components.Text
|
|
43
|
+
import vn.momo.kits.const.AppTheme
|
|
44
|
+
import vn.momo.kits.const.Radius
|
|
45
|
+
import vn.momo.kits.const.Spacing
|
|
46
|
+
import vn.momo.kits.const.Typography
|
|
47
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
48
|
+
import vn.momo.kits.layout.ScreenLayout
|
|
49
|
+
import vn.momo.kits.layout.SideBarEdge
|
|
50
|
+
import vn.momo.kits.modifier.noFeedbackClickable
|
|
51
|
+
import vn.momo.kits.modifier.setAutomationId
|
|
52
|
+
|
|
53
|
+
// Figma Fold-Design-System MT1/MT2, mirrored from RN DuoTabBar.tsx.
|
|
54
|
+
private val ITEM_SIZE = DuoMetrics.controlSize
|
|
55
|
+
private val ICON_SIZE = 28.dp
|
|
56
|
+
private val QR_ICON_SIZE = 24.dp
|
|
57
|
+
private val RAIL_PADDING = 6.dp
|
|
58
|
+
private val RAIL_GAP = Spacing.XS
|
|
59
|
+
// selection bubble overflows the 48 rail cell by 5 above and below
|
|
60
|
+
private val RAIL_BUBBLE_WIDTH = 44.dp
|
|
61
|
+
private val RAIL_BUBBLE_HEIGHT = 58.dp
|
|
62
|
+
private val FLOATING_HEIGHT = 62.dp
|
|
63
|
+
private val FLOATING_PADDING = Spacing.XS
|
|
64
|
+
private val FLOATING_QR_SIZE = 44.dp
|
|
65
|
+
// floating bar spans 6 of the 8 portrait grid columns, like the content
|
|
66
|
+
private const val GRID_COLUMNS = 8
|
|
67
|
+
private const val GRID_SPAN = 6
|
|
68
|
+
// RN reanimated spring { damping: 18, stiffness: 220, mass: 0.8 }
|
|
69
|
+
private val BUBBLE_SPRING = spring<Dp>(dampingRatio = 0.68f, stiffness = 275f, visibilityThreshold = Dp(0.5f))
|
|
70
|
+
|
|
71
|
+
// Space from the window bottom the rail stack takes, so the header side bar stays above it.
|
|
72
|
+
internal val LocalDuoRailHeight = compositionLocalOf { 0.dp }
|
|
73
|
+
|
|
74
|
+
internal fun duoRailHeight(itemCount: Int, hasQr: Boolean): Dp =
|
|
75
|
+
Spacing.XL + RAIL_PADDING * 2 + ITEM_SIZE * itemCount + RAIL_GAP * (itemCount - 1).coerceAtLeast(0) +
|
|
76
|
+
if (hasQr) Spacing.M + ITEM_SIZE else 0.dp
|
|
77
|
+
|
|
78
|
+
// Space tab content leaves under the floating bar; 0 when the rail sits in the side strip or off Duo.
|
|
79
|
+
internal fun duoFloatingTabBarInset(layout: ScreenLayout): Dp =
|
|
80
|
+
if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + Spacing.M + layout.insetBottom else 0.dp
|
|
81
|
+
|
|
82
|
+
// iPhone Duo tab bar: glass rail in the side bar, or a floating glass bar when unfolded portrait.
|
|
83
|
+
@Composable
|
|
84
|
+
internal fun BoxScope.DuoTabBar(
|
|
85
|
+
layout: ScreenLayout,
|
|
86
|
+
items: List<BottomTabItem>,
|
|
87
|
+
floatingButton: BottomTabFloatingButton?,
|
|
88
|
+
selectedIndex: Int,
|
|
89
|
+
onTabSelected: (Int) -> Unit,
|
|
90
|
+
) {
|
|
91
|
+
if (layout.sideBar != null) {
|
|
92
|
+
DuoRailTabBar(layout, items, floatingButton, selectedIndex, onTabSelected)
|
|
93
|
+
} else {
|
|
94
|
+
DuoFloatingTabBar(layout, items, floatingButton, selectedIndex, onTabSelected)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Composable
|
|
99
|
+
private fun BoxScope.DuoRailTabBar(
|
|
100
|
+
layout: ScreenLayout,
|
|
101
|
+
items: List<BottomTabItem>,
|
|
102
|
+
floatingButton: BottomTabFloatingButton?,
|
|
103
|
+
selectedIndex: Int,
|
|
104
|
+
onTabSelected: (Int) -> Unit,
|
|
105
|
+
) {
|
|
106
|
+
val sideBar = layout.sideBar ?: return
|
|
107
|
+
val isRight = sideBar.edge == SideBarEdge.RIGHT
|
|
108
|
+
val bubbleOffset by animateDpAsState(
|
|
109
|
+
targetValue = (ITEM_SIZE + RAIL_GAP) * selectedIndex.coerceAtLeast(0),
|
|
110
|
+
animationSpec = BUBBLE_SPRING,
|
|
111
|
+
label = "DuoRailBubble",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
Column(
|
|
115
|
+
modifier = Modifier
|
|
116
|
+
.align(if (isRight) AbsoluteAlignment.BottomRight else AbsoluteAlignment.BottomLeft)
|
|
117
|
+
.width(sideBar.width)
|
|
118
|
+
.padding(bottom = Spacing.XL)
|
|
119
|
+
.absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
|
|
120
|
+
horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
|
|
121
|
+
verticalArrangement = Arrangement.spacedBy(Spacing.M),
|
|
122
|
+
) {
|
|
123
|
+
Box(
|
|
124
|
+
Modifier
|
|
125
|
+
.width(ITEM_SIZE)
|
|
126
|
+
.tabBarMaterial(RoundedCornerShape(Radius.XL))
|
|
127
|
+
.setAutomationId("bottom_tab_bar")
|
|
128
|
+
) {
|
|
129
|
+
Box(
|
|
130
|
+
Modifier
|
|
131
|
+
.offset(
|
|
132
|
+
x = (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
|
|
133
|
+
y = RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2 + bubbleOffset,
|
|
134
|
+
)
|
|
135
|
+
.size(RAIL_BUBBLE_WIDTH, RAIL_BUBBLE_HEIGHT)
|
|
136
|
+
.background(AppTheme.current.colors.background.selected, CircleShape)
|
|
137
|
+
)
|
|
138
|
+
Column(
|
|
139
|
+
modifier = Modifier.padding(vertical = RAIL_PADDING),
|
|
140
|
+
verticalArrangement = Arrangement.spacedBy(RAIL_GAP),
|
|
141
|
+
) {
|
|
142
|
+
items.forEachIndexed { index, item ->
|
|
143
|
+
Box(
|
|
144
|
+
modifier = Modifier
|
|
145
|
+
.size(ITEM_SIZE)
|
|
146
|
+
.noFeedbackClickable { onTabSelected(index) },
|
|
147
|
+
contentAlignment = Alignment.Center,
|
|
148
|
+
) {
|
|
149
|
+
DuoTabIcon(item, selected = index == selectedIndex)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (floatingButton != null) {
|
|
155
|
+
QrButton(floatingButton, size = ITEM_SIZE)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Composable
|
|
161
|
+
private fun BoxScope.DuoFloatingTabBar(
|
|
162
|
+
layout: ScreenLayout,
|
|
163
|
+
items: List<BottomTabItem>,
|
|
164
|
+
floatingButton: BottomTabFloatingButton?,
|
|
165
|
+
selectedIndex: Int,
|
|
166
|
+
onTabSelected: (Int) -> Unit,
|
|
167
|
+
) {
|
|
168
|
+
val columnWidth = (layout.width - Spacing.M * 2 - Spacing.M * (GRID_COLUMNS - 1)) / GRID_COLUMNS
|
|
169
|
+
val barWidth = columnWidth * GRID_SPAN + Spacing.M * (GRID_SPAN - 1)
|
|
170
|
+
val useQr = floatingButton != null && items.size % 2 == 0
|
|
171
|
+
val middle = items.size / 2
|
|
172
|
+
val cellCount = items.size + if (useQr) 1 else 0
|
|
173
|
+
val visualIndex = if (useQr && selectedIndex >= middle) selectedIndex + 1 else selectedIndex
|
|
174
|
+
|
|
175
|
+
Box(
|
|
176
|
+
modifier = Modifier
|
|
177
|
+
.align(Alignment.BottomCenter)
|
|
178
|
+
.padding(bottom = layout.insetBottom + Spacing.M)
|
|
179
|
+
.width(barWidth)
|
|
180
|
+
.height(FLOATING_HEIGHT)
|
|
181
|
+
.tabBarMaterial(RoundedCornerShape(FLOATING_HEIGHT / 2))
|
|
182
|
+
.padding(FLOATING_PADDING)
|
|
183
|
+
.setAutomationId("bottom_tab_bar")
|
|
184
|
+
) {
|
|
185
|
+
BoxWithConstraints(Modifier.fillMaxSize()) {
|
|
186
|
+
val cellWidth = maxWidth / cellCount
|
|
187
|
+
val bubbleOffset by animateDpAsState(
|
|
188
|
+
targetValue = cellWidth * visualIndex.coerceAtLeast(0),
|
|
189
|
+
animationSpec = BUBBLE_SPRING,
|
|
190
|
+
label = "DuoFloatingBubble",
|
|
191
|
+
)
|
|
192
|
+
Box(
|
|
193
|
+
Modifier
|
|
194
|
+
.offset(x = bubbleOffset)
|
|
195
|
+
.width(cellWidth)
|
|
196
|
+
.fillMaxHeight()
|
|
197
|
+
.background(AppTheme.current.colors.background.selected, CircleShape)
|
|
198
|
+
)
|
|
199
|
+
Row(Modifier.fillMaxSize()) {
|
|
200
|
+
items.forEachIndexed { index, item ->
|
|
201
|
+
if (useQr && index == middle) {
|
|
202
|
+
Box(Modifier.weight(1f).fillMaxHeight(), contentAlignment = Alignment.Center) {
|
|
203
|
+
QrButton(floatingButton, size = FLOATING_QR_SIZE)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
FloatingCell(item, selected = index == selectedIndex) { onTabSelected(index) }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Composable
|
|
214
|
+
private fun RowScope.FloatingCell(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
|
|
215
|
+
val colors = AppTheme.current.colors
|
|
216
|
+
Column(
|
|
217
|
+
modifier = Modifier
|
|
218
|
+
.weight(1f)
|
|
219
|
+
.fillMaxHeight()
|
|
220
|
+
.noFeedbackClickable(onClick = onClick),
|
|
221
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
222
|
+
verticalArrangement = Arrangement.spacedBy(Spacing.XXS, Alignment.CenterVertically),
|
|
223
|
+
) {
|
|
224
|
+
DuoTabIcon(item, selected)
|
|
225
|
+
Text(
|
|
226
|
+
text = item.label,
|
|
227
|
+
style = Typography.labelXsMedium,
|
|
228
|
+
color = if (selected) colors.primary else colors.text.default,
|
|
229
|
+
maxLines = 1,
|
|
230
|
+
overflow = TextOverflow.Ellipsis,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@Composable
|
|
236
|
+
private fun DuoTabIcon(item: BottomTabItem, selected: Boolean) {
|
|
237
|
+
val colors = AppTheme.current.colors
|
|
238
|
+
val badgeLabel = item.badgeLabel?.takeUnless { it.toIntOrNull() == 0 }
|
|
239
|
+
Box {
|
|
240
|
+
Icon(
|
|
241
|
+
source = item.icon,
|
|
242
|
+
size = ICON_SIZE,
|
|
243
|
+
color = if (selected) colors.primary else colors.text.default,
|
|
244
|
+
)
|
|
245
|
+
when {
|
|
246
|
+
badgeLabel == null && !item.showDot -> Unit
|
|
247
|
+
badgeLabel.isNullOrEmpty() -> BadgeDot(
|
|
248
|
+
size = DotSize.Small,
|
|
249
|
+
modifier = Modifier.align(Alignment.TopEnd),
|
|
250
|
+
)
|
|
251
|
+
else -> Badge(
|
|
252
|
+
badgeLabel,
|
|
253
|
+
modifier = Modifier.align(Alignment.TopStart).offset(x = ICON_SIZE / 2, y = -Spacing.XS),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@Composable
|
|
260
|
+
private fun QrButton(data: BottomTabFloatingButton, size: Dp) {
|
|
261
|
+
Box(
|
|
262
|
+
modifier = Modifier
|
|
263
|
+
.size(size)
|
|
264
|
+
.clip(CircleShape)
|
|
265
|
+
.background(AppTheme.current.colors.primary)
|
|
266
|
+
.noFeedbackClickable(onClick = data.onPress)
|
|
267
|
+
.setAutomationId(data.label),
|
|
268
|
+
contentAlignment = Alignment.Center,
|
|
269
|
+
) {
|
|
270
|
+
Icon(source = data.icon, color = Color.White, size = QR_ICON_SIZE)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// RN falls back to this when native glass is missing; Compose has no backdrop blur, so it always does.
|
|
275
|
+
@Composable
|
|
276
|
+
private fun Modifier.tabBarMaterial(shape: Shape): Modifier {
|
|
277
|
+
val colors = AppTheme.current.colors
|
|
278
|
+
return shadow(3.dp, shape)
|
|
279
|
+
.background(colors.background.surface)
|
|
280
|
+
.border(1.dp, colors.border.default, shape)
|
|
281
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
package vn.momo.kits.navigation.component
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.*
|
|
4
|
+
import androidx.compose.animation.core.CubicBezierEasing
|
|
5
|
+
import androidx.compose.animation.core.LinearEasing
|
|
6
|
+
import androidx.compose.animation.core.animateDpAsState
|
|
7
|
+
import androidx.compose.animation.core.tween
|
|
8
|
+
import androidx.compose.foundation.ScrollState
|
|
9
|
+
import androidx.compose.foundation.background
|
|
10
|
+
import androidx.compose.foundation.border
|
|
11
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
12
|
+
import androidx.compose.foundation.layout.*
|
|
13
|
+
import androidx.compose.material3.AlertDialogDefaults
|
|
14
|
+
import androidx.compose.material3.FloatingActionButton
|
|
15
|
+
import androidx.compose.material3.FloatingActionButtonDefaults
|
|
16
|
+
import androidx.compose.material3.FloatingActionButtonElevation
|
|
17
|
+
import androidx.compose.runtime.*
|
|
18
|
+
import androidx.compose.runtime.saveable.rememberSaveable
|
|
19
|
+
import androidx.compose.ui.Alignment
|
|
20
|
+
import androidx.compose.ui.Modifier
|
|
21
|
+
import androidx.compose.ui.graphics.Color
|
|
22
|
+
import androidx.compose.ui.unit.Dp
|
|
23
|
+
import androidx.compose.ui.unit.dp
|
|
24
|
+
import kotlinx.coroutines.delay
|
|
25
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
26
|
+
import kotlinx.coroutines.flow.collectLatest
|
|
27
|
+
import kotlinx.coroutines.launch
|
|
28
|
+
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
29
|
+
import vn.momo.kits.components.Icon
|
|
30
|
+
import vn.momo.kits.components.Text
|
|
31
|
+
import vn.momo.kits.const.Colors
|
|
32
|
+
import vn.momo.kits.const.Typography
|
|
33
|
+
import vn.momo.kits.modifier.conditional
|
|
34
|
+
import kotlin.time.Duration.Companion.milliseconds
|
|
35
|
+
|
|
36
|
+
enum class FABSize { SMALL, DEFAULT }
|
|
37
|
+
enum class FABPosition { END, CENTER }
|
|
38
|
+
|
|
39
|
+
data class FloatingButtonProps(
|
|
40
|
+
val icon: String,
|
|
41
|
+
val iconColor: Color? = null,
|
|
42
|
+
val label: String? = null,
|
|
43
|
+
val onClick: () -> Unit,
|
|
44
|
+
val size: FABSize = FABSize.DEFAULT,
|
|
45
|
+
val bottom: Dp? = null,
|
|
46
|
+
val scrollState: ScrollState? = null,
|
|
47
|
+
val position: FABPosition? = FABPosition.END,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
private val FabSmallSize = 36.dp
|
|
51
|
+
private val FabDefaultSize = 48.dp
|
|
52
|
+
private val FabExpandedWidth = 80.dp
|
|
53
|
+
private val FabIconSizeSmall = 12.dp
|
|
54
|
+
private val FabIconSizeDefault = 24.dp
|
|
55
|
+
private val FabPaddingHorizontal = 12.dp
|
|
56
|
+
|
|
57
|
+
@Composable
|
|
58
|
+
internal fun FloatingButton(
|
|
59
|
+
scrollPosition: Int,
|
|
60
|
+
bottom: Dp,
|
|
61
|
+
onClick: () -> Unit,
|
|
62
|
+
containerColor: Color,
|
|
63
|
+
contentColor: Color = containerColor,
|
|
64
|
+
icon: String,
|
|
65
|
+
iconColor: Color? = null,
|
|
66
|
+
text: String? = null,
|
|
67
|
+
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(4.dp),
|
|
68
|
+
size: FABSize = FABSize.DEFAULT,
|
|
69
|
+
position: FABPosition = FABPosition.END,
|
|
70
|
+
keyboardSize: Dp = 0.dp,
|
|
71
|
+
) {
|
|
72
|
+
val scrollStateFlow = remember { MutableStateFlow(scrollPosition) }
|
|
73
|
+
var lastScrollPosition by rememberSaveable { mutableStateOf(0) }
|
|
74
|
+
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
|
75
|
+
|
|
76
|
+
val coroutineScope = rememberCoroutineScope()
|
|
77
|
+
|
|
78
|
+
// Animations
|
|
79
|
+
val baseSize = if (size == FABSize.SMALL) FabSmallSize else FabDefaultSize
|
|
80
|
+
val width by animateDpAsState(
|
|
81
|
+
targetValue = if (isExpanded && text != null) FabExpandedWidth else baseSize,
|
|
82
|
+
animationSpec = tween(100, easing = CubicBezierEasing(0.2f, 0.2f, 0.2f, 0.2f))
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// Scroll listener
|
|
86
|
+
LaunchedEffect(text) {
|
|
87
|
+
if (text != null) {
|
|
88
|
+
coroutineScope.launch {
|
|
89
|
+
scrollStateFlow.collectLatest { newScroll ->
|
|
90
|
+
isExpanded = newScroll <= lastScrollPosition
|
|
91
|
+
delay(100.milliseconds)
|
|
92
|
+
lastScrollPosition = newScroll
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
LaunchedEffect(scrollPosition) {
|
|
99
|
+
scrollStateFlow.value = scrollPosition
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Box(
|
|
103
|
+
modifier = Modifier
|
|
104
|
+
.fillMaxSize()
|
|
105
|
+
.padding(end = 12.dp, bottom = bottom)
|
|
106
|
+
.offset(y = -keyboardSize)
|
|
107
|
+
.background(Color.Transparent),
|
|
108
|
+
contentAlignment = if (position == FABPosition.END) Alignment.BottomEnd else Alignment.BottomCenter,
|
|
109
|
+
) {
|
|
110
|
+
FloatingActionButton(
|
|
111
|
+
onClick = onClick,
|
|
112
|
+
shape = AlertDialogDefaults.shape,
|
|
113
|
+
containerColor = containerColor,
|
|
114
|
+
contentColor = contentColor,
|
|
115
|
+
elevation = elevation,
|
|
116
|
+
interactionSource = remember { MutableInteractionSource() },
|
|
117
|
+
modifier = Modifier
|
|
118
|
+
.sizeIn(minWidth = width)
|
|
119
|
+
.height(baseSize)
|
|
120
|
+
.conditional(IsShowBaseLineDebug) {
|
|
121
|
+
border(1.dp, Colors.blue_03)
|
|
122
|
+
}
|
|
123
|
+
) {
|
|
124
|
+
Row(
|
|
125
|
+
modifier = Modifier.padding(
|
|
126
|
+
start = if (isExpanded) FabPaddingHorizontal else 0.dp,
|
|
127
|
+
end = if (isExpanded) FabPaddingHorizontal else 0.dp,
|
|
128
|
+
),
|
|
129
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
130
|
+
horizontalArrangement = if (isExpanded) Arrangement.Start else Arrangement.Center
|
|
131
|
+
) {
|
|
132
|
+
Icon(
|
|
133
|
+
source = icon,
|
|
134
|
+
size = if (size == FABSize.SMALL) FabIconSizeSmall else FabIconSizeDefault,
|
|
135
|
+
color = iconColor ?: Color.White
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
AnimatedVisibility(
|
|
139
|
+
visible = isExpanded,
|
|
140
|
+
enter = fadeInExpandAnimation,
|
|
141
|
+
exit = fadeOutShrinkAnimation,
|
|
142
|
+
) {
|
|
143
|
+
Row {
|
|
144
|
+
Spacer(Modifier.width(12.dp))
|
|
145
|
+
Text(
|
|
146
|
+
text = text ?: "",
|
|
147
|
+
color = Color.White,
|
|
148
|
+
style = Typography.actionDefaultBold
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private val fadeOutShrinkAnimation = fadeOut(
|
|
158
|
+
animationSpec = tween(100, easing = LinearEasing)
|
|
159
|
+
) + shrinkHorizontally(
|
|
160
|
+
animationSpec = tween(100, easing = LinearEasing),
|
|
161
|
+
shrinkTowards = Alignment.Start
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
private val fadeInExpandAnimation = fadeIn(
|
|
165
|
+
animationSpec = tween(100, easing = LinearEasing)
|
|
166
|
+
) + expandHorizontally(
|
|
167
|
+
animationSpec = tween(100, easing = LinearEasing),
|
|
168
|
+
expandFrom = Alignment.Start
|
|
169
|
+
)
|