@momo-kits/native-kits 0.167.1-sizescale.2-debug → 0.168.1-beta.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.md +45 -3
- package/README.md +2 -2
- package/compose/build.gradle.kts +9 -1
- package/compose/build.gradle.kts.backup +8 -0
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +3 -4
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +353 -113
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +2 -2
- 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 +24 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.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 +16 -9
- 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 +6 -3
- 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/Spacing.kt +11 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +7 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +309 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
- 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 +8 -7
- 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 +134 -81
- 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 +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +126 -29
- 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 +116 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +2 -3
- 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/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +1 -3
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +10 -11
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
- package/gradle/libs.versions.toml +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -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 +0 -1
- 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 +160 -147
- package/ios/Input/InputPhoneNumber.swift +12 -9
- package/ios/Input/InputSearch.swift +107 -77
- package/ios/Input/InputTextArea.swift +128 -145
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +33 -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 +380 -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 +105 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +133 -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 +934 -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 +140 -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 +6 -1
- 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 +106 -54
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +3 -3
- package/ios/Typography/Text.swift +81 -19
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- 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/local.properties +2 -2
- package/package.json +1 -1
- 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/KitSizeScale.kt +0 -56
- package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Example.xcscheme +0 -32
- package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/WorkspaceSettings.xcsettings +0 -16
- package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
- package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -60
- 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/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
package vn.momo.kits.navigation.bottomtab
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.Spring
|
|
4
|
+
import androidx.compose.animation.core.animateDpAsState
|
|
5
|
+
import androidx.compose.animation.core.spring
|
|
6
|
+
import androidx.compose.foundation.background
|
|
7
|
+
import androidx.compose.foundation.border
|
|
8
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
9
|
+
import androidx.compose.foundation.layout.Box
|
|
10
|
+
import androidx.compose.foundation.layout.BoxScope
|
|
11
|
+
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
12
|
+
import androidx.compose.foundation.layout.Column
|
|
13
|
+
import androidx.compose.foundation.layout.Row
|
|
14
|
+
import androidx.compose.foundation.layout.RowScope
|
|
15
|
+
import androidx.compose.foundation.layout.absolutePadding
|
|
16
|
+
import androidx.compose.foundation.layout.fillMaxHeight
|
|
17
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
18
|
+
import androidx.compose.foundation.layout.height
|
|
19
|
+
import androidx.compose.foundation.layout.offset
|
|
20
|
+
import androidx.compose.foundation.layout.padding
|
|
21
|
+
import androidx.compose.foundation.layout.size
|
|
22
|
+
import androidx.compose.foundation.layout.width
|
|
23
|
+
import androidx.compose.foundation.shape.CircleShape
|
|
24
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
25
|
+
import androidx.compose.runtime.Composable
|
|
26
|
+
import androidx.compose.runtime.compositionLocalOf
|
|
27
|
+
import androidx.compose.runtime.getValue
|
|
28
|
+
import androidx.compose.ui.AbsoluteAlignment
|
|
29
|
+
import androidx.compose.ui.Alignment
|
|
30
|
+
import androidx.compose.ui.Modifier
|
|
31
|
+
import androidx.compose.ui.draw.clip
|
|
32
|
+
import androidx.compose.ui.draw.shadow
|
|
33
|
+
import androidx.compose.ui.graphics.Color
|
|
34
|
+
import androidx.compose.ui.graphics.Shape
|
|
35
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
36
|
+
import androidx.compose.ui.unit.Dp
|
|
37
|
+
import androidx.compose.ui.unit.dp
|
|
38
|
+
import vn.momo.kits.components.Badge
|
|
39
|
+
import vn.momo.kits.components.BadgeDot
|
|
40
|
+
import vn.momo.kits.components.DotSize
|
|
41
|
+
import vn.momo.kits.components.Icon
|
|
42
|
+
import vn.momo.kits.components.Text
|
|
43
|
+
import vn.momo.kits.const.AppTheme
|
|
44
|
+
import vn.momo.kits.const.Radius
|
|
45
|
+
import vn.momo.kits.const.Spacing
|
|
46
|
+
import vn.momo.kits.const.Typography
|
|
47
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
48
|
+
import vn.momo.kits.layout.ScreenLayout
|
|
49
|
+
import vn.momo.kits.layout.SideBarEdge
|
|
50
|
+
import vn.momo.kits.modifier.noFeedbackClickable
|
|
51
|
+
import vn.momo.kits.modifier.setAutomationId
|
|
52
|
+
|
|
53
|
+
// Figma Fold-Design-System MT1/MT2, mirrored from RN DuoTabBar.tsx.
|
|
54
|
+
private val ITEM_SIZE = DuoMetrics.controlSize
|
|
55
|
+
private val ICON_SIZE = 28.dp
|
|
56
|
+
private val QR_ICON_SIZE = 24.dp
|
|
57
|
+
private val RAIL_PADDING = 6.dp
|
|
58
|
+
private val RAIL_GAP = Spacing.XXS
|
|
59
|
+
// selection bubble overflows the 48 rail cell by 5 above and below
|
|
60
|
+
private val RAIL_BUBBLE_WIDTH = 44.dp
|
|
61
|
+
private val RAIL_BUBBLE_HEIGHT = 58.dp
|
|
62
|
+
private val FLOATING_HEIGHT = 62.dp
|
|
63
|
+
private val FLOATING_PADDING = Spacing.XS
|
|
64
|
+
private val FLOATING_QR_SIZE = 44.dp
|
|
65
|
+
// floating bar spans 6 of the 8 portrait grid columns, like the content
|
|
66
|
+
private const val GRID_COLUMNS = 8
|
|
67
|
+
private const val GRID_SPAN = 6
|
|
68
|
+
// RN reanimated spring { damping: 18, stiffness: 220, mass: 0.8 }
|
|
69
|
+
private val BUBBLE_SPRING = spring<Dp>(dampingRatio = 0.68f, stiffness = 275f, visibilityThreshold = Dp(0.5f))
|
|
70
|
+
|
|
71
|
+
// Space from the window bottom the rail stack and its gap take, so the header side bar stays above it.
|
|
72
|
+
internal val LocalDuoRailReserve = compositionLocalOf { 0.dp }
|
|
73
|
+
|
|
74
|
+
// Rail plus the QR button under it; compressed, the rail is a single button.
|
|
75
|
+
internal fun duoRailHeight(itemCount: Int, hasQr: Boolean, compressed: Boolean = false): Dp {
|
|
76
|
+
val rail = if (compressed) ITEM_SIZE
|
|
77
|
+
else RAIL_PADDING * 2 + ITEM_SIZE * itemCount + RAIL_GAP * (itemCount - 1).coerceAtLeast(0)
|
|
78
|
+
return rail + if (hasQr) Spacing.M + ITEM_SIZE else 0.dp
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Space tab content leaves under the floating bar; 0 when the rail sits in the side strip or off Duo.
|
|
82
|
+
internal fun duoFloatingTabBarInset(layout: ScreenLayout): Dp =
|
|
83
|
+
if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + Spacing.M + layout.insetBottom else 0.dp
|
|
84
|
+
|
|
85
|
+
// iPhone Duo tab bar: glass rail in the side bar, or a floating glass bar when unfolded portrait.
|
|
86
|
+
@Composable
|
|
87
|
+
internal fun BoxScope.DuoTabBar(
|
|
88
|
+
layout: ScreenLayout,
|
|
89
|
+
items: List<BottomTabItem>,
|
|
90
|
+
floatingButton: BottomTabFloatingButton?,
|
|
91
|
+
selectedIndex: Int,
|
|
92
|
+
onTabSelected: (Int) -> Unit,
|
|
93
|
+
compression: DuoBarCompression = DuoBarCompression(),
|
|
94
|
+
) {
|
|
95
|
+
if (layout.sideBar != null) {
|
|
96
|
+
DuoRailTabBar(layout, items, floatingButton, selectedIndex, onTabSelected, compression)
|
|
97
|
+
} else {
|
|
98
|
+
DuoFloatingTabBar(layout, items, floatingButton, selectedIndex, onTabSelected)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E). When the
|
|
103
|
+
// side bar is short it can shrink to one button showing the current tab (HIG "Tab bar compressed");
|
|
104
|
+
// tapping it expands the rail and compresses the toolbar.
|
|
105
|
+
@Composable
|
|
106
|
+
private fun BoxScope.DuoRailTabBar(
|
|
107
|
+
layout: ScreenLayout,
|
|
108
|
+
items: List<BottomTabItem>,
|
|
109
|
+
floatingButton: BottomTabFloatingButton?,
|
|
110
|
+
selectedIndex: Int,
|
|
111
|
+
onTabSelected: (Int) -> Unit,
|
|
112
|
+
compression: DuoBarCompression,
|
|
113
|
+
) {
|
|
114
|
+
val sideBar = layout.sideBar ?: return
|
|
115
|
+
val isRight = sideBar.edge == SideBarEdge.RIGHT
|
|
116
|
+
|
|
117
|
+
Column(
|
|
118
|
+
modifier = Modifier
|
|
119
|
+
.align(if (isRight) AbsoluteAlignment.BottomRight else AbsoluteAlignment.BottomLeft)
|
|
120
|
+
.width(sideBar.width)
|
|
121
|
+
.padding(bottom = sideBar.bottom)
|
|
122
|
+
.absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
|
|
123
|
+
horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
|
|
124
|
+
verticalArrangement = Arrangement.spacedBy(Spacing.M),
|
|
125
|
+
) {
|
|
126
|
+
val current = items.getOrNull(selectedIndex)
|
|
127
|
+
if (compression.tabBarCompressed && current != null) {
|
|
128
|
+
Box(
|
|
129
|
+
modifier = Modifier
|
|
130
|
+
.size(ITEM_SIZE)
|
|
131
|
+
.tabBarMaterial(CircleShape)
|
|
132
|
+
.noFeedbackClickable(onClick = compression.expandTabBar)
|
|
133
|
+
.setAutomationId("bottom_tab_bar"),
|
|
134
|
+
contentAlignment = Alignment.Center,
|
|
135
|
+
) {
|
|
136
|
+
DuoTabIcon(current, selected = true)
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
Rail(items, selectedIndex) {
|
|
140
|
+
onTabSelected(it)
|
|
141
|
+
compression.onTabSelected()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (floatingButton != null) {
|
|
145
|
+
QrButton(floatingButton, size = ITEM_SIZE)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Glass column of tab items with the sliding selection bubble.
|
|
151
|
+
@Composable
|
|
152
|
+
private fun Rail(items: List<BottomTabItem>, selectedIndex: Int, onItemClick: (Int) -> Unit) {
|
|
153
|
+
val bubbleOffset by animateDpAsState(
|
|
154
|
+
targetValue = (ITEM_SIZE + RAIL_GAP) * selectedIndex.coerceAtLeast(0),
|
|
155
|
+
animationSpec = BUBBLE_SPRING,
|
|
156
|
+
label = "DuoRailBubble",
|
|
157
|
+
)
|
|
158
|
+
Box(
|
|
159
|
+
Modifier
|
|
160
|
+
.width(ITEM_SIZE)
|
|
161
|
+
.tabBarMaterial(RoundedCornerShape(Radius.XL))
|
|
162
|
+
.setAutomationId("bottom_tab_bar")
|
|
163
|
+
) {
|
|
164
|
+
Box(
|
|
165
|
+
Modifier
|
|
166
|
+
.offset(
|
|
167
|
+
x = (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
|
|
168
|
+
y = RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2 + bubbleOffset,
|
|
169
|
+
)
|
|
170
|
+
.size(RAIL_BUBBLE_WIDTH, RAIL_BUBBLE_HEIGHT)
|
|
171
|
+
.background(AppTheme.current.colors.background.selected, CircleShape)
|
|
172
|
+
)
|
|
173
|
+
Column(
|
|
174
|
+
modifier = Modifier.padding(vertical = RAIL_PADDING),
|
|
175
|
+
verticalArrangement = Arrangement.spacedBy(RAIL_GAP),
|
|
176
|
+
) {
|
|
177
|
+
items.forEachIndexed { index, item ->
|
|
178
|
+
Box(
|
|
179
|
+
modifier = Modifier
|
|
180
|
+
.size(ITEM_SIZE)
|
|
181
|
+
.noFeedbackClickable { onItemClick(index) },
|
|
182
|
+
contentAlignment = Alignment.Center,
|
|
183
|
+
) {
|
|
184
|
+
DuoTabIcon(item, selected = index == selectedIndex)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@Composable
|
|
192
|
+
private fun BoxScope.DuoFloatingTabBar(
|
|
193
|
+
layout: ScreenLayout,
|
|
194
|
+
items: List<BottomTabItem>,
|
|
195
|
+
floatingButton: BottomTabFloatingButton?,
|
|
196
|
+
selectedIndex: Int,
|
|
197
|
+
onTabSelected: (Int) -> Unit,
|
|
198
|
+
) {
|
|
199
|
+
val columnWidth = (layout.width - Spacing.M * 2 - Spacing.M * (GRID_COLUMNS - 1)) / GRID_COLUMNS
|
|
200
|
+
val barWidth = columnWidth * GRID_SPAN + Spacing.M * (GRID_SPAN - 1)
|
|
201
|
+
val useQr = floatingButton != null && items.size % 2 == 0
|
|
202
|
+
val middle = items.size / 2
|
|
203
|
+
val cellCount = items.size + if (useQr) 1 else 0
|
|
204
|
+
val visualIndex = if (useQr && selectedIndex >= middle) selectedIndex + 1 else selectedIndex
|
|
205
|
+
|
|
206
|
+
Box(
|
|
207
|
+
modifier = Modifier
|
|
208
|
+
.align(Alignment.BottomCenter)
|
|
209
|
+
.padding(bottom = layout.insetBottom + Spacing.M)
|
|
210
|
+
.width(barWidth)
|
|
211
|
+
.height(FLOATING_HEIGHT)
|
|
212
|
+
.tabBarMaterial(RoundedCornerShape(FLOATING_HEIGHT / 2))
|
|
213
|
+
.padding(FLOATING_PADDING)
|
|
214
|
+
.setAutomationId("bottom_tab_bar")
|
|
215
|
+
) {
|
|
216
|
+
BoxWithConstraints(Modifier.fillMaxSize()) {
|
|
217
|
+
val cellWidth = maxWidth / cellCount
|
|
218
|
+
val bubbleOffset by animateDpAsState(
|
|
219
|
+
targetValue = cellWidth * visualIndex.coerceAtLeast(0),
|
|
220
|
+
animationSpec = BUBBLE_SPRING,
|
|
221
|
+
label = "DuoFloatingBubble",
|
|
222
|
+
)
|
|
223
|
+
Box(
|
|
224
|
+
Modifier
|
|
225
|
+
.offset(x = bubbleOffset)
|
|
226
|
+
.width(cellWidth)
|
|
227
|
+
.fillMaxHeight()
|
|
228
|
+
.background(AppTheme.current.colors.background.selected, CircleShape)
|
|
229
|
+
)
|
|
230
|
+
Row(Modifier.fillMaxSize()) {
|
|
231
|
+
items.forEachIndexed { index, item ->
|
|
232
|
+
if (useQr && index == middle) {
|
|
233
|
+
Box(Modifier.weight(1f).fillMaxHeight(), contentAlignment = Alignment.Center) {
|
|
234
|
+
QrButton(floatingButton, size = FLOATING_QR_SIZE)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
FloatingCell(item, selected = index == selectedIndex) { onTabSelected(index) }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@Composable
|
|
245
|
+
private fun RowScope.FloatingCell(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
|
|
246
|
+
val colors = AppTheme.current.colors
|
|
247
|
+
Column(
|
|
248
|
+
modifier = Modifier
|
|
249
|
+
.weight(1f)
|
|
250
|
+
.fillMaxHeight()
|
|
251
|
+
.noFeedbackClickable(onClick = onClick),
|
|
252
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
253
|
+
verticalArrangement = Arrangement.spacedBy(Spacing.XXS, Alignment.CenterVertically),
|
|
254
|
+
) {
|
|
255
|
+
DuoTabIcon(item, selected)
|
|
256
|
+
Text(
|
|
257
|
+
text = item.label,
|
|
258
|
+
style = Typography.labelXsMedium,
|
|
259
|
+
color = if (selected) colors.primary else colors.text.default,
|
|
260
|
+
maxLines = 1,
|
|
261
|
+
overflow = TextOverflow.Ellipsis,
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@Composable
|
|
267
|
+
private fun DuoTabIcon(item: BottomTabItem, selected: Boolean) {
|
|
268
|
+
val colors = AppTheme.current.colors
|
|
269
|
+
val badgeLabel = item.badgeLabel?.takeUnless { it.toIntOrNull() == 0 }
|
|
270
|
+
Box {
|
|
271
|
+
Icon(
|
|
272
|
+
source = item.icon,
|
|
273
|
+
size = ICON_SIZE,
|
|
274
|
+
color = if (selected) colors.primary else colors.text.default,
|
|
275
|
+
)
|
|
276
|
+
when {
|
|
277
|
+
badgeLabel == null && !item.showDot -> Unit
|
|
278
|
+
badgeLabel.isNullOrEmpty() -> BadgeDot(
|
|
279
|
+
size = DotSize.Small,
|
|
280
|
+
modifier = Modifier.align(Alignment.TopEnd),
|
|
281
|
+
)
|
|
282
|
+
else -> Badge(
|
|
283
|
+
badgeLabel,
|
|
284
|
+
modifier = Modifier.align(Alignment.TopStart).offset(x = ICON_SIZE / 2, y = -Spacing.XS),
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@Composable
|
|
291
|
+
private fun QrButton(data: BottomTabFloatingButton, size: Dp) {
|
|
292
|
+
Box(
|
|
293
|
+
modifier = Modifier
|
|
294
|
+
.size(size)
|
|
295
|
+
.clip(CircleShape)
|
|
296
|
+
.background(AppTheme.current.colors.primary)
|
|
297
|
+
.noFeedbackClickable(onClick = data.onPress)
|
|
298
|
+
.setAutomationId(data.label),
|
|
299
|
+
contentAlignment = Alignment.Center,
|
|
300
|
+
) {
|
|
301
|
+
Icon(source = data.icon, color = Color.White, size = QR_ICON_SIZE)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// RN falls back to this when native glass is missing; Compose has no backdrop blur, so it always does.
|
|
306
|
+
@Composable
|
|
307
|
+
private fun Modifier.tabBarMaterial(shape: Shape): Modifier {
|
|
308
|
+
val colors = AppTheme.current.colors
|
|
309
|
+
return shadow(3.dp, shape)
|
|
310
|
+
.background(colors.background.surface)
|
|
311
|
+
.border(1.dp, colors.border.default, shape)
|
|
312
|
+
}
|
|
@@ -18,10 +18,14 @@ import androidx.compose.foundation.layout.width
|
|
|
18
18
|
import androidx.compose.foundation.layout.wrapContentHeight
|
|
19
19
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
20
20
|
import androidx.compose.runtime.Composable
|
|
21
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
22
|
+
import androidx.compose.runtime.compositionLocalOf
|
|
21
23
|
import androidx.compose.runtime.getValue
|
|
22
24
|
import androidx.compose.ui.Alignment
|
|
23
25
|
import androidx.compose.ui.Modifier
|
|
24
26
|
import androidx.compose.ui.draw.clipToBounds
|
|
27
|
+
import androidx.compose.ui.draw.drawBehind
|
|
28
|
+
import androidx.compose.ui.geometry.Offset
|
|
25
29
|
import androidx.compose.ui.graphics.graphicsLayer
|
|
26
30
|
import androidx.compose.ui.graphics.Brush
|
|
27
31
|
import androidx.compose.ui.graphics.Color
|
|
@@ -33,12 +37,13 @@ import androidx.compose.ui.unit.dp
|
|
|
33
37
|
import vn.momo.kits.components.Icon
|
|
34
38
|
import vn.momo.kits.components.Image
|
|
35
39
|
import vn.momo.kits.components.InputSearchProps
|
|
36
|
-
import vn.momo.kits.const.AppStatusBar
|
|
37
40
|
import vn.momo.kits.const.AppTheme
|
|
38
41
|
import vn.momo.kits.const.Colors
|
|
39
42
|
import vn.momo.kits.components.PopupNotifyProps
|
|
40
43
|
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
41
44
|
import vn.momo.kits.const.Spacing
|
|
45
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
46
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
42
47
|
import vn.momo.kits.modifier.activeOpacityClickable
|
|
43
48
|
import vn.momo.kits.modifier.conditional
|
|
44
49
|
import vn.momo.kits.modifier.setAutomationId
|
|
@@ -47,33 +52,83 @@ import vn.momo.kits.navigation.LocalNavigator
|
|
|
47
52
|
import vn.momo.kits.navigation.LocalOptions
|
|
48
53
|
import vn.momo.kits.navigation.LocalScrollState
|
|
49
54
|
import vn.momo.kits.navigation.getInputSearchType
|
|
55
|
+
import vn.momo.kits.navigation.usesDuoGlassHeader
|
|
56
|
+
import vn.momo.kits.utils.horizontalSafeAreaPadding
|
|
50
57
|
|
|
51
58
|
@InternalApi
|
|
52
59
|
const val HEADER_HEIGHT = 52
|
|
53
60
|
internal enum class InputSearchType { None, Header, Animated }
|
|
54
61
|
enum class TitlePosition { LEFT, CENTER }
|
|
55
62
|
|
|
63
|
+
private val HEADER_ACTIONS_WIDTH = 172.dp
|
|
64
|
+
|
|
65
|
+
// Extra width the 48pt back and toolkit take on the iPhone Duo status bar row.
|
|
66
|
+
private val STATUS_BAR_ROW_ACTIONS_EXTRA = 55.dp
|
|
67
|
+
|
|
56
68
|
@Suppress("FrequentlyChangingValue")
|
|
57
69
|
@Composable
|
|
58
|
-
internal fun
|
|
70
|
+
internal fun headerOpacity(): Float {
|
|
59
71
|
val options = LocalOptions.current
|
|
60
|
-
val navigator = LocalNavigator.current
|
|
61
72
|
val scrollState = LocalScrollState.current
|
|
62
|
-
val headerRightWidthPx = LocalHeaderRightWidthPx.current
|
|
63
|
-
val inputSearchType = getInputSearchType(options)
|
|
64
|
-
|
|
65
73
|
val opacityHeight = when (val header = options.headerType) {
|
|
66
74
|
is HeaderType.Animated -> with(LocalDensity.current) { header.layoutOffSet.roundToPx() }
|
|
67
75
|
else -> HEADER_HEIGHT
|
|
68
76
|
}
|
|
69
77
|
val opacity by animateFloatAsState(
|
|
70
|
-
targetValue =
|
|
71
|
-
0f
|
|
72
|
-
|
|
73
|
-
(scrollState.value / opacityHeight.toFloat())
|
|
74
|
-
.coerceIn(0f, 1f)
|
|
78
|
+
targetValue = when {
|
|
79
|
+
options.headerType is HeaderType.Transparent -> 0f
|
|
80
|
+
opacityHeight <= 0 -> if (scrollState.value > 0) 1f else 0f
|
|
81
|
+
else -> (scrollState.value / opacityHeight.toFloat()).coerceIn(0f, 1f)
|
|
75
82
|
}
|
|
76
83
|
)
|
|
84
|
+
return opacity
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Back button and toolkit share the bar with the title unless the iPhone Duo side bar holds them.
|
|
88
|
+
@Composable
|
|
89
|
+
internal fun headerTitleMaxWidth(): Dp {
|
|
90
|
+
val layout = getScreenLayout()
|
|
91
|
+
val clearance = layout.statusBarClearance
|
|
92
|
+
val clearanceWidth = clearance?.let { it.left + it.right } ?: 0.dp
|
|
93
|
+
if (layout.sideBar != null) return layout.contentWidth - Spacing.M * 2 - clearanceWidth
|
|
94
|
+
return layout.width - HEADER_ACTIONS_WIDTH - (if (clearance != null) STATUS_BAR_ROW_ACTIONS_EXTRA else 0.dp) - clearanceWidth
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Scroll distance over which the Duo glass bar fades in.
|
|
98
|
+
private val DUO_GLASS_FADE_DISTANCE = 16.dp
|
|
99
|
+
|
|
100
|
+
// RN DuoGlassBackground fallback: translucent surface with a hairline once content scrolls under it.
|
|
101
|
+
@Suppress("FrequentlyChangingValue")
|
|
102
|
+
@Composable
|
|
103
|
+
private fun Modifier.duoGlassBar(): Modifier {
|
|
104
|
+
val scrollState = LocalScrollState.current
|
|
105
|
+
val fadePx = with(LocalDensity.current) { DUO_GLASS_FADE_DISTANCE.toPx() }
|
|
106
|
+
val alpha by animateFloatAsState((scrollState.value / fadePx).coerceIn(0f, 1f), label = "DuoGlassAlpha")
|
|
107
|
+
val colors = AppTheme.current.colors
|
|
108
|
+
return background(colors.background.surface.copy(alpha = 0.8f * alpha))
|
|
109
|
+
.drawBehind {
|
|
110
|
+
val stroke = 0.5.dp.toPx()
|
|
111
|
+
drawLine(
|
|
112
|
+
color = colors.border.default.copy(alpha = alpha),
|
|
113
|
+
start = Offset(0f, size.height - stroke / 2),
|
|
114
|
+
end = Offset(size.width, size.height - stroke / 2),
|
|
115
|
+
strokeWidth = stroke,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Composable
|
|
121
|
+
internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
122
|
+
val options = LocalOptions.current
|
|
123
|
+
val navigator = LocalNavigator.current
|
|
124
|
+
val headerRightWidthPx = LocalHeaderRightWidthPx.current
|
|
125
|
+
val inputSearchType = getInputSearchType(options)
|
|
126
|
+
val layout = getScreenLayout()
|
|
127
|
+
val useSideBar = layout.sideBar != null
|
|
128
|
+
// iPhone Duo inner portrait: the row sits on the status bar, between its clusters
|
|
129
|
+
val clearance = layout.statusBarClearance
|
|
130
|
+
val statusBarRow = clearance != null && !useSideBar
|
|
131
|
+
val opacity = headerOpacity()
|
|
77
132
|
|
|
78
133
|
val headerColor = getHeaderColor(options.headerType, opacity, options.tintColor, Colors.black_17)
|
|
79
134
|
|
|
@@ -92,13 +147,15 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
92
147
|
// Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
|
|
93
148
|
val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
|
|
94
149
|
|
|
150
|
+
val useDuoGlass = options.usesDuoGlassHeader(layout)
|
|
95
151
|
val headerImage = AppTheme.current.assets.headerBackground
|
|
96
|
-
.takeUnless { options.headerType is HeaderType.Transparent }
|
|
152
|
+
.takeUnless { options.headerType is HeaderType.Transparent || useDuoGlass }
|
|
97
153
|
|
|
98
154
|
Box(
|
|
99
|
-
Modifier.height(
|
|
155
|
+
Modifier.height(layout.headerHeight)
|
|
100
156
|
.fillMaxWidth()
|
|
101
|
-
.
|
|
157
|
+
.then(if (useDuoGlass) Modifier.duoGlassBar() else Modifier)
|
|
158
|
+
.conditional(headerImage.isNullOrEmpty() && !useDuoGlass) {
|
|
102
159
|
background(background.copy(alpha = animatedAlpha))
|
|
103
160
|
}
|
|
104
161
|
.conditional(IsShowBaseLineDebug) {
|
|
@@ -125,21 +182,34 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
125
182
|
}
|
|
126
183
|
}
|
|
127
184
|
Row(
|
|
128
|
-
modifier = Modifier.height(
|
|
185
|
+
modifier = Modifier.height(layout.headerBarHeight)
|
|
129
186
|
.fillMaxWidth()
|
|
130
|
-
.
|
|
187
|
+
.horizontalSafeAreaPadding()
|
|
188
|
+
.padding(
|
|
189
|
+
start = Spacing.M + (clearance?.left ?: 0.dp),
|
|
190
|
+
end = Spacing.M + (clearance?.right ?: 0.dp),
|
|
191
|
+
),
|
|
131
192
|
verticalAlignment = Alignment.CenterVertically,
|
|
132
193
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
133
194
|
) {
|
|
134
|
-
if(!options.hiddenBack) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
navigator.onBackSafe {
|
|
141
|
-
|
|
142
|
-
|
|
195
|
+
if (!options.hiddenBack && !useSideBar) {
|
|
196
|
+
if (statusBarRow) {
|
|
197
|
+
SideBarButton(
|
|
198
|
+
icon = "arrow-back",
|
|
199
|
+
headerColor = headerColor,
|
|
200
|
+
automationId = "btn_navigation_back",
|
|
201
|
+
onClick = { navigator.onBackSafe { } },
|
|
202
|
+
)
|
|
203
|
+
} else {
|
|
204
|
+
BackButton(
|
|
205
|
+
borderColor = headerColor.borderColor,
|
|
206
|
+
backgroundButton = headerColor.backgroundButton,
|
|
207
|
+
tintIconColor = headerColor.tintIconColor,
|
|
208
|
+
onBackHandler = {
|
|
209
|
+
navigator.onBackSafe { }
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
}
|
|
143
213
|
Spacer(Modifier.width(Spacing.S))
|
|
144
214
|
}
|
|
145
215
|
|
|
@@ -156,20 +226,42 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
156
226
|
Box(Modifier.onGloballyPositioned {
|
|
157
227
|
if(headerRightWidthPx.intValue != it.size.width) headerRightWidthPx.intValue = it.size.width
|
|
158
228
|
}){
|
|
159
|
-
|
|
229
|
+
val headerRight = options.headerRight
|
|
230
|
+
when {
|
|
231
|
+
useSideBar && headerRight is HeaderRight.Toolkit -> Unit
|
|
232
|
+
statusBarRow && headerRight is HeaderRight.Toolkit -> Toolkit(
|
|
233
|
+
headerRight = headerRight,
|
|
234
|
+
tintColor = options.tintColor,
|
|
235
|
+
headerColor = headerColor,
|
|
236
|
+
layout = ToolkitLayout.SideBarRow,
|
|
237
|
+
)
|
|
238
|
+
else -> CompositionLocalProvider(LocalHeaderColor provides headerColor) {
|
|
239
|
+
HeaderRight(headerRight, options.tintColor, headerColor)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
160
242
|
}
|
|
161
243
|
}
|
|
162
244
|
if (centerTitle) {
|
|
245
|
+
// on the status bar row the clusters are lopsided, so both sides keep the wider one's room
|
|
246
|
+
val centerInset = if (clearance != null) {
|
|
247
|
+
val back = if (options.hiddenBack || useSideBar) 0.dp else DuoMetrics.controlSize + Spacing.S
|
|
248
|
+
val right = with(LocalDensity.current) { headerRightWidthPx.intValue.toDp() }
|
|
249
|
+
maxOf(clearance.left + back, clearance.right + right) + Spacing.S
|
|
250
|
+
} else {
|
|
251
|
+
0.dp
|
|
252
|
+
}
|
|
163
253
|
Box(
|
|
164
|
-
modifier = Modifier.height(
|
|
254
|
+
modifier = Modifier.height(layout.headerBarHeight)
|
|
165
255
|
.fillMaxWidth()
|
|
166
|
-
.
|
|
256
|
+
.horizontalSafeAreaPadding()
|
|
257
|
+
.padding(horizontal = Spacing.M + centerInset),
|
|
167
258
|
contentAlignment = Alignment.Center
|
|
168
259
|
) {
|
|
169
260
|
HeaderContent(
|
|
170
261
|
headerTitle = options.headerTitle,
|
|
171
262
|
tintIconColor = titleColor,
|
|
172
263
|
titlePosition = titlePosition,
|
|
264
|
+
centerWidthFraction = if (clearance != null) 1f else 0.5f,
|
|
173
265
|
modifier = Modifier.fillMaxWidth()
|
|
174
266
|
)
|
|
175
267
|
}
|
|
@@ -203,6 +295,7 @@ internal fun HeaderContent(
|
|
|
203
295
|
headerTitle: HeaderTitle,
|
|
204
296
|
tintIconColor: Color,
|
|
205
297
|
titlePosition: TitlePosition = TitlePosition.LEFT,
|
|
298
|
+
centerWidthFraction: Float = 0.5f,
|
|
206
299
|
modifier: Modifier = Modifier,
|
|
207
300
|
){
|
|
208
301
|
Box(
|
|
@@ -214,7 +307,7 @@ internal fun HeaderContent(
|
|
|
214
307
|
HeaderTitle(
|
|
215
308
|
title = headerTitle.title,
|
|
216
309
|
color = tintIconColor,
|
|
217
|
-
modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER)
|
|
310
|
+
modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) centerWidthFraction else 1f),
|
|
218
311
|
textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
|
|
219
312
|
)
|
|
220
313
|
}
|
|
@@ -326,6 +419,10 @@ data class HeaderBackProps(
|
|
|
326
419
|
|
|
327
420
|
@InternalApi
|
|
328
421
|
data class HeaderColor(val tintIconColor: Color, val backgroundButton: Color, val borderColor: Color)
|
|
422
|
+
|
|
423
|
+
// The enclosing header's colors, so a custom header right follows the bar (SwiftUI `\.headerColor`).
|
|
424
|
+
internal val LocalHeaderColor = compositionLocalOf<HeaderColor?> { null }
|
|
425
|
+
|
|
329
426
|
internal fun getHeaderColor(headerType: HeaderType, opacity: Float, tintColor: Color?, defaultColor: Color): HeaderColor{
|
|
330
427
|
return if(headerType is HeaderType.Animated) {
|
|
331
428
|
if (opacity == 1f || !headerType.isSurface)
|
|
@@ -3,23 +3,27 @@ package vn.momo.kits.navigation.component
|
|
|
3
3
|
import androidx.compose.animation.core.animateFloatAsState
|
|
4
4
|
import androidx.compose.foundation.background
|
|
5
5
|
import androidx.compose.foundation.layout.Box
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxHeight
|
|
6
7
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
7
8
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
8
9
|
import androidx.compose.foundation.layout.height
|
|
9
10
|
import androidx.compose.runtime.Composable
|
|
10
11
|
import androidx.compose.runtime.getValue
|
|
11
12
|
import androidx.compose.ui.Modifier
|
|
12
|
-
import androidx.compose.ui.
|
|
13
|
+
import androidx.compose.ui.draw.clipToBounds
|
|
14
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
13
15
|
import androidx.compose.ui.graphics.Brush
|
|
14
16
|
import androidx.compose.ui.graphics.Color
|
|
15
17
|
import androidx.compose.ui.platform.LocalDensity
|
|
16
18
|
import androidx.compose.ui.unit.dp
|
|
17
19
|
import vn.momo.kits.components.Image
|
|
18
|
-
import vn.momo.kits.const.AppStatusBar
|
|
19
20
|
import vn.momo.kits.const.AppTheme
|
|
21
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
20
22
|
import vn.momo.kits.modifier.conditional
|
|
21
23
|
import vn.momo.kits.navigation.LocalOptions
|
|
22
24
|
import vn.momo.kits.navigation.LocalScrollState
|
|
25
|
+
import vn.momo.kits.navigation.getInputSearchType
|
|
26
|
+
import vn.momo.kits.navigation.usesDuoGlassHeader
|
|
23
27
|
|
|
24
28
|
enum class AnimatedHeaderRatio(val value: Float){
|
|
25
29
|
RATIO_16_9(16f / 9f),
|
|
@@ -29,6 +33,9 @@ enum class AnimatedHeaderRatio(val value: Float){
|
|
|
29
33
|
|
|
30
34
|
internal val HEADER_BACKGROUND_HEIGHT = 154.dp
|
|
31
35
|
|
|
36
|
+
// RN DuoScrollGradient: extended gradient runs this far below the header
|
|
37
|
+
private val DUO_GRADIENT_EXTRA = 52.dp
|
|
38
|
+
|
|
32
39
|
@Suppress("FrequentlyChangingValue")
|
|
33
40
|
@Composable
|
|
34
41
|
internal fun HeaderBackground() {
|
|
@@ -38,7 +45,8 @@ internal fun HeaderBackground() {
|
|
|
38
45
|
val scrollState = LocalScrollState.current
|
|
39
46
|
val backgroundColor = options.backgroundColor ?: AppTheme.current.colors.background.default
|
|
40
47
|
|
|
41
|
-
val
|
|
48
|
+
val layout = getScreenLayout()
|
|
49
|
+
val minHeight = layout.headerHeight
|
|
42
50
|
val maxHeight = HEADER_BACKGROUND_HEIGHT
|
|
43
51
|
val opacity by animateFloatAsState(
|
|
44
52
|
targetValue = (1 - (scrollState.value * 1f / HEADER_HEIGHT * 1f)).coerceIn(0f, 1f),
|
|
@@ -57,20 +65,32 @@ internal fun HeaderBackground() {
|
|
|
57
65
|
else -> 0.dp
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
// iPhone Duo glass header: the gradient leaves with the content, as the quick return header does
|
|
69
|
+
val useDuoGlass = options.usesDuoGlassHeader(layout)
|
|
70
|
+
// RN HeaderExtendHeader with search on the side bar layout: the gradient fills the header box,
|
|
71
|
+
// which for extended shrinks from 52pt below the header over the first 100pt of scroll
|
|
72
|
+
val duoSearch = layout.sideBar != null && getInputSearchType(options) != InputSearchType.None
|
|
73
|
+
val fillsBox = useDuoGlass || duoSearch
|
|
74
|
+
val duoModifier = when {
|
|
75
|
+
useDuoGlass -> {
|
|
76
|
+
val extra = if (options.headerType is HeaderType.Extended) DUO_GRADIENT_EXTRA else 0.dp
|
|
77
|
+
Modifier.height(minHeight + extra).graphicsLayer { translationY = -scrollState.value.toFloat() }
|
|
78
|
+
}
|
|
79
|
+
duoSearch && options.headerType is HeaderType.Extended -> {
|
|
80
|
+
val progress = (with(density) { scrollState.value.toDp() } / 100.dp).coerceIn(0f, 1f)
|
|
81
|
+
Modifier.height(minHeight + DUO_GRADIENT_EXTRA * (1 - progress))
|
|
82
|
+
}
|
|
83
|
+
duoSearch -> Modifier.height(minHeight)
|
|
84
|
+
else -> Modifier.height(height)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Box(Modifier.fillMaxWidth().then(duoModifier).clipToBounds().background(backgroundColor)) {
|
|
88
|
+
// on Duo the fade spans the gradient box; a banner image keeps its 375:154 shape, clipped
|
|
61
89
|
Box(Modifier
|
|
62
|
-
.height(maxHeight)
|
|
63
90
|
.fillMaxWidth()
|
|
91
|
+
.then(if (fillsBox && !theme.isHeaderImage()) Modifier.fillMaxHeight() else Modifier.height(maxHeight))
|
|
64
92
|
.conditional(!theme.isHeaderImage()) {
|
|
65
|
-
background(Brush.
|
|
66
|
-
colors = listOf(
|
|
67
|
-
color,
|
|
68
|
-
backgroundColor
|
|
69
|
-
),
|
|
70
|
-
start = Offset(0f, 0f),
|
|
71
|
-
end = Offset(0f, with(density) { maxHeight.toPx() })
|
|
72
|
-
)
|
|
73
|
-
)
|
|
93
|
+
background(Brush.verticalGradient(colors = listOf(color, backgroundColor)))
|
|
74
94
|
}
|
|
75
95
|
){
|
|
76
96
|
if (!theme.isHeaderImage()) return@Box
|