@momo-kits/native-kits 0.300.3-debug → 0.300.4-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.md +51 -7
- package/README.md +2 -2
- package/compose/build.gradle.kts +10 -2
- package/compose/build.gradle.kts.backup +8 -0
- package/compose/compose.podspec +1 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
- package/gradle/libs.versions.toml +3 -14
- package/gradle/wrapper/gradle-wrapper.properties +0 -2
- package/gradle.properties +3 -5
- package/ios/Application/ApplicationEnvironment.swift +24 -6
- package/ios/Application/DesignSystemKits.swift +100 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +8 -2
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +171 -148
- package/ios/Input/InputPhoneNumber.swift +10 -9
- package/ios/Input/InputSearch.swift +107 -78
- package/ios/Input/InputTextArea.swift +133 -148
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +32 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +415 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +138 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +7 -3
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +17 -14
- package/ios/Typography/Text.swift +156 -20
- package/ios/Typography/Typography.swift +2 -2
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -3
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/publish.sh +19 -2
- package/scripts/gen-ios-icons.mjs +36 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- package/ios/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
package vn.momo.kits.navigation.bottomtab
|
|
2
2
|
|
|
3
|
+
import androidx.compose.animation.core.Animatable
|
|
4
|
+
import androidx.compose.animation.core.AnimationVector1D
|
|
5
|
+
import androidx.compose.animation.core.Easing
|
|
3
6
|
import androidx.compose.animation.core.tween
|
|
4
|
-
import androidx.compose.animation.fadeIn
|
|
5
|
-
import androidx.compose.animation.fadeOut
|
|
6
|
-
import androidx.compose.animation.scaleIn
|
|
7
|
-
import androidx.compose.animation.scaleOut
|
|
8
7
|
import androidx.compose.foundation.background
|
|
9
8
|
import androidx.compose.foundation.layout.*
|
|
10
9
|
import androidx.compose.runtime.Composable
|
|
10
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
11
11
|
import androidx.compose.runtime.DisposableEffect
|
|
12
12
|
import androidx.compose.runtime.LaunchedEffect
|
|
13
|
+
import androidx.compose.runtime.getValue
|
|
14
|
+
import androidx.compose.runtime.key
|
|
15
|
+
import androidx.compose.runtime.mutableIntStateOf
|
|
16
|
+
import androidx.compose.runtime.mutableStateOf
|
|
13
17
|
import androidx.compose.runtime.remember
|
|
18
|
+
import androidx.compose.runtime.saveable.rememberSaveable
|
|
19
|
+
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
|
20
|
+
import androidx.compose.runtime.setValue
|
|
21
|
+
import androidx.compose.runtime.staticCompositionLocalOf
|
|
14
22
|
import androidx.compose.ui.Alignment
|
|
15
23
|
import androidx.compose.ui.Modifier
|
|
24
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
25
|
+
import androidx.compose.ui.layout.layout
|
|
16
26
|
import androidx.compose.ui.unit.dp
|
|
17
|
-
import androidx.
|
|
18
|
-
import androidx.navigation.compose.composable
|
|
19
|
-
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
20
|
-
import androidx.navigation.compose.rememberNavController
|
|
27
|
+
import androidx.compose.ui.zIndex
|
|
21
28
|
import vn.momo.kits.const.AppNavigationBar
|
|
22
29
|
import vn.momo.kits.const.AppTheme
|
|
23
30
|
import vn.momo.kits.const.Spacing
|
|
24
31
|
import vn.momo.kits.navigation.KeyboardOptions
|
|
32
|
+
import vn.momo.kits.navigation.LocalMaxApi
|
|
25
33
|
import vn.momo.kits.navigation.LocalNavigation
|
|
26
34
|
import vn.momo.kits.navigation.LocalNavigator
|
|
27
35
|
import vn.momo.kits.navigation.NavigationOptions
|
|
@@ -29,6 +37,21 @@ import vn.momo.kits.navigation.StackScreen
|
|
|
29
37
|
import vn.momo.kits.navigation.component.HeaderType
|
|
30
38
|
import vn.momo.kits.platform.BackHandler
|
|
31
39
|
import vn.momo.kits.platform.reportFloatingArrowBack
|
|
40
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
41
|
+
import kotlinx.coroutines.launch
|
|
42
|
+
// AI-GENERATED START: use the typed tab haptic pattern
|
|
43
|
+
import vn.momo.maxapi.contract.device.vibration.model.VibrateStyle
|
|
44
|
+
// AI-GENERATED END: use the typed tab haptic pattern
|
|
45
|
+
|
|
46
|
+
private val inOutQuad = Easing { t -> if (t < 0.5f) 2 * t * t else 1 - 2 * (1 - t) * (1 - t) }
|
|
47
|
+
internal val tabTimingSpec = tween<Float>(durationMillis = 200, easing = inOutQuad)
|
|
48
|
+
|
|
49
|
+
internal val LocalBottomTabFocused = staticCompositionLocalOf { true }
|
|
50
|
+
|
|
51
|
+
private val Hidden = Modifier.layout { measurable, constraints ->
|
|
52
|
+
val placeable = measurable.measure(constraints)
|
|
53
|
+
layout(placeable.width, placeable.height) {}
|
|
54
|
+
}
|
|
32
55
|
|
|
33
56
|
private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
|
|
34
57
|
internal fun setBottomTabOption(index: Int, options: NavigationOptions){
|
|
@@ -46,21 +69,21 @@ internal fun getBottomTabOption(index: Int): NavigationOptions? {
|
|
|
46
69
|
fun BottomTab(
|
|
47
70
|
items: List<BottomTabItem>,
|
|
48
71
|
floatingButton: BottomTabFloatingButton? = null,
|
|
72
|
+
// iPhone Duo: when the side bar can't hold both bars, which one starts compressed;
|
|
73
|
+
// tapping the compressed one swaps them
|
|
74
|
+
compressionBehavior: DuoBarCompressionBehavior = DuoBarCompressionBehavior.TOOLBAR,
|
|
49
75
|
initialRouteName: String? = null,
|
|
50
76
|
onTabPress: ((index: Int, switchTab: () -> Unit) -> Unit)? = null,
|
|
51
77
|
backToInitialTab: Boolean = true,
|
|
52
78
|
) {
|
|
53
79
|
val navigation = LocalNavigation.current
|
|
54
80
|
val navigator = LocalNavigator.current
|
|
55
|
-
val
|
|
56
|
-
val
|
|
57
|
-
"option${items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0)}"
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
bottomTabOptionItems = items.mapIndexed { index, item ->
|
|
61
|
-
item.options ?: NavigationOptions()
|
|
62
|
-
}.toMutableList()
|
|
81
|
+
val maxApi = LocalMaxApi.current
|
|
82
|
+
val startIndex = remember { items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0) }
|
|
63
83
|
|
|
84
|
+
remember(items) {
|
|
85
|
+
bottomTabOptionItems = items.map { it.options ?: NavigationOptions() }.toMutableList()
|
|
86
|
+
}
|
|
64
87
|
|
|
65
88
|
LaunchedEffect(Unit){
|
|
66
89
|
navigation.markAsBottomTabRoot()
|
|
@@ -70,98 +93,124 @@ fun BottomTab(
|
|
|
70
93
|
)
|
|
71
94
|
}
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
val
|
|
76
|
-
val
|
|
96
|
+
var selectedIndex by rememberSaveable { mutableIntStateOf(startIndex) }
|
|
97
|
+
var loadedTabs by rememberSaveable { mutableStateOf(listOf(startIndex)) }
|
|
98
|
+
val tabStateHolder = rememberSaveableStateHolder()
|
|
99
|
+
val selectTab: (Int) -> Unit = { index ->
|
|
100
|
+
if (index !in loadedTabs) loadedTabs = loadedTabs + index
|
|
101
|
+
selectedIndex = index
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
val onFirstTab = selectedIndex == 0
|
|
105
|
+
val isFirstTab = onFirstTab || !backToInitialTab
|
|
106
|
+
val backToFirstTab = { selectTab(0) }
|
|
107
|
+
|
|
108
|
+
var lastFocusedIndex by rememberSaveable { mutableStateOf<Int?>(null) }
|
|
109
|
+
val tabProgress = remember { mutableMapOf<Int, Animatable<Float, AnimationVector1D>>() }
|
|
110
|
+
fun progressOf(index: Int) = tabProgress.getOrPut(index) {
|
|
111
|
+
Animatable(if (index == lastFocusedIndex) 1f else 0f)
|
|
112
|
+
}
|
|
113
|
+
LaunchedEffect(selectedIndex) {
|
|
114
|
+
lastFocusedIndex = selectedIndex
|
|
115
|
+
tabProgress.forEach { (index, progress) ->
|
|
116
|
+
if (index != selectedIndex) launch { progress.animateTo(0f, tabTimingSpec) }
|
|
117
|
+
}
|
|
118
|
+
progressOf(selectedIndex).animateTo(1f, tabTimingSpec)
|
|
119
|
+
}
|
|
77
120
|
|
|
78
121
|
DisposableEffect(isFirstTab) {
|
|
79
122
|
reportFloatingArrowBack(!isFirstTab)
|
|
80
|
-
navigator.tabBackHandler = if (isFirstTab) null else ({
|
|
81
|
-
|
|
123
|
+
navigator.tabBackHandler = if (isFirstTab) null else ({ backToFirstTab() })
|
|
124
|
+
|
|
82
125
|
onDispose {
|
|
83
126
|
reportFloatingArrowBack(false)
|
|
84
127
|
navigator.tabBackHandler = null
|
|
85
128
|
}
|
|
86
129
|
}
|
|
87
130
|
|
|
131
|
+
val layout = getScreenLayout()
|
|
132
|
+
val hasQr = floatingButton != null
|
|
133
|
+
val compression = rememberDuoBarCompression(compressionBehavior, items.size, hasQr, layout)
|
|
134
|
+
val railReserve = layout.sideBar?.takeIf { layout.isIPhoneDuo }?.let {
|
|
135
|
+
it.bottom + duoRailHeight(items.size, hasQr, compressed = compression.tabBarCompressed) + DUO_GROUP_GAP
|
|
136
|
+
} ?: 0.dp
|
|
137
|
+
val onTabSelected: (Int) -> Unit = { index ->
|
|
138
|
+
// AI-GENERATED START: preserve the light haptic on tab selection
|
|
139
|
+
runCatching { maxApi?.vibrate(VibrateStyle.LIGHT) }
|
|
140
|
+
// AI-GENERATED END: preserve the light haptic on tab selection
|
|
141
|
+
val switchTab = {
|
|
142
|
+
if (index != selectedIndex) selectTab(index)
|
|
143
|
+
}
|
|
144
|
+
onTabPress?.invoke(index, switchTab) ?: switchTab()
|
|
145
|
+
}
|
|
146
|
+
|
|
88
147
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
|
|
89
148
|
Box(modifier = Modifier
|
|
90
149
|
.fillMaxSize()
|
|
150
|
+
.background(AppTheme.current.colors.background.default)
|
|
91
151
|
) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
152
|
+
CompositionLocalProvider(
|
|
153
|
+
LocalDuoRailReserve provides railReserve,
|
|
154
|
+
LocalDuoBarCompression provides compression,
|
|
95
155
|
) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
animationSpec = tween(200)
|
|
156
|
+
loadedTabs.forEach { index ->
|
|
157
|
+
val item = items.getOrNull(index) ?: return@forEach
|
|
158
|
+
val focused = index == selectedIndex
|
|
159
|
+
val progress = progressOf(index)
|
|
160
|
+
key(index) {
|
|
161
|
+
Box(
|
|
162
|
+
Modifier
|
|
163
|
+
.fillMaxSize()
|
|
164
|
+
.zIndex(if (focused) 0f else -1f)
|
|
165
|
+
.then(if (focused) Modifier.graphicsLayer {
|
|
166
|
+
alpha = 0.3f + 0.7f * progress.value
|
|
167
|
+
scaleX = 0.97f + 0.03f * progress.value
|
|
168
|
+
scaleY = scaleX
|
|
169
|
+
} else Hidden)
|
|
170
|
+
) {
|
|
171
|
+
CompositionLocalProvider(LocalBottomTabFocused provides focused) {
|
|
172
|
+
tabStateHolder.SaveableStateProvider(index) {
|
|
173
|
+
StackScreen(
|
|
174
|
+
content = item.screen,
|
|
175
|
+
navigationOptions = getBottomTabOption(index),
|
|
176
|
+
bottomTabIndex = index,
|
|
177
|
+
onBackHandler = {
|
|
178
|
+
if (backToInitialTab && index != 0) {
|
|
179
|
+
backToFirstTab()
|
|
180
|
+
} else {
|
|
181
|
+
navigator.pop()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
125
184
|
)
|
|
126
|
-
}
|
|
127
|
-
) {
|
|
128
|
-
val option = getBottomTabOption(index)
|
|
129
|
-
|
|
130
|
-
StackScreen(
|
|
131
|
-
content = item.screen,
|
|
132
|
-
navigationOptions = option,
|
|
133
|
-
bottomTabIndex = index,
|
|
134
|
-
onBackHandler = {
|
|
135
|
-
if (backToInitialTab && "option$index" != startRoute) {
|
|
136
|
-
backToStartTab()
|
|
137
|
-
} else {
|
|
138
|
-
navigator.pop()
|
|
139
185
|
}
|
|
140
186
|
}
|
|
141
|
-
|
|
187
|
+
}
|
|
142
188
|
}
|
|
143
189
|
}
|
|
144
190
|
}
|
|
145
191
|
}
|
|
146
|
-
BackHandler(enabled = !
|
|
147
|
-
if (backToInitialTab)
|
|
192
|
+
BackHandler(enabled = !onFirstTab) {
|
|
193
|
+
if (backToInitialTab) backToFirstTab() else navigator.pop()
|
|
148
194
|
}
|
|
149
|
-
|
|
150
|
-
|
|
195
|
+
if (layout.isIPhoneDuo) {
|
|
196
|
+
DuoTabBar(
|
|
197
|
+
layout = layout,
|
|
151
198
|
items = items,
|
|
152
199
|
floatingButton = floatingButton,
|
|
153
|
-
|
|
154
|
-
onTabSelected =
|
|
155
|
-
|
|
156
|
-
val targetRoute = "option$index"
|
|
157
|
-
if (navController.currentBackStackEntry?.destination?.route != targetRoute) {
|
|
158
|
-
navController.navigate(targetRoute)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
onTabPress?.invoke(index, switchTab) ?: switchTab()
|
|
162
|
-
}
|
|
200
|
+
selectedIndex = selectedIndex,
|
|
201
|
+
onTabSelected = onTabSelected,
|
|
202
|
+
compression = compression,
|
|
163
203
|
)
|
|
164
|
-
|
|
204
|
+
} else {
|
|
205
|
+
Column {
|
|
206
|
+
BottomTabBar(
|
|
207
|
+
items = items,
|
|
208
|
+
floatingButton = floatingButton,
|
|
209
|
+
selectedIndex = selectedIndex,
|
|
210
|
+
onTabSelected = onTabSelected,
|
|
211
|
+
)
|
|
212
|
+
Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
|
|
213
|
+
}
|
|
165
214
|
}
|
|
166
215
|
}
|
|
167
216
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
package vn.momo.kits.navigation.bottomtab
|
|
2
2
|
|
|
3
|
-
import androidx.compose.animation.core.
|
|
4
|
-
import androidx.compose.animation.core.animateDpAsState
|
|
5
|
-
import androidx.compose.animation.core.tween
|
|
3
|
+
import androidx.compose.animation.core.Animatable
|
|
6
4
|
import androidx.compose.foundation.background
|
|
7
5
|
import androidx.compose.foundation.border
|
|
8
6
|
import androidx.compose.foundation.layout.*
|
|
9
7
|
import androidx.compose.foundation.shape.CircleShape
|
|
10
8
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
11
9
|
import androidx.compose.runtime.Composable
|
|
10
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
12
11
|
import androidx.compose.runtime.getValue
|
|
12
|
+
import androidx.compose.runtime.mutableIntStateOf
|
|
13
|
+
import androidx.compose.runtime.remember
|
|
14
|
+
import androidx.compose.runtime.saveable.rememberSaveable
|
|
15
|
+
import androidx.compose.runtime.setValue
|
|
13
16
|
import androidx.compose.ui.Alignment
|
|
14
17
|
import androidx.compose.ui.Modifier
|
|
15
18
|
import androidx.compose.ui.draw.clip
|
|
@@ -17,18 +20,18 @@ import androidx.compose.ui.graphics.Brush
|
|
|
17
20
|
import androidx.compose.ui.graphics.Color
|
|
18
21
|
import androidx.compose.ui.text.style.TextAlign
|
|
19
22
|
import androidx.compose.ui.text.style.TextOverflow
|
|
23
|
+
import androidx.compose.ui.unit.Dp
|
|
24
|
+
import androidx.compose.ui.unit.IntOffset
|
|
20
25
|
import androidx.compose.ui.unit.dp
|
|
21
|
-
import androidx.navigation.NavController
|
|
22
|
-
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
23
26
|
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
24
27
|
import vn.momo.kits.components.*
|
|
25
28
|
import vn.momo.kits.const.*
|
|
26
29
|
import vn.momo.kits.modifier.InternalApi
|
|
27
30
|
import vn.momo.kits.modifier.conditional
|
|
28
|
-
import vn.momo.kits.modifier.
|
|
29
|
-
import vn.momo.kits.
|
|
31
|
+
import vn.momo.kits.modifier.activeOpacityClickable
|
|
32
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
33
|
+
import vn.momo.kits.utils.horizontalSafeAreaPadding
|
|
30
34
|
|
|
31
|
-
internal val floatingButtonWidth = 75.dp
|
|
32
35
|
@InternalApi
|
|
33
36
|
const val BOTTOM_TAB_BAR_HEIGHT = 64
|
|
34
37
|
|
|
@@ -36,28 +39,24 @@ const val BOTTOM_TAB_BAR_HEIGHT = 64
|
|
|
36
39
|
internal fun BottomTabBar(
|
|
37
40
|
items: List<BottomTabItem>,
|
|
38
41
|
floatingButton: BottomTabFloatingButton? = null,
|
|
39
|
-
|
|
42
|
+
selectedIndex: Int,
|
|
40
43
|
onTabSelected: (Int) -> Unit,
|
|
41
44
|
) {
|
|
42
|
-
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
|
43
|
-
val selectedIndex = items.indexOfFirst { "option${items.indexOf(it)}" == currentRoute }
|
|
44
45
|
|
|
45
|
-
val
|
|
46
|
+
val layout = getScreenLayout()
|
|
46
47
|
val mid = items.size / 2
|
|
47
48
|
|
|
48
|
-
val
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
screenWidth / items.size
|
|
52
|
-
}
|
|
53
|
-
val adjustSize = if(floatingButton != null && selectedIndex >= mid) floatingButtonWidth else 0.dp
|
|
54
|
-
val targetOffset = tabWidth * selectedIndex + tabWidth / 2 - screenWidth / 2 + adjustSize
|
|
49
|
+
val floating = floatingButton?.takeIf { items.size % 2 == 0 }
|
|
50
|
+
val slotWidth = layout.contentWidth / (items.size + if (floating != null) 1 else 0)
|
|
51
|
+
fun slotOf(index: Int) = if (floating != null && index >= mid) index + 1 else index
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
var indicatorSlot by rememberSaveable { mutableIntStateOf(0) }
|
|
54
|
+
val indicatorX = remember { Animatable(indicatorSlot.toFloat()) }
|
|
55
|
+
LaunchedEffect(selectedIndex) {
|
|
56
|
+
val target = slotOf(selectedIndex)
|
|
57
|
+
indicatorSlot = target
|
|
58
|
+
indicatorX.animateTo(target.toFloat(), tabTimingSpec)
|
|
59
|
+
}
|
|
61
60
|
|
|
62
61
|
Box(
|
|
63
62
|
contentAlignment = Alignment.BottomCenter
|
|
@@ -69,20 +68,21 @@ internal fun BottomTabBar(
|
|
|
69
68
|
.background(AppTheme.current.colors.background.surface)
|
|
70
69
|
.conditional(IsShowBaseLineDebug) {
|
|
71
70
|
border(1.dp, Colors.blue_03)
|
|
72
|
-
}
|
|
71
|
+
}
|
|
72
|
+
.horizontalSafeAreaPadding(),
|
|
73
73
|
horizontalArrangement = Arrangement.SpaceAround,
|
|
74
74
|
) {
|
|
75
|
-
if (
|
|
75
|
+
if (floating == null) {
|
|
76
76
|
RenderTabBarItem(items, 0, items.size, selectedIndex, onTabSelected)
|
|
77
77
|
} else {
|
|
78
78
|
RenderTabBarItem(items, 0, mid, selectedIndex, onTabSelected)
|
|
79
|
-
Spacer(modifier = Modifier.width(
|
|
79
|
+
Spacer(modifier = Modifier.width(slotWidth))
|
|
80
80
|
RenderTabBarItem(items, mid, items.size, selectedIndex, onTabSelected)
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
Box(modifier = Modifier
|
|
85
|
-
.offset(
|
|
85
|
+
.offset { IntOffset((layout.insetLeft + slotWidth * (indicatorX.value + 0.5f) - layout.width / 2).roundToPx(), (-BOTTOM_TAB_BAR_HEIGHT + 2).dp.roundToPx()) }
|
|
86
86
|
.height(2.dp)
|
|
87
87
|
.width(44.dp)
|
|
88
88
|
.background(
|
|
@@ -101,7 +101,11 @@ internal fun BottomTabBar(
|
|
|
101
101
|
)
|
|
102
102
|
)
|
|
103
103
|
)
|
|
104
|
-
|
|
104
|
+
floating?.let {
|
|
105
|
+
Box(Modifier.offset(x = (layout.insetLeft - layout.insetRight) / 2)) {
|
|
106
|
+
FloatingButton(it, slotWidth)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -126,17 +130,10 @@ internal fun RowScope.RenderTabBarItem(
|
|
|
126
130
|
|
|
127
131
|
@Composable
|
|
128
132
|
internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
fun formatLabel(label: String): String? {
|
|
134
|
-
if (isNumber(label) && label.toInt() == 0) {
|
|
135
|
-
return null
|
|
136
|
-
}
|
|
137
|
-
return label
|
|
138
|
-
}
|
|
139
|
-
val badgeLabel = item.badgeLabel?.let { formatLabel(it) }
|
|
133
|
+
// RN parity: "0" hides the badge, "" renders a dot.
|
|
134
|
+
val badgeLabel = item.badgeLabel?.takeUnless { it.toIntOrNull() == 0 }
|
|
135
|
+
val isNumber = badgeLabel?.toIntOrNull() != null
|
|
136
|
+
val showDot = badgeLabel?.isEmpty() ?: item.showDot
|
|
140
137
|
|
|
141
138
|
Box(modifier = Modifier
|
|
142
139
|
.fillMaxSize()
|
|
@@ -144,7 +141,7 @@ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> U
|
|
|
144
141
|
.conditional(IsShowBaseLineDebug) {
|
|
145
142
|
border(1.dp, Colors.blue_03)
|
|
146
143
|
}
|
|
147
|
-
.
|
|
144
|
+
.activeOpacityClickable {
|
|
148
145
|
onClick()
|
|
149
146
|
},
|
|
150
147
|
contentAlignment = Alignment.BottomStart
|
|
@@ -152,18 +149,22 @@ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> U
|
|
|
152
149
|
Column(
|
|
153
150
|
modifier = Modifier
|
|
154
151
|
.fillMaxSize()
|
|
155
|
-
.padding(horizontal = Spacing.S, vertical = Spacing.S)
|
|
156
|
-
.noFeedbackClickable {
|
|
157
|
-
onClick()
|
|
158
|
-
},
|
|
152
|
+
.padding(horizontal = Spacing.S, vertical = Spacing.S),
|
|
159
153
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
160
154
|
verticalArrangement = Arrangement.Center
|
|
161
155
|
) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
Box(modifier = Modifier.padding(Spacing.XS)) {
|
|
157
|
+
Icon(
|
|
158
|
+
source = item.icon,
|
|
159
|
+
size = 28.dp,
|
|
160
|
+
color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint)
|
|
161
|
+
val badgeAnchor = Modifier.matchParentSize().wrapContentSize(Alignment.TopEnd, unbounded = true)
|
|
162
|
+
if (showDot) {
|
|
163
|
+
BadgeDot(size = DotSize.Small, modifier = badgeAnchor.offset(x = 4.dp, y = (-4).dp))
|
|
164
|
+
} else if (!badgeLabel.isNullOrEmpty()) {
|
|
165
|
+
Badge(badgeLabel, modifier = badgeAnchor.offset(x = if (isNumber) 8.dp else 12.dp, y = (-8).dp))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
167
168
|
Text(
|
|
168
169
|
text = item.label,
|
|
169
170
|
color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint,
|
|
@@ -172,28 +173,19 @@ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> U
|
|
|
172
173
|
overflow = TextOverflow.Ellipsis
|
|
173
174
|
)
|
|
174
175
|
}
|
|
175
|
-
if (badgeLabel != null) {
|
|
176
|
-
if (badgeLabel.isEmpty())
|
|
177
|
-
BadgeDot(
|
|
178
|
-
size = DotSize.Small,
|
|
179
|
-
modifier = Modifier.offset(x = 50.dp, y = (-48).dp)
|
|
180
|
-
)
|
|
181
|
-
else
|
|
182
|
-
Badge(badgeLabel, modifier = Modifier.offset(x = 44.dp, y = (-42).dp))
|
|
183
|
-
}
|
|
184
176
|
}
|
|
185
177
|
}
|
|
186
178
|
|
|
187
179
|
@Composable
|
|
188
|
-
internal fun FloatingButton(data: BottomTabFloatingButton) {
|
|
180
|
+
internal fun FloatingButton(data: BottomTabFloatingButton, width: Dp) {
|
|
189
181
|
Column(
|
|
190
182
|
modifier = Modifier
|
|
191
|
-
.width(
|
|
183
|
+
.width(width)
|
|
192
184
|
.padding(horizontal = Spacing.XXS, vertical = Spacing.S)
|
|
193
185
|
.conditional(IsShowBaseLineDebug) {
|
|
194
186
|
border(1.dp, Colors.blue_03)
|
|
195
187
|
}
|
|
196
|
-
.
|
|
188
|
+
.activeOpacityClickable {
|
|
197
189
|
data.onPress()
|
|
198
190
|
},
|
|
199
191
|
verticalArrangement = Arrangement.Bottom,
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
}
|