@momo-kits/native-kits 0.164.1-beta.5-debug → 0.164.1-beta.6-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/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +11 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +10 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +12 -6
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +2 -20
- package/gradle.properties +1 -1
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/package.json +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -15
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -35
package/compose/build.gradle.kts
CHANGED
package/compose/compose.podspec
CHANGED
|
@@ -94,8 +94,6 @@ fun NavigationContainer(
|
|
|
94
94
|
setNavigator?.invoke(navigator)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
SwipeBackHaptic(maxApi)
|
|
98
|
-
|
|
99
97
|
// Gesture pops never reach Navigator.dismissScreen(), so prune the registry from
|
|
100
98
|
// the back stack itself. collectLatest lets a rapid re-push cancel a pending prune.
|
|
101
99
|
LaunchedEffect(navController, registry) {
|
|
@@ -4,11 +4,14 @@ import androidx.compose.foundation.layout.Box
|
|
|
4
4
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
5
5
|
import androidx.compose.runtime.Composable
|
|
6
6
|
import androidx.compose.runtime.MutableState
|
|
7
|
+
import androidx.compose.runtime.collectAsState
|
|
8
|
+
import androidx.compose.runtime.getValue
|
|
7
9
|
import androidx.compose.runtime.mutableStateOf
|
|
8
10
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
9
11
|
import androidx.compose.ui.Alignment
|
|
10
12
|
import androidx.compose.ui.Modifier
|
|
11
13
|
import androidx.navigation.NavController
|
|
14
|
+
import androidx.navigation.NavGraph
|
|
12
15
|
import androidx.navigation.toRoute
|
|
13
16
|
import kotlinx.coroutines.CoroutineScope
|
|
14
17
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -79,8 +82,14 @@ class Navigator(
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
// The swipe-back opt-out predicate in StackScreen needs this: the root screen must keep custom
|
|
82
|
-
// back handling so it still reaches maxApi.dismiss.
|
|
83
|
-
|
|
85
|
+
// back handling so it still reaches maxApi.dismiss. It has to read currentBackStack rather than
|
|
86
|
+
// previousBackStackEntry — the latter walks a plain ArrayDeque, so a composable reading it never
|
|
87
|
+
// recomposes and the predicate would freeze at the value it had on first composition.
|
|
88
|
+
@Composable
|
|
89
|
+
internal fun rememberCanPop(): Boolean {
|
|
90
|
+
val backStack by navController.currentBackStack.collectAsState()
|
|
91
|
+
return backStack.count { it.destination !is NavGraph } > 1
|
|
92
|
+
}
|
|
84
93
|
|
|
85
94
|
fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
|
|
86
95
|
scope.launch {
|
|
@@ -169,6 +169,10 @@ internal fun StackScreen(
|
|
|
169
169
|
RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// Hoisted out of the `||` chain below: short-circuiting would drop the back-stack subscription
|
|
173
|
+
// whenever an earlier term is true.
|
|
174
|
+
val canPop = navigator.rememberCanPop()
|
|
175
|
+
|
|
172
176
|
// Port of RN's `hasCustomBackHandling`: this handler is registered inside the NavHost
|
|
173
177
|
// destination, so while it is enabled it beats NavHost's own predictive-back handler and
|
|
174
178
|
// swallows the swipe. Enable it only when a plain pop is the wrong answer.
|
|
@@ -177,9 +181,13 @@ internal fun StackScreen(
|
|
|
177
181
|
options.onBackHandler != null ||
|
|
178
182
|
options.headerBackProps.preventBack != null ||
|
|
179
183
|
OverplayComponentRegistry.isOverplayShowing() ||
|
|
180
|
-
!
|
|
184
|
+
!canPop
|
|
185
|
+
|
|
186
|
+
// BottomTab supplies a tab-aware handler; falling through to onBackSafe() there would dismiss
|
|
187
|
+
// the whole mini-app instead of returning to tab 0.
|
|
188
|
+
val onBack = onBackHandler ?: { navigator.onBackSafe() }
|
|
181
189
|
|
|
182
|
-
BackHandler(hasCustomBackHandling
|
|
190
|
+
BackHandler(hasCustomBackHandling, onBack)
|
|
183
191
|
|
|
184
192
|
CompositionLocalProvider(
|
|
185
193
|
StackScreenScrollableState provides options.scrollData.scrollState,
|
|
@@ -18,7 +18,9 @@ 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
|
|
21
22
|
import androidx.compose.runtime.remember
|
|
23
|
+
import androidx.compose.runtime.setValue
|
|
22
24
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
23
25
|
import androidx.compose.ui.Modifier
|
|
24
26
|
import androidx.compose.ui.draw.drawBehind
|
|
@@ -43,12 +45,14 @@ private const val EDGE_SHADOW_MAX_ALPHA = 0.3f
|
|
|
43
45
|
private val EDGE_SHADOW_WIDTH = 8.dp
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
* Kill switch for the swipe-back gesture. Set to `false` to
|
|
47
|
-
*
|
|
48
|
-
* on the
|
|
48
|
+
* Kill switch for the swipe-back gesture. Set to `false` to make kit back handling always win,
|
|
49
|
+
* which disables Android's predictive back drag, without a kit release. It has no effect on iOS —
|
|
50
|
+
* the host owns the edge gesture there. Screen transitions stay on the parallax curve either way.
|
|
49
51
|
*/
|
|
50
52
|
object SwipeBackConfig {
|
|
51
|
-
|
|
53
|
+
// Snapshot-backed so a host toggle takes effect on the next frame instead of waiting for an
|
|
54
|
+
// unrelated recomposition of StackScreen.
|
|
55
|
+
var enabled: Boolean by mutableStateOf(true)
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
// LinearEasing is mandatory, not a style choice: SeekableTransitionState.seekTo() maps the drag
|
|
@@ -106,8 +110,10 @@ internal fun AnimatedVisibilityScope.StackTransitionDecor(
|
|
|
106
110
|
content: @Composable () -> Unit,
|
|
107
111
|
) {
|
|
108
112
|
val coordinator = LocalStackTransition.current
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
// attach() writes state that derivedStateOf { topStackId } reads in the same pass, so it has to
|
|
114
|
+
// stay out of the composable body.
|
|
115
|
+
DisposableEffect(coordinator, id, isStackRoute) {
|
|
116
|
+
coordinator.attach(id, isStackRoute)
|
|
111
117
|
onDispose { coordinator.detach(id) }
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -17,7 +17,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
|
|
17
17
|
import androidx.compose.ui.InternalComposeUiApi
|
|
18
18
|
import androidx.compose.ui.Modifier
|
|
19
19
|
import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
|
|
20
|
-
import androidx.compose.ui.backhandler.BackHandler as ComposeBackHandler
|
|
21
20
|
import androidx.compose.ui.graphics.Color
|
|
22
21
|
import androidx.compose.ui.graphics.NativePaint
|
|
23
22
|
import androidx.compose.ui.graphics.toArgb
|
|
@@ -39,7 +38,6 @@ import platform.Foundation.NSBundle
|
|
|
39
38
|
import platform.UIKit.UIColor
|
|
40
39
|
import platform.UIKit.UIDevice
|
|
41
40
|
import platform.UIKit.UIScreen
|
|
42
|
-
import vn.momo.kits.navigation.SwipeBackConfig
|
|
43
41
|
|
|
44
42
|
actual fun getPlatformName(): String = "iOS"
|
|
45
43
|
|
|
@@ -62,30 +60,14 @@ actual fun getStatusBarHeight(): Dp {
|
|
|
62
60
|
return WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
// An enabled kit BackHandler sits innermost and therefore consumes the edge-pan gesture — that is
|
|
66
|
-
// how a screen opts out of swipe-back (the port of RN's `hasCustomBackHandling`).
|
|
67
|
-
@Suppress("DEPRECATION")
|
|
68
|
-
@OptIn(ExperimentalComposeUiApi::class)
|
|
69
63
|
@Composable
|
|
70
64
|
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
|
|
71
|
-
ComposeBackHandler(enabled = enabled, onBack = onBack)
|
|
72
65
|
}
|
|
73
66
|
|
|
74
67
|
@OptIn(InternalComposeUiApi::class)
|
|
75
68
|
@Composable
|
|
76
69
|
actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit) {
|
|
77
|
-
val
|
|
78
|
-
if (platformOwner != null && SwipeBackConfig.enabled) {
|
|
79
|
-
// UIKitNavigationEventInput is registered on the scene's dispatcher. Providing a fresh
|
|
80
|
-
// NavigationEventDispatcher() here would shadow it with an input-less one that never sees
|
|
81
|
-
// a single edge-pan event, enabled or not — so pass the platform owner straight through.
|
|
82
|
-
content()
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Bare UIView hosts have no scene dispatcher, and BackHandler/PredictiveBackHandler error() on
|
|
87
|
-
// a null owner. This detached fallback is also what the kill switch falls back to.
|
|
88
|
-
val fallbackOwner = remember {
|
|
70
|
+
val navigationEventDispatcherOwner = remember {
|
|
89
71
|
object : NavigationEventDispatcherOwner {
|
|
90
72
|
override val navigationEventDispatcher = NavigationEventDispatcher().apply {
|
|
91
73
|
isEnabled = false
|
|
@@ -94,7 +76,7 @@ actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit
|
|
|
94
76
|
}
|
|
95
77
|
|
|
96
78
|
CompositionLocalProvider(
|
|
97
|
-
LocalCompatNavigationEventDispatcherOwner provides
|
|
79
|
+
LocalCompatNavigationEventDispatcherOwner provides navigationEventDispatcherOwner,
|
|
98
80
|
content = content
|
|
99
81
|
)
|
|
100
82
|
}
|
package/gradle.properties
CHANGED
package/ios/native-kits.podspec
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
package vn.momo.kits.platform
|
|
2
|
-
|
|
3
|
-
import androidx.compose.runtime.Composable
|
|
4
|
-
import androidx.compose.runtime.State
|
|
5
|
-
import androidx.compose.runtime.mutableStateOf
|
|
6
|
-
import androidx.compose.runtime.remember
|
|
7
|
-
|
|
8
|
-
// No passive observer exists at androidx.activity 1.10.x — a second enabled callback would steal
|
|
9
|
-
// the gesture from NavHost. Android's system back gesture emits its own platform haptic on commit.
|
|
10
|
-
// Revisit if androidx.activity >= 1.12 is adopted; this then becomes a copy of the iOS actual.
|
|
11
|
-
@Composable
|
|
12
|
-
actual fun rememberBackGestureProgress(): State<Float?> = remember { mutableStateOf(null) }
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
package vn.momo.kits.navigation
|
|
2
|
-
|
|
3
|
-
import androidx.compose.runtime.Composable
|
|
4
|
-
import androidx.compose.runtime.LaunchedEffect
|
|
5
|
-
import androidx.compose.runtime.snapshotFlow
|
|
6
|
-
import vn.momo.kits.platform.getScreenDimensions
|
|
7
|
-
import vn.momo.kits.platform.rememberBackGestureProgress
|
|
8
|
-
import vn.momo.maxapi.IMaxApi
|
|
9
|
-
|
|
10
|
-
// RN fires the tick at a flat 100 px of thumb travel; keep it unscaled so the physical distance is
|
|
11
|
-
// the same on every device.
|
|
12
|
-
private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 100f
|
|
13
|
-
|
|
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
|
-
*/
|
|
18
|
-
@Composable
|
|
19
|
-
internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
|
|
20
|
-
if (maxApi == null) return
|
|
21
|
-
|
|
22
|
-
val progress = rememberBackGestureProgress()
|
|
23
|
-
val screenWidth = getScreenDimensions().width.toFloat()
|
|
24
|
-
|
|
25
|
-
LaunchedEffect(maxApi, screenWidth) {
|
|
26
|
-
if (screenWidth <= 0f) return@LaunchedEffect
|
|
27
|
-
val threshold = SWIPE_BACK_HAPTIC_THRESHOLD_DP / screenWidth
|
|
28
|
-
var isSendHaptic = false
|
|
29
|
-
snapshotFlow { progress.value }.collect { fraction ->
|
|
30
|
-
val travelled = fraction ?: 0f
|
|
31
|
-
if (travelled >= threshold && !isSendHaptic) {
|
|
32
|
-
runCatching { maxApi.triggerEventVibration("light", callback = null) }
|
|
33
|
-
isSendHaptic = true
|
|
34
|
-
} else if (travelled < threshold && isSendHaptic) {
|
|
35
|
-
isSendHaptic = false
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
package vn.momo.kits.platform
|
|
2
|
-
|
|
3
|
-
import androidx.compose.runtime.Composable
|
|
4
|
-
import androidx.compose.runtime.State
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Progress of an in-flight back gesture as a fraction of the container width, or `null` while no
|
|
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
|
-
*/
|
|
14
|
-
@Composable
|
|
15
|
-
expect fun rememberBackGestureProgress(): State<Float?>
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
package vn.momo.kits.platform
|
|
2
|
-
|
|
3
|
-
import androidx.compose.runtime.Composable
|
|
4
|
-
import androidx.compose.runtime.LaunchedEffect
|
|
5
|
-
import androidx.compose.runtime.State
|
|
6
|
-
import androidx.compose.runtime.mutableStateOf
|
|
7
|
-
import androidx.compose.runtime.remember
|
|
8
|
-
import androidx.compose.ui.InternalComposeUiApi
|
|
9
|
-
import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
|
|
10
|
-
import androidx.navigationevent.NavigationEventTransitionState
|
|
11
|
-
|
|
12
|
-
// NavigationEventDispatcher.transitionState is a passive observer: UIKitNavigationEventInput
|
|
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.
|
|
15
|
-
@OptIn(InternalComposeUiApi::class)
|
|
16
|
-
@Composable
|
|
17
|
-
actual fun rememberBackGestureProgress(): State<Float?> {
|
|
18
|
-
val dispatcher = LocalCompatNavigationEventDispatcherOwner.current?.navigationEventDispatcher
|
|
19
|
-
val progress = remember { mutableStateOf<Float?>(null) }
|
|
20
|
-
|
|
21
|
-
LaunchedEffect(dispatcher) {
|
|
22
|
-
if (dispatcher == null) {
|
|
23
|
-
progress.value = null
|
|
24
|
-
return@LaunchedEffect
|
|
25
|
-
}
|
|
26
|
-
dispatcher.transitionState.collect { state ->
|
|
27
|
-
progress.value = (state as? NavigationEventTransitionState.InProgress)
|
|
28
|
-
?.takeIf { it.direction == NavigationEventTransitionState.TRANSITIONING_BACK }
|
|
29
|
-
?.latestEvent
|
|
30
|
-
?.progress
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return progress
|
|
35
|
-
}
|