@momo-kits/native-kits 0.161.1-beta.15-debug → 0.161.2-beta.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 (35) hide show
  1. package/compose/build.gradle.kts +9 -3
  2. package/compose/build.gradle.kts.backup +8 -2
  3. package/compose/compose.podspec +1 -1
  4. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +9 -2
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +8 -14
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +324 -117
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +4 -3
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +4 -0
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +5 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +5 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +19 -14
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +17 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +27 -12
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +1 -28
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +63 -49
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +62 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +23 -2
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +12 -4
  22. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +33 -0
  23. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +18 -8
  24. package/gradle/libs.versions.toml +3 -1
  25. package/gradle.properties +1 -1
  26. package/ios/Application/ApplicationEnvironment.swift +2 -6
  27. package/ios/Input/Input.swift +50 -21
  28. package/ios/Input/InputPhoneNumber.swift +17 -17
  29. package/ios/StatusBarTap/StatusBarTap.h +13 -0
  30. package/ios/StatusBarTap/StatusBarTap.m +75 -0
  31. package/ios/Typography/Text.swift +19 -14
  32. package/ios/Typography/Typography.swift +22 -1
  33. package/ios/native-kits.podspec +2 -1
  34. package/package.json +1 -1
  35. package/settings.gradle.kts +15 -3
@@ -59,8 +59,9 @@ import vn.momo.kits.modifier.DeprecatedModifier
59
59
  import vn.momo.kits.modifier.conditional
60
60
  import vn.momo.kits.modifier.shadow
61
61
  import vn.momo.kits.navigation.component.SnackBar
62
- import vn.momo.kits.platform.getOSVersion
62
+ import vn.momo.kits.platform.supportsImePadding
63
63
  import vn.momo.kits.utils.getAppStatusBarHeight
64
+ import vn.momo.kits.utils.getNavigationBarHeight
64
65
 
