@momo-kits/native-kits 0.156.6-beta.4 → 0.156.6-dialog.1-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 (128) hide show
  1. package/build.gradle.kts +11 -0
  2. package/compose/MoMoComposeKits.podspec +54 -0
  3. package/compose/build.gradle.kts +183 -0
  4. package/compose/build.gradle.kts.backup +183 -0
  5. package/compose/compose.podspec +48 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +270 -0
  7. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  8. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  9. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  10. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  11. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  12. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +57 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +95 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +201 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +222 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +48 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +305 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +720 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +121 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +405 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +83 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +340 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +348 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +94 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +136 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +63 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +148 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +185 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +116 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +447 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +172 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +255 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +231 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +234 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +254 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +241 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +364 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +92 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +352 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +103 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +70 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +17 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +96 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +96 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +92 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +130 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +214 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +177 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +205 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +239 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +191 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +13 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +189 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +285 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +59 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +68 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +14 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +50 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +238 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +118 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +98 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +209 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +227 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +484 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +162 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +226 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +187 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +277 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +80 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +306 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +31 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +370 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +129 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +63 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +62 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +88 -0
  107. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +325 -0
  108. package/gradle/libs.versions.toml +57 -0
  109. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  110. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  111. package/gradle.properties +26 -0
  112. package/gradlew +252 -0
  113. package/gradlew.bat +94 -0
  114. package/ios/Image/Image.swift +1 -5
  115. package/local.properties +8 -0
  116. package/package.json +1 -1
  117. package/settings.gradle.kts +52 -0
  118. package/example/ios/Example.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  119. package/example/ios/Example.xcworkspace/xcuserdata/sophia.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  120. package/example/ios/Example.xcworkspace/xcuserdata/sophia.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  121. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
  122. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
  123. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
  124. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
  125. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
  126. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +0 -58
  127. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/lottie-ios.xcscheme +0 -58
  128. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sophia.xcuserdatad/xcschemes/xcschememanagement.plist +0 -46
