@momo-kits/native-kits 0.160.1-test.1-debug → 0.160.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.
- package/.claude/settings.local.json +3 -9
- package/compose/build.gradle.kts +9 -3
- package/compose/build.gradle.kts.backup +8 -2
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +7 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +324 -117
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +5 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +5 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +19 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +7 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +15 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +63 -55
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +9 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +12 -4
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +33 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +7 -1
- package/gradle/libs.versions.toml +2 -0
- package/gradle.properties +1 -1
- package/ios/Input/Input.swift +33 -4
- package/ios/StatusBarTap/StatusBarTap.h +13 -0
- package/ios/StatusBarTap/StatusBarTap.m +75 -0
- package/ios/native-kits.podspec +2 -1
- package/local.properties +1 -1
- package/package.json +1 -1
- package/settings.gradle.kts +15 -3
|
@@ -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 =
|
|
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(
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
}
|
|
@@ -3,6 +3,7 @@ package vn.momo.kits.navigation
|
|
|
3
3
|
import androidx.compose.animation.*
|
|
4
4
|
import androidx.compose.animation.core.tween
|
|
5
5
|
import androidx.compose.runtime.*
|
|
6
|
+
import androidx.compose.runtime.saveable.rememberSaveable
|
|
6
7
|
import androidx.compose.ui.unit.Dp
|
|
7
8
|
import androidx.navigation.compose.NavHost
|
|
8
9
|
import androidx.navigation.compose.composable
|
|
@@ -49,7 +50,9 @@ fun NavigationContainer(
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
val
|
|
53
|
+
val screenId = rememberSaveable { DynamicScreenRegistry.nextId() }
|
|
54
|
+
DynamicScreenRegistry.bind(screenId, initialScreenName, initialScreen, options)
|
|
55
|
+
val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
|
|
53
56
|
|
|
54
57
|
ProvideNavigationEventDispatcherOwner {
|
|
55
58
|
CompositionLocalProvider(
|
|
@@ -132,6 +135,9 @@ fun NavigationContainer(
|
|
|
132
135
|
DisposableEffect(Unit) {
|
|
133
136
|
onDispose {
|
|
134
137
|
navigator.dispose()
|
|
138
|
+
// AI-GENERATED START: unregister initial screen on dispose to avoid leaking a stale registry entry
|
|
139
|
+
DynamicScreenRegistry.unregisterScreen(screenId)
|
|
140
|
+
// AI-GENERATED END: unregister initial screen on dispose to avoid leaking a stale registry entry
|
|
135
141
|
}
|
|
136
142
|
}
|
|
137
143
|
}
|
|
@@ -28,13 +28,6 @@ class Navigator(
|
|
|
28
28
|
) {
|
|
29
29
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
30
30
|
|
|
31
|
-
internal var currentScreenId: Int = -1
|
|
32
|
-
private set
|
|
33
|
-
|
|
34
|
-
internal fun setCurrentScreenId(id: Int) {
|
|
35
|
-
currentScreenId = id
|
|
36
|
-
}
|
|
37
|
-
|
|
38
31
|
fun push(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
39
32
|
val route = DynamicScreenRegistry.register(screenName, content, options)
|
|
40
33
|
navController.navigate(DynamicScreenRoute(route.id))
|
|
@@ -129,9 +122,7 @@ class Navigator(
|
|
|
129
122
|
barrierDismissible: Boolean = true,
|
|
130
123
|
onDismiss: (() -> Unit)? = null
|
|
131
124
|
){
|
|
132
|
-
|
|
133
|
-
val id = currentScreenId.takeIf { it != -1 } ?: DynamicScreenRegistry.getLatestScreen()?.id ?: -1
|
|
134
|
-
// AI-GENERATED END: use per-Navigator currentScreenId; fall back to global registry if not yet set
|
|
125
|
+
val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
|
|
135
126
|
OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.MODAL, false, barrierDismissible, onDismiss)
|
|
136
127
|
}
|
|
137
128
|
|
|
@@ -142,16 +133,12 @@ class Navigator(
|
|
|
142
133
|
onDismiss: (() -> Unit)? = null,
|
|
143
134
|
bottomSheetHeader: BottomHeader? = null
|
|
144
135
|
){
|
|
145
|
-
|
|
146
|
-
val id = currentScreenId.takeIf { it != -1 } ?: DynamicScreenRegistry.getLatestScreen()?.id ?: -1
|
|
147
|
-
// AI-GENERATED END: use per-Navigator currentScreenId; fall back to global registry if not yet set
|
|
136
|
+
val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
|
|
148
137
|
OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.BOTTOM_SHEET, isSurface, barrierDismissible, onDismiss, bottomSheetHeader)
|
|
149
138
|
}
|
|
150
139
|
|
|
151
140
|
fun showSnackBar(snackBar: SnackBar, onDismiss: (() -> Unit)? = null) {
|
|
152
|
-
|
|
153
|
-
val id = currentScreenId.takeIf { it != -1 } ?: DynamicScreenRegistry.getLatestScreen()?.id ?: -1
|
|
154
|
-
// AI-GENERATED END: use per-Navigator currentScreenId; fall back to global registry if not yet set
|
|
141
|
+
val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
|
|
155
142
|
scope.launch {
|
|
156
143
|
OverplayComponentRegistry.registerOverplay(
|
|
157
144
|
id = id,
|
|
@@ -209,6 +196,18 @@ object DynamicScreenRegistry {
|
|
|
209
196
|
return DynamicScreenRoute(id)
|
|
210
197
|
}
|
|
211
198
|
|
|
199
|
+
fun nextId(): Int = idCounter++
|
|
200
|
+
|
|
201
|
+
fun bind(id: Int, screenName: String, content: @Composable () -> Unit, options: NavigationOptions?) {
|
|
202
|
+
screens[id] = DynamicScreen(
|
|
203
|
+
id = id,
|
|
204
|
+
name = screenName,
|
|
205
|
+
content = content,
|
|
206
|
+
options = options
|
|
207
|
+
)
|
|
208
|
+
if (id >= idCounter) idCounter = id + 1 // keep counter ahead after process-death restore
|
|
209
|
+
}
|
|
210
|
+
|
|
212
211
|
fun unregisterScreen(id: Int) {
|
|
213
212
|
screens.remove(id)
|
|
214
213
|
}
|
|
@@ -90,12 +90,6 @@ internal fun StackScreen(
|
|
|
90
90
|
val density = LocalDensity.current
|
|
91
91
|
val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions) }
|
|
92
92
|
|
|
93
|
-
if (bottomTabIndex == -1) {
|
|
94
|
-
LaunchedEffect(id) {
|
|
95
|
-
navigator.setCurrentScreenId(id)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
93
|
val options by navigation.currentOptions
|
|
100
94
|
|
|
101
95
|
// Auto tracking state
|
|
@@ -156,6 +150,10 @@ internal fun StackScreen(
|
|
|
156
150
|
val footerHeightPx = remember { mutableIntStateOf(0) }
|
|
157
151
|
val headerRightWidthPx = remember { mutableIntStateOf(0) }
|
|
158
152
|
|
|
153
|
+
if (navigation.options.scrollData.scrollToTopCallback != null) {
|
|
154
|
+
RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
|
|
155
|
+
}
|
|
156
|
+
|
|
159
157
|
BackHandler(true) { navigator.onBackSafe() }
|
|
160
158
|
|
|
161
159
|
CompositionLocalProvider(
|
|
@@ -166,15 +164,16 @@ internal fun StackScreen(
|
|
|
166
164
|
LocalFooterHeightPx provides footerHeightPx,
|
|
167
165
|
LocalHeaderRightWidthPx provides headerRightWidthPx
|
|
168
166
|
) {
|
|
169
|
-
Box(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}
|
|
178
177
|
) {
|
|
179
178
|
Box(Modifier.zIndex(1f)) {
|
|
180
179
|
HeaderBackground()
|
|
@@ -184,7 +183,7 @@ internal fun StackScreen(
|
|
|
184
183
|
Header(onBackHandler)
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
Column
|
|
186
|
+
Column(Modifier.zIndex(6f)) {
|
|
188
187
|
SearchAnimated(isScrollInProgress = scrollInProcess)
|
|
189
188
|
}
|
|
190
189
|
|
|
@@ -196,7 +195,7 @@ internal fun StackScreen(
|
|
|
196
195
|
FooterContent()
|
|
197
196
|
}
|
|
198
197
|
|
|
199
|
-
Box(Modifier.zIndex(7f)){
|
|
198
|
+
Box(Modifier.zIndex(7f)) {
|
|
200
199
|
FloatingContent()
|
|
201
200
|
}
|
|
202
201
|
|
|
@@ -206,15 +205,15 @@ internal fun StackScreen(
|
|
|
206
205
|
}
|
|
207
206
|
|
|
208
207
|
@Composable
|
|
209
|
-
fun FloatingContent(){
|
|
208
|
+
fun FloatingContent() {
|
|
210
209
|
val options = LocalOptions.current
|
|
211
210
|
val density = LocalDensity.current
|
|
212
211
|
val footerHeightPx = LocalFooterHeightPx.current
|
|
213
212
|
val scrollState = LocalScrollState.current
|
|
214
213
|
|
|
215
214
|
val fabProps = options.floatingButtonProps ?: return
|
|
216
|
-
val bottomPadding = fabProps.bottom
|
|
217
|
-
|
|
215
|
+
val bottomPadding = fabProps.bottom
|
|
216
|
+
?: (Spacing.M + if (options.footerComponent != null) with(density) { footerHeightPx.intValue.toDp() } else 0.dp)
|
|
218
217
|
|
|
219
218
|
FloatingButton(
|
|
220
219
|
scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
|
|
@@ -230,17 +229,19 @@ fun FloatingContent(){
|
|
|
230
229
|
}
|
|
231
230
|
|
|
232
231
|
@Composable
|
|
233
|
-
fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
232
|
+
fun ColumnScope.MainContent(content: @Composable () -> Unit) {
|
|
234
233
|
val options = LocalOptions.current
|
|
235
234
|
val inputSearchType = getInputSearchType(options)
|
|
236
235
|
val density = LocalDensity.current
|
|
237
236
|
val scrollState = LocalScrollState.current
|
|
238
237
|
|
|
239
|
-
Spacer(
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
)
|
|
242
243
|
)
|
|
243
|
-
if (inputSearchType == InputSearchType.Animated){
|
|
244
|
+
if (inputSearchType == InputSearchType.Animated) {
|
|
244
245
|
val scrollDp = with(density) { scrollState.value.toDp() }
|
|
245
246
|
|
|
246
247
|
val animatedTopPadding by animateDpAsState(
|
|
@@ -249,35 +250,36 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
|
249
250
|
)
|
|
250
251
|
Spacer(Modifier.height(animatedTopPadding))
|
|
251
252
|
}
|
|
252
|
-
Column
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
253
|
+
Column(
|
|
254
|
+
Modifier
|
|
255
|
+
.fillMaxWidth()
|
|
256
|
+
.weight(1f)
|
|
257
|
+
.conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
|
|
258
|
+
verticalScroll(scrollState)
|
|
259
|
+
}
|
|
258
260
|
) {
|
|
259
261
|
ScreenContent(content = content)
|
|
260
262
|
}
|
|
261
263
|
|
|
262
|
-
if (options.footerComponent != null){
|
|
264
|
+
if (options.footerComponent != null) {
|
|
263
265
|
val footerHeight = LocalFooterHeightPx.current
|
|
264
|
-
val footerHeightDp = with(density) { footerHeight.
|
|
266
|
+
val footerHeightDp = with(density) { footerHeight.intValue.toDp() }
|
|
265
267
|
Spacer(Modifier.height(footerHeightDp))
|
|
266
268
|
}
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
@Composable
|
|
270
|
-
fun ScreenContent(content: @Composable () -> Unit){
|
|
272
|
+
fun ScreenContent(content: @Composable () -> Unit) {
|
|
271
273
|
val scrollState = LocalScrollState.current
|
|
272
274
|
val options = LocalOptions.current
|
|
273
275
|
|
|
274
|
-
if (options.headerType is HeaderType.Animated){
|
|
276
|
+
if (options.headerType is HeaderType.Animated) {
|
|
275
277
|
val animatedHeader = options.headerType
|
|
276
278
|
Box {
|
|
277
|
-
Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)){
|
|
279
|
+
Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)) {
|
|
278
280
|
animatedHeader.composable.invoke(scrollState.value)
|
|
279
281
|
}
|
|
280
|
-
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)) {
|
|
281
283
|
content()
|
|
282
284
|
}
|
|
283
285
|
}
|
|
@@ -287,9 +289,9 @@ fun ScreenContent(content: @Composable () -> Unit){
|
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
@Composable
|
|
290
|
-
fun FooterContent(){
|
|
292
|
+
fun FooterContent() {
|
|
291
293
|
val options = LocalOptions.current
|
|
292
|
-
if (options.footerComponent != null){
|
|
294
|
+
if (options.footerComponent != null) {
|
|
293
295
|
val ime = WindowInsets.ime
|
|
294
296
|
val density = LocalDensity.current
|
|
295
297
|
val imeBottom = ime.getBottom(density)
|
|
@@ -301,11 +303,11 @@ fun FooterContent(){
|
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
@Composable
|
|
304
|
-
fun OverplayView(bottomTabIndex: Int, id: Int){
|
|
306
|
+
fun OverplayView(bottomTabIndex: Int, id: Int) {
|
|
305
307
|
val overplayType = OverplayComponentRegistry.getOverplayType()
|
|
306
308
|
|
|
307
309
|
if (overplayType != null) {
|
|
308
|
-
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()) {
|
|
309
311
|
if (bottomTabIndex != -1) return@Box
|
|
310
312
|
if (OverplayComponentRegistry.currentRootId() != id) return@Box
|
|
311
313
|
OverplayComponentRegistry.OverlayComponent()
|
|
@@ -328,22 +330,24 @@ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
|
|
|
328
330
|
Box(Modifier.onGloballyPositioned {
|
|
329
331
|
if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
|
|
330
332
|
}) {
|
|
331
|
-
Box(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
) {
|
|
339
342
|
footerComponent.invoke()
|
|
340
343
|
}
|
|
341
344
|
|
|
342
|
-
Box(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
Box(
|
|
346
|
+
modifier = Modifier
|
|
347
|
+
.fillMaxWidth()
|
|
348
|
+
.height(6.dp)
|
|
349
|
+
.offset(x = 0.dp, y = (-6).dp)
|
|
350
|
+
.background(shadowBrush)
|
|
347
351
|
)
|
|
348
352
|
}
|
|
349
353
|
}
|
|
@@ -353,6 +357,7 @@ data class InputSearchLayoutParams(
|
|
|
353
357
|
val startPadding: Dp,
|
|
354
358
|
val endPadding: Dp
|
|
355
359
|
)
|
|
360
|
+
|
|
356
361
|
@Composable
|
|
357
362
|
fun SearchAnimated(
|
|
358
363
|
isScrollInProgress: Boolean
|
|
@@ -467,6 +472,7 @@ internal fun isKeyboardVisible(): Boolean {
|
|
|
467
472
|
val density = LocalDensity.current
|
|
468
473
|
return ime.getBottom(density) > 0
|
|
469
474
|
}
|
|
475
|
+
|
|
470
476
|
private fun quantize(value: Int, stepPx: Int): Int {
|
|
471
477
|
if (stepPx <= 1) return value
|
|
472
478
|
return (value / stepPx) * stepPx
|
|
@@ -546,13 +552,15 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
|
|
|
546
552
|
|
|
547
553
|
val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
|
|
548
554
|
|
|
549
|
-
internal fun getInputSearchType(options: NavigationOptions): InputSearchType{
|
|
555
|
+
internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
|
|
550
556
|
return when (val headerType = options.headerType) {
|
|
551
557
|
is HeaderType.DefaultOrExtended -> when {
|
|
552
558
|
headerType.inputSearchProps == null -> InputSearchType.None
|
|
553
559
|
headerType.useAnimated -> InputSearchType.Animated
|
|
554
560
|
else -> InputSearchType.Header
|
|
555
561
|
}
|
|
562
|
+
|
|
556
563
|
else -> InputSearchType.None
|
|
557
564
|
}
|
|
558
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
|
+
}
|
|
@@ -44,13 +44,21 @@ fun supportsImePadding(): Boolean = when (val v = getOSVersion()) {
|
|
|
44
44
|
is OSVersion.IOS -> true
|
|
45
45
|
}
|
|
46
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
|
+
*/
|
|
47
54
|
@Composable
|
|
48
55
|
expect fun LottieAnimation(
|
|
49
56
|
path: String,
|
|
50
57
|
tintColor: Color? = null,
|
|
51
58
|
bgColor: Color? = null,
|
|
52
59
|
placedAsOverlay: Boolean = false,
|
|
53
|
-
modifier: Modifier = Modifier
|
|
60
|
+
modifier: Modifier = Modifier,
|
|
61
|
+
useCompose: Boolean = false
|
|
54
62
|
)
|
|
55
63
|
|
|
56
64
|
expect fun NativePaint.setColor(
|
|
@@ -24,13 +24,21 @@ fun getResource(name: String): DrawableResource {
|
|
|
24
24
|
@Composable
|
|
25
25
|
fun readJson(name: String): String {
|
|
26
26
|
val path = name.plus(".json")
|
|
27
|
+
val candidatePaths = listOf(
|
|
28
|
+
path,
|
|
29
|
+
"composeResources/vn.momo.compose.resources/$path",
|
|
30
|
+
)
|
|
27
31
|
|
|
28
32
|
val jsonContent by rememberState(path, { "" }) {
|
|
29
33
|
val cached = resourceCache.getOrPut(path) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
var content = ""
|
|
35
|
+
for (candidate in candidatePaths) {
|
|
36
|
+
try {
|
|
37
|
+
content = readResourceBytes(candidate).decodeToString()
|
|
38
|
+
break
|
|
39
|
+
} catch (_: Exception) {
|
|
40
|
+
// try next candidate
|
|
41
|
+
}
|
|
34
42
|
}
|
|
35
43
|
ResourceCache.JSON(content)
|
|
36
44
|
} as ResourceCache.JSON
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package vn.momo.kits.navigation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.DisposableEffect
|
|
5
|
+
import platform.Foundation.NSNotification
|
|
6
|
+
import platform.Foundation.NSNotificationCenter
|
|
7
|
+
import platform.Foundation.NSOperationQueue
|
|
8
|
+
import platform.darwin.NSObjectProtocol
|
|
9
|
+
|
|
10
|
+
private const val STATUS_BAR_TAPPED_NOTIFICATION = "statusBarSelected"
|
|
11
|
+
|
|
12
|
+
@Composable
|
|
13
|
+
internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
|
|
14
|
+
val maxApi = LocalMaxApi.current
|
|
15
|
+
DisposableEffect(callback) {
|
|
16
|
+
val cb = callback
|
|
17
|
+
val token: NSObjectProtocol? = if (cb != null) {
|
|
18
|
+
NSNotificationCenter.defaultCenter.addObserverForName(
|
|
19
|
+
name = STATUS_BAR_TAPPED_NOTIFICATION,
|
|
20
|
+
`object` = null,
|
|
21
|
+
queue = NSOperationQueue.mainQueue,
|
|
22
|
+
) { _: NSNotification? ->
|
|
23
|
+
runCatching { cb() }.onFailure {
|
|
24
|
+
maxApi?.logFile("[ScrollToTop] callback threw: ${it.message}") {}
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else null
|
|
29
|
+
onDispose {
|
|
30
|
+
token?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -98,8 +98,14 @@ actual fun LottieAnimation(
|
|
|
98
98
|
tintColor: Color?,
|
|
99
99
|
bgColor: Color?,
|
|
100
100
|
placedAsOverlay: Boolean,
|
|
101
|
-
modifier: Modifier
|
|
101
|
+
modifier: Modifier,
|
|
102
|
+
useCompose: Boolean
|
|
102
103
|
) {
|
|
104
|
+
if (useCompose) {
|
|
105
|
+
ComposeLottieAnimation(path, tintColor, bgColor, modifier)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
var animation by remember { mutableStateOf<CompatibleAnimation?>(null) }
|
|
104
110
|
|
|
105
111
|
LaunchedEffect(Unit) {
|
|
@@ -13,6 +13,7 @@ androidGradlePlugin = "8.13.2"
|
|
|
13
13
|
r8 = "8.9.35"
|
|
14
14
|
kotlinx-datetime = "0.7.1"
|
|
15
15
|
airbnb-lottie = "5.2.0"
|
|
16
|
+
compottie = "2.2.1"
|
|
16
17
|
androidx-activity = "1.9.1"
|
|
17
18
|
androidx-appcompat = "1.7.0"
|
|
18
19
|
material = "1.10.0"
|
|
@@ -38,6 +39,7 @@ coil-multiplatform-compose = { module = "io.coil-kt.coil3:coil-compose", version
|
|
|
38
39
|
coil-multiplatform-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3-multiplatform" }
|
|
39
40
|
r8 = { module = "com.android.tools:r8", version.ref = "r8" }
|
|
40
41
|
airbnb-lottie = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "airbnb-lottie" }
|
|
42
|
+
compottie = { module = "io.github.alexzhirkevich:compottie", version.ref = "compottie" }
|
|
41
43
|
native-max-api = { group = "vn.momo.maxapi", name = "NativeMaxApi", version.ref = "nativemaxapi" }
|
|
42
44
|
kits = { module = "vn.momo.kits:kits", version.ref = "kits" }
|
|
43
45
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
package/gradle.properties
CHANGED