65
66
  enum class HeaderType {
66
67
  DEFAULT,
@@ -102,7 +103,7 @@ fun Screen(
102
103
  val keyboardController = LocalSoftwareKeyboardController.current
103
104
 
104
105
  val isKeyboardVisible = isKeyboardVisible()
105
- val indicator = getAppStatusBarHeight()
106
+ val indicator = getNavigationBarHeight()
106
107
  val bottomPadding = if (isKeyboardVisible) 0.dp else indicator
107
108
 
108
109
  val headerHeight = if (animatedHeader !== null)
@@ -134,7 +135,7 @@ fun Screen(
134
135
  Box(
135
136
  Modifier.fillMaxSize()
136
137
  .background(backgroundColor ?: AppTheme.current.colors.background.default)
137
- .conditional(useAvoidKeyboard && getOSVersion() > 29) {
138
+ .conditional(useAvoidKeyboard && supportsImePadding()) {
138
139
  imePadding()
139
140
  }.then(DeprecatedModifier())
140
141
  ) {
@@ -21,6 +21,10 @@ import androidx.compose.ui.graphics.Color
21
21
  import androidx.compose.ui.graphics.PathEffect
22
22
  import androidx.compose.ui.graphics.StrokeCap
23
23
  import androidx.compose.ui.unit.dp
24
+ import io.ktor.util.Platform
25
+ import vn.momo.kits.application.IsShowBaseLineDebug
26
+ import vn.momo.kits.const.Colors
27
+ import vn.momo.kits.modifier.conditional
24
28
  import vn.momo.kits.platform.getPlatformName
25
29
  import vn.momo.kits.platform.getStatusBarHeight
26
30
 
@@ -221,6 +221,7 @@ fun Input(
221
221
  onBlur: () -> Unit = {},
222
222
  loading: Boolean = false,
223
223
  required: Boolean = false,
224
+ maxLength: Int? = null,
224
225
  fontWeight: InputFontWeight = InputFontWeight.REGULAR,
225
226
  keyboardType: KeyboardType = KeyboardType.Text,
226
227
  modifier: Modifier = Modifier,
@@ -319,7 +320,10 @@ fun Input(
319
320
  onBlur()
320
321
  }
321
322
  },
322
- onValueChange = onChangeText,
323
+ onValueChange = { newText ->
324
+ val limitedText = maxLength?.let { newText.take(it) } ?: newText
325
+ onChangeText(limitedText)
326
+ },
323
327
  decorationBox = { innerTextField ->
324
328
  // Floating label
325
329
  if (floatingValue.isNotEmpty() || floatingIcon.isNotEmpty()) {
@@ -8,6 +8,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
8
8
  import androidx.compose.animation.core.tween
9
9
  import androidx.compose.foundation.background
10
10
  import androidx.compose.foundation.border
11
+ import androidx.compose.ui.draw.alpha
11
12
  import androidx.compose.foundation.clickable
12
13
  import androidx.compose.foundation.layout.Arrangement
13
14
  import androidx.compose.foundation.layout.Box
@@ -129,8 +130,11 @@ fun InputOTP(
129
130
  if (!it.isFocused && isBlurred) onBlur()
130
131
  if (it.isFocused && !isBlurred) isBlurred = true
131
132
  },
132
- decorationBox = { _ ->
133
+ decorationBox = { innerTextField ->
133
134
  Box {
135
+ Box(
136
+ modifier = Modifier.fillMaxWidth().height(56.dp).alpha(0f)
137
+ ) { innerTextField() }
134
138
  if (floatingValue.isNotEmpty()) {
135
139
  Box(
136
140
  modifier = Modifier.wrapContentSize()
@@ -105,6 +105,8 @@ data class InputSearchProps(
105
105
  val iconModifier: Modifier = Modifier,
106
106
  val onClearPress: () -> Unit = {},
107
107
  val leftPosition: Dp? = null,
108
+ val placeholderCustomRender: (@Composable () -> Unit)? = null,
109
+ val searchIcon: (@Composable () -> Unit)? = null
108
110
  )
109
111
 
110
112
  @Composable
@@ -145,12 +147,13 @@ fun InputSearch(
145
147
  )
146
148
  }
147
149
 
148
- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
149
- .fillMaxWidth()
150
- .height(36.dp)
151
- .conditional(IsShowBaseLineDebug) {
152
- border(1.dp, Colors.blue_03)
153
- }
150
+ Row(
151
+ verticalAlignment = Alignment.CenterVertically, modifier = Modifier
152
+ .fillMaxWidth()
153
+ .height(36.dp)
154
+ .conditional(IsShowBaseLineDebug) {
155
+ border(1.dp, Colors.blue_03)
156
+ }
154
157
  ) {
155
158
  BasicTextField(
156
159
  enabled = !inputSearchProps.disabled,
@@ -184,13 +187,14 @@ fun InputSearch(
184
187
  horizontalArrangement = Arrangement.Start,
185
188
  verticalAlignment = Alignment.CenterVertically
186
189
  ) {
190
+
187
191
  Row(
188
192
  modifier = Modifier.padding(
189
193
  horizontal = Spacing.M
190
194
  ),
191
195
  verticalAlignment = Alignment.CenterVertically
192
196
  ) {
193
- Icon(
197
+ inputSearchProps.searchIcon?.invoke() ?: Icon(
194
198
  source = "navigation_search",
195
199
  modifier = Modifier.padding(end = Spacing.XS),
196
200
  size = 24.dp,
@@ -198,13 +202,14 @@ fun InputSearch(
198
202
  )
199
203
  Box(Modifier.weight(1f)) {
200
204
  if (inputSearchProps.text.value.isEmpty()) {
201
- Text(
202
- text = inputSearchProps.placeholder,
203
- style = placeHolderStyle,
204
- maxLines = 1,
205
- color = placeholderColor,
206
- overflow = TextOverflow.Ellipsis
207
- )
205
+ inputSearchProps.placeholderCustomRender?.invoke()
206
+ ?: Text(
207
+ text = inputSearchProps.placeholder,
208
+ style = placeHolderStyle,
209
+ maxLines = 1,
210
+ color = placeholderColor,
211
+ overflow = TextOverflow.Ellipsis
212
+ )
208
213
  }
209
214
  innerTextField()
210
215
  }
@@ -0,0 +1,17 @@
1
+ package vn.momo.kits.components
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.CompositionLocalProvider
5
+ import vn.momo.kits.application.ScaleSizeMaxRate
6
+
7
+ @Composable
8
+ fun ScaleSizeScope(
9
+ scaleSizeMaxRate: Float? = null,
10
+ content: @Composable () -> Unit
11
+ ) {
12
+ CompositionLocalProvider(
13
+ ScaleSizeMaxRate provides scaleSizeMaxRate,
14
+ ) {
15
+ content()
16
+ }
17
+ }
@@ -7,11 +7,15 @@ import androidx.compose.ui.text.TextStyle
7
7
  import androidx.compose.ui.text.font.FontFamily
8
8
  import androidx.compose.ui.text.font.FontWeight
9
9
  import androidx.compose.ui.text.style.TextDecoration
10
- import androidx.compose.ui.unit.*
10
+ import androidx.compose.ui.unit.Dp
11
+ import androidx.compose.ui.unit.TextUnit
12
+ import androidx.compose.ui.unit.TextUnitType
13
+ import androidx.compose.ui.unit.dp
14
+ import androidx.compose.ui.unit.sp
11
15
  import org.jetbrains.compose.resources.Font
12
16
  import org.jetbrains.compose.resources.FontResource
13
17
  import org.jetbrains.compose.resources.InternalResourceApi
14
- import vn.momo.kits.application.UseFontScaleSystem
18
+ import vn.momo.kits.application.ScaleSizeMaxRate
15
19
  import vn.momo.kits.platform.getScreenDimensions
16
20
  import vn.momo.uikits.resources.Res
17
21
  import vn.momo.uikits.resources.momosignature
@@ -29,23 +33,34 @@ import kotlin.math.max
29
33
  import kotlin.math.min
30
34
 
31
35
  const val DEFAULT_SCREEN_SIZE = 375f
32
- const val MAX_FONT_SCALE = 1.2f
36
+ const val MAX_FONT_SCALE = 1.5f
37
+ const val MAX_DEVICE_SCALE = 5
33
38
 
34
39
  @Composable
35
40
  fun scaleSize(size: Float): Float {
36
- if (UseFontScaleSystem) {
37
- val deviceWidth = getScreenDimensions().width
38
- val deviceScale = deviceWidth / DEFAULT_SCREEN_SIZE
39
- val fontScale = LocalDensity.current.fontScale
41
+ val scaleSizeMaxRate: Float = ScaleSizeMaxRate.current ?: MAX_FONT_SCALE
42
+ val deviceWidth = getScreenDimensions().width
43
+ val deviceScale = deviceWidth / DEFAULT_SCREEN_SIZE
40
44
 
41
- val maxSize = size * MAX_FONT_SCALE
42
- val fontSizeScaleDevice = if (deviceScale > 1) deviceScale * size else size
43
- val fontSizeScaleOS = if (fontScale > 1) fontScale * size else size
45
+ val density = LocalDensity.current
46
+ val fontScale = density.fontScale
47
+
48
+ var fontSizeScaleDevice = size
49
+ var fontSizeScaleOS = size
44
50
 
45
- return min(max(fontSizeScaleDevice, fontSizeScaleOS), maxSize)
51
+ if (deviceScale > 1) {
52
+ fontSizeScaleDevice =
53
+ min(deviceScale * fontSizeScaleDevice, fontSizeScaleDevice + MAX_DEVICE_SCALE)
46
54
  }
47
55
 
48
- return size
56
+ if (fontScale > 1) {
57
+ fontSizeScaleOS = min(fontScale * fontSizeScaleOS, fontSizeScaleOS * scaleSizeMaxRate)
58
+ }
59
+
60
+ return max(
61
+ fontSizeScaleDevice,
62
+ fontSizeScaleOS
63
+ )
49
64
  }
50
65
 
51
66
  @Composable
@@ -96,4 +96,5 @@ data class KeyboardOptions(
96
96
  data class ScrollData(
97
97
  val scrollable: Boolean = true,
98
98
  val scrollState: ScrollableState? = null,
99
+ val scrollToTopCallback: (() -> Unit)? = null,
99
100
  )
@@ -8,9 +8,6 @@ import androidx.navigation.compose.NavHost
8
8
  import androidx.navigation.compose.composable
9
9
  import androidx.navigation.compose.rememberNavController
10
10
  import androidx.navigation.toRoute
11
- import kotlinx.coroutines.flow.catch
12
- import kotlinx.coroutines.flow.distinctUntilChanged
13
- import kotlinx.coroutines.flow.map
14
11
  import vn.momo.kits.application.*
15
12
  import vn.momo.kits.const.*
16
13
  import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
@@ -52,19 +49,6 @@ fun NavigationContainer(
52
49
  }
53
50
  }
54
51
 
55
- LaunchedEffect(maxApi) {
56
- val api = maxApi ?: return@LaunchedEffect
57
- runCatching {
58
- api.observer("use_font_scale")
59
- .map { parseUseFontScale(it) }
60
- .distinctUntilChanged()
61
- .catch { /* ignore observer errors, keep current value */ }
62
- .collect { enabled ->
63
- UseFontScaleSystem = enabled
64
- }
65
- }
66
- }
67
-
68
52
  val startDestination = DynamicScreenRegistry.register(initialScreenName, initialScreen, options)
69
53
 
70
54
  ProvideNavigationEventDispatcherOwner {
@@ -77,6 +61,7 @@ fun NavigationContainer(
77
61
  ApplicationContext provides mergedContext,
78
62
  AppConfig provides config,
79
63
  AppLanguage provides language,
64
+ ScaleSizeMaxRate provides mergedContext?.scaleSizeMaxRate,
80
65
  ) {
81
66
  LaunchedEffect(Unit) {
82
67
  setNavigator?.invoke(navigator)
@@ -154,15 +139,3 @@ fun NavigationContainer(
154
139
  val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> {
155
140
  error("No MaxApi provided")
156
141
  }
157
-
158
- private fun parseUseFontScale(raw: Any?): Boolean = when (raw) {
159
- null -> true
160
- is Boolean -> raw
161
- is String -> raw.isBlank() || !raw.equals("false", ignoreCase = true)
162
- is Number -> raw.toInt() != 0
163
- is Map<*, *> -> {
164
- val nested = raw["response"] ?: raw["value"] ?: raw["data"]
165
- if (nested === raw) true else parseUseFontScale(nested)
166
- }
167
- else -> true
168
- }
@@ -0,0 +1,8 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+
5
+ @Composable
6
+ internal expect fun RegisterScrollToTop(callback: (() -> Unit)?)
7
+
8
+
@@ -62,7 +62,7 @@ import vn.momo.kits.navigation.component.HeaderRight
62
62
  import vn.momo.kits.navigation.component.HeaderType
63
63
  import vn.momo.kits.navigation.component.InputSearchType
64
64
  import vn.momo.kits.platform.BackHandler
65
- import vn.momo.kits.platform.getOSVersion
65
+ import vn.momo.kits.platform.supportsImePadding
66
66
  import vn.momo.kits.navigation.tracking.ScreenTracker
67
67
  import vn.momo.kits.navigation.tracking.ScreenTrackingState
68
68
  import kotlinx.coroutines.delay
@@ -150,6 +150,10 @@ internal fun StackScreen(
150
150
  val footerHeightPx = remember { mutableIntStateOf(0) }
151
151
  val headerRightWidthPx = remember { mutableIntStateOf(0) }
152
152
 
153
+ if (navigation.options.scrollData.scrollToTopCallback != null) {
154
+ RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
155
+ }
156
+
153
157
  BackHandler(true) { navigator.onBackSafe() }
154
158
 
155
159
  CompositionLocalProvider(
@@ -160,15 +164,16 @@ internal fun StackScreen(
160
164
  LocalFooterHeightPx provides footerHeightPx,
161
165
  LocalHeaderRightWidthPx provides headerRightWidthPx
162
166
  ) {
163
- Box(Modifier
164
- .fillMaxSize()
165
- .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
166
- .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
167
- hideKeyboardOnTap()
168
- }
169
- .conditional(options.keyboardOptions.useAvoidKeyboard && getOSVersion() > 29) {
170
- imePadding()
171
- }
167
+ Box(
168
+ Modifier
169
+ .fillMaxSize()
170
+ .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
171
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
172
+ hideKeyboardOnTap()
173
+ }
174
+ .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
175
+ imePadding()
176
+ }
172
177
  ) {
173
178
  Box(Modifier.zIndex(1f)) {
174
179
  HeaderBackground()
@@ -178,7 +183,7 @@ internal fun StackScreen(
178
183
  Header(onBackHandler)
179
184
  }
180
185
 
181
- Column (Modifier.zIndex(6f)) {
186
+ Column(Modifier.zIndex(6f)) {
182
187
  SearchAnimated(isScrollInProgress = scrollInProcess)
183
188
  }
184
189
 
@@ -190,7 +195,7 @@ internal fun StackScreen(
190
195
  FooterContent()
191
196
  }
192
197
 
193
- Box(Modifier.zIndex(7f)){
198
+ Box(Modifier.zIndex(7f)) {
194
199
  FloatingContent()
195
200
  }
196
201
 
@@ -200,15 +205,15 @@ internal fun StackScreen(
200
205
  }
201
206
 
202
207
  @Composable
203
- fun FloatingContent(){
208
+ fun FloatingContent() {
204
209
  val options = LocalOptions.current
205
210
  val density = LocalDensity.current
206
211
  val footerHeightPx = LocalFooterHeightPx.current
207
212
  val scrollState = LocalScrollState.current
208
213
 
209
214
  val fabProps = options.floatingButtonProps ?: return
210
- val bottomPadding = fabProps.bottom ?:
211
- (Spacing.M + if (options.footerComponent != null) with(density){ footerHeightPx.intValue.toDp() } else 0.dp)
215
+ val bottomPadding = fabProps.bottom
216
+ ?: (Spacing.M + if (options.footerComponent != null) with(density) { footerHeightPx.intValue.toDp() } else 0.dp)
212
217
 
213
218
  FloatingButton(
214
219
  scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
@@ -224,17 +229,19 @@ fun FloatingContent(){
224
229
  }
225
230
 
226
231
  @Composable
227
- fun ColumnScope.MainContent(content: @Composable ()-> Unit){
232
+ fun ColumnScope.MainContent(content: @Composable () -> Unit) {
228
233
  val options = LocalOptions.current
229
234
  val inputSearchType = getInputSearchType(options)
230
235
  val density = LocalDensity.current
231
236
  val scrollState = LocalScrollState.current
232
237
 
233
- Spacer(Modifier.height(
234
- if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
235
- AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp)
238
+ Spacer(
239
+ Modifier.height(
240
+ if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
241
+ AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp
242
+ )
236
243
  )
237
- if (inputSearchType == InputSearchType.Animated){
244
+ if (inputSearchType == InputSearchType.Animated) {
238
245
  val scrollDp = with(density) { scrollState.value.toDp() }
239
246
 
240
247
  val animatedTopPadding by animateDpAsState(
@@ -243,17 +250,18 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
243
250
  )
244
251
  Spacer(Modifier.height(animatedTopPadding))
245
252
  }
246
- Column (Modifier
247
- .fillMaxWidth()
248
- .weight(1f)
249
- .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
250
- verticalScroll(scrollState)
251
- }
253
+ Column(
254
+ Modifier
255
+ .fillMaxWidth()
256
+ .weight(1f)
257
+ .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
258
+ verticalScroll(scrollState)
259
+ }
252
260
  ) {
253
261
  ScreenContent(content = content)
254
262
  }
255
263
 
256
- if (options.footerComponent != null){
264
+ if (options.footerComponent != null) {
257
265
  val footerHeight = LocalFooterHeightPx.current
258
266
  val footerHeightDp = with(density) { footerHeight.value.toDp() }
259
267
  Spacer(Modifier.height(footerHeightDp))
@@ -261,17 +269,17 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
261
269
  }
262
270
 
263
271
  @Composable
264
- fun ScreenContent(content: @Composable () -> Unit){
272
+ fun ScreenContent(content: @Composable () -> Unit) {
265
273
  val scrollState = LocalScrollState.current
266
274
  val options = LocalOptions.current
267
275
 
268
- if (options.headerType is HeaderType.Animated){
276
+ if (options.headerType is HeaderType.Animated) {
269
277
  val animatedHeader = options.headerType
270
278
  Box {
271
- Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)){
279
+ Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)) {
272
280
  animatedHeader.composable.invoke(scrollState.value)
273
281
  }
274
- Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)){
282
+ Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
275
283
  content()
276
284
  }
277
285
  }
@@ -281,9 +289,9 @@ fun ScreenContent(content: @Composable () -> Unit){
281
289
  }
282
290
 
283
291
  @Composable
284
- fun FooterContent(){
292
+ fun FooterContent() {
285
293
  val options = LocalOptions.current
286
- if (options.footerComponent != null){
294
+ if (options.footerComponent != null) {
287
295
  val ime = WindowInsets.ime
288
296
  val density = LocalDensity.current
289
297
  val imeBottom = ime.getBottom(density)
@@ -295,11 +303,11 @@ fun FooterContent(){
295
303
  }
296
304
 
297
305
  @Composable
298
- fun OverplayView(bottomTabIndex: Int, id: Int){
306
+ fun OverplayView(bottomTabIndex: Int, id: Int) {
299
307
  val overplayType = OverplayComponentRegistry.getOverplayType()
300
308
 
301
309
  if (overplayType != null) {
302
- Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()){
310
+ Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()) {
303
311
  if (bottomTabIndex != -1) return@Box
304
312
  if (OverplayComponentRegistry.currentRootId() != id) return@Box
305
313
  OverplayComponentRegistry.OverlayComponent()
@@ -322,22 +330,24 @@ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
322
330
  Box(Modifier.onGloballyPositioned {
323
331
  if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
324
332
  }) {
325
- Box(Modifier
326
- .fillMaxWidth()
327
- .background(AppTheme.current.colors.background.surface)
328
- .conditional(IsShowBaseLineDebug) {
329
- border(1.dp, Colors.blue_03)
330
- }
331
- .padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
332
- ){
333
+ Box(
334
+ Modifier
335
+ .fillMaxWidth()
336
+ .background(AppTheme.current.colors.background.surface)
337
+ .conditional(IsShowBaseLineDebug) {
338
+ border(1.dp, Colors.blue_03)
339
+ }
340
+ .padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
341
+ ) {
333
342
  footerComponent.invoke()
334
343
  }
335
344
 
336
- Box(modifier = Modifier
337
- .fillMaxWidth()
338
- .height(6.dp)
339
- .offset(x = 0.dp, y = (-6).dp)
340
- .background(shadowBrush)
345
+ Box(
346
+ modifier = Modifier
347
+ .fillMaxWidth()
348
+ .height(6.dp)
349
+ .offset(x = 0.dp, y = (-6).dp)
350
+ .background(shadowBrush)
341
351
  )
342
352
  }
343
353
  }
@@ -347,6 +357,7 @@ data class InputSearchLayoutParams(
347
357
  val startPadding: Dp,
348
358
  val endPadding: Dp
349
359
  )
360
+
350
361
  @Composable
351
362
  fun SearchAnimated(
352
363
  isScrollInProgress: Boolean
@@ -461,6 +472,7 @@ internal fun isKeyboardVisible(): Boolean {
461
472
  val density = LocalDensity.current
462
473
  return ime.getBottom(density) > 0
463
474
  }
475
+
464
476
  private fun quantize(value: Int, stepPx: Int): Int {
465
477
  if (stepPx <= 1) return value
466
478
  return (value / stepPx) * stepPx
@@ -540,13 +552,15 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
540
552
 
541
553
  val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
542
554
 
543
- internal fun getInputSearchType(options: NavigationOptions): InputSearchType{
555
+ internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
544
556
  return when (val headerType = options.headerType) {
545
557
  is HeaderType.DefaultOrExtended -> when {
546
558
  headerType.inputSearchProps == null -> InputSearchType.None
547
559
  headerType.useAnimated -> InputSearchType.Animated
548
560
  else -> InputSearchType.Header
549
561
  }
562
+
550
563
  else -> InputSearchType.None
551
564
  }
552
565
  }
566
+
@@ -0,0 +1,62 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.foundation.Image
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.fillMaxSize
7
+ import androidx.compose.runtime.Composable
8
+ import androidx.compose.runtime.getValue
9
+ import androidx.compose.ui.Modifier
10
+ import androidx.compose.ui.graphics.Color
11
+ import io.github.alexzhirkevich.compottie.Compottie
12
+ import io.github.alexzhirkevich.compottie.ExperimentalCompottieApi
13
+ import io.github.alexzhirkevich.compottie.LottieCompositionSpec
14
+ import io.github.alexzhirkevich.compottie.dynamic.rememberLottieDynamicProperties
15
+ import io.github.alexzhirkevich.compottie.rememberLottieComposition
16
+ import io.github.alexzhirkevich.compottie.rememberLottiePainter
17
+ import vn.momo.kits.utils.readJson
18
+
19
+ @OptIn(ExperimentalCompottieApi::class)
20
+ @Composable
21
+ internal fun ComposeLottieAnimation(
22
+ path: String,
23
+ tintColor: Color?,
24
+ bgColor: Color?,
25
+ modifier: Modifier,
26
+ ) {
27
+ val json = readJson(path)
28
+
29
+ if (json.isEmpty()) {
30
+ Box(modifier.background(bgColor ?: Color.Transparent))
31
+ return
32
+ }
33
+
34
+ val composition by rememberLottieComposition(json) {
35
+ LottieCompositionSpec.JsonString(json)
36
+ }
37
+
38
+ // Recolor only the fill/stroke color slots (matching the native lottie-ios per-keypath tint)
39
+ // instead of a global ColorFilter.tint, which would flatten a multicolor animation to a silhouette.
40
+ val dynamicProperties = if (tintColor != null) {
41
+ rememberLottieDynamicProperties(tintColor) {
42
+ shapeLayer("**") {
43
+ fill("**") { color { tintColor } }
44
+ stroke("**") { color { tintColor } }
45
+ }
46
+ }
47
+ } else {
48
+ null
49
+ }
50
+
51
+ Box(modifier.background(bgColor ?: Color.Transparent)) {
52
+ Image(
53
+ painter = rememberLottiePainter(
54
+ composition = composition,
55
+ iterations = Compottie.IterateForever,
56
+ dynamicProperties = dynamicProperties,
57
+ ),
58
+ contentDescription = null,
59
+ modifier = Modifier.fillMaxSize(),
60
+ )
61
+ }
62
+ }
@@ -30,14 +30,35 @@ expect fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit
30
30
  @Composable
31
31
  expect fun getScreenHeight(): Dp
32
32
 
33
- expect fun getOSVersion(): Int
33
+ sealed interface OSVersion {
34
+ val value: Int
35
+ data class Android(val sdk: Int) : OSVersion { override val value: Int get() = sdk }
36
+ data class IOS(val major: Int) : OSVersion { override val value: Int get() = major }
34
37
 
38
+ operator fun compareTo(other: Int): Int = value.compareTo(other)
39
+ }
40
+ expect fun getOSVersion(): OSVersion
41
+
42
+ fun supportsImePadding(): Boolean = when (val v = getOSVersion()) {
43
+ is OSVersion.Android -> v.sdk > 29
44
+ is OSVersion.IOS -> true
45
+ }
46
+
47
+ /**
48
+ * @param useCompose when `true`, renders via the pure-Compose Compottie engine instead of the
49
+ * platform-native engine (airbnb-lottie on Android, lottie-ios on iOS). Defaults to `false` for
50
+ * backward compatibility. Note: when `useCompose = true`, `tintColor` recolors only the fill/stroke
51
+ * color slots (matching the native lottie-ios per-keypath tint), so gradient and text-layer colors
52
+ * are left untinted; `placedAsOverlay` is a no-op.
53
+ */
35
54
  @Composable
36
55
  expect fun LottieAnimation(
37
56
  path: String,
38
57
  tintColor: Color? = null,
39
58
  bgColor: Color? = null,
40
- modifier: Modifier = Modifier
59
+ placedAsOverlay: Boolean = false,
60
+ modifier: Modifier = Modifier,
61
+ useCompose: Boolean = false
41
62
  )
42
63
 
43
64
  expect fun NativePaint.setColor(