@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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 +36 -8
- package/README.md +2 -2
- package/compose/build.gradle.kts +15 -1
- package/compose/build.gradle.kts.backup +14 -0
- package/compose/compose.podspec +1 -1
- 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/LiteScreen.kt +168 -52
- 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 +53 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
- 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 +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
- 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/StackScreen.kt +153 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
- 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 +84 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
- 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 +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
- 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/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
- 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 +94 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/MaxApi.swift +12 -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 +102 -75
- 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 +128 -9
- package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
- package/ios/Navigation/Component/Header.swift +531 -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 +377 -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 +5 -6
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +194 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
- package/ios/Navigation/StackScreen.swift +820 -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 -136
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +140 -0
- package/ios/Popup/PopupNotify.swift +56 -73
- 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/Text.swift +61 -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 +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- 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/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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package vn.momo.kits.layout
|
|
2
2
|
|
|
3
|
+
import androidx.compose.foundation.layout.WindowInsets
|
|
4
|
+
import androidx.compose.foundation.layout.systemBars
|
|
3
5
|
import androidx.compose.runtime.Composable
|
|
4
6
|
import androidx.compose.runtime.Immutable
|
|
5
7
|
import androidx.compose.runtime.compositionLocalOf
|
|
@@ -11,7 +13,10 @@ import androidx.compose.ui.unit.Dp
|
|
|
11
13
|
import androidx.compose.ui.unit.dp
|
|
12
14
|
import vn.momo.kits.const.Spacing
|
|
13
15
|
import vn.momo.kits.navigation.component.HEADER_HEIGHT
|
|
14
|
-
import vn.momo.kits.platform.
|
|
16
|
+
import vn.momo.kits.platform.ReservedRegion
|
|
17
|
+
import vn.momo.kits.platform.getScreenDimensions
|
|
18
|
+
import vn.momo.kits.platform.isPhoneIdiom
|
|
19
|
+
import vn.momo.kits.platform.rememberReservedRegions
|
|
15
20
|
import vn.momo.kits.utils.getAppStatusBarHeight
|
|
16
21
|
import vn.momo.kits.utils.horizontalSafeAreaInsets
|
|
17
22
|
|
|
@@ -23,13 +28,22 @@ data class SideBarLayout(
|
|
|
23
28
|
val width: Dp,
|
|
24
29
|
// Preferred window y of the first control; the column is still kept above footer and bottom bars.
|
|
25
30
|
val top: Dp,
|
|
31
|
+
// Room kept free above the window bottom, clear of a camera at the bottom end of the strip.
|
|
32
|
+
val bottom: Dp,
|
|
26
33
|
)
|
|
27
34
|
|
|
35
|
+
// Room the header row keeps from each side for the status bar clock and battery clusters.
|
|
36
|
+
@Immutable
|
|
37
|
+
data class StatusBarClearance(val left: Dp, val right: Dp)
|
|
38
|
+
|
|
28
39
|
@Immutable
|
|
29
40
|
data class ScreenLayout(
|
|
30
41
|
val width: Dp,
|
|
31
42
|
val height: Dp,
|
|
32
43
|
val isIPhoneDuo: Boolean,
|
|
44
|
+
// Raw top / bottom safe area; overlays and the Duo floating tab bar keep clear of them.
|
|
45
|
+
val insetTop: Dp,
|
|
46
|
+
val insetBottom: Dp,
|
|
33
47
|
val insetLeft: Dp,
|
|
34
48
|
val insetRight: Dp,
|
|
35
49
|
val contentWidth: Dp,
|
|
@@ -38,6 +52,13 @@ data class ScreenLayout(
|
|
|
38
52
|
val headerStatusBarHeight: Dp,
|
|
39
53
|
// iPhone Duo moves back and toolkit into a column in the side safe area; null while they stay in the bar.
|
|
40
54
|
val sideBar: SideBarLayout? = null,
|
|
55
|
+
// Port of RN `device.ts` isIPhoneDuo: the screen stays on the outer display, so Split View still matches.
|
|
56
|
+
val isIPhoneDuoDevice: Boolean = isIPhoneDuo,
|
|
57
|
+
// Portrait iPhone Duo puts the status bar in the side strip, about twice as tall as the strip is wide.
|
|
58
|
+
// Stored so it survives consumeHorizontalInsets().
|
|
59
|
+
val duoTopInset: Dp = insetTop,
|
|
60
|
+
// iPhone Duo inner display in portrait: the header row shares the status bar band, between its clusters.
|
|
61
|
+
val statusBarClearance: StatusBarClearance? = null,
|
|
41
62
|
) {
|
|
42
63
|
val headerBarHeight: Dp get() = headerHeight - headerStatusBarHeight
|
|
43
64
|
}
|
|
@@ -50,11 +71,26 @@ internal object DuoMetrics {
|
|
|
50
71
|
val itemPitch = 50.dp
|
|
51
72
|
// Portrait: below the camera/clock cluster in the strip (ends near y 151).
|
|
52
73
|
val firstItemTop = 170.dp
|
|
53
|
-
//
|
|
54
|
-
// (ends near y 60) and leaves the longest toolkit ~36pt above the footer.
|
|
74
|
+
// Inner display in landscape: below its status cluster (ends near y 98).
|
|
55
75
|
val firstItemTopLandscape = 120.dp
|
|
76
|
+
// Window height from which the inner display is in use (outer is 466pt tall in landscape, inner 669pt).
|
|
77
|
+
val innerMinHeight = 600.dp
|
|
78
|
+
// Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
|
|
79
|
+
// lines up with the camera punchout (y 24–66 at the top of the strip).
|
|
80
|
+
val outerHeaderHeight = 76.dp
|
|
81
|
+
val outerCameraCenterY = 45.dp
|
|
82
|
+
// Outer display in landscape has no status bar in the strip: controls keep this far from
|
|
83
|
+
// both ends unless a reserved region (the camera at either end) asks for more.
|
|
84
|
+
val edgeInset = 24.dp
|
|
85
|
+
// Inner display in portrait: the clock and connectivity cluster sits at the top right corner
|
|
86
|
+
// (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
|
|
87
|
+
val innerStatusBarCenterY = 47.dp
|
|
88
|
+
val innerStatusBarWidth = 120.dp
|
|
56
89
|
}
|
|
57
90
|
|
|
91
|
+
// Widest short side of any single-display iPhone.
|
|
92
|
+
private val IPHONE_MAX_SHORT_SIDE = 440.dp
|
|
93
|
+
|
|
58
94
|
private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
|
|
59
95
|
|
|
60
96
|
// A smaller top inset means the status bar is not on the top edge.
|
|
@@ -69,10 +105,21 @@ fun getScreenLayout(): ScreenLayout =
|
|
|
69
105
|
|
|
70
106
|
@Composable
|
|
71
107
|
internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
|
|
72
|
-
val
|
|
108
|
+
val isPhone = isPhoneIdiom()
|
|
73
109
|
if (parent != null) {
|
|
74
|
-
|
|
75
|
-
|
|
110
|
+
// the parent's row center reproduces its own centered geometry
|
|
111
|
+
val titleCenterY = parent.headerStatusBarHeight + parent.headerBarHeight / 2
|
|
112
|
+
val statusBarClearance = parent.statusBarClearance?.takeIf { topInset >= STATUS_BAR_MIN_INSET }
|
|
113
|
+
val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
|
|
114
|
+
statusBarRowGeometry()
|
|
115
|
+
} else {
|
|
116
|
+
headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null, titleCenterY)
|
|
117
|
+
}
|
|
118
|
+
return parent.copy(
|
|
119
|
+
headerHeight = headerHeight,
|
|
120
|
+
headerStatusBarHeight = headerStatusBarHeight,
|
|
121
|
+
statusBarClearance = statusBarClearance,
|
|
122
|
+
)
|
|
76
123
|
}
|
|
77
124
|
|
|
78
125
|
val density = LocalDensity.current
|
|
@@ -81,17 +128,28 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
|
|
|
81
128
|
val insets = horizontalSafeAreaInsets
|
|
82
129
|
val leftPx = insets.getLeft(density, layoutDirection)
|
|
83
130
|
val rightPx = insets.getRight(density, layoutDirection)
|
|
131
|
+
val bottomPx = WindowInsets.systemBars.getBottom(density)
|
|
132
|
+
val screen = getScreenDimensions()
|
|
133
|
+
val reservedRegions = rememberReservedRegions()
|
|
84
134
|
|
|
85
|
-
return remember(size, leftPx, rightPx, topInset, density) {
|
|
135
|
+
return remember(size, leftPx, rightPx, bottomPx, topInset, screen, density, reservedRegions) {
|
|
86
136
|
with(density) {
|
|
137
|
+
val width = size.width.toDp()
|
|
138
|
+
val height = size.height.toDp()
|
|
87
139
|
screenLayoutOf(
|
|
88
|
-
width =
|
|
89
|
-
height =
|
|
140
|
+
width = width,
|
|
141
|
+
height = height,
|
|
90
142
|
insetLeft = leftPx.toDp(),
|
|
91
143
|
insetRight = rightPx.toDp(),
|
|
92
144
|
topInset = topInset,
|
|
93
|
-
|
|
145
|
+
bottomInset = bottomPx.toDp(),
|
|
146
|
+
isPhone = isPhone,
|
|
94
147
|
asymmetricInsets = leftPx != rightPx,
|
|
148
|
+
isDuoDevice = isPhone && maxOf(
|
|
149
|
+
minOf(screen.width, screen.height).dp,
|
|
150
|
+
minOf(width, height),
|
|
151
|
+
) > IPHONE_MAX_SHORT_SIDE,
|
|
152
|
+
reservedRegions = reservedRegions,
|
|
95
153
|
)
|
|
96
154
|
}
|
|
97
155
|
}
|
|
@@ -101,10 +159,72 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
|
|
|
101
159
|
internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
|
|
102
160
|
copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
|
|
103
161
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
162
|
+
// titleCenterY: window y the title row centers on for the outer display in landscape.
|
|
163
|
+
private fun headerGeometry(
|
|
164
|
+
topInset: Dp,
|
|
165
|
+
isPhone: Boolean,
|
|
166
|
+
width: Dp,
|
|
167
|
+
height: Dp,
|
|
168
|
+
hasSideBar: Boolean,
|
|
169
|
+
titleCenterY: Dp,
|
|
170
|
+
): Pair<Dp, Dp> {
|
|
171
|
+
val headerHeight = topInset + HEADER_HEIGHT.dp
|
|
172
|
+
if (!isPhone) return headerHeight to topInset
|
|
173
|
+
if (hasSideBar && topInset < STATUS_BAR_MIN_INSET) {
|
|
174
|
+
if (height > width && width < DuoMetrics.innerMinHeight) {
|
|
175
|
+
// outer display: the row starts below the top so the title centers on the camera
|
|
176
|
+
return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
|
|
177
|
+
}
|
|
178
|
+
if (width > height && height < DuoMetrics.innerMinHeight) {
|
|
179
|
+
// reaches 8pt below a side bar control centered on the row
|
|
180
|
+
val centered = maxOf(IPHONE_HEADER_MIN_HEIGHT, titleCenterY + DuoMetrics.controlSize / 2 + Spacing.S)
|
|
181
|
+
return centered to maxOf(0.dp, titleCenterY * 2 - centered)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Centered on the clock digits; the cluster's region center sits above them.
|
|
188
|
+
private fun statusBarRowGeometry(): Pair<Dp, Dp> = DuoMetrics.innerStatusBarCenterY * 2 to 0.dp
|
|
189
|
+
|
|
190
|
+
private data class StripOcclusion(val top: Dp, val topCenter: Dp, val bottom: Dp)
|
|
191
|
+
|
|
192
|
+
// How far system regions reach into a side strip from its top and from its bottom end
|
|
193
|
+
// (e.g. the camera, which sits at either end depending on the rotation), and the center y
|
|
194
|
+
// of the region reaching furthest from the top.
|
|
195
|
+
private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
|
|
196
|
+
var top = 0.dp
|
|
197
|
+
var topCenter = 0.dp
|
|
198
|
+
var bottom = 0.dp
|
|
199
|
+
regions.forEach { region ->
|
|
200
|
+
if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
|
|
201
|
+
val centerY = region.y + region.height / 2
|
|
202
|
+
if (centerY < windowHeight / 2) {
|
|
203
|
+
if (region.y + region.height > top) {
|
|
204
|
+
top = region.y + region.height
|
|
205
|
+
topCenter = centerY
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
bottom = maxOf(bottom, windowHeight - region.y)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return StripOcclusion(top, topCenter, bottom)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// How far the clock and battery clusters of a status bar on the top edge reach in from each side;
|
|
215
|
+
// null without reserved regions there.
|
|
216
|
+
private fun statusBarClusters(regions: List<ReservedRegion>, windowWidth: Dp, topInset: Dp): StatusBarClearance? {
|
|
217
|
+
val midX = windowWidth / 2
|
|
218
|
+
val clusters = regions.filter {
|
|
219
|
+
it.y < topInset && it.y + it.height <= topInset && (it.x + it.width <= midX || it.x >= midX)
|
|
220
|
+
}
|
|
221
|
+
if (clusters.isEmpty()) return null
|
|
222
|
+
var left = 0.dp
|
|
223
|
+
var right = 0.dp
|
|
224
|
+
clusters.forEach {
|
|
225
|
+
if (it.x < midX) left = maxOf(left, it.x + it.width) else right = maxOf(right, windowWidth - it.x)
|
|
226
|
+
}
|
|
227
|
+
return StatusBarClearance(left, right)
|
|
108
228
|
}
|
|
109
229
|
|
|
110
230
|
// Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
|
|
@@ -120,38 +240,70 @@ private fun screenLayoutOf(
|
|
|
120
240
|
insetLeft: Dp,
|
|
121
241
|
insetRight: Dp,
|
|
122
242
|
topInset: Dp,
|
|
123
|
-
|
|
243
|
+
bottomInset: Dp,
|
|
244
|
+
isPhone: Boolean,
|
|
124
245
|
asymmetricInsets: Boolean,
|
|
246
|
+
isDuoDevice: Boolean,
|
|
247
|
+
reservedRegions: List<ReservedRegion>,
|
|
125
248
|
): ScreenLayout {
|
|
126
|
-
val
|
|
127
|
-
val
|
|
249
|
+
val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
|
|
250
|
+
val isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight
|
|
251
|
+
val statusBarClearance = if (isIPhoneDuo && isInnerPortrait && topInset >= STATUS_BAR_MIN_INSET) {
|
|
252
|
+
statusBarClusters(reservedRegions, width, topInset)
|
|
253
|
+
?: StatusBarClearance(left = 0.dp, right = DuoMetrics.innerStatusBarWidth + Spacing.S)
|
|
254
|
+
} else {
|
|
255
|
+
null
|
|
256
|
+
}
|
|
128
257
|
|
|
129
258
|
var sideBar: SideBarLayout? = null
|
|
259
|
+
var titleCenterY = 0.dp
|
|
130
260
|
val sideInset = maxOf(insetLeft, insetRight)
|
|
131
261
|
// narrower strips cannot host a control 12pt from the inner edge
|
|
132
262
|
if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
|
|
263
|
+
val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
|
|
264
|
+
val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
|
|
265
|
+
// clear of the camera and of the status bar cluster, which grows for calls and Live
|
|
266
|
+
// Activities; region frames already include the system's margin
|
|
267
|
+
val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
|
|
133
268
|
// UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
|
|
134
269
|
val top = when {
|
|
270
|
+
statusBarClearance != null -> statusBarRowGeometry().first + Spacing.M
|
|
135
271
|
topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
|
|
136
|
-
width
|
|
137
|
-
|
|
272
|
+
width <= height -> DuoMetrics.firstItemTop
|
|
273
|
+
height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
|
|
274
|
+
// outer display in landscape: no status bar in the strip
|
|
275
|
+
else -> DuoMetrics.edgeInset
|
|
138
276
|
}
|
|
277
|
+
// outer display in landscape: the title row centers on the camera when it sits at the
|
|
278
|
+
// top end, otherwise on the back button
|
|
279
|
+
titleCenterY = if (occlusion.top > top) occlusion.topCenter else top + DuoMetrics.controlSize / 2
|
|
139
280
|
sideBar = SideBarLayout(
|
|
140
|
-
edge =
|
|
281
|
+
edge = edge,
|
|
141
282
|
width = sideInset,
|
|
142
|
-
top = top,
|
|
283
|
+
top = maxOf(top, occlusion.top),
|
|
284
|
+
bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
|
|
143
285
|
)
|
|
144
286
|
}
|
|
287
|
+
val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
|
|
288
|
+
statusBarRowGeometry()
|
|
289
|
+
} else {
|
|
290
|
+
headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
|
|
291
|
+
}
|
|
145
292
|
|
|
146
293
|
return ScreenLayout(
|
|
147
294
|
width = width,
|
|
148
295
|
height = height,
|
|
149
296
|
isIPhoneDuo = isIPhoneDuo,
|
|
297
|
+
insetTop = topInset,
|
|
298
|
+
insetBottom = bottomInset,
|
|
150
299
|
insetLeft = insetLeft,
|
|
151
300
|
insetRight = insetRight,
|
|
152
301
|
contentWidth = width - insetLeft - insetRight,
|
|
153
302
|
headerHeight = headerHeight,
|
|
154
303
|
headerStatusBarHeight = headerStatusBarHeight,
|
|
155
304
|
sideBar = sideBar,
|
|
305
|
+
isIPhoneDuoDevice = isDuoDevice,
|
|
306
|
+
duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
|
|
307
|
+
statusBarClearance = statusBarClearance,
|
|
156
308
|
)
|
|
157
309
|
}
|
|
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.offset
|
|
|
20
20
|
import androidx.compose.foundation.layout.padding
|
|
21
21
|
import androidx.compose.foundation.layout.size
|
|
22
22
|
import androidx.compose.foundation.layout.width
|
|
23
|
+
import androidx.compose.foundation.layout.absolutePadding
|
|
23
24
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
24
25
|
import androidx.compose.runtime.Composable
|
|
25
26
|
import androidx.compose.runtime.DisposableEffect
|
|
@@ -51,6 +52,10 @@ import vn.momo.kits.modifier.conditional
|
|
|
51
52
|
import vn.momo.kits.modifier.noFeedbackClickable
|
|
52
53
|
import vn.momo.kits.platform.BackHandler
|
|
53
54
|
import vn.momo.kits.platform.getScreenHeight
|
|
55
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
56
|
+
|
|
57
|
+
// iPhone Duo inner display: center the sheet instead of spanning the width (RN BottomSheet).
|
|
58
|
+
private val DUO_REGULAR_MIN_WIDTH = 600.dp
|
|
54
59
|
|
|
55
60
|
@Composable
|
|
56
61
|
internal fun BottomSheet(
|
|
@@ -63,7 +68,11 @@ internal fun BottomSheet(
|
|
|
63
68
|
footerComponent: (@Composable () -> Unit)? = null,
|
|
64
69
|
onDismiss: (() -> Unit)?
|
|
65
70
|
) {
|
|
66
|
-
val
|
|
71
|
+
val layout = getScreenLayout()
|
|
72
|
+
val isDuo = layout.isIPhoneDuoDevice
|
|
73
|
+
val isDuoCentered = isDuo && layout.width >= DUO_REGULAR_MIN_WIDTH
|
|
74
|
+
// the screen is the outer display on iPhone Duo, even when unfolded
|
|
75
|
+
val screenHeightDp = if (isDuo) layout.height else getScreenHeight()
|
|
67
76
|
val screenHeightPx = with(LocalDensity.current) { screenHeightDp.toPx() }
|
|
68
77
|
val sheetOffset = remember { Animatable(screenHeightPx) }
|
|
69
78
|
|
|
@@ -148,13 +157,32 @@ internal fun BottomSheet(
|
|
|
148
157
|
}
|
|
149
158
|
) {
|
|
150
159
|
val parentHeight = with(LocalDensity.current) { constraints.maxHeight.toDp() }
|
|
160
|
+
val maxHeight = if (isDuo) {
|
|
161
|
+
parentHeight - maxOf(layout.duoTopInset, Spacing.XL)
|
|
162
|
+
} else {
|
|
163
|
+
parentHeight - 90.dp
|
|
164
|
+
}
|
|
165
|
+
// landscape side buttons span the full height; in portrait the side status bar only covers the top
|
|
166
|
+
val sideInset = isDuo && !isDuoCentered && layout.width > layout.height
|
|
151
167
|
|
|
152
168
|
Box(
|
|
153
169
|
modifier = Modifier
|
|
154
170
|
.align(Alignment.BottomCenter)
|
|
155
171
|
.offset { IntOffset(x = 0, y = sheetOffset.value.toInt()) }
|
|
156
|
-
.
|
|
157
|
-
|
|
172
|
+
.then(
|
|
173
|
+
if (isDuoCentered) {
|
|
174
|
+
// keeps the portrait width in landscape, like system sheets
|
|
175
|
+
Modifier.width(
|
|
176
|
+
minOf(
|
|
177
|
+
minOf(layout.width, layout.height) - Spacing.L * 2,
|
|
178
|
+
layout.width - maxOf(layout.insetLeft, layout.insetRight, Spacing.L) * 2,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
} else {
|
|
182
|
+
Modifier.fillMaxWidth()
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
.heightIn(max = maxHeight)
|
|
158
186
|
.background(
|
|
159
187
|
color = if (isSurface) AppTheme.current.colors.background.surface else AppTheme.current.colors.background.default,
|
|
160
188
|
shape = RoundedCornerShape(topStart = Radius.M, topEnd = Radius.M)
|
|
@@ -168,7 +196,13 @@ internal fun BottomSheet(
|
|
|
168
196
|
val bottomInset = if (useBottomInset) {
|
|
169
197
|
(AppNavigationBar.current - keyboardSizeState().value).coerceAtLeast(0.dp)
|
|
170
198
|
} else 0.dp
|
|
171
|
-
Column(
|
|
199
|
+
Column(
|
|
200
|
+
Modifier
|
|
201
|
+
.padding(bottom = bottomInset)
|
|
202
|
+
.conditional(sideInset) {
|
|
203
|
+
absolutePadding(left = layout.insetLeft, right = layout.insetRight)
|
|
204
|
+
}
|
|
205
|
+
) {
|
|
172
206
|
Box(
|
|
173
207
|
modifier = Modifier
|
|
174
208
|
.height(72.dp)
|
|
@@ -249,6 +283,11 @@ internal fun BottomSheet(
|
|
|
249
283
|
content()
|
|
250
284
|
}
|
|
251
285
|
BottomSheetFooter(footerComponent)
|
|
286
|
+
} else if (isDuo) {
|
|
287
|
+
// content shrinks so a tall sheet keeps its header
|
|
288
|
+
Box(Modifier.weight(1f, fill = false)) {
|
|
289
|
+
content()
|
|
290
|
+
}
|
|
252
291
|
} else {
|
|
253
292
|
content()
|
|
254
293
|
}
|
|
@@ -9,6 +9,7 @@ import androidx.compose.foundation.clickable
|
|
|
9
9
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
10
10
|
import androidx.compose.foundation.layout.Box
|
|
11
11
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
12
|
+
import androidx.compose.foundation.layout.padding
|
|
12
13
|
import androidx.compose.foundation.layout.wrapContentSize
|
|
13
14
|
import androidx.compose.runtime.*
|
|
14
15
|
import androidx.compose.ui.Alignment
|
|
@@ -25,6 +26,7 @@ import vn.momo.kits.modifier.conditional
|
|
|
25
26
|
import vn.momo.kits.modifier.noFeedbackClickable
|
|
26
27
|
import vn.momo.kits.navigation.tracking.ScreenTracker
|
|
27
28
|
import vn.momo.kits.platform.BackHandler
|
|
29
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
28
30
|
|
|
29
31
|
@Composable
|
|
30
32
|
internal fun ModalScreen(
|
|
@@ -100,12 +102,18 @@ internal fun ModalScreen(
|
|
|
100
102
|
closeEvent()
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
val layout = getScreenLayout()
|
|
106
|
+
|
|
103
107
|
Box(
|
|
104
108
|
modifier = Modifier
|
|
105
109
|
.fillMaxSize()
|
|
106
110
|
.background(Color.Black.copy(alpha = 0.6f * alpha.value))
|
|
107
111
|
.noFeedbackClickable {
|
|
108
112
|
if(barrierDismissible) closeEvent()
|
|
113
|
+
}
|
|
114
|
+
// center below the side status bar and above the home indicator
|
|
115
|
+
.conditional(layout.isIPhoneDuoDevice) {
|
|
116
|
+
padding(top = layout.duoTopInset, bottom = layout.insetBottom)
|
|
109
117
|
},
|
|
110
118
|
contentAlignment = Alignment.Center
|
|
111
119
|
) {
|
|
@@ -30,6 +30,9 @@ class Navigation(
|
|
|
30
30
|
headerType: HeaderType? = null,
|
|
31
31
|
scrollData: ScrollData? = null,
|
|
32
32
|
backgroundColor: Color? = null,
|
|
33
|
+
// AI-GENERATED START: custom screen background
|
|
34
|
+
backgroundComponent: (@Composable () -> Unit)? = null,
|
|
35
|
+
// AI-GENERATED END: custom screen background
|
|
33
36
|
tintColor: Color? = null,
|
|
34
37
|
footerComponent: (@Composable () -> Unit)? = null,
|
|
35
38
|
footerTransparent: Boolean? = null,
|
|
@@ -48,6 +51,9 @@ class Navigation(
|
|
|
48
51
|
headerType = headerType ?: options.headerType,
|
|
49
52
|
scrollData = scrollData ?: options.scrollData,
|
|
50
53
|
backgroundColor = backgroundColor ?: options.backgroundColor,
|
|
54
|
+
// AI-GENERATED START: custom screen background
|
|
55
|
+
backgroundComponent = backgroundComponent ?: options.backgroundComponent,
|
|
56
|
+
// AI-GENERATED END: custom screen background
|
|
51
57
|
tintColor = tintColor ?: options.tintColor,
|
|
52
58
|
footerComponent = footerComponent ?: options.footerComponent,
|
|
53
59
|
footerTransparent = footerTransparent ?: options.footerTransparent,
|
|
@@ -85,6 +91,10 @@ data class NavigationOptions(
|
|
|
85
91
|
val headerType: HeaderType = HeaderType.Default(),
|
|
86
92
|
val scrollData: ScrollData = ScrollData(),
|
|
87
93
|
val backgroundColor: Color? = null,
|
|
94
|
+
// AI-GENERATED START: custom screen background (SwiftUI `NavigationOptions.background`)
|
|
95
|
+
/** Drawn instead of `backgroundColor` over the whole screen: under the header, outside the keyboard padding. */
|
|
96
|
+
val backgroundComponent: @Composable (() -> Unit)? = null,
|
|
97
|
+
// AI-GENERATED END: custom screen background (SwiftUI `NavigationOptions.background`)
|
|
88
98
|
val tintColor: Color? = null,
|
|
89
99
|
val footerComponent: @Composable (() -> Unit)? = null,
|
|
90
100
|
val footerTransparent: Boolean = false,
|