@momo-kits/native-kits 0.160.6-debug → 0.160.7-beta.5-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 +31 -0
- 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/InputOTP.kt +4 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +5 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +72 -68
- 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/StatusBarTap/StatusBarTap.h +13 -0
- package/ios/StatusBarTap/StatusBarTap.m +75 -0
- package/ios/native-kits.podspec +2 -1
- package/local.properties +8 -0
- package/package.json +1 -1
- package/settings.gradle.kts +15 -3
|
@@ -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
|
|
@@ -131,7 +132,9 @@ fun InputOTP(
|
|
|
131
132
|
},
|
|
132
133
|
decorationBox = { innerTextField ->
|
|
133
134
|
Box {
|
|
134
|
-
Box(
|
|
135
|
+
Box(
|
|
136
|
+
modifier = Modifier.fillMaxWidth().height(56.dp).alpha(0f)
|
|
137
|
+
) { innerTextField() }
|
|
135
138
|
if (floatingValue.isNotEmpty()) {
|
|
136
139
|
Box(
|
|
137
140
|
modifier = Modifier.wrapContentSize()
|
|
@@ -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,7 @@ fun NavigationContainer(
|
|
|
132
135
|
DisposableEffect(Unit) {
|
|
133
136
|
onDispose {
|
|
134
137
|
navigator.dispose()
|
|
138
|
+
DynamicScreenRegistry.unregisterScreen(screenId)
|
|
135
139
|
}
|
|
136
140
|
}
|
|
137
141
|
}
|
|
@@ -196,6 +196,18 @@ object DynamicScreenRegistry {
|
|
|
196
196
|
return DynamicScreenRoute(id)
|
|
197
197
|
}
|
|
198
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
|
+
|
|
199
211
|
fun unregisterScreen(id: Int) {
|
|
200
212
|
screens.remove(id)
|
|
201
213
|
}
|
|
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Column
|
|
|
11
11
|
import androidx.compose.foundation.layout.ColumnScope
|
|
12
12
|
import androidx.compose.foundation.layout.Spacer
|
|
13
13
|
import androidx.compose.foundation.layout.WindowInsets
|
|
14
|
+
import androidx.compose.foundation.layout.asPaddingValues
|
|
14
15
|
import androidx.compose.foundation.layout.aspectRatio
|
|
15
16
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
16
17
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
@@ -27,10 +28,12 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|
|
27
28
|
import androidx.compose.runtime.Composable
|
|
28
29
|
import androidx.compose.runtime.CompositionLocalProvider
|
|
29
30
|
import androidx.compose.runtime.LaunchedEffect
|
|
31
|
+
import androidx.compose.runtime.State
|
|
30
32
|
import androidx.compose.runtime.getValue
|
|
31
33
|
import androidx.compose.runtime.mutableIntStateOf
|
|
32
34
|
import androidx.compose.runtime.mutableStateOf
|
|
33
35
|
import androidx.compose.runtime.remember
|
|
36
|
+
import androidx.compose.runtime.rememberUpdatedState
|
|
34
37
|
import androidx.compose.runtime.snapshotFlow
|
|
35
38
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
36
39
|
import androidx.compose.ui.Alignment
|
|
@@ -150,6 +153,10 @@ internal fun StackScreen(
|
|
|
150
153
|
val footerHeightPx = remember { mutableIntStateOf(0) }
|
|
151
154
|
val headerRightWidthPx = remember { mutableIntStateOf(0) }
|
|
152
155
|
|
|
156
|
+
if (navigation.options.scrollData.scrollToTopCallback != null) {
|
|
157
|
+
RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
BackHandler(true) { navigator.onBackSafe() }
|
|
154
161
|
|
|
155
162
|
CompositionLocalProvider(
|
|
@@ -160,37 +167,35 @@ internal fun StackScreen(
|
|
|
160
167
|
LocalFooterHeightPx provides footerHeightPx,
|
|
161
168
|
LocalHeaderRightWidthPx provides headerRightWidthPx
|
|
162
169
|
) {
|
|
163
|
-
Box(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
Box(
|
|
171
|
+
Modifier
|
|
172
|
+
.fillMaxSize()
|
|
173
|
+
.background(options.backgroundColor ?: AppTheme.current.colors.background.default)
|
|
174
|
+
.conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
|
|
175
|
+
hideKeyboardOnTap()
|
|
176
|
+
}
|
|
177
|
+
.conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
|
|
178
|
+
imePadding()
|
|
179
|
+
}
|
|
172
180
|
) {
|
|
173
181
|
Box(Modifier.zIndex(1f)) {
|
|
174
182
|
HeaderBackground()
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
Box(Modifier.zIndex(
|
|
185
|
+
Box(Modifier.zIndex(4f)) {
|
|
178
186
|
Header(onBackHandler)
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
Column
|
|
189
|
+
Column(Modifier.zIndex(5f)) {
|
|
182
190
|
SearchAnimated(isScrollInProgress = scrollInProcess)
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
Column(Modifier.zIndex(2f).fillMaxSize()) {
|
|
186
194
|
MainContent(content = content)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
Box(Modifier.zIndex(4f).align(Alignment.BottomCenter)) {
|
|
190
195
|
FooterContent()
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
Box(Modifier.zIndex(
|
|
198
|
+
Box(Modifier.zIndex(6f)) {
|
|
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
|
-
|
|
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(
|
|
234
|
-
|
|
235
|
-
|
|
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,35 +250,31 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
|
243
250
|
)
|
|
244
251
|
Spacer(Modifier.height(animatedTopPadding))
|
|
245
252
|
}
|
|
246
|
-
Column
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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){
|
|
257
|
-
val footerHeight = LocalFooterHeightPx.current
|
|
258
|
-
val footerHeightDp = with(density) { footerHeight.value.toDp() }
|
|
259
|
-
Spacer(Modifier.height(footerHeightDp))
|
|
260
|
-
}
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
@Composable
|
|
264
|
-
fun ScreenContent(content: @Composable () -> Unit){
|
|
267
|
+
fun ScreenContent(content: @Composable () -> Unit) {
|
|
265
268
|
val scrollState = LocalScrollState.current
|
|
266
269
|
val options = LocalOptions.current
|
|
267
270
|
|
|
268
|
-
if (options.headerType is HeaderType.Animated){
|
|
271
|
+
if (options.headerType is HeaderType.Animated) {
|
|
269
272
|
val animatedHeader = options.headerType
|
|
270
273
|
Box {
|
|
271
|
-
Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)){
|
|
274
|
+
Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)) {
|
|
272
275
|
animatedHeader.composable.invoke(scrollState.value)
|
|
273
276
|
}
|
|
274
|
-
Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)){
|
|
277
|
+
Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
|
|
275
278
|
content()
|
|
276
279
|
}
|
|
277
280
|
}
|
|
@@ -281,25 +284,27 @@ fun ScreenContent(content: @Composable () -> Unit){
|
|
|
281
284
|
}
|
|
282
285
|
|
|
283
286
|
@Composable
|
|
284
|
-
fun FooterContent(){
|
|
287
|
+
fun FooterContent() {
|
|
285
288
|
val options = LocalOptions.current
|
|
286
|
-
if (options.footerComponent != null){
|
|
287
|
-
val
|
|
288
|
-
val
|
|
289
|
-
val imeBottom = ime.getBottom(density)
|
|
290
|
-
val thresholdPx = with(density) { 50.dp.toPx() }
|
|
291
|
-
val isKeyboardVisible = imeBottom > thresholdPx
|
|
292
|
-
val bottomPadding = if (isKeyboardVisible) 0.dp else AppNavigationBar.current
|
|
289
|
+
if (options.footerComponent != null) {
|
|
290
|
+
val keyboardSize = keyboardSizeState()
|
|
291
|
+
val bottomPadding = (AppNavigationBar.current - keyboardSize.value).coerceAtLeast(0.dp)
|
|
293
292
|
Footer(footerComponent = options.footerComponent, bottomPadding = bottomPadding)
|
|
294
293
|
}
|
|
295
294
|
}
|
|
296
295
|
|
|
297
296
|
@Composable
|
|
298
|
-
fun
|
|
297
|
+
fun keyboardSizeState(): State<Dp> {
|
|
298
|
+
val bottom = WindowInsets.ime.asPaddingValues()
|
|
299
|
+
return rememberUpdatedState(bottom.calculateBottomPadding())
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@Composable
|
|
303
|
+
fun OverplayView(bottomTabIndex: Int, id: Int) {
|
|
299
304
|
val overplayType = OverplayComponentRegistry.getOverplayType()
|
|
300
305
|
|
|
301
306
|
if (overplayType != null) {
|
|
302
|
-
Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else
|
|
307
|
+
Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 7f).fillMaxSize()) {
|
|
303
308
|
if (bottomTabIndex != -1) return@Box
|
|
304
309
|
if (OverplayComponentRegistry.currentRootId() != id) return@Box
|
|
305
310
|
OverplayComponentRegistry.OverlayComponent()
|
|
@@ -322,22 +327,24 @@ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
|
|
|
322
327
|
Box(Modifier.onGloballyPositioned {
|
|
323
328
|
if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
|
|
324
329
|
}) {
|
|
325
|
-
Box(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
Box(
|
|
331
|
+
Modifier
|
|
332
|
+
.fillMaxWidth()
|
|
333
|
+
.background(AppTheme.current.colors.background.surface)
|
|
334
|
+
.conditional(IsShowBaseLineDebug) {
|
|
335
|
+
border(1.dp, Colors.blue_03)
|
|
336
|
+
}
|
|
337
|
+
.padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
|
|
338
|
+
) {
|
|
333
339
|
footerComponent.invoke()
|
|
334
340
|
}
|
|
335
341
|
|
|
336
|
-
Box(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
342
|
+
Box(
|
|
343
|
+
modifier = Modifier
|
|
344
|
+
.fillMaxWidth()
|
|
345
|
+
.height(6.dp)
|
|
346
|
+
.offset(x = 0.dp, y = (-6).dp)
|
|
347
|
+
.background(shadowBrush)
|
|
341
348
|
)
|
|
342
349
|
}
|
|
343
350
|
}
|
|
@@ -347,6 +354,7 @@ data class InputSearchLayoutParams(
|
|
|
347
354
|
val startPadding: Dp,
|
|
348
355
|
val endPadding: Dp
|
|
349
356
|
)
|
|
357
|
+
|
|
350
358
|
@Composable
|
|
351
359
|
fun SearchAnimated(
|
|
352
360
|
isScrollInProgress: Boolean
|
|
@@ -455,12 +463,6 @@ fun SearchAnimated(
|
|
|
455
463
|
}
|
|
456
464
|
}
|
|
457
465
|
|
|
458
|
-
@Composable
|
|
459
|
-
internal fun isKeyboardVisible(): Boolean {
|
|
460
|
-
val ime = WindowInsets.ime
|
|
461
|
-
val density = LocalDensity.current
|
|
462
|
-
return ime.getBottom(density) > 0
|
|
463
|
-
}
|
|
464
466
|
private fun quantize(value: Int, stepPx: Int): Int {
|
|
465
467
|
if (stepPx <= 1) return value
|
|
466
468
|
return (value / stepPx) * stepPx
|
|
@@ -540,13 +542,15 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
|
|
|
540
542
|
|
|
541
543
|
val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
|
|
542
544
|
|
|
543
|
-
internal fun getInputSearchType(options: NavigationOptions): InputSearchType{
|
|
545
|
+
internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
|
|
544
546
|
return when (val headerType = options.headerType) {
|
|
545
547
|
is HeaderType.DefaultOrExtended -> when {
|
|
546
548
|
headerType.inputSearchProps == null -> InputSearchType.None
|
|
547
549
|
headerType.useAnimated -> InputSearchType.Animated
|
|
548
550
|
else -> InputSearchType.Header
|
|
549
551
|
}
|
|
552
|
+
|
|
550
553
|
else -> InputSearchType.None
|
|
551
554
|
}
|
|
552
555
|
}
|
|
556
|
+
|
|
@@ -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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// Posts `MoMoStatusBarTap.notificationName` on every status-bar tap.
|
|
6
|
+
/// The swizzle installs automatically at framework load (`+load`) — calling
|
|
7
|
+
/// `install` is a no-op kept for explicit/manual triggering.
|
|
8
|
+
@interface MoMoStatusBarTap : NSObject
|
|
9
|
+
@property (class, nonatomic, readonly) NSNotificationName notificationName;
|
|
10
|
+
+ (void)install;
|
|
11
|
+
@end
|
|
12
|
+
|
|
13
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
#import <objc/message.h>
|
|
3
|
+
#import <objc/runtime.h>
|
|
4
|
+
#import "StatusBarTap.h"
|
|
5
|
+
|
|
6
|
+
// Performs the actual swizzle once per process. Safe to call multiple times.
|
|
7
|
+
static void mm_installStatusBarTapSwizzleOnce(void) {
|
|
8
|
+
static dispatch_once_t onceToken;
|
|
9
|
+
dispatch_once(&onceToken, ^{
|
|
10
|
+
Class cls = [UIStatusBarManager class];
|
|
11
|
+
SEL originalSelector = NSSelectorFromString(@"handleTapAction:");
|
|
12
|
+
SEL swizzledSelector = NSSelectorFromString(@"mm_handleTapAction:");
|
|
13
|
+
|
|
14
|
+
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
|
|
15
|
+
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
|
|
16
|
+
if (!originalMethod || !swizzledMethod) return;
|
|
17
|
+
|
|
18
|
+
// Verify the original method matches the (id) -> void shape we expect.
|
|
19
|
+
// Encoding may include byte offsets (e.g. "v32@0:8@16") so we filter
|
|
20
|
+
// digits out before comparing.
|
|
21
|
+
const char *encPtr = method_getTypeEncoding(originalMethod);
|
|
22
|
+
if (encPtr) {
|
|
23
|
+
NSMutableString *enc = [NSMutableString string];
|
|
24
|
+
for (const char *p = encPtr; *p; p++) {
|
|
25
|
+
if (!(*p >= '0' && *p <= '9')) [enc appendFormat:@"%c", *p];
|
|
26
|
+
}
|
|
27
|
+
if (![enc isEqualToString:@"v@:@"]) return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
BOOL didAdd = class_addMethod(cls,
|
|
31
|
+
originalSelector,
|
|
32
|
+
method_getImplementation(swizzledMethod),
|
|
33
|
+
method_getTypeEncoding(swizzledMethod));
|
|
34
|
+
if (didAdd) {
|
|
35
|
+
class_replaceMethod(cls,
|
|
36
|
+
swizzledSelector,
|
|
37
|
+
method_getImplementation(originalMethod),
|
|
38
|
+
method_getTypeEncoding(originalMethod));
|
|
39
|
+
} else {
|
|
40
|
+
method_exchangeImplementations(originalMethod, swizzledMethod);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@implementation MoMoStatusBarTap
|
|
46
|
+
+ (NSNotificationName)notificationName { return @"statusBarSelected"; }
|
|
47
|
+
+ (void)install { mm_installStatusBarTapSwizzleOnce(); }
|
|
48
|
+
@end
|
|
49
|
+
|
|
50
|
+
// MARK: - Swizzle target on UIStatusBarManager
|
|
51
|
+
// `mm_handleTapAction:` is the replacement IMP. After the add/exchange,
|
|
52
|
+
// this selector points at the original UIKit IMP — calling it inside
|
|
53
|
+
// the body invokes UIKit's real handler so UIScrollView.scrollsToTop
|
|
54
|
+
// and any other side effects continue to work.
|
|
55
|
+
|
|
56
|
+
@interface UIStatusBarManager (MoMoStatusBarTap)
|
|
57
|
+
@end
|
|
58
|
+
|
|
59
|
+
@implementation UIStatusBarManager (MoMoStatusBarTap)
|
|
60
|
+
|
|
61
|
+
// Auto-install at framework load. Commented out so the swizzle stays
|
|
62
|
+
// opt-in via `[MoMoStatusBarTap install]`. Uncomment to revert to
|
|
63
|
+
// auto-installation before main().
|
|
64
|
+
//
|
|
65
|
+
// + (void)load {
|
|
66
|
+
// mm_installStatusBarTapSwizzleOnce();
|
|
67
|
+
// }
|
|
68
|
+
|
|
69
|
+
- (void)mm_handleTapAction:(id)action {
|
|
70
|
+
[self mm_handleTapAction:action];
|
|
71
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:MoMoStatusBarTap.notificationName
|
|
72
|
+
object:nil];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@end
|
package/ios/native-kits.podspec
CHANGED
|
@@ -10,7 +10,8 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.ios.deployment_target = '15.0'
|
|
11
11
|
s.swift_version = '5.0'
|
|
12
12
|
|
|
13
|
-
s.source_files = "**/*.swift"
|
|
13
|
+
s.source_files = "**/*.{swift,m,h}"
|
|
14
|
+
s.public_header_files = "StatusBarTap/*.h"
|
|
14
15
|
s.framework = 'SwiftUI', 'Combine'
|
|
15
16
|
s.dependency 'SDWebImageSwiftUI'
|
|
16
17
|
s.dependency 'lottie-ios'
|
package/local.properties
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## This file must *NOT* be checked into Version Control Systems,
|
|
2
|
+
# as it contains information specific to your local configuration.
|
|
3
|
+
#
|
|
4
|
+
# Location of the SDK. This is only used by Gradle.
|
|
5
|
+
# For customization when using a Version Control System, please read the
|
|
6
|
+
# header note.
|
|
7
|
+
#Mon Dec 22 10:07:29 ICT 2025
|
|
8
|
+
sdk.dir=/Users/phuc/Library/Android/sdk
|