@momo-kits/native-kits 0.164.1-beta.9-debug → 0.164.1-headerconfig.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 +7 -178
- package/CLAUDE.md +2 -0
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +3 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +18 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +6 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +30 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +8 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +9 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +4 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +104 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +3 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +8 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +38 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +20 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +10 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +20 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +11 -27
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +4 -10
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +19 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +41 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +75 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +5 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +5 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +6 -3
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +8 -23
- package/gradle/libs.versions.toml +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/Navigation/NavigatorManager.swift +0 -27
- package/ios/Colors+Radius+Spacing/Theme.swift +2 -2
- package/ios/Typography/KitDesignSystem.swift +17 -0
- package/ios/Typography/Text.swift +25 -1
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/local.properties +2 -2
- package/package.json +1 -1
- package/publish.sh +25 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ComposeBackGesture.ios.kt +0 -65
|
@@ -18,9 +18,7 @@ import androidx.compose.runtime.Stable
|
|
|
18
18
|
import androidx.compose.runtime.derivedStateOf
|
|
19
19
|
import androidx.compose.runtime.getValue
|
|
20
20
|
import androidx.compose.runtime.mutableStateMapOf
|
|
21
|
-
import androidx.compose.runtime.mutableStateOf
|
|
22
21
|
import androidx.compose.runtime.remember
|
|
23
|
-
import androidx.compose.runtime.setValue
|
|
24
22
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
25
23
|
import androidx.compose.ui.Modifier
|
|
26
24
|
import androidx.compose.ui.draw.drawBehind
|
|
@@ -44,21 +42,14 @@ private const val SCRIM_MAX_ALPHA = 0.6f
|
|
|
44
42
|
private const val EDGE_SHADOW_MAX_ALPHA = 0.3f
|
|
45
43
|
private val EDGE_SHADOW_WIDTH = 8.dp
|
|
46
44
|
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
* which disables the predictive-back drag on both platforms — the gesture then pops without
|
|
50
|
-
* following the finger, and fires no haptic. Screen transitions stay on the parallax curve either
|
|
51
|
-
* way.
|
|
52
|
-
*/
|
|
45
|
+
/** Kill switch: `false` restores pre-DS-760 back handling without a kit release. Transitions
|
|
46
|
+
* stay on the new curve either way. */
|
|
53
47
|
object SwipeBackConfig {
|
|
54
|
-
|
|
55
|
-
// unrelated recomposition of StackScreen.
|
|
56
|
-
var enabled: Boolean by mutableStateOf(true)
|
|
48
|
+
var enabled: Boolean = true
|
|
57
49
|
}
|
|
58
50
|
|
|
59
|
-
// LinearEasing is mandatory
|
|
60
|
-
//
|
|
61
|
-
// finger mid-gesture.
|
|
51
|
+
// LinearEasing is mandatory: seekTo() maps drag fraction onto the timeline, so a curve would
|
|
52
|
+
// decouple the card from the finger.
|
|
62
53
|
private val stackSpec = tween<IntOffset>(STACK_TRANSITION_DURATION, easing = LinearEasing)
|
|
63
54
|
|
|
64
55
|
internal fun AnimatedContentTransitionScope<NavBackStackEntry>.stackEnter(): EnterTransition =
|
|
@@ -76,13 +67,9 @@ internal fun AnimatedContentTransitionScope<NavBackStackEntry>.stackPopExit(): E
|
|
|
76
67
|
slideOutHorizontally(stackSpec) { it }
|
|
77
68
|
|
|
78
69
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* Screen ids come from a single monotonic process-wide counter and NavHost keeps both entries
|
|
83
|
-
* composed for the whole transition segment, so "highest composed stack id" is stable in both
|
|
84
|
-
* directions. `currentBackStackEntry` is not usable here: it flips at frame 0 of a pop, which
|
|
85
|
-
* would make the scrim vanish instantly instead of fading.
|
|
70
|
+
* Which composed screen is the top card. Ids are monotonic and NavHost keeps both entries composed
|
|
71
|
+
* for the whole segment, so "highest id" is stable both ways. `currentBackStackEntry` is not: it
|
|
72
|
+
* flips at frame 0 of a pop and the scrim would vanish instead of fading.
|
|
86
73
|
*/
|
|
87
74
|
@Stable
|
|
88
75
|
internal class StackTransitionCoordinator {
|
|
@@ -111,10 +98,8 @@ internal fun AnimatedVisibilityScope.StackTransitionDecor(
|
|
|
111
98
|
content: @Composable () -> Unit,
|
|
112
99
|
) {
|
|
113
100
|
val coordinator = LocalStackTransition.current
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
DisposableEffect(coordinator, id, isStackRoute) {
|
|
117
|
-
coordinator.attach(id, isStackRoute)
|
|
101
|
+
coordinator.attach(id, isStackRoute)
|
|
102
|
+
DisposableEffect(coordinator, id) {
|
|
118
103
|
onDispose { coordinator.detach(id) }
|
|
119
104
|
}
|
|
120
105
|
|
|
@@ -147,8 +132,7 @@ internal fun AnimatedVisibilityScope.StackTransitionDecor(
|
|
|
147
132
|
Box(
|
|
148
133
|
Modifier
|
|
149
134
|
.matchParentSize()
|
|
150
|
-
//
|
|
151
|
-
// in the composable body would recompose the whole screen every gesture frame.
|
|
135
|
+
// read inside drawBehind: in the body this would recompose every gesture frame
|
|
152
136
|
.drawBehind {
|
|
153
137
|
drawRect(
|
|
154
138
|
brush = shadowBrush,
|
|
@@ -7,19 +7,13 @@ import vn.momo.kits.platform.getScreenDimensions
|
|
|
7
7
|
import vn.momo.kits.platform.rememberBackGestureProgress
|
|
8
8
|
import vn.momo.maxapi.IMaxApi
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 100f
|
|
10
|
+
// Unscaled, so the tick lands at the same physical distance on every device.
|
|
11
|
+
private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 50f
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
* Fires one light tick once the back gesture has travelled ~100dp, re-arming when the finger drops
|
|
16
|
-
* back below the threshold. iOS only — [rememberBackGestureProgress] is always `null` on Android.
|
|
17
|
-
*/
|
|
13
|
+
/** One light tick per crossing, re-armed below the threshold. iOS only. */
|
|
18
14
|
@Composable
|
|
19
15
|
internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
|
|
20
|
-
|
|
21
|
-
// moves — a tick there would be a phantom.
|
|
22
|
-
if (maxApi == null || !SwipeBackConfig.enabled) return
|
|
16
|
+
if (maxApi == null) return
|
|
23
17
|
|
|
24
18
|
val progress = rememberBackGestureProgress()
|
|
25
19
|
val screenWidth = getScreenDimensions().width.toFloat()
|
|
@@ -8,12 +8,14 @@ import androidx.compose.animation.scaleOut
|
|
|
8
8
|
import androidx.compose.foundation.background
|
|
9
9
|
import androidx.compose.foundation.layout.*
|
|
10
10
|
import androidx.compose.runtime.Composable
|
|
11
|
+
import androidx.compose.runtime.DisposableEffect
|
|
11
12
|
import androidx.compose.runtime.LaunchedEffect
|
|
12
13
|
import androidx.compose.ui.Alignment
|
|
13
14
|
import androidx.compose.ui.Modifier
|
|
14
15
|
import androidx.compose.ui.unit.dp
|
|
15
16
|
import androidx.navigation.compose.NavHost
|
|
16
17
|
import androidx.navigation.compose.composable
|
|
18
|
+
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
17
19
|
import androidx.navigation.compose.rememberNavController
|
|
18
20
|
import vn.momo.kits.const.AppNavigationBar
|
|
19
21
|
import vn.momo.kits.const.AppTheme
|
|
@@ -24,6 +26,7 @@ import vn.momo.kits.navigation.LocalNavigator
|
|
|
24
26
|
import vn.momo.kits.navigation.NavigationOptions
|
|
25
27
|
import vn.momo.kits.navigation.StackScreen
|
|
26
28
|
import vn.momo.kits.navigation.component.HeaderType
|
|
29
|
+
import vn.momo.kits.platform.reportFloatingArrowBack
|
|
27
30
|
|
|
28
31
|
private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
|
|
29
32
|
internal fun setBottomTabOption(index: Int, options: NavigationOptions){
|
|
@@ -59,6 +62,22 @@ fun BottomTab(
|
|
|
59
62
|
)
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
// Tab 0 keeps the seeked transition; other tabs go to the host's floating arrow. Matches RN.
|
|
66
|
+
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
|
67
|
+
val isFirstTab = currentRoute == null || currentRoute == "option0"
|
|
68
|
+
|
|
69
|
+
DisposableEffect(isFirstTab) {
|
|
70
|
+
reportFloatingArrowBack(!isFirstTab)
|
|
71
|
+
navigator.tabBackHandler = if (isFirstTab) null else ({
|
|
72
|
+
navController.popBackStack(navController.graph.startDestinationRoute!!, false)
|
|
73
|
+
})
|
|
74
|
+
// both live outside this composition
|
|
75
|
+
onDispose {
|
|
76
|
+
reportFloatingArrowBack(false)
|
|
77
|
+
navigator.tabBackHandler = null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
|
|
63
82
|
Box(modifier = Modifier
|
|
64
83
|
.fillMaxSize()
|
|
@@ -15,11 +15,14 @@ import androidx.compose.foundation.layout.offset
|
|
|
15
15
|
import androidx.compose.foundation.layout.padding
|
|
16
16
|
import androidx.compose.foundation.layout.size
|
|
17
17
|
import androidx.compose.foundation.layout.width
|
|
18
|
+
import androidx.compose.foundation.layout.wrapContentHeight
|
|
18
19
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
19
20
|
import androidx.compose.runtime.Composable
|
|
20
21
|
import androidx.compose.runtime.getValue
|
|
21
22
|
import androidx.compose.ui.Alignment
|
|
22
23
|
import androidx.compose.ui.Modifier
|
|
24
|
+
import androidx.compose.ui.draw.clipToBounds
|
|
25
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
23
26
|
import androidx.compose.ui.graphics.Brush
|
|
24
27
|
import androidx.compose.ui.graphics.Color
|
|
25
28
|
import androidx.compose.ui.layout.onGloballyPositioned
|
|
@@ -28,6 +31,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|
|
28
31
|
import androidx.compose.ui.unit.Dp
|
|
29
32
|
import androidx.compose.ui.unit.dp
|
|
30
33
|
import vn.momo.kits.components.Icon
|
|
34
|
+
import vn.momo.kits.components.Image
|
|
31
35
|
import vn.momo.kits.components.InputSearchProps
|
|
32
36
|
import vn.momo.kits.const.AppStatusBar
|
|
33
37
|
import vn.momo.kits.const.AppTheme
|
|
@@ -88,15 +92,38 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
88
92
|
// Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
|
|
89
93
|
val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
|
|
90
94
|
|
|
95
|
+
val headerImage = AppTheme.current.assets.headerBackground
|
|
96
|
+
.takeUnless { options.headerType is HeaderType.Transparent }
|
|
97
|
+
|
|
91
98
|
Box(
|
|
92
99
|
Modifier.height(AppStatusBar.current + HEADER_HEIGHT.dp)
|
|
93
100
|
.fillMaxWidth()
|
|
94
|
-
.
|
|
101
|
+
.conditional(headerImage.isNullOrEmpty()) {
|
|
102
|
+
background(background.copy(alpha = animatedAlpha))
|
|
103
|
+
}
|
|
95
104
|
.conditional(IsShowBaseLineDebug) {
|
|
96
105
|
border(1.dp, Colors.blue_03)
|
|
97
106
|
},
|
|
98
107
|
contentAlignment = Alignment.BottomCenter
|
|
99
108
|
) {
|
|
109
|
+
if (!headerImage.isNullOrEmpty()) {
|
|
110
|
+
Box(
|
|
111
|
+
Modifier
|
|
112
|
+
.matchParentSize()
|
|
113
|
+
.clipToBounds()
|
|
114
|
+
.graphicsLayer { alpha = animatedAlpha }
|
|
115
|
+
.background(background)
|
|
116
|
+
) {
|
|
117
|
+
Image(
|
|
118
|
+
source = headerImage,
|
|
119
|
+
loading = false,
|
|
120
|
+
modifier = Modifier
|
|
121
|
+
.fillMaxWidth()
|
|
122
|
+
.wrapContentHeight(Alignment.Top, unbounded = true)
|
|
123
|
+
.height(HEADER_BACKGROUND_HEIGHT),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
100
127
|
Row(
|
|
101
128
|
modifier = Modifier.height(HEADER_HEIGHT.dp)
|
|
102
129
|
.fillMaxWidth()
|
|
@@ -191,8 +218,18 @@ internal fun HeaderContent(
|
|
|
191
218
|
textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
|
|
192
219
|
)
|
|
193
220
|
}
|
|
194
|
-
is HeaderTitle.Journey -> {
|
|
195
|
-
|
|
221
|
+
is HeaderTitle.Journey -> {
|
|
222
|
+
HeaderJourney(
|
|
223
|
+
data = headerTitle,
|
|
224
|
+
tintIconColor = tintIconColor
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
is HeaderTitle.Location -> {
|
|
228
|
+
HeaderLocation(
|
|
229
|
+
data = headerTitle,
|
|
230
|
+
tintIconColor = tintIconColor
|
|
231
|
+
)
|
|
232
|
+
}
|
|
196
233
|
is HeaderTitle.User -> {
|
|
197
234
|
HeaderUser(
|
|
198
235
|
data = headerTitle
|
|
@@ -274,6 +311,7 @@ sealed class HeaderType {
|
|
|
274
311
|
val aspectRatio: AnimatedHeaderRatio = AnimatedHeaderRatio.RATIO_16_9,
|
|
275
312
|
val isSurface: Boolean = true,
|
|
276
313
|
val layoutOffSet: Dp = 56.dp,
|
|
314
|
+
val height: Dp? = null,
|
|
277
315
|
val composable: @Composable (scrollState: Int) -> Unit = {}
|
|
278
316
|
) : HeaderType()
|
|
279
317
|
|
|
@@ -27,6 +27,8 @@ enum class AnimatedHeaderRatio(val value: Float){
|
|
|
27
27
|
RATIO_3_2(3f / 2f)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
internal val HEADER_BACKGROUND_HEIGHT = 154.dp
|
|
31
|
+
|
|
30
32
|
@Suppress("FrequentlyChangingValue")
|
|
31
33
|
@Composable
|
|
32
34
|
internal fun HeaderBackground() {
|
|
@@ -37,7 +39,7 @@ internal fun HeaderBackground() {
|
|
|
37
39
|
val backgroundColor = options.backgroundColor ?: AppTheme.current.colors.background.default
|
|
38
40
|
|
|
39
41
|
val minHeight = AppStatusBar.current + HEADER_HEIGHT.dp
|
|
40
|
-
val maxHeight =
|
|
42
|
+
val maxHeight = HEADER_BACKGROUND_HEIGHT
|
|
41
43
|
val opacity by animateFloatAsState(
|
|
42
44
|
targetValue = (1 - (scrollState.value * 1f / HEADER_HEIGHT * 1f)).coerceIn(0f, 1f),
|
|
43
45
|
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package vn.momo.kits.navigation.component
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
4
|
+
import androidx.compose.foundation.layout.Box
|
|
5
|
+
import androidx.compose.foundation.layout.Column
|
|
6
|
+
import androidx.compose.foundation.layout.Row
|
|
7
|
+
import androidx.compose.foundation.layout.Spacer
|
|
8
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
9
|
+
import androidx.compose.foundation.layout.height
|
|
10
|
+
import androidx.compose.foundation.layout.width
|
|
11
|
+
import androidx.compose.foundation.layout.widthIn
|
|
12
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
13
|
+
import androidx.compose.runtime.Composable
|
|
14
|
+
import androidx.compose.ui.Alignment
|
|
15
|
+
import androidx.compose.ui.Modifier
|
|
16
|
+
import androidx.compose.ui.draw.clip
|
|
17
|
+
import androidx.compose.ui.graphics.Color
|
|
18
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
19
|
+
import androidx.compose.ui.unit.dp
|
|
20
|
+
import vn.momo.kits.components.Icon
|
|
21
|
+
import vn.momo.kits.components.Skeleton
|
|
22
|
+
import vn.momo.kits.components.Text
|
|
23
|
+
import vn.momo.kits.const.Spacing
|
|
24
|
+
import vn.momo.kits.const.Typography
|
|
25
|
+
import vn.momo.kits.modifier.activeOpacityClickable
|
|
26
|
+
import vn.momo.kits.platform.getScreenDimensions
|
|
27
|
+
|
|
28
|
+
@Composable
|
|
29
|
+
internal fun HeaderJourney(
|
|
30
|
+
data: HeaderTitle,
|
|
31
|
+
tintIconColor: Color,
|
|
32
|
+
) {
|
|
33
|
+
if (data !is HeaderTitle.Journey) return
|
|
34
|
+
if (data.isLoading) return TitleJourneyShimmer()
|
|
35
|
+
|
|
36
|
+
val maxWidth = getScreenDimensions().width.dp - 172.dp
|
|
37
|
+
val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
|
|
38
|
+
val iconTint = parseHeaderTint(data.iconColor) ?: tint
|
|
39
|
+
|
|
40
|
+
Column(
|
|
41
|
+
modifier = Modifier
|
|
42
|
+
.widthIn(max = maxWidth)
|
|
43
|
+
.activeOpacityClickable(enabled = data.onPress != null, onClick = { data.onPress?.invoke() }),
|
|
44
|
+
verticalArrangement = Arrangement.Center,
|
|
45
|
+
) {
|
|
46
|
+
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
47
|
+
Text(
|
|
48
|
+
text = data.start,
|
|
49
|
+
color = tint,
|
|
50
|
+
style = Typography.actionXsBold,
|
|
51
|
+
maxLines = 1,
|
|
52
|
+
overflow = TextOverflow.Ellipsis,
|
|
53
|
+
modifier = Modifier.weight(1f, fill = false),
|
|
54
|
+
)
|
|
55
|
+
Spacer(Modifier.width(Spacing.S))
|
|
56
|
+
if (data.icon.isNotBlank()) {
|
|
57
|
+
Icon(source = data.icon, size = 16.dp, color = iconTint)
|
|
58
|
+
} else {
|
|
59
|
+
Spacer(Modifier.width(16.dp))
|
|
60
|
+
}
|
|
61
|
+
Spacer(Modifier.width(Spacing.S))
|
|
62
|
+
if (!data.end.isNullOrEmpty()) {
|
|
63
|
+
Text(
|
|
64
|
+
text = data.end,
|
|
65
|
+
color = tint,
|
|
66
|
+
style = Typography.actionXsBold,
|
|
67
|
+
maxLines = 1,
|
|
68
|
+
overflow = TextOverflow.Ellipsis,
|
|
69
|
+
modifier = Modifier.weight(1f, fill = false),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
Spacer(Modifier.width(Spacing.XS))
|
|
73
|
+
Icon(source = "16_arrow_chevron_down_small", size = 16.dp, color = tint)
|
|
74
|
+
}
|
|
75
|
+
if (!data.description.isNullOrEmpty()) {
|
|
76
|
+
Text(
|
|
77
|
+
text = data.description,
|
|
78
|
+
color = tint,
|
|
79
|
+
style = Typography.descriptionXsRegular,
|
|
80
|
+
maxLines = 1,
|
|
81
|
+
overflow = TextOverflow.Ellipsis,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Composable
|
|
88
|
+
private fun TitleJourneyShimmer() {
|
|
89
|
+
Column(modifier = Modifier.fillMaxWidth()) {
|
|
90
|
+
Box(modifier = Modifier.height(18.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
|
|
91
|
+
Spacer(Modifier.height(Spacing.XXS))
|
|
92
|
+
Box(modifier = Modifier.height(12.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
internal fun parseHeaderTint(value: String?): Color? {
|
|
97
|
+
val raw = value?.trim()?.removePrefix("#") ?: return null
|
|
98
|
+
val hex = when (raw.length) {
|
|
99
|
+
3 -> "FF" + raw.map { "$it$it" }.joinToString("")
|
|
100
|
+
6 -> "FF$raw"
|
|
101
|
+
8 -> raw
|
|
102
|
+
else -> return null
|
|
103
|
+
}
|
|
104
|
+
val v = hex.toLongOrNull(16) ?: return null
|
|
105
|
+
return Color(v)
|
|
106
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package vn.momo.kits.navigation.component
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Box
|
|
4
|
+
import androidx.compose.foundation.layout.Column
|
|
5
|
+
import androidx.compose.foundation.layout.Row
|
|
6
|
+
import androidx.compose.foundation.layout.Spacer
|
|
7
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
8
|
+
import androidx.compose.foundation.layout.height
|
|
9
|
+
import androidx.compose.foundation.layout.width
|
|
10
|
+
import androidx.compose.foundation.layout.widthIn
|
|
11
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
12
|
+
import androidx.compose.runtime.Composable
|
|
13
|
+
import androidx.compose.ui.Alignment
|
|
14
|
+
import androidx.compose.ui.Modifier
|
|
15
|
+
import androidx.compose.ui.draw.clip
|
|
16
|
+
import androidx.compose.ui.graphics.Color
|
|
17
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
18
|
+
import androidx.compose.ui.unit.dp
|
|
19
|
+
import vn.momo.kits.components.Icon
|
|
20
|
+
import vn.momo.kits.components.Skeleton
|
|
21
|
+
import vn.momo.kits.components.Text
|
|
22
|
+
import vn.momo.kits.const.Spacing
|
|
23
|
+
import vn.momo.kits.const.Typography
|
|
24
|
+
import vn.momo.kits.modifier.activeOpacityClickable
|
|
25
|
+
import vn.momo.kits.platform.getScreenDimensions
|
|
26
|
+
|
|
27
|
+
@Composable
|
|
28
|
+
internal fun HeaderLocation(
|
|
29
|
+
data: HeaderTitle,
|
|
30
|
+
tintIconColor: Color,
|
|
31
|
+
) {
|
|
32
|
+
if (data !is HeaderTitle.Location) return
|
|
33
|
+
if (data.isLoading) return TitleLocationShimmer()
|
|
34
|
+
|
|
35
|
+
val maxWidth = getScreenDimensions().width.dp - 172.dp
|
|
36
|
+
val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
|
|
37
|
+
|
|
38
|
+
Column(
|
|
39
|
+
modifier = Modifier
|
|
40
|
+
.widthIn(max = maxWidth)
|
|
41
|
+
.activeOpacityClickable(enabled = data.onPress != null, onClick = { data.onPress?.invoke() }),
|
|
42
|
+
horizontalAlignment = Alignment.Start,
|
|
43
|
+
) {
|
|
44
|
+
if (!data.description.isNullOrEmpty()) {
|
|
45
|
+
Text(
|
|
46
|
+
text = data.description,
|
|
47
|
+
color = tint,
|
|
48
|
+
style = Typography.descriptionXsRegular,
|
|
49
|
+
maxLines = 1,
|
|
50
|
+
overflow = TextOverflow.Ellipsis,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
54
|
+
Text(
|
|
55
|
+
text = data.location,
|
|
56
|
+
color = tint,
|
|
57
|
+
style = Typography.actionXsBold,
|
|
58
|
+
maxLines = 1,
|
|
59
|
+
overflow = TextOverflow.Ellipsis,
|
|
60
|
+
modifier = Modifier.weight(1f, fill = false),
|
|
61
|
+
)
|
|
62
|
+
Spacer(Modifier.width(Spacing.XS))
|
|
63
|
+
Icon(source = "16_arrow_chevron_down_small", size = 16.dp, color = tint)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Composable
|
|
69
|
+
private fun TitleLocationShimmer() {
|
|
70
|
+
Column(modifier = Modifier.fillMaxWidth()) {
|
|
71
|
+
Box(modifier = Modifier.height(12.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
|
|
72
|
+
Spacer(Modifier.height(Spacing.XXS))
|
|
73
|
+
Box(modifier = Modifier.height(18.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -12,6 +12,8 @@ import vn.momo.kits.components.Text
|
|
|
12
12
|
import vn.momo.kits.const.Typography
|
|
13
13
|
import vn.momo.kits.modifier.setAutomationId
|
|
14
14
|
|
|
15
|
+
internal const val HEADER_FONT_FAMILY = "MoMoTrustDisplay"
|
|
16
|
+
|
|
15
17
|
@Composable
|
|
16
18
|
internal fun HeaderTitle(
|
|
17
19
|
title: String = "",
|
|
@@ -27,6 +29,7 @@ internal fun HeaderTitle(
|
|
|
27
29
|
fontSize = 15.sp,
|
|
28
30
|
lineHeight = 22.sp,
|
|
29
31
|
),
|
|
32
|
+
fontFamily = HEADER_FONT_FAMILY,
|
|
30
33
|
color = color,
|
|
31
34
|
maxLines = 1,
|
|
32
35
|
overflow = TextOverflow.Ellipsis
|
|
@@ -16,6 +16,7 @@ import androidx.compose.ui.draw.clip
|
|
|
16
16
|
import androidx.compose.ui.draw.clipToBounds
|
|
17
17
|
import androidx.compose.ui.layout.ContentScale
|
|
18
18
|
import androidx.compose.ui.text.TextStyle
|
|
19
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
19
20
|
import androidx.compose.ui.text.style.TextOverflow
|
|
20
21
|
import androidx.compose.ui.unit.Dp
|
|
21
22
|
import androidx.compose.ui.unit.dp
|
|
@@ -28,6 +29,7 @@ import vn.momo.kits.const.AppTheme
|
|
|
28
29
|
import vn.momo.kits.const.Colors
|
|
29
30
|
import vn.momo.kits.const.Spacing
|
|
30
31
|
import vn.momo.kits.const.Typography
|
|
32
|
+
import vn.momo.kits.const.getFontFamily
|
|
31
33
|
import vn.momo.kits.modifier.activeOpacityClickable
|
|
32
34
|
import vn.momo.kits.navigation.component.HeaderTitle.User
|
|
33
35
|
import vn.momo.kits.platform.getScreenDimensions
|
|
@@ -77,6 +79,7 @@ internal fun HeaderUser(
|
|
|
77
79
|
text = data.title,
|
|
78
80
|
color = data.tintColor,
|
|
79
81
|
style = Typography.actionXsBold,
|
|
82
|
+
fontFamily = HEADER_FONT_FAMILY,
|
|
80
83
|
maxLines = 1,
|
|
81
84
|
overflow = TextOverflow.Ellipsis
|
|
82
85
|
)
|
|
@@ -303,7 +306,8 @@ private fun ManyAvatar(options: Options, modifier: Modifier = Modifier, urls: Av
|
|
|
303
306
|
BasicText(
|
|
304
307
|
text = "+$more",
|
|
305
308
|
style = TextStyle(
|
|
306
|
-
color = Colors.pink_03
|
|
309
|
+
color = Colors.pink_03,
|
|
310
|
+
fontFamily = getFontFamily(AppTheme.current.font, FontWeight.Normal)
|
|
307
311
|
),
|
|
308
312
|
maxLines = 1,
|
|
309
313
|
autoSize = TextAutoSize.StepBased(
|
|
@@ -3,13 +3,10 @@ package vn.momo.kits.platform
|
|
|
3
3
|
import androidx.compose.runtime.Composable
|
|
4
4
|
import androidx.compose.runtime.State
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
* gesture is running. Observing only — it must never consume the gesture, otherwise it would steal
|
|
9
|
-
* it from NavHost.
|
|
10
|
-
*
|
|
11
|
-
* Android always returns `null`: its NavHost runs on `androidx.activity`'s OnBackPressedDispatcher,
|
|
12
|
-
* which exposes progress only to the top-most enabled callback, and that has to stay NavHost's.
|
|
13
|
-
*/
|
|
6
|
+
/** Fraction of container width, `null` when idle. Observe only — consuming would steal the
|
|
7
|
+
* gesture from NavHost. Always `null` on Android. */
|
|
14
8
|
@Composable
|
|
15
9
|
expect fun rememberBackGestureProgress(): State<Float?>
|
|
10
|
+
|
|
11
|
+
/** Android is a no-op: no host-owned recogniser to hand the gesture to. */
|
|
12
|
+
expect fun reportFloatingArrowBack(shouldUse: Boolean)
|
|
@@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
|
|
|
5
5
|
import androidx.compose.ui.graphics.Color
|
|
6
6
|
import androidx.compose.ui.graphics.NativePaint
|
|
7
7
|
import androidx.compose.ui.unit.Dp
|
|
8
|
+
import vn.momo.kits.navigation.Navigator
|
|
8
9
|
|
|
9
10
|
data class ScreenDimension(
|
|
10
11
|
val width: Int,
|
|
@@ -24,8 +25,9 @@ expect fun getStatusBarHeight(): Dp
|
|
|
24
25
|
@Composable
|
|
25
26
|
expect fun BackHandler(enabled: Boolean, onBack: () -> Unit)
|
|
26
27
|
|
|
28
|
+
@Suppress("ParamsComparedByRef")
|
|
27
29
|
@Composable
|
|
28
|
-
expect fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit)
|
|
30
|
+
expect fun ProvideNavigationEventDispatcherOwner(navigator: Navigator, content: @Composable () -> Unit)
|
|
29
31
|
|
|
30
32
|
@Composable
|
|
31
33
|
expect fun getScreenHeight(): Dp
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package vn.momo.kits.navigation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.DisposableEffect
|
|
5
|
+
import androidx.compose.runtime.remember
|
|
6
|
+
import androidx.navigationevent.NavigationEvent
|
|
7
|
+
import androidx.navigationevent.NavigationEventDispatcher
|
|
8
|
+
import androidx.navigationevent.NavigationEventInput
|
|
9
|
+
|
|
10
|
+
@Suppress("ParamsComparedByRef")
|
|
11
|
+
@Composable
|
|
12
|
+
internal fun AttachHostBackGesture(dispatcher: NavigationEventDispatcher, navigator: Navigator) {
|
|
13
|
+
val input = remember(dispatcher) { HostNavigationEventInput() }
|
|
14
|
+
val target = remember(input, navigator) {
|
|
15
|
+
object : HostBackTarget {
|
|
16
|
+
override fun canPop() = navigator.canPop()
|
|
17
|
+
override fun onBack() = navigator.onBackSafe()
|
|
18
|
+
override fun backStarted() = input.backStarted()
|
|
19
|
+
override fun backProgressed(progress: Float) = input.backProgressed(progress)
|
|
20
|
+
override fun backCompleted() = input.backCompleted()
|
|
21
|
+
override fun backCancelled() = input.backCancelled()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
DisposableEffect(dispatcher, target) {
|
|
26
|
+
dispatcher.addInput(input)
|
|
27
|
+
HostBackGestureBridge.attach(target)
|
|
28
|
+
onDispose {
|
|
29
|
+
HostBackGestureBridge.detach(target)
|
|
30
|
+
dispatcher.removeInput(input)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
internal class HostNavigationEventInput : NavigationEventInput() {
|
|
36
|
+
fun backStarted() = dispatchOnBackStarted(NavigationEvent(swipeEdge = NavigationEvent.EDGE_LEFT))
|
|
37
|
+
|
|
38
|
+
fun backProgressed(progress: Float) = dispatchOnBackProgressed(
|
|
39
|
+
NavigationEvent(swipeEdge = NavigationEvent.EDGE_LEFT, progress = progress)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
fun backCancelled() = dispatchOnBackCancelled()
|
|
43
|
+
|
|
44
|
+
fun backCompleted() = dispatchOnBackCompleted()
|
|
45
|
+
}
|
|
@@ -8,10 +8,9 @@ import androidx.compose.runtime.remember
|
|
|
8
8
|
import androidx.compose.ui.InternalComposeUiApi
|
|
9
9
|
import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
|
|
10
10
|
import androidx.navigationevent.NavigationEventTransitionState
|
|
11
|
+
import vn.momo.kits.navigation.HostBackGestureBridge
|
|
11
12
|
|
|
12
|
-
//
|
|
13
|
-
// publishes every edge-pan event to it without any handler consuming the gesture. Its `progress`
|
|
14
|
-
// is touch.x / view width, i.e. a plain fraction of the screen width.
|
|
13
|
+
// transitionState is a passive observer; its progress is touch.x / view width.
|
|
15
14
|
@OptIn(InternalComposeUiApi::class)
|
|
16
15
|
@Composable
|
|
17
16
|
actual fun rememberBackGestureProgress(): State<Float?> {
|
|
@@ -33,3 +32,7 @@ actual fun rememberBackGestureProgress(): State<Float?> {
|
|
|
33
32
|
|
|
34
33
|
return progress
|
|
35
34
|
}
|
|
35
|
+
|
|
36
|
+
actual fun reportFloatingArrowBack(shouldUse: Boolean) {
|
|
37
|
+
HostBackGestureBridge.shouldUseFloatingArrowBack = shouldUse
|
|
38
|
+
}
|
|
@@ -39,7 +39,8 @@ import platform.Foundation.NSBundle
|
|
|
39
39
|
import platform.UIKit.UIColor
|
|
40
40
|
import platform.UIKit.UIDevice
|
|
41
41
|
import platform.UIKit.UIScreen
|
|
42
|
-
import vn.momo.kits.navigation.
|
|
42
|
+
import vn.momo.kits.navigation.AttachHostBackGesture
|
|
43
|
+
import vn.momo.kits.navigation.Navigator
|
|
43
44
|
|
|
44
45
|
actual fun getPlatformName(): String = "iOS"
|
|
45
46
|
|
|
@@ -62,9 +63,6 @@ actual fun getStatusBarHeight(): Dp {
|
|
|
62
63
|
return WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
// An enabled kit BackHandler is registered inside the NavHost destination, so it out-ranks
|
|
66
|
-
// NavHost's own predictive-back handler and swallows the gesture — that is how a screen opts out
|
|
67
|
-
// of swipe-back (the port of RN's `hasCustomBackHandling`).
|
|
68
66
|
@Suppress("DEPRECATION")
|
|
69
67
|
@OptIn(ExperimentalComposeUiApi::class)
|
|
70
68
|
@Composable
|
|
@@ -74,30 +72,17 @@ actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
|
|
|
74
72
|
|
|
75
73
|
@OptIn(InternalComposeUiApi::class)
|
|
76
74
|
@Composable
|
|
77
|
-
actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit) {
|
|
78
|
-
val
|
|
79
|
-
if (platformOwner != null) {
|
|
80
|
-
// UIKitNavigationEventInput is registered on the scene's dispatcher, and that dispatcher
|
|
81
|
-
// having enabled handlers is what arms Compose's UIKitBackGestureRecognizer. Providing a
|
|
82
|
-
// fresh NavigationEventDispatcher() here would shadow it with an input-less one that never
|
|
83
|
-
// sees a single edge-pan event — no recogniser, and no finger-following seek.
|
|
84
|
-
ObserveBackGestureOwnership(platformOwner.navigationEventDispatcher)
|
|
85
|
-
content()
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Bare UIView hosts have no scene dispatcher, and BackHandler/PredictiveBackHandler error() on
|
|
90
|
-
// a null owner. Disabled: nothing can feed it, so it exists only to keep those from throwing.
|
|
91
|
-
val fallbackOwner = remember {
|
|
75
|
+
actual fun ProvideNavigationEventDispatcherOwner(navigator: Navigator, content: @Composable () -> Unit) {
|
|
76
|
+
val navigationEventDispatcherOwner = remember {
|
|
92
77
|
object : NavigationEventDispatcherOwner {
|
|
93
|
-
override val navigationEventDispatcher = NavigationEventDispatcher()
|
|
94
|
-
isEnabled = false
|
|
95
|
-
}
|
|
78
|
+
override val navigationEventDispatcher = NavigationEventDispatcher()
|
|
96
79
|
}
|
|
97
80
|
}
|
|
98
81
|
|
|
82
|
+
AttachHostBackGesture(navigationEventDispatcherOwner.navigationEventDispatcher, navigator)
|
|
83
|
+
|
|
99
84
|
CompositionLocalProvider(
|
|
100
|
-
LocalCompatNavigationEventDispatcherOwner provides
|
|
85
|
+
LocalCompatNavigationEventDispatcherOwner provides navigationEventDispatcherOwner,
|
|
101
86
|
content = content
|
|
102
87
|
)
|
|
103
88
|
}
|