@momo-kits/native-kits 0.168.1-beta.25-debug → 0.168.1-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/package.json +1 -1
  2. package/build.gradle.kts +0 -11
  3. package/compose/build.gradle.kts +0 -198
  4. package/compose/build.gradle.kts.backup +0 -198
  5. package/compose/compose.podspec +0 -47
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -20
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -153
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReduceMotion.android.kt +0 -17
  12. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +0 -6
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -110
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +0 -62
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -1071
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -81
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -357
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -115
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -86
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -478
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -265
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -235
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -265
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -370
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -108
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -308
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -330
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -207
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -92
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -560
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -86
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -501
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -236
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -180
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -17
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -194
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -287
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -321
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -499
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -148
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -116
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -422
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -732
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -232
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -237
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +0 -97
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoLiquidSelection.kt +0 -380
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +0 -436
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -455
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -105
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -106
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -399
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -39
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -192
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -133
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -74
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReduceMotion.kt +0 -7
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +0 -20
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
  124. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -42
  125. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  126. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  127. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  128. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  129. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -218
  130. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReduceMotion.ios.kt +0 -48
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +0 -138
  132. package/gradle/libs.versions.toml +0 -69
  133. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  134. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  135. package/gradle.properties +0 -26
  136. package/gradlew +0 -252
  137. package/gradlew.bat +0 -94
  138. package/ios-demo/README.md +0 -120
  139. package/settings.gradle.kts +0 -64