@@ -0,0 +1,227 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.foundation.layout.Box
4
+ import androidx.compose.foundation.layout.fillMaxSize
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.staticCompositionLocalOf
7
+ import androidx.compose.ui.Alignment
8
+ import androidx.compose.ui.Modifier
9
+ import androidx.navigation.NavController
10
+ import kotlinx.coroutines.CoroutineScope
11
+ import kotlinx.coroutines.Dispatchers
12
+ import kotlinx.coroutines.SupervisorJob
13
+ import kotlinx.coroutines.cancel
14
+ import kotlinx.coroutines.delay
15
+ import kotlinx.coroutines.launch
16
+ import kotlinx.serialization.Serializable
17
+ import vn.momo.kits.components.PopupAction
18
+ import vn.momo.kits.components.PopupNotify
19
+ import vn.momo.kits.navigation.BottomHeader.*
20
+ import vn.momo.kits.navigation.component.SnackBar
21
+ import vn.momo.maxapi.IMaxApi
22
+
23
+ class Navigator(
24
+ private val navController: NavController,
25
+ private val maxApi: IMaxApi?
26
+ ) {
27
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
28
+
29
+ fun push(content: @Composable () -> Unit, options: NavigationOptions? = null) {
30
+ val route = DynamicScreenRegistry.register(content, options)
31
+ navController.navigate(DynamicScreenRoute(route.id))
32
+ }
33
+
34
+ fun replace(content: @Composable () -> Unit, options: NavigationOptions? = null) {
35
+ if (navController.previousBackStackEntry != null){
36
+ val latestScreen = DynamicScreenRegistry.getLatestScreen()
37
+ latestScreen?.let { DynamicScreenRegistry.unregisterScreen(it.id) }
38
+ navController.popBackStack()
39
+ push(content, options)
40
+ }
41
+ }
42
+
43
+ fun onBackSafe(callBack: (() -> Unit)? = null) {
44
+ val latestScreen = DynamicScreenRegistry.getLatestScreen()
45
+ val options = latestScreen?.options
46
+ if (options?.onBackHandler != null) {
47
+ options.onBackHandler.invoke()
48
+ return
49
+ }
50
+ val preventBack = options?.headerBackProps?.preventBack
51
+ if (preventBack != null) {
52
+ showModal(
53
+ content = {
54
+ PopupNotify(preventBack.copy(
55
+ primary = PopupAction(
56
+ title = preventBack.primary?.title ?: "",
57
+ onPress = {
58
+ preventBack.primary?.onPress?.invoke()
59
+ pop(2, callBack) // close popup notify, then close the screen
60
+ }
61
+ )
62
+ ))
63
+ }
64
+ )
65
+ } else {
66
+ pop(1, callBack)
67
+ }
68
+ }
69
+
70
+ fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
71
+ scope.launch {
72
+ repeat(count) {
73
+ dismissEntry()
74
+ }
75
+ callBack?.invoke()
76
+ }
77
+ }
78
+
79
+ private suspend fun dismissEntry() {
80
+ if (navController.previousBackStackEntry != null) {
81
+ navController.popBackStack()
82
+ delay(300L)
83
+ DynamicScreenRegistry.getLatestScreen()?.let {
84
+ DynamicScreenRegistry.unregisterScreen(it.id)
85
+ }
86
+ } else {
87
+ maxApi?.dismiss { }
88
+ }
89
+ }
90
+
91
+ fun present(content: @Composable () -> Unit, options: NavigationOptions? = null) {
92
+ val route = DynamicScreenRegistry.register(content, options)
93
+ navController.navigate(DynamicDialogRoute(route.id))
94
+ }
95
+
96
+ fun reset(content: @Composable () -> Unit, options: NavigationOptions? = null) {
97
+ DynamicScreenRegistry.unregisterAll()
98
+
99
+ val route = DynamicScreenRegistry.register(content, options)
100
+ navController.navigate(DynamicScreenRoute(route.id)) {
101
+ popUpTo(0) { inclusive = true }
102
+ }
103
+ }
104
+
105
+ fun showModal(
106
+ content: @Composable () -> Unit,
107
+ barrierDismissible: Boolean = true,
108
+ onDismiss: (() -> Unit)? = null
109
+ ){
110
+ val route = DynamicScreenRegistry.register({
111
+ ModalScreen(
112
+ content = content,
113
+ barrierDismissible = barrierDismissible,
114
+ onDismiss = onDismiss
115
+ )
116
+ }, null)
117
+ navController.navigate(DynamicModalRoute(route.id))
118
+ }
119
+
120
+ fun showBottomSheet(
121
+ content: @Composable () -> Unit,
122
+ isSurface: Boolean = false,
123
+ barrierDismissible: Boolean = true,
124
+ onDismiss: (() -> Unit)? = null,
125
+ bottomSheetHeader: BottomHeader? = null
126
+ ){
127
+ val route = DynamicScreenRegistry.register({
128
+ BottomSheet(
129
+ content = content,
130
+ header = bottomSheetHeader ?: Title(),
131
+ isSurface = isSurface,
132
+ barrierDismissible = barrierDismissible,
133
+ onDismiss = onDismiss
134
+ )
135
+ }, null)
136
+ navController.navigate(DynamicBottomSheetRoute(route.id))
137
+ }
138
+
139
+ fun showSnackBar(snackBar: SnackBar, onDismiss: (() -> Unit)? = null) {
140
+ val capturedSnackBar = snackBar
141
+ val capturedOnDismiss = onDismiss
142
+ val route = DynamicScreenRegistry.register({
143
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
144
+ SnackBar(capturedSnackBar, capturedOnDismiss)
145
+ }
146
+ }, null)
147
+ navController.navigate(DynamicSnackBarRoute(route.id))
148
+ }
149
+
150
+ fun hideSnackBar() {
151
+ scope.launch {
152
+ if (navController.previousBackStackEntry != null) {
153
+ navController.popBackStack()
154
+ delay(250L)
155
+ DynamicScreenRegistry.getLatestScreen()?.let {
156
+ DynamicScreenRegistry.unregisterScreen(it.id)
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+
163
+ fun dispose(){
164
+ scope.cancel()
165
+ }
166
+ }
167
+
168
+ val LocalNavigator = staticCompositionLocalOf<Navigator> {
169
+ error("No NavigationStack provided")
170
+ }
171
+
172
+ @Serializable
173
+ data class DynamicScreenRoute(val id: Int)
174
+
175
+ @Serializable
176
+ data class DynamicDialogRoute(val id: Int)
177
+
178
+ @Serializable
179
+ data class DynamicBottomSheetRoute(val id: Int)
180
+
181
+ @Serializable
182
+ data class DynamicModalRoute(val id: Int)
183
+
184
+ @Serializable
185
+ data class DynamicSnackBarRoute(val id: Int)
186
+
187
+ data class DynamicScreen(
188
+ val id: Int,
189
+ val content: @Composable () -> Unit,
190
+ var options: NavigationOptions? = null
191
+ )
192
+
193
+ object DynamicScreenRegistry {
194
+ private val screens = mutableMapOf<Int, DynamicScreen>()
195
+ private var idCounter = 1
196
+
197
+ fun register(content: @Composable () -> Unit, options: NavigationOptions?): DynamicScreenRoute {
198
+ val id = idCounter++
199
+ screens[id] = DynamicScreen(
200
+ id = id,
201
+ content = content,
202
+ options = options
203
+ )
204
+ return DynamicScreenRoute(id)
205
+ }
206
+
207
+ fun unregisterScreen(id: Int) {
208
+ screens.remove(id)
209
+ }
210
+
211
+ fun getLatestScreen(): DynamicScreen?{
212
+ return screens.entries.lastOrNull()?.value
213
+ }
214
+
215
+ fun unregisterAll(){
216
+ val ids = screens.keys.toList()
217
+ for (id in ids) {
218
+ unregisterScreen(id)
219
+ }
220
+ }
221
+
222
+ fun getScreen(id: Int): DynamicScreen? = screens[id]
223
+
224
+ fun setOptions(id: Int, options: NavigationOptions){
225
+ screens[id]?.options = options
226
+ }
227
+ }
@@ -0,0 +1,484 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.animation.animateColorAsState
4
+ import androidx.compose.animation.core.animateDpAsState
5
+ import androidx.compose.foundation.ScrollState
6
+ import androidx.compose.foundation.background
7
+ import androidx.compose.foundation.border
8
+ import androidx.compose.foundation.gestures.ScrollableState
9
+ import androidx.compose.foundation.layout.Box
10
+ import androidx.compose.foundation.layout.Column
11
+ import androidx.compose.foundation.layout.ColumnScope
12
+ import androidx.compose.foundation.layout.Spacer
13
+ import androidx.compose.foundation.layout.WindowInsets
14
+ import androidx.compose.foundation.layout.aspectRatio
15
+ import androidx.compose.foundation.layout.fillMaxSize
16
+ import androidx.compose.foundation.layout.fillMaxWidth
17
+ import androidx.compose.foundation.layout.height
18
+ import androidx.compose.foundation.layout.ime
19
+ import androidx.compose.foundation.layout.imePadding
20
+ import androidx.compose.foundation.layout.offset
21
+ import androidx.compose.foundation.layout.padding
22
+ import androidx.compose.foundation.lazy.LazyListState
23
+ import androidx.compose.foundation.lazy.rememberLazyListState
24
+ import androidx.compose.foundation.rememberScrollState
25
+ import androidx.compose.foundation.verticalScroll
26
+ import androidx.compose.material.ExperimentalMaterialApi
27
+ import androidx.compose.runtime.Composable
28
+ import androidx.compose.runtime.CompositionLocalProvider
29
+ import androidx.compose.runtime.LaunchedEffect
30
+ import androidx.compose.runtime.getValue
31
+ import androidx.compose.runtime.mutableIntStateOf
32
+ import androidx.compose.runtime.mutableStateOf
33
+ import androidx.compose.runtime.remember
34
+ import androidx.compose.runtime.snapshotFlow
35
+ import androidx.compose.runtime.staticCompositionLocalOf
36
+ import androidx.compose.ui.Alignment
37
+ import androidx.compose.ui.Modifier
38
+ import androidx.compose.ui.graphics.Brush
39
+ import androidx.compose.ui.graphics.Color
40
+ import androidx.compose.ui.graphics.lerp
41
+ import androidx.compose.ui.layout.onGloballyPositioned
42
+ import androidx.compose.ui.platform.LocalDensity
43
+ import androidx.compose.ui.unit.Dp
44
+ import androidx.compose.ui.unit.dp
45
+ import androidx.compose.ui.unit.min
46
+ import androidx.compose.ui.zIndex
47
+ import vn.momo.kits.components.InputSearch
48
+ import vn.momo.kits.const.AppNavigationBar
49
+ import vn.momo.kits.const.AppStatusBar
50
+ import vn.momo.kits.const.AppTheme
51
+ import vn.momo.kits.application.IsShowBaseLineDebug
52
+ import vn.momo.kits.const.Colors
53
+ import vn.momo.kits.const.Spacing
54
+ import vn.momo.kits.modifier.conditional
55
+ import vn.momo.kits.modifier.hideKeyboardOnTap
56
+ import vn.momo.kits.navigation.component.FABPosition
57
+ import vn.momo.kits.navigation.component.FloatingButton
58
+ import vn.momo.kits.navigation.component.HEADER_HEIGHT
59
+ import vn.momo.kits.navigation.component.Header
60
+ import vn.momo.kits.navigation.component.HeaderBackground
61
+ import vn.momo.kits.navigation.component.HeaderRight
62
+ import vn.momo.kits.navigation.component.HeaderType
63
+ import vn.momo.kits.navigation.component.InputSearchType
64
+ import vn.momo.kits.platform.BackHandler
65
+ import vn.momo.kits.platform.getAndroidBuildVersion
66
+
67
+ internal val LocalFooterHeightPx = staticCompositionLocalOf { mutableIntStateOf(0) }
68
+ internal val LocalHeaderRightWidthPx = staticCompositionLocalOf { mutableIntStateOf(0) }
69
+
70
+ @OptIn(ExperimentalMaterialApi::class)
71
+ @Composable
72
+ internal fun StackScreen(
73
+ content: @Composable () -> Unit,
74
+ navigationOptions: NavigationOptions? = null,
75
+ id: Int = -1,
76
+ bottomTabIndex: Int = -1,
77
+ onBackHandler: (() -> Unit)? = null,
78
+ ) {
79
+ val navigator = LocalNavigator.current
80
+ val statusBar = AppStatusBar.current
81
+ val density = LocalDensity.current
82
+ val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions) }
83
+
84
+ val options by navigation.currentOptions
85
+
86
+ val limit = with(density) {
87
+ (statusBar).toPx() + HEADER_HEIGHT
88
+ }.toInt()
89
+
90
+ val (scrollState, scrollInProcess) = if (options.scrollData.scrollState is LazyListState)
91
+ (options.scrollData.scrollState as? LazyListState ?: rememberLazyListState()).proxyScrollState(limit, 15)
92
+ else
93
+ (options.scrollData.scrollState as? ScrollState ?: rememberScrollState()).proxyLimitedScrollState(limit, 15)
94
+
95
+ val footerHeightPx = remember { mutableIntStateOf(0) }
96
+ val headerRightWidthPx = remember { mutableIntStateOf(0) }
97
+
98
+ BackHandler(true) { navigator.onBackSafe() }
99
+
100
+ CompositionLocalProvider(
101
+ StackScreenScrollableState provides options.scrollData.scrollState,
102
+ LocalNavigation provides navigation,
103
+ LocalScrollState provides scrollState,
104
+ LocalOptions provides options,
105
+ LocalFooterHeightPx provides footerHeightPx,
106
+ LocalHeaderRightWidthPx provides headerRightWidthPx
107
+ ) {
108
+ Box(Modifier
109
+ .fillMaxSize()
110
+ .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
111
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
112
+ hideKeyboardOnTap()
113
+ }
114
+ .conditional(options.keyboardOptions.useAvoidKeyboard && getAndroidBuildVersion() > 29) {
115
+ imePadding()
116
+ }
117
+ ) {
118
+ Box(Modifier.zIndex(1f)) {
119
+ HeaderBackground()
120
+ }
121
+
122
+ Box(Modifier.zIndex(5f)) {
123
+ Header(onBackHandler)
124
+ }
125
+
126
+ Column (Modifier.zIndex(6f)) {
127
+ SearchAnimated(isScrollInProgress = scrollInProcess)
128
+ }
129
+
130
+ Column(Modifier.zIndex(2f).fillMaxSize()) {
131
+ MainContent(content = content)
132
+ }
133
+
134
+ Box(Modifier.zIndex(4f).align(Alignment.BottomCenter)) {
135
+ FooterContent()
136
+ }
137
+
138
+ Box(Modifier.zIndex(7f)){
139
+ FloatingContent()
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ @Composable
146
+ fun FloatingContent(){
147
+ val options = LocalOptions.current
148
+ val density = LocalDensity.current
149
+ val footerHeightPx = LocalFooterHeightPx.current
150
+ val scrollState = LocalScrollState.current
151
+
152
+ val fabProps = options.floatingButtonProps ?: return
153
+ val bottomPadding = fabProps.bottom ?:
154
+ (Spacing.M + if (options.footerComponent != null) with(density){ footerHeightPx.intValue.toDp() } else 0.dp)
155
+
156
+ FloatingButton(
157
+ scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
158
+ onClick = fabProps.onClick,
159
+ containerColor = AppTheme.current.colors.primary,
160
+ bottom = bottomPadding,
161
+ icon = fabProps.icon,
162
+ iconColor = fabProps.iconColor,
163
+ text = fabProps.label,
164
+ size = fabProps.size,
165
+ position = fabProps.position ?: FABPosition.END,
166
+ )
167
+ }
168
+
169
+ @Composable
170
+ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
171
+ val options = LocalOptions.current
172
+ val inputSearchType = getInputSearchType(options)
173
+ val density = LocalDensity.current
174
+ val scrollState = LocalScrollState.current
175
+
176
+ Spacer(Modifier.height(
177
+ if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
178
+ AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp)
179
+ )
180
+ if (inputSearchType == InputSearchType.Animated){
181
+ val scrollDp = with(density) { scrollState.value.toDp() }
182
+
183
+ val animatedTopPadding by animateDpAsState(
184
+ targetValue = (HEADER_HEIGHT.dp - scrollDp).coerceIn(0.dp, HEADER_HEIGHT.dp),
185
+ label = "AnimatedTopPadding"
186
+ )
187
+ Spacer(Modifier.height(animatedTopPadding))
188
+ }
189
+ Column (Modifier
190
+ .fillMaxWidth()
191
+ .weight(1f)
192
+ .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
193
+ verticalScroll(scrollState)
194
+ }
195
+ ) {
196
+ ScreenContent(content = content)
197
+ }
198
+
199
+ if (options.footerComponent != null){
200
+ val footerHeight = LocalFooterHeightPx.current
201
+ val footerHeightDp = with(density) { footerHeight.value.toDp() }
202
+ Spacer(Modifier.height(footerHeightDp))
203
+ }
204
+ }
205
+
206
+ @Composable
207
+ fun ScreenContent(content: @Composable () -> Unit){
208
+ val scrollState = LocalScrollState.current
209
+ val options = LocalOptions.current
210
+
211
+ if (options.headerType is HeaderType.Animated){
212
+ val animatedHeader = options.headerType
213
+ Box {
214
+ Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)){
215
+ animatedHeader.composable.invoke(scrollState.value)
216
+ }
217
+ Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)){
218
+ content()
219
+ }
220
+ }
221
+ } else {
222
+ content()
223
+ }
224
+ }
225
+
226
+ @Composable
227
+ fun FooterContent(){
228
+ val options = LocalOptions.current
229
+ if (options.footerComponent != null){
230
+ val ime = WindowInsets.ime
231
+ val density = LocalDensity.current
232
+ val imeBottom = ime.getBottom(density)
233
+ val thresholdPx = with(density) { 50.dp.toPx() }
234
+ val isKeyboardVisible = imeBottom > thresholdPx
235
+ val bottomPadding = min(AppNavigationBar.current, if (isKeyboardVisible) 0.dp else 21.dp)
236
+ Footer(footerComponent = options.footerComponent, bottomPadding = bottomPadding)
237
+ }
238
+ }
239
+
240
+
241
+
242
+ @Composable
243
+ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
244
+ if (footerComponent == null) return
245
+
246
+ val footerHeightPx = LocalFooterHeightPx.current
247
+
248
+ val shadowBrush = remember {
249
+ Brush.verticalGradient(
250
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.05f))
251
+ )
252
+ }
253
+
254
+ Box(Modifier.onGloballyPositioned {
255
+ if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
256
+ }) {
257
+ Box(Modifier
258
+ .fillMaxWidth()
259
+ .background(AppTheme.current.colors.background.surface)
260
+ .conditional(IsShowBaseLineDebug) {
261
+ border(1.dp, Colors.blue_03)
262
+ }
263
+ .padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
264
+ ){
265
+ footerComponent.invoke()
266
+ }
267
+
268
+ Box(modifier = Modifier
269
+ .fillMaxWidth()
270
+ .height(6.dp)
271
+ .offset(x = 0.dp, y = (-6).dp)
272
+ .background(shadowBrush)
273
+ )
274
+ }
275
+ }
276
+
277
+ data class InputSearchLayoutParams(
278
+ val topPadding: Dp,
279
+ val startPadding: Dp,
280
+ val endPadding: Dp
281
+ )
282
+ @Composable
283
+ fun SearchAnimated(
284
+ isScrollInProgress: Boolean
285
+ ) {
286
+ val scrollState = LocalScrollState.current
287
+ val options = LocalOptions.current
288
+ val navigator = LocalNavigator.current
289
+ val density = LocalDensity.current
290
+
291
+ val scrollDp = with(density) { scrollState.value.toDp() }
292
+
293
+ val inputSearchType = getInputSearchType(options)
294
+ val headerRightWidthPx = LocalHeaderRightWidthPx.current
295
+ val headerRightWidthDp = with(density) { headerRightWidthPx.intValue.toDp() }
296
+
297
+ if (inputSearchType == InputSearchType.None) return
298
+ val inputSearchProps = (options.headerType as? HeaderType.DefaultOrExtended)?.inputSearchProps ?: return
299
+
300
+ val minTopPadding = AppStatusBar.current
301
+ val maxTopPadding = AppStatusBar.current + HEADER_HEIGHT.dp
302
+ val minStartPadding = Spacing.M
303
+ val maxStartPadding = if (options.hiddenBack) Spacing.M else 52.dp
304
+ val minEndPadding = Spacing.M
305
+ val maxEndPadding = headerRightWidthDp + if (options.headerRight is HeaderRight.None) Spacing.M else Spacing.M * 2
306
+
307
+ val targetColor = lerp(
308
+ AppTheme.current.colors.background.surface,
309
+ AppTheme.current.colors.background.default,
310
+ (scrollDp / minTopPadding).coerceIn(0f, 1f)
311
+ )
312
+ val animatedColor by animateColorAsState(targetValue = targetColor, label = "BackgroundColor")
313
+
314
+ val layoutParams = when (inputSearchType) {
315
+ InputSearchType.Header -> {
316
+ InputSearchLayoutParams(
317
+ topPadding = minTopPadding,
318
+ startPadding = maxStartPadding,
319
+ endPadding = maxEndPadding
320
+ )
321
+ }
322
+
323
+ InputSearchType.Animated -> {
324
+ val animatedTopPadding by animateDpAsState(
325
+ targetValue = (maxTopPadding - scrollDp).coerceIn(minTopPadding, maxTopPadding),
326
+ label = "AnimatedTopPadding"
327
+ )
328
+
329
+ val animatedStartPadding by animateDpAsState(
330
+ targetValue = (minStartPadding + scrollDp).coerceIn(minStartPadding, maxStartPadding),
331
+ label = "AnimatedStartPadding"
332
+ )
333
+
334
+ val animatedEndPadding by animateDpAsState(
335
+ targetValue = (minEndPadding + scrollDp).coerceIn(minEndPadding, maxEndPadding),
336
+ label = "AnimatedEndPadding"
337
+ )
338
+
339
+ val maxPadding = remember(maxTopPadding, maxStartPadding, maxEndPadding) {
340
+ maxOf(maxEndPadding, maxStartPadding, maxTopPadding)
341
+ }
342
+ val snapScrollValue = with(density) { maxPadding.toPx().toInt() }
343
+
344
+ LaunchedEffect(isScrollInProgress && scrollState.value != 0 && scrollState.value != snapScrollValue) {
345
+ if (scrollDp < maxPadding) {
346
+ if (!isScrollInProgress) {
347
+ val midpoint = (maxTopPadding - minTopPadding) / 2
348
+
349
+ if (scrollDp < midpoint) {
350
+ scrollState.animateScrollTo(0)
351
+ } else {
352
+ scrollState.animateScrollTo(snapScrollValue)
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ InputSearchLayoutParams(
359
+ topPadding = animatedTopPadding,
360
+ startPadding = animatedStartPadding,
361
+ endPadding = animatedEndPadding
362
+ )
363
+ }
364
+
365
+ InputSearchType.None -> return
366
+ }
367
+
368
+ Spacer(Modifier.height(layoutParams.topPadding))
369
+ Box(
370
+ modifier = Modifier
371
+ .height(HEADER_HEIGHT.dp)
372
+ .fillMaxWidth()
373
+ .padding(
374
+ start = layoutParams.startPadding,
375
+ end = layoutParams.endPadding
376
+ ),
377
+ contentAlignment = Alignment.Center
378
+ ) {
379
+ InputSearch(
380
+ inputSearchProps = inputSearchProps.copy(
381
+ backgroundColor = animatedColor,
382
+ onPressButtonText = {
383
+ navigator.pop()
384
+ }
385
+ )
386
+ )
387
+ }
388
+ }
389
+
390
+ @Composable
391
+ internal fun isKeyboardVisible(): Boolean {
392
+ val ime = WindowInsets.ime
393
+ val density = LocalDensity.current
394
+ return ime.getBottom(density) > 0
395
+ }
396
+ private fun quantize(value: Int, stepPx: Int): Int {
397
+ if (stepPx <= 1) return value
398
+ return (value / stepPx) * stepPx
399
+ }
400
+
401
+ @Composable
402
+ fun LazyListState.proxyScrollState(
403
+ thresholdPx: Int = 200,
404
+ stepPx: Int = 4
405
+ ): Pair<ScrollState, Boolean> {
406
+ val scrollState = rememberScrollState()
407
+ val locked = remember { mutableStateOf(false) }
408
+
409
+ LaunchedEffect(this, scrollState, thresholdPx, stepPx) {
410
+ snapshotFlow { firstVisibleItemIndex to firstVisibleItemScrollOffset }
411
+ .collect { (index, offset) ->
412
+ val rawTarget = if (index == 0) offset else SCROLLED_PAST_FIRST_ITEM_THRESHOLD
413
+ val shouldLock = rawTarget > thresholdPx
414
+ if (locked.value != shouldLock) locked.value = shouldLock
415
+
416
+ val desired = if (shouldLock) thresholdPx else quantize(rawTarget, stepPx)
417
+
418
+ if (scrollState.value != desired) {
419
+ scrollState.scrollTo(desired.coerceAtLeast(0))
420
+ }
421
+ }
422
+ }
423
+
424
+ val inProgress = remember { mutableStateOf(false) }
425
+ LaunchedEffect(this, thresholdPx) {
426
+ snapshotFlow { isScrollInProgress }
427
+ .collect { progressing ->
428
+ inProgress.value = if (locked.value) false else progressing
429
+ }
430
+ }
431
+
432
+ return scrollState to inProgress.value
433
+ }
434
+
435
+ @Composable
436
+ fun ScrollState.proxyLimitedScrollState(
437
+ thresholdPx: Int = 200,
438
+ stepPx: Int = 4
439
+ ): Pair<ScrollState, Boolean> {
440
+ val proxy = rememberScrollState()
441
+ val locked = remember { mutableStateOf(false) }
442
+
443
+ LaunchedEffect(this, proxy, thresholdPx, stepPx) {
444
+ snapshotFlow { value }
445
+ .collect { rawTarget ->
446
+ val shouldLock = rawTarget > thresholdPx
447
+ if (locked.value != shouldLock) locked.value = shouldLock
448
+
449
+ val desired = if (shouldLock) thresholdPx else quantize(rawTarget, stepPx)
450
+
451
+ if (proxy.value != desired) {
452
+ proxy.scrollTo(desired.coerceAtLeast(0))
453
+ }
454
+ }
455
+ }
456
+
457
+ val inProgress = remember { mutableStateOf(false) }
458
+ LaunchedEffect(this, thresholdPx) {
459
+ snapshotFlow { isScrollInProgress }
460
+ .collect { progressing ->
461
+ inProgress.value = if (locked.value) false else progressing
462
+ }
463
+ }
464
+
465
+ return proxy to inProgress.value
466
+ }
467
+
468
+ private const val SCROLLED_PAST_FIRST_ITEM_THRESHOLD = 10_000
469
+
470
+ internal val LocalScrollState = staticCompositionLocalOf<ScrollState> { error("No Scroll State provided") }
471
+ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error("No NavigationOptions provided") }
472
+
473
+ val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
474
+
475
+ internal fun getInputSearchType(options: NavigationOptions): InputSearchType{
476
+ return when (val headerType = options.headerType) {
477
+ is HeaderType.DefaultOrExtended -> when {
478
+ headerType.inputSearchProps == null -> InputSearchType.None
479
+ headerType.useAnimated -> InputSearchType.Animated
480
+ else -> InputSearchType.Header
481
+ }
482
+ else -> InputSearchType.None
483
+ }
484
+ }