@momo-kits/native-kits 0.164.1-beta.13 → 0.164.1-beta.14-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.
Files changed (140) hide show
  1. package/build.gradle.kts +11 -0
  2. package/compose/build.gradle.kts +190 -0
  3. package/compose/build.gradle.kts.backup +190 -0
  4. package/compose/compose.podspec +47 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +150 -0
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +106 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +826 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +134 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +214 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +216 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +182 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +328 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +97 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +217 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +470 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +131 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +203 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +175 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +263 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +52 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +38 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +39 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +299 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +103 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +191 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +386 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +605 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +157 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +39 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +156 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +320 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +81 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +309 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +360 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +15 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +69 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +105 -0
  123. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  124. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +125 -0
  125. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  126. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +35 -0
  127. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  128. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +220 -0
  129. package/gradle/libs.versions.toml +69 -0
  130. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  131. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  132. package/gradle.properties +26 -0
  133. package/gradlew +252 -0
  134. package/gradlew.bat +94 -0
  135. package/ios/native-kits.podspec +1 -1
  136. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  137. package/ios-demo/README.md +120 -0
  138. package/local.properties +8 -0
  139. package/package.json +1 -1
  140. package/settings.gradle.kts +64 -0
@@ -0,0 +1,157 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.animation.AnimatedContentTransitionScope
4
+ import androidx.compose.animation.AnimatedVisibilityScope
5
+ import androidx.compose.animation.EnterExitState
6
+ import androidx.compose.animation.EnterTransition
7
+ import androidx.compose.animation.ExitTransition
8
+ import androidx.compose.animation.core.LinearEasing
9
+ import androidx.compose.animation.core.animateFloat
10
+ import androidx.compose.animation.core.tween
11
+ import androidx.compose.animation.slideInHorizontally
12
+ import androidx.compose.animation.slideOutHorizontally
13
+ import androidx.compose.foundation.layout.Box
14
+ import androidx.compose.foundation.layout.fillMaxSize
15
+ import androidx.compose.runtime.Composable
16
+ import androidx.compose.runtime.DisposableEffect
17
+ import androidx.compose.runtime.Stable
18
+ import androidx.compose.runtime.derivedStateOf
19
+ import androidx.compose.runtime.getValue
20
+ import androidx.compose.runtime.mutableStateMapOf
21
+ import androidx.compose.runtime.remember
22
+ import androidx.compose.runtime.staticCompositionLocalOf
23
+ import androidx.compose.ui.Modifier
24
+ import androidx.compose.ui.draw.drawBehind
25
+ import androidx.compose.ui.geometry.Offset
26
+ import androidx.compose.ui.geometry.Size
27
+ import androidx.compose.ui.graphics.Brush
28
+ import androidx.compose.ui.graphics.Color
29
+ import androidx.compose.ui.platform.LocalDensity
30
+ import androidx.compose.ui.unit.IntOffset
31
+ import androidx.compose.ui.unit.dp
32
+ import androidx.navigation.NavBackStackEntry
33
+ import androidx.navigation.NavDestination.Companion.hasRoute
34
+ import vn.momo.kits.const.Colors
35
+ import kotlin.math.roundToInt
36
+
37
+ internal const val STACK_TRANSITION_DURATION = 300
38
+
39
+ // Ported 1:1 from the RN kit's `forStackTransitionWithOverlay` so both kits look identical.
40
+ private const val PARALLAX_RATIO = 0.3f
41
+ private const val SCRIM_MAX_ALPHA = 0.6f
42
+ private const val EDGE_SHADOW_MAX_ALPHA = 0.3f
43
+ private val EDGE_SHADOW_WIDTH = 8.dp
44
+
45
+ /**
46
+ * Kill switch for the swipe-back gesture. Set to `false` to restore pre-DS-760 behaviour (kit
47
+ * back handling always wins, iOS edge pan inert) without a kit release. Screen transitions stay
48
+ * on the new parallax curve either way.
49
+ */
50
+ object SwipeBackConfig {
51
+ var enabled: Boolean = true
52
+ }
53
+
54
+ // LinearEasing is mandatory, not a style choice: SeekableTransitionState.seekTo() maps the drag
55
+ // fraction straight onto the animation timeline, so any curve would decouple the card from the
56
+ // finger mid-gesture.
57
+ private val stackSpec = tween<IntOffset>(STACK_TRANSITION_DURATION, easing = LinearEasing)
58
+
59
+ internal fun AnimatedContentTransitionScope<NavBackStackEntry>.stackEnter(): EnterTransition =
60
+ slideInHorizontally(stackSpec) { it }
61
+
62
+ internal fun AnimatedContentTransitionScope<NavBackStackEntry>.stackExit(): ExitTransition =
63
+ if (targetState.destination.hasRoute<DynamicDialogRoute>()) ExitTransition.None
64
+ else slideOutHorizontally(stackSpec) { -(it * PARALLAX_RATIO).roundToInt() }
65
+
66
+ internal fun AnimatedContentTransitionScope<NavBackStackEntry>.stackPopEnter(): EnterTransition =
67
+ if (initialState.destination.hasRoute<DynamicDialogRoute>()) EnterTransition.None
68
+ else slideInHorizontally(stackSpec) { -(it * PARALLAX_RATIO).roundToInt() }
69
+
70
+ internal fun AnimatedContentTransitionScope<NavBackStackEntry>.stackPopExit(): ExitTransition =
71
+ slideOutHorizontally(stackSpec) { it }
72
+
73
+ /**
74
+ * Tracks which screen ids are currently composed so a screen can tell whether it is the top card
75
+ * or the one underneath.
76
+ *
77
+ * Screen ids come from a single monotonic process-wide counter and NavHost keeps both entries
78
+ * composed for the whole transition segment, so "highest composed stack id" is stable in both
79
+ * directions. `currentBackStackEntry` is not usable here: it flips at frame 0 of a pop, which
80
+ * would make the scrim vanish instantly instead of fading.
81
+ */
82
+ @Stable
83
+ internal class StackTransitionCoordinator {
84
+ private val composedRoutes = mutableStateMapOf<Int, Boolean>()
85
+
86
+ val topStackId: Int by derivedStateOf {
87
+ composedRoutes.entries.filter { it.value }.maxOfOrNull { it.key } ?: -1
88
+ }
89
+
90
+ fun attach(id: Int, isStackRoute: Boolean) {
91
+ composedRoutes[id] = isStackRoute
92
+ }
93
+
94
+ fun detach(id: Int) {
95
+ composedRoutes.remove(id)
96
+ }
97
+ }
98
+
99
+ internal val LocalStackTransition = staticCompositionLocalOf { StackTransitionCoordinator() }
100
+
101
+ /** Draws the covered-screen scrim and the top card's left-edge shadow around a stack destination. */
102
+ @Composable
103
+ internal fun AnimatedVisibilityScope.StackTransitionDecor(
104
+ id: Int,
105
+ isStackRoute: Boolean,
106
+ content: @Composable () -> Unit,
107
+ ) {
108
+ val coordinator = LocalStackTransition.current
109
+ coordinator.attach(id, isStackRoute)
110
+ DisposableEffect(coordinator, id) {
111
+ onDispose { coordinator.detach(id) }
112
+ }
113
+
114
+ val isTopCard = isStackRoute && id == coordinator.topStackId
115
+ val isCovered = isStackRoute && !isTopCard
116
+
117
+ val scrimAlpha = transition.animateFloat(
118
+ transitionSpec = { tween(STACK_TRANSITION_DURATION, easing = LinearEasing) },
119
+ label = "stackScrimAlpha",
120
+ ) { state -> if (isCovered && state != EnterExitState.Visible) SCRIM_MAX_ALPHA else 0f }
121
+
122
+ val shadowAlpha = transition.animateFloat(
123
+ transitionSpec = { tween(STACK_TRANSITION_DURATION, easing = LinearEasing) },
124
+ label = "stackEdgeShadowAlpha",
125
+ ) { state -> if (isTopCard && state == EnterExitState.Visible) EDGE_SHADOW_MAX_ALPHA else 0f }
126
+
127
+ val shadowWidthPx = with(LocalDensity.current) { EDGE_SHADOW_WIDTH.toPx() }
128
+ val shadowBrush = remember(shadowWidthPx) {
129
+ Brush.horizontalGradient(
130
+ colors = listOf(Color.Transparent, Colors.black_20),
131
+ startX = -shadowWidthPx,
132
+ endX = 0f,
133
+ )
134
+ }
135
+
136
+ Box(Modifier.fillMaxSize()) {
137
+ content()
138
+
139
+ if (isStackRoute) {
140
+ Box(
141
+ Modifier
142
+ .matchParentSize()
143
+ // Both alphas are read inside drawBehind so a drag only re-draws; reading them
144
+ // in the composable body would recompose the whole screen every gesture frame.
145
+ .drawBehind {
146
+ drawRect(
147
+ brush = shadowBrush,
148
+ topLeft = Offset(-shadowWidthPx, 0f),
149
+ size = Size(shadowWidthPx, size.height),
150
+ alpha = shadowAlpha.value,
151
+ )
152
+ drawRect(Colors.pink_09, alpha = scrimAlpha.value)
153
+ }
154
+ )
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,39 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.LaunchedEffect
5
+ import androidx.compose.runtime.snapshotFlow
6
+ import vn.momo.kits.platform.getScreenDimensions
7
+ import vn.momo.kits.platform.rememberBackGestureProgress
8
+ import vn.momo.maxapi.IMaxApi
9
+
10
+ // RN fires the tick at a flat 100 px of thumb travel; keep it unscaled so the physical distance is
11
+ // the same on every device.
12
+ private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 50f
13
+
14
+ /**
15
+ * Fires one light tick once the back gesture has travelled ~100dp, re-arming when the finger drops
16
+ * back below the threshold. iOS only — [rememberBackGestureProgress] is always `null` on Android.
17
+ */
18
+ @Composable
19
+ internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
20
+ if (maxApi == null) return
21
+
22
+ val progress = rememberBackGestureProgress()
23
+ val screenWidth = getScreenDimensions().width.toFloat()
24
+
25
+ LaunchedEffect(maxApi, screenWidth) {
26
+ if (screenWidth <= 0f) return@LaunchedEffect
27
+ val threshold = SWIPE_BACK_HAPTIC_THRESHOLD_DP / screenWidth
28
+ var isSendHaptic = false
29
+ snapshotFlow { progress.value }.collect { fraction ->
30
+ val travelled = fraction ?: 0f
31
+ if (travelled >= threshold && !isSendHaptic) {
32
+ runCatching { maxApi.triggerEventVibration("light", callback = null) }
33
+ isSendHaptic = true
34
+ } else if (travelled < threshold && isSendHaptic) {
35
+ isSendHaptic = false
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,156 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.animation.core.tween
4
+ import androidx.compose.animation.fadeIn
5
+ import androidx.compose.animation.fadeOut
6
+ import androidx.compose.animation.scaleIn
7
+ import androidx.compose.animation.scaleOut
8
+ import androidx.compose.foundation.background
9
+ import androidx.compose.foundation.layout.*
10
+ import androidx.compose.runtime.Composable
11
+ import androidx.compose.runtime.LaunchedEffect
12
+ import androidx.compose.ui.Alignment
13
+ import androidx.compose.ui.Modifier
14
+ import androidx.compose.ui.unit.dp
15
+ import androidx.navigation.compose.NavHost
16
+ import androidx.navigation.compose.composable
17
+ import androidx.navigation.compose.rememberNavController
18
+ import vn.momo.kits.const.AppNavigationBar
19
+ import vn.momo.kits.const.AppTheme
20
+ import vn.momo.kits.const.Spacing
21
+ import vn.momo.kits.navigation.KeyboardOptions
22
+ import vn.momo.kits.navigation.LocalNavigation
23
+ import vn.momo.kits.navigation.LocalNavigator
24
+ import vn.momo.kits.navigation.NavigationOptions
25
+ import vn.momo.kits.navigation.StackScreen
26
+ import vn.momo.kits.navigation.component.HeaderType
27
+
28
+ private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
29
+ internal fun setBottomTabOption(index: Int, options: NavigationOptions){
30
+ if (index in bottomTabOptionItems.indices) {
31
+ bottomTabOptionItems[index] = options
32
+ }
33
+ }
34
+ internal fun getBottomTabOption(index: Int): NavigationOptions? {
35
+ return if (index in bottomTabOptionItems.indices) {
36
+ bottomTabOptionItems[index]
37
+ } else null
38
+ }
39
+
40
+ @Composable
41
+ fun BottomTab(
42
+ items: List<BottomTabItem>,
43
+ floatingButton: BottomTabFloatingButton? = null
44
+ ) {
45
+ val navigation = LocalNavigation.current
46
+ val navigator = LocalNavigator.current
47
+ val navController = rememberNavController()
48
+
49
+ bottomTabOptionItems = items.mapIndexed { index, item ->
50
+ item.options ?: NavigationOptions()
51
+ }.toMutableList()
52
+
53
+
54
+ LaunchedEffect(Unit){
55
+ navigation.markAsBottomTabRoot()
56
+ navigation.setOptions(
57
+ headerType = HeaderType.None,
58
+ keyboardOptions = KeyboardOptions(useAvoidKeyboard = false)
59
+ )
60
+ }
61
+
62
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
63
+ Box(modifier = Modifier
64
+ .fillMaxSize()
65
+ ) {
66
+ NavHost(
67
+ navController = navController,
68
+ startDestination = "option0"
69
+ ) {
70
+ items.forEachIndexed { index, item ->
71
+ composable(
72
+ route = "option$index",
73
+ enterTransition = {
74
+ fadeIn(animationSpec = tween(200)) +
75
+ scaleIn(
76
+ initialScale = 0.97f,
77
+ animationSpec = tween(200)
78
+ )
79
+ },
80
+ exitTransition = {
81
+ fadeOut(animationSpec = tween(200)) +
82
+ scaleOut(
83
+ targetScale = 0.97f,
84
+ animationSpec = tween(200)
85
+ )
86
+ },
87
+ popEnterTransition = {
88
+ fadeIn(animationSpec = tween(200)) +
89
+ scaleIn(
90
+ initialScale = 0.97f,
91
+ animationSpec = tween(200)
92
+ )
93
+ },
94
+ popExitTransition = {
95
+ fadeOut(animationSpec = tween(200)) +
96
+ scaleOut(
97
+ targetScale = 0.97f,
98
+ animationSpec = tween(200)
99
+ )
100
+ }
101
+ ) {
102
+ val option = getBottomTabOption(index)
103
+
104
+ StackScreen(
105
+ content = item.screen,
106
+ navigationOptions = option,
107
+ bottomTabIndex = index,
108
+ onBackHandler = {
109
+ if (index != 0) {
110
+ navController.popBackStack(
111
+ navController.graph.startDestinationRoute!!,
112
+ inclusive = false
113
+ )
114
+ } else {
115
+ navigator.pop()
116
+ }
117
+ }
118
+ )
119
+ }
120
+ }
121
+ }
122
+ }
123
+ Column {
124
+ BottomTabBar(
125
+ items = items,
126
+ floatingButton = floatingButton,
127
+ navController = navController,
128
+ onTabSelected = {
129
+ val currentRoute = navController.currentBackStackEntry?.destination?.route
130
+ val targetRoute = "option$it"
131
+ if (currentRoute != targetRoute) {
132
+ navController.navigate(targetRoute)
133
+ }
134
+ }
135
+ )
136
+ Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
137
+ }
138
+ }
139
+ }
140
+
141
+ data class BottomTabItem(
142
+ val name: String,
143
+ val label: String,
144
+ val icon: String,
145
+ val showDot: Boolean = false,
146
+ val badgeLabel: String? = null,
147
+ val screen: @Composable () -> Unit,
148
+ val options: NavigationOptions? = null,
149
+ val initialParams: Any? = null
150
+ )
151
+
152
+ data class BottomTabFloatingButton(
153
+ val icon: String,
154
+ val label: String,
155
+ val onPress: () -> Unit,
156
+ )
@@ -0,0 +1,225 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.animation.core.FastOutSlowInEasing
4
+ import androidx.compose.animation.core.animateDpAsState
5
+ import androidx.compose.animation.core.tween
6
+ import androidx.compose.foundation.background
7
+ import androidx.compose.foundation.border
8
+ import androidx.compose.foundation.layout.*
9
+ import androidx.compose.foundation.shape.CircleShape
10
+ import androidx.compose.foundation.shape.RoundedCornerShape
11
+ import androidx.compose.runtime.Composable
12
+ import androidx.compose.runtime.getValue
13
+ import androidx.compose.ui.Alignment
14
+ import androidx.compose.ui.Modifier
15
+ import androidx.compose.ui.draw.clip
16
+ import androidx.compose.ui.graphics.Brush
17
+ import androidx.compose.ui.graphics.Color
18
+ import androidx.compose.ui.text.style.TextAlign
19
+ import androidx.compose.ui.text.style.TextOverflow
20
+ import androidx.compose.ui.unit.dp
21
+ import androidx.navigation.NavController
22
+ import androidx.navigation.compose.currentBackStackEntryAsState
23
+ import vn.momo.kits.application.IsShowBaseLineDebug
24
+ import vn.momo.kits.components.*
25
+ import vn.momo.kits.const.*
26
+ import vn.momo.kits.modifier.InternalApi
27
+ import vn.momo.kits.modifier.conditional
28
+ import vn.momo.kits.modifier.noFeedbackClickable
29
+ import vn.momo.kits.platform.getScreenDimensions
30
+
31
+ internal val floatingButtonWidth = 75.dp
32
+ @InternalApi
33
+ const val BOTTOM_TAB_BAR_HEIGHT = 64
34
+
35
+ @Composable
36
+ internal fun BottomTabBar(
37
+ items: List<BottomTabItem>,
38
+ floatingButton: BottomTabFloatingButton? = null,
39
+ navController: NavController,
40
+ onTabSelected: (Int) -> Unit,
41
+ ) {
42
+ val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
43
+ val selectedIndex = items.indexOfFirst { "option${items.indexOf(it)}" == currentRoute }
44
+
45
+ val screenWidth = getScreenDimensions().width.dp
46
+ val mid = items.size / 2
47
+
48
+ val tabWidth = if (floatingButton != null) {
49
+ (screenWidth - floatingButtonWidth) / items.size
50
+ } else {
51
+ screenWidth / items.size
52
+ }
53
+ val adjustSize = if(floatingButton != null && selectedIndex >= mid) floatingButtonWidth else 0.dp
54
+ val targetOffset = tabWidth * selectedIndex + tabWidth / 2 - screenWidth / 2 + adjustSize
55
+
56
+ val indicatorOffsetX by animateDpAsState(
57
+ targetValue = targetOffset,
58
+ animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
59
+ label = "IndicatorX"
60
+ )
61
+
62
+ Box(
63
+ contentAlignment = Alignment.BottomCenter
64
+ ) {
65
+ Row(
66
+ modifier = Modifier
67
+ .fillMaxWidth()
68
+ .height(BOTTOM_TAB_BAR_HEIGHT.dp)
69
+ .background(AppTheme.current.colors.background.surface)
70
+ .conditional(IsShowBaseLineDebug) {
71
+ border(1.dp, Colors.blue_03)
72
+ },
73
+ horizontalArrangement = Arrangement.SpaceAround,
74
+ ) {
75
+ if (floatingButton == null) {
76
+ RenderTabBarItem(items, 0, items.size, selectedIndex, onTabSelected)
77
+ } else {
78
+ RenderTabBarItem(items, 0, mid, selectedIndex, onTabSelected)
79
+ Spacer(modifier = Modifier.width(floatingButtonWidth).padding(horizontal = Spacing.XXS))
80
+ RenderTabBarItem(items, mid, items.size, selectedIndex, onTabSelected)
81
+ }
82
+ }
83
+
84
+ Box(modifier = Modifier
85
+ .offset(x = indicatorOffsetX, y = (-BOTTOM_TAB_BAR_HEIGHT + 2).dp)
86
+ .height(2.dp)
87
+ .width(44.dp)
88
+ .background(
89
+ color = AppTheme.current.colors.primary ,
90
+ shape = RoundedCornerShape(bottomStart = 2.dp, bottomEnd = 2.dp)
91
+ )
92
+ )
93
+
94
+ Box(modifier = Modifier
95
+ .fillMaxWidth()
96
+ .height(6.dp)
97
+ .offset(x = 0.dp, y = (-BOTTOM_TAB_BAR_HEIGHT).dp)
98
+ .background(
99
+ brush = Brush.verticalGradient(
100
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.05f))
101
+ )
102
+ )
103
+ )
104
+ floatingButton?.let { FloatingButton(it) }
105
+ }
106
+ }
107
+
108
+ @Composable
109
+ internal fun RowScope.RenderTabBarItem(
110
+ items: List<BottomTabItem>,
111
+ fromIndex: Int,
112
+ toIndex: Int,
113
+ selectedIndex: Int,
114
+ onTabSelected: (Int) -> Unit
115
+ ){
116
+ for (index in fromIndex until toIndex) {
117
+ Box(modifier = Modifier.weight(1f)) {
118
+ TabBarItem(
119
+ item = items[index],
120
+ selected = index == selectedIndex,
121
+ onClick = { onTabSelected(index) }
122
+ )
123
+ }
124
+ }
125
+ }
126
+
127
+ @Composable
128
+ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
129
+ fun isNumber(label: String): Boolean {
130
+ val numberRegex = "^\\d+$".toRegex()
131
+ return numberRegex.matches(label)
132
+ }
133
+ fun formatLabel(label: String): String? {
134
+ if (isNumber(label) && label.toInt() == 0) {
135
+ return null
136
+ }
137
+ return label
138
+ }
139
+ val badgeLabel = item.badgeLabel?.let { formatLabel(it) }
140
+
141
+ Box(modifier = Modifier
142
+ .fillMaxSize()
143
+ .padding(horizontal = Spacing.XXS)
144
+ .conditional(IsShowBaseLineDebug) {
145
+ border(1.dp, Colors.blue_03)
146
+ }
147
+ .noFeedbackClickable {
148
+ onClick()
149
+ },
150
+ contentAlignment = Alignment.BottomStart
151
+ ){
152
+ Column(
153
+ modifier = Modifier
154
+ .fillMaxSize()
155
+ .padding(horizontal = Spacing.S, vertical = Spacing.S)
156
+ .noFeedbackClickable {
157
+ onClick()
158
+ },
159
+ horizontalAlignment = Alignment.CenterHorizontally,
160
+ verticalArrangement = Arrangement.Center
161
+ ) {
162
+ Icon(
163
+ source = item.icon,
164
+ size = 28.dp,
165
+ modifier = Modifier.padding(Spacing.XS),
166
+ color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint)
167
+ Text(
168
+ text = item.label,
169
+ color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint,
170
+ style = if (selected) Typography.labelXsMedium else Typography.descriptionXsRegular,
171
+ maxLines = 1,
172
+ overflow = TextOverflow.Ellipsis
173
+ )
174
+ }
175
+ if (badgeLabel != null) {
176
+ if (badgeLabel.isEmpty())
177
+ BadgeDot(
178
+ size = DotSize.Small,
179
+ modifier = Modifier.offset(x = 50.dp, y = (-48).dp)
180
+ )
181
+ else
182
+ Badge(badgeLabel, modifier = Modifier.offset(x = 44.dp, y = (-42).dp))
183
+ }
184
+ }
185
+ }
186
+
187
+ @Composable
188
+ internal fun FloatingButton(data: BottomTabFloatingButton) {
189
+ Column(
190
+ modifier = Modifier
191
+ .width(floatingButtonWidth)
192
+ .padding(horizontal = Spacing.XXS, vertical = Spacing.S)
193
+ .conditional(IsShowBaseLineDebug) {
194
+ border(1.dp, Colors.blue_03)
195
+ }
196
+ .noFeedbackClickable {
197
+ data.onPress()
198
+ },
199
+ verticalArrangement = Arrangement.Bottom,
200
+ horizontalAlignment = Alignment.CenterHorizontally
201
+
202
+ ){
203
+ CurvedContainer()
204
+ Box(
205
+ modifier = Modifier
206
+ .size(48.dp)
207
+ .clip(CircleShape)
208
+ .background(AppTheme.current.colors.primary),
209
+ contentAlignment = Alignment.Center
210
+ ) {
211
+ Icon(source = data.icon, color = Color.White, size = 28.dp)
212
+ }
213
+ Spacer(modifier = Modifier.height(4.dp))
214
+ Text(
215
+ text = data.label,
216
+ style = Typography.labelXsMedium,
217
+ color = Colors.black_01,
218
+ textAlign = TextAlign.Center,
219
+ modifier = Modifier
220
+ .height(14.dp)
221
+ .fillMaxWidth()
222
+ .background(AppTheme.current.colors.primary, shape = RoundedCornerShape(size = Radius.XL))
223
+ )
224
+ }
225
+ }
@@ -0,0 +1,86 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.foundation.Canvas
4
+ import androidx.compose.foundation.layout.Box
5
+ import androidx.compose.foundation.layout.height
6
+ import androidx.compose.foundation.layout.offset
7
+ import androidx.compose.foundation.layout.width
8
+ import androidx.compose.runtime.Composable
9
+ import androidx.compose.ui.Alignment
10
+ import androidx.compose.ui.Modifier
11
+ import androidx.compose.ui.graphics.Path
12
+ import androidx.compose.ui.platform.LocalDensity
13
+ import androidx.compose.ui.unit.Dp
14
+ import androidx.compose.ui.unit.IntOffset
15
+ import androidx.compose.ui.unit.dp
16
+ import androidx.compose.ui.zIndex
17
+ import vn.momo.kits.const.AppTheme
18
+
19
+ @Composable
20
+ internal fun CurvedContainer(
21
+ circleSize: Dp = 48.dp,
22
+ tabBarItemIconSize: Dp = 28.dp,
23
+ circleOverSize: Dp = 2.dp,
24
+ width: Dp = 71.dp,
25
+ height: Dp = 16.dp
26
+ ) {
27
+ val theme = AppTheme.current
28
+ val density = LocalDensity.current
29
+ val widthPx = with(density) { width.toPx()}
30
+
31
+ val circleSizePx = with(density) { circleSize.toPx() }
32
+ val tabBarItemIconSizePx = with(density) { tabBarItemIconSize.toPx() }
33
+ val circleOverSizePx = with(density) { circleOverSize.toPx() }
34
+
35
+ val circlePositionTop = circleSizePx - tabBarItemIconSizePx - 10f
36
+ val circleOverTop = circlePositionTop + circleOverSizePx
37
+ val circleR = circleSizePx / 2
38
+ val circleOverR = circleR + circleOverSizePx
39
+
40
+ val centerX = widthPx / 2
41
+
42
+ val curvedPath = getPath(
43
+ circleOverTop = circleOverTop,
44
+ centerX = centerX,
45
+ circleOverR = circleOverR,
46
+ widthPx = widthPx
47
+ )
48
+
49
+ Box(
50
+ contentAlignment = Alignment.BottomCenter
51
+ ){
52
+ Canvas(
53
+ modifier = Modifier
54
+ .width(width)
55
+ .height(height)
56
+ .offset{ IntOffset(x = 0, y = 24) }
57
+ .zIndex(-1f)
58
+ ) {
59
+ drawPath(path = curvedPath, color = theme.colors.background.surface)
60
+ }
61
+ }
62
+ }
63
+
64
+ private fun getPath(circleOverTop: Float, centerX: Float, circleOverR: Float, widthPx: Float): Path{
65
+ return Path().apply {
66
+ moveTo(0f, circleOverTop)
67
+ lineTo(centerX - circleOverR - 40f, circleOverTop)
68
+
69
+ cubicTo(
70
+ centerX - circleOverR, circleOverTop,
71
+ centerX - circleOverR, 0f,
72
+ centerX, 0f
73
+ )
74
+
75
+ cubicTo(
76
+ centerX + circleOverR, 0f,
77
+ centerX + circleOverR, circleOverTop,
78
+ centerX + circleOverR + 40f, circleOverTop
79
+ )
80
+
81
+ lineTo(widthPx, circleOverTop)
82
+ lineTo(widthPx, 60f)
83
+ lineTo(0f, 60f)
84
+ close()
85
+ }
86
+ }