@@ -1,86 +0,0 @@
1
- package vn.momo.kits.navigation.bottomtab
2
-
3
- import androidx.compose.foundation.Canvas
4
- import androidx.compose.foundation.layout.Box
5
- import androidx.compose.foundation.layout.height
6
- import androidx.compose.foundation.layout.offset
7
- import androidx.compose.foundation.layout.width
8
- import androidx.compose.runtime.Composable
9
- import androidx.compose.ui.Alignment
10
- import androidx.compose.ui.Modifier
11
- import androidx.compose.ui.graphics.Path
12
- import androidx.compose.ui.platform.LocalDensity
13
- import androidx.compose.ui.unit.Dp
14
- import androidx.compose.ui.unit.IntOffset
15
- import androidx.compose.ui.unit.dp
16
- import androidx.compose.ui.zIndex
17
- import vn.momo.kits.const.AppTheme
18
-
19
- @Composable
20
- internal fun CurvedContainer(
21
- circleSize: Dp = 48.dp,
22
- tabBarItemIconSize: Dp = 28.dp,
23
- circleOverSize: Dp = 2.dp,
24
- width: Dp = 71.dp,
25
- height: Dp = 16.dp
26
- ) {
27
- val theme = AppTheme.current
28
- val density = LocalDensity.current
29
- val widthPx = with(density) { width.toPx()}
30
-
31
- val circleSizePx = with(density) { circleSize.toPx() }
32
- val tabBarItemIconSizePx = with(density) { tabBarItemIconSize.toPx() }
33
- val circleOverSizePx = with(density) { circleOverSize.toPx() }
34
-
35
- val circlePositionTop = circleSizePx - tabBarItemIconSizePx - 10f
36
- val circleOverTop = circlePositionTop + circleOverSizePx
37
- val circleR = circleSizePx / 2
38
- val circleOverR = circleR + circleOverSizePx
39
-
40
- val centerX = widthPx / 2
41
-
42
- val curvedPath = getPath(
43
- circleOverTop = circleOverTop,
44
- centerX = centerX,
45
- circleOverR = circleOverR,
46
- widthPx = widthPx
47
- )
48
-
49
- Box(
50
- contentAlignment = Alignment.BottomCenter
51
- ){
52
- Canvas(
53
- modifier = Modifier
54
- .width(width)
55
- .height(height)
56
- .offset{ IntOffset(x = 0, y = 24) }
57
- .zIndex(-1f)
58
- ) {
59
- drawPath(path = curvedPath, color = theme.colors.background.surface)
60
- }
61
- }
62
- }
63
-
64
- private fun getPath(circleOverTop: Float, centerX: Float, circleOverR: Float, widthPx: Float): Path{
65
- return Path().apply {
66
- moveTo(0f, circleOverTop)
67
- lineTo(centerX - circleOverR - 40f, circleOverTop)
68
-
69
- cubicTo(
70
- centerX - circleOverR, circleOverTop,
71
- centerX - circleOverR, 0f,
72
- centerX, 0f
73
- )
74
-
75
- cubicTo(
76
- centerX + circleOverR, 0f,
77
- centerX + circleOverR, circleOverTop,
78
- centerX + circleOverR + 40f, circleOverTop
79
- )
80
-
81
- lineTo(widthPx, circleOverTop)
82
- lineTo(widthPx, 60f)
83
- lineTo(0f, 60f)
84
- close()
85
- }
86
- }
@@ -1,97 +0,0 @@
1
- package vn.momo.kits.navigation.bottomtab
2
-
3
- import androidx.compose.runtime.Composable
4
- import androidx.compose.runtime.Stable
5
- import androidx.compose.runtime.compositionLocalOf
6
- import androidx.compose.runtime.getValue
7
- import androidx.compose.runtime.mutableStateOf
8
- import androidx.compose.runtime.remember
9
- import androidx.compose.runtime.setValue
10
- import androidx.compose.ui.unit.Dp
11
- import vn.momo.kits.const.Spacing
12
- import vn.momo.kits.layout.DuoMetrics
13
- import vn.momo.kits.layout.ScreenLayout
14
-
15
- // iPhone Duo: which side bar bar starts compressed when the strip cannot hold both whole.
16
- enum class DuoBarCompressionBehavior { TOOLBAR, TAB_BAR }
17
-
18
- private enum class DuoBar { TOOLBAR, TAB_BAR }
19
-
20
- @Stable
21
- internal class DuoBarCompression(
22
- val toolbarCompressed: Boolean = false,
23
- val tabBarCompressed: Boolean = false,
24
- // the open rail needs the toolbar button slot too: only Back stays
25
- val toolbarHidden: Boolean = false,
26
- val expandToolbar: () -> Unit = {},
27
- val expandTabBar: () -> Unit = {},
28
- // a rail opened from the compressed state closes once a tab is picked
29
- val onTabSelected: () -> Unit = {},
30
- // side bar columns report their full height so the tab bar knows what is left
31
- val setToolbarHeight: (Dp) -> Unit = {},
32
- )
33
-
34
- internal val LocalDuoBarCompression = compositionLocalOf { DuoBarCompression() }
35
-
36
- // space kept between the top and the bottom groups (HIG Vertical controls)
37
- internal val DUO_GROUP_GAP = Spacing.M
38
- // Back, then the right toolkit: help + home capsule
39
- private val DEFAULT_TOOLBAR_HEIGHT = DuoMetrics.controlSize + Spacing.M + DuoMetrics.itemPitch * 2
40
- // Back, then the single button the toolbar folds into
41
- private val COMPRESSED_TOOLBAR_HEIGHT = DuoMetrics.controlSize * 2 + Spacing.M
42
-
43
- // Port of RN DuoBarCompression (HIG, Designing for iPhone Duo: Vertical controls):
44
- // - both fit: both stay whole
45
- // - one fits whole beside the other compressed: one is compressed, tapping it swaps them
46
- // - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
47
- // again once a tab is picked)
48
- @Composable
49
- internal fun rememberDuoBarCompression(
50
- behavior: DuoBarCompressionBehavior,
51
- tabCount: Int,
52
- hasQr: Boolean,
53
- layout: ScreenLayout,
54
- ): DuoBarCompression {
55
- var toolbarHeight by remember { mutableStateOf(DEFAULT_TOOLBAR_HEIGHT) }
56
- var expanded by remember(behavior) {
57
- mutableStateOf(if (behavior == DuoBarCompressionBehavior.TOOLBAR) DuoBar.TAB_BAR else DuoBar.TOOLBAR)
58
- }
59
- var opened by remember { mutableStateOf<DuoBar?>(null) }
60
- val setToolbarHeight = remember { { height: Dp -> toolbarHeight = height } }
61
- val sideBar = layout.sideBar
62
-
63
- return remember(sideBar, layout.height, tabCount, hasQr, toolbarHeight, expanded, opened) {
64
- if (sideBar == null) return@remember DuoBarCompression()
65
- val room = layout.height - sideBar.bottom - sideBar.top - DUO_GROUP_GAP
66
- val tabBar = duoRailHeight(tabCount, hasQr)
67
- val compressedTabBar = duoRailHeight(1, hasQr, compressed = true)
68
- fun fits(toolbar: Dp, tabs: Dp) = toolbar + tabs <= room
69
-
70
- when {
71
- fits(toolbarHeight, tabBar) -> DuoBarCompression(setToolbarHeight = setToolbarHeight)
72
- fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar) && fits(toolbarHeight, compressedTabBar) -> DuoBarCompression(
73
- toolbarCompressed = expanded == DuoBar.TAB_BAR,
74
- tabBarCompressed = expanded == DuoBar.TOOLBAR,
75
- expandToolbar = { expanded = DuoBar.TOOLBAR },
76
- expandTabBar = { expanded = DuoBar.TAB_BAR },
77
- setToolbarHeight = setToolbarHeight,
78
- )
79
- // an opened rail always shows, even when only the group gap is short: it closes once a tab is picked
80
- opened == DuoBar.TAB_BAR -> DuoBarCompression(
81
- toolbarCompressed = true,
82
- toolbarHidden = true,
83
- expandToolbar = { opened = DuoBar.TOOLBAR },
84
- expandTabBar = { opened = DuoBar.TAB_BAR },
85
- onTabSelected = { opened = null },
86
- setToolbarHeight = setToolbarHeight,
87
- )
88
- else -> DuoBarCompression(
89
- toolbarCompressed = opened != DuoBar.TOOLBAR,
90
- tabBarCompressed = true,
91
- expandToolbar = { opened = DuoBar.TOOLBAR },
92
- expandTabBar = { opened = DuoBar.TAB_BAR },
93
- setToolbarHeight = setToolbarHeight,
94
- )
95
- }
96
- }
97
- }
@@ -1,380 +0,0 @@
1
- package vn.momo.kits.navigation.bottomtab
2
-
3
- import androidx.compose.animation.core.Animatable
4
- import androidx.compose.animation.core.SpringSpec
5
- import androidx.compose.animation.core.spring
6
- import androidx.compose.animation.core.tween
7
- import androidx.compose.foundation.gestures.awaitEachGesture
8
- import androidx.compose.foundation.gestures.awaitFirstDown
9
- import androidx.compose.foundation.interaction.MutableInteractionSource
10
- import androidx.compose.foundation.interaction.PressInteraction
11
- import androidx.compose.runtime.Composable
12
- import androidx.compose.runtime.LaunchedEffect
13
- import androidx.compose.runtime.Stable
14
- import androidx.compose.runtime.getValue
15
- import androidx.compose.runtime.mutableFloatStateOf
16
- import androidx.compose.runtime.mutableIntStateOf
17
- import androidx.compose.runtime.mutableStateOf
18
- import androidx.compose.runtime.remember
19
- import androidx.compose.runtime.rememberCoroutineScope
20
- import androidx.compose.runtime.rememberUpdatedState
21
- import androidx.compose.runtime.setValue
22
- import androidx.compose.runtime.snapshotFlow
23
- import androidx.compose.ui.Modifier
24
- import androidx.compose.ui.geometry.Offset
25
- import androidx.compose.ui.graphics.graphicsLayer
26
- import androidx.compose.ui.input.pointer.pointerInput
27
- import androidx.compose.ui.input.pointer.util.VelocityTracker
28
- import androidx.compose.ui.platform.LocalDensity
29
- import androidx.compose.ui.unit.Dp
30
- import androidx.compose.ui.unit.dp
31
- import kotlinx.coroutines.CoroutineScope
32
- import kotlinx.coroutines.Job
33
- import kotlinx.coroutines.delay
34
- import kotlinx.coroutines.launch
35
- import vn.momo.kits.platform.rememberReduceMotion
36
- import kotlin.math.abs
37
- import kotlin.math.min
38
- import kotlin.math.roundToInt
39
-
40
- // iOS 26 tab bar selection motion without the glass material, port of RN useLiquidSelection.ts.
41
- // Constants mirror RN; springs are RN (mass, stiffness, damping) converted to mass 1.
42
- private val SPRING = spring<Float>(dampingRatio = 0.68f, stiffness = 275f)
43
- // the lens trails the finger a little while dragged (RN damping 24, stiffness 600, mass 0.6)
44
- private val DRAG_SPRING = spring<Float>(dampingRatio = 0.63f, stiffness = 1000f)
45
- private val REDUCED_TWEEN = tween<Float>(durationMillis = 200)
46
- // while pressed the selection bubble grows into a lens past the bar's edges (80x54 to about 96x70)
47
- private const val LENS_SCALE_X = 1.2f
48
- private const val LENS_SCALE_Y = 1.3f
49
- // a moving lens stretches along its path and thins across it, eased per frame
50
- private const val STRETCH_PER_POINT = 0.012f
51
- private const val MAX_STRETCH = 0.25f
52
- private const val STRETCH_EASING = 0.25f
53
- private const val REST_STRETCH = 0.002f
54
- // a tap is shorter than the lift: the lens finishes rising, then settles, one smooth pulse
55
- private const val TAP_LIFT_MILLIS = 120
56
- private const val BAR_SWELL = 0.04f
57
- private const val ICON_POP = 1.15f
58
- private const val ICON_POP_MILLIS = 120
59
- // below it the tab's clickable handles the press as a tap
60
- private val DRAG_SLOP = 10.dp
61
- // UIScrollView rubber band constant
62
- private const val RUBBER_BAND = 0.55f
63
- // a cancelled press or a vetoed switch: the lens returns to the selected tab after this
64
- private const val RESYNC_DELAY_MILLIS = 100L
65
-
66
- internal enum class LiquidAxis { X, Y }
67
-
68
- // resistance past the first and last tab, the further the finger goes the less the lens follows
69
- private fun rubberBand(value: Float, max: Float, dimension: Float): Float {
70
- fun band(overshoot: Float) = (1 - 1 / (overshoot * RUBBER_BAND / dimension + 1)) * dimension
71
- return when {
72
- value < 0 -> -band(-value)
73
- value > max -> max + band(value - max)
74
- else -> value
75
- }
76
- }
77
-
78
- @Stable
79
- internal class DuoLiquidSelection(
80
- private val scope: CoroutineScope,
81
- private val axis: LiquidAxis,
82
- private val density: Float,
83
- ) {
84
- var count = 0
85
- // px between two neighbouring tab centers / to the first tab's center inside the drag modifier
86
- var pitch = 0f
87
- var firstCenter = 0f
88
- var reduceMotion = false
89
- var onSelect: (Int) -> Unit = {}
90
- var onTick: () -> Unit = {}
91
-
92
- val offset = Animatable(0f)
93
- val lift = Animatable(0f)
94
- var stretch by mutableFloatStateOf(0f)
95
- private set
96
- var selectedIndex by mutableIntStateOf(0)
97
- private set
98
-
99
- // where the lens is heading, so a recomposition doesn't restart a spring already going there
100
- private var destination = 0f
101
- private var dragging = false
102
- private var hovered = 0
103
- private var measured = false
104
- private var resyncJob: Job? = null
105
-
106
- private fun move(target: Float, velocity: Float? = null, spec: SpringSpec<Float> = SPRING) {
107
- if (destination == target && velocity == null) return
108
- destination = target
109
- scope.launch {
110
- if (reduceMotion) offset.animateTo(target, REDUCED_TWEEN)
111
- else offset.animateTo(target, spec, initialVelocity = velocity ?: offset.velocity)
112
- }
113
- }
114
-
115
- private fun animateLift(target: Float) {
116
- scope.launch { if (reduceMotion) lift.snapTo(0f) else lift.animateTo(target, SPRING) }
117
- }
118
-
119
- suspend fun sync(index: Int) {
120
- selectedIndex = index
121
- val target = index * pitch
122
- // snap on first layout so the lens doesn't slide in from the leading edge
123
- if (measured) {
124
- move(target)
125
- } else {
126
- destination = target
127
- offset.snapTo(target)
128
- }
129
- measured = pitch > 0
130
- }
131
-
132
- suspend fun trackStretch() {
133
- var previous = offset.value
134
- snapshotFlow { offset.value }.collect { current ->
135
- if (reduceMotion) {
136
- stretch = 0f
137
- } else {
138
- val target = min(abs(current - previous) / density * STRETCH_PER_POINT, MAX_STRETCH)
139
- // eased both ways: a slow frame (the spring turning at its overshoot) must not snap the shape
140
- val next = stretch + (target - stretch) * STRETCH_EASING
141
- stretch = if (next < REST_STRETCH) 0f else next
142
- }
143
- previous = current
144
- }
145
- }
146
-
147
- // returns the lens to the selected tab unless a switch to where it is heading happened
148
- private fun resync() {
149
- resyncJob?.cancel()
150
- resyncJob = scope.launch {
151
- delay(RESYNC_DELAY_MILLIS)
152
- val target = selectedIndex * pitch
153
- if (!dragging && destination != target) move(target)
154
- }
155
- }
156
-
157
- fun pressIn(index: Int) {
158
- resyncJob?.cancel()
159
- // the lens heads to the pressed tab, not only the selected one
160
- move(index * pitch)
161
- animateLift(1f)
162
- }
163
-
164
- // false when a drag took the press over
165
- fun pressOut(): Boolean {
166
- if (dragging) return false
167
- if (!reduceMotion) {
168
- scope.launch {
169
- lift.animateTo(1f, tween(TAP_LIFT_MILLIS))
170
- lift.animateTo(0f, SPRING)
171
- }
172
- }
173
- // the lens keeps heading to the pressed tab: onClick switches to it, a cancelled press brings it back
174
- resync()
175
- return true
176
- }
177
-
178
- private fun position(point: Offset) = (if (axis == LiquidAxis.X) point.x else point.y) - firstCenter
179
-
180
- private fun nearest(value: Float): Int =
181
- if (pitch > 0) (value / pitch).roundToInt().coerceIn(0, (count - 1).coerceAtLeast(0)) else 0
182
-
183
- private fun dragStart(point: Offset) {
184
- dragging = true
185
- resyncJob?.cancel()
186
- hovered = nearest(position(point))
187
- animateLift(1f)
188
- }
189
-
190
- private fun dragUpdate(point: Offset) {
191
- val value = position(point)
192
- val target = nearest(value)
193
- if (target != hovered) {
194
- hovered = target
195
- onTick()
196
- }
197
- val follow = rubberBand(value, (count - 1) * pitch, pitch)
198
- if (reduceMotion) {
199
- destination = follow
200
- scope.launch { offset.snapTo(follow) }
201
- } else {
202
- move(follow, spec = DRAG_SPRING)
203
- }
204
- }
205
-
206
- private fun dragEnd(point: Offset?, velocity: Float) {
207
- dragging = false
208
- animateLift(0f)
209
- if (point == null) {
210
- move(selectedIndex * pitch, velocity)
211
- return
212
- }
213
- val target = nearest(position(point))
214
- move(target * pitch, velocity)
215
- onSelect(target)
216
- resync()
217
- }
218
-
219
- // The drag consumes the pointer once past the slop, which cancels the tab's clickable,
220
- // so a press is either a tap or a drag.
221
- val dragGesture: Modifier = Modifier.pointerInput(Unit) {
222
- val slop = DRAG_SLOP.toPx()
223
- awaitEachGesture {
224
- val down = awaitFirstDown(requireUnconsumed = false)
225
- val tracker = VelocityTracker()
226
- tracker.addPosition(down.uptimeMillis, down.position)
227
- var active = false
228
- var last = down.position
229
- while (true) {
230
- val change = awaitPointerEvent().changes.firstOrNull { it.id == down.id }
231
- if (change == null) {
232
- if (active) dragEnd(null, 0f)
233
- break
234
- }
235
- tracker.addPosition(change.uptimeMillis, change.position)
236
- if (!change.pressed) {
237
- if (active) {
238
- change.consume()
239
- val velocity = tracker.calculateVelocity()
240
- dragEnd(change.position, if (axis == LiquidAxis.X) velocity.x else velocity.y)
241
- }
242
- break
243
- }
244
- last = change.position
245
- if (!active && (last - down.position).getDistance() > slop) {
246
- active = true
247
- dragStart(last)
248
- }
249
- if (active) {
250
- change.consume()
251
- dragUpdate(last)
252
- }
253
- }
254
- }
255
- }
256
-
257
- val bubbleTransform: Modifier = Modifier.graphicsLayer {
258
- val along = 1 + stretch
259
- val across = 1 - stretch * 0.5f
260
- val lensX = 1 + lift.value * (LENS_SCALE_X - 1)
261
- val lensY = 1 + lift.value * (LENS_SCALE_Y - 1)
262
- if (axis == LiquidAxis.X) {
263
- translationX = offset.value
264
- scaleX = lensX * along
265
- scaleY = lensY * across
266
- } else {
267
- translationY = offset.value
268
- scaleX = lensX * across
269
- scaleY = lensY * along
270
- }
271
- }
272
-
273
- val barTransform: Modifier = Modifier.graphicsLayer {
274
- scaleX = 1 + lift.value * BAR_SWELL
275
- scaleY = scaleX
276
- }
277
-
278
- // active tint of a tab follows the lens over it
279
- fun activeAlpha(index: Int): Float {
280
- if (pitch <= 0) return if (selectedIndex == index) 1f else 0f
281
- return (1 - abs(offset.value - index * pitch) / pitch).coerceIn(0f, 1f)
282
- }
283
- }
284
-
285
- @Composable
286
- internal fun rememberDuoLiquidSelection(
287
- count: Int,
288
- pitch: Dp,
289
- firstCenter: Dp,
290
- axis: LiquidAxis,
291
- index: Int,
292
- onSelect: (Int) -> Unit,
293
- onTick: () -> Unit,
294
- ): DuoLiquidSelection {
295
- val scope = rememberCoroutineScope()
296
- val density = LocalDensity.current
297
- val selection = remember(axis) { DuoLiquidSelection(scope, axis, density.density) }
298
- val reduceMotion = rememberReduceMotion()
299
- val pitchPx = with(density) { pitch.toPx() }
300
- selection.count = count
301
- selection.pitch = pitchPx
302
- selection.firstCenter = with(density) { firstCenter.toPx() }
303
- selection.reduceMotion = reduceMotion
304
- selection.onSelect = onSelect
305
- selection.onTick = onTick
306
-
307
- LaunchedEffect(selection, index, pitchPx) { selection.sync(index) }
308
- LaunchedEffect(selection) { selection.trackStretch() }
309
- return selection
310
- }
311
-
312
- // Press tracking for one tab: lifts the lens over it and, like the system tab bar, still picks a
313
- // held tab when released. Returns the long-press handler for the tab's combinedClickable.
314
- @Composable
315
- internal fun DuoLiquidSelection.rememberPressTracking(
316
- index: Int,
317
- interactionSource: MutableInteractionSource,
318
- onSelect: () -> Unit,
319
- onLongPress: (() -> Unit)?,
320
- ): () -> Unit {
321
- val longPressed = remember { mutableStateOf(false) }
322
- val currentOnSelect by rememberUpdatedState(onSelect)
323
- val currentOnLongPress by rememberUpdatedState(onLongPress)
324
- LaunchedEffect(interactionSource, index) {
325
- interactionSource.interactions.collect {
326
- when (it) {
327
- is PressInteraction.Press -> {
328
- longPressed.value = false
329
- pressIn(index)
330
- }
331
- is PressInteraction.Release -> if (pressOut() && longPressed.value) currentOnSelect()
332
- is PressInteraction.Cancel -> pressOut()
333
- }
334
- }
335
- }
336
- return remember {
337
- {
338
- longPressed.value = true
339
- currentOnLongPress?.invoke()
340
- }
341
- }
342
- }
343
-
344
- // The tab the lens lands on pops.
345
- @Composable
346
- internal fun DuoLiquidSelection.rememberIconPop(index: Int): Animatable<Float, *> {
347
- val pop = remember { Animatable(1f) }
348
- val isSelected = selectedIndex == index
349
- val initial = remember { mutableStateOf(true) }
350
- LaunchedEffect(isSelected) {
351
- if (initial.value) {
352
- initial.value = false
353
- return@LaunchedEffect
354
- }
355
- if (isSelected && !reduceMotion) {
356
- pop.animateTo(ICON_POP, tween(ICON_POP_MILLIS))
357
- pop.animateTo(1f, SPRING)
358
- }
359
- }
360
- return pop
361
- }
362
-
363
- // Single button version of the lift, e.g. the compressed rail.
364
- @Composable
365
- internal fun Modifier.pressLift(interactionSource: MutableInteractionSource): Modifier {
366
- val lift = remember { Animatable(0f) }
367
- val reduceMotion by rememberUpdatedState(rememberReduceMotion())
368
- LaunchedEffect(interactionSource) {
369
- interactionSource.interactions.collect {
370
- when (it) {
371
- is PressInteraction.Press -> if (!reduceMotion) launch { lift.animateTo(1f, SPRING) }
372
- is PressInteraction.Release, is PressInteraction.Cancel -> launch { lift.animateTo(0f, SPRING) }
373
- }
374
- }
375
- }
376
- return graphicsLayer {
377
- scaleX = 1 + lift.value * (LENS_SCALE_X - 1) * 0.5f
378
- scaleY = scaleX
379
- }
380
- }