@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-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 +32 -0
- 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/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/Divider.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 +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/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 +232 -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/NavigationContainer.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
- 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 +66 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -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 +81 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -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 +4 -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 +86 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/ComposeApi.swift +2 -1
- package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
- package/ios/Application/Screen.swift +3 -2
- 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/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 +123 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +312 -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 +97 -8
- package/ios/Navigation/BottomTab/BottomTab.swift +187 -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 +493 -0
- package/ios/Navigation/Component/HeaderBackground.swift +134 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +386 -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 +176 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
- package/ios/Navigation/HostBackGestureBridge.swift +57 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
- package/ios/Navigation/Navigation.swift +190 -0
- package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
- package/ios/Navigation/StackScreen.swift +782 -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/.claude/settings.local.json +0 -185
- 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/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/StackScreen.swift +0 -492
- package/local.properties +0 -8
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
package vn.momo.kits.layout
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.WindowInsets
|
|
4
|
+
import androidx.compose.foundation.layout.systemBars
|
|
5
|
+
import androidx.compose.runtime.Composable
|
|
6
|
+
import androidx.compose.runtime.Immutable
|
|
7
|
+
import androidx.compose.runtime.compositionLocalOf
|
|
8
|
+
import androidx.compose.runtime.remember
|
|
9
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
10
|
+
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
11
|
+
import androidx.compose.ui.platform.LocalWindowInfo
|
|
12
|
+
import androidx.compose.ui.unit.Dp
|
|
13
|
+
import androidx.compose.ui.unit.dp
|
|
14
|
+
import vn.momo.kits.const.Spacing
|
|
15
|
+
import vn.momo.kits.navigation.component.HEADER_HEIGHT
|
|
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
|
|
20
|
+
import vn.momo.kits.utils.getAppStatusBarHeight
|
|
21
|
+
import vn.momo.kits.utils.horizontalSafeAreaInsets
|
|
22
|
+
|
|
23
|
+
enum class SideBarEdge { LEFT, RIGHT }
|
|
24
|
+
|
|
25
|
+
@Immutable
|
|
26
|
+
data class SideBarLayout(
|
|
27
|
+
val edge: SideBarEdge,
|
|
28
|
+
val width: Dp,
|
|
29
|
+
// Preferred window y of the first control; the column is still kept above footer and bottom bars.
|
|
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,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@Immutable
|
|
36
|
+
data class ScreenLayout(
|
|
37
|
+
val width: Dp,
|
|
38
|
+
val height: Dp,
|
|
39
|
+
val isIPhoneDuo: Boolean,
|
|
40
|
+
// Raw top / bottom safe area; overlays and the Duo floating tab bar keep clear of them.
|
|
41
|
+
val insetTop: Dp,
|
|
42
|
+
val insetBottom: Dp,
|
|
43
|
+
val insetLeft: Dp,
|
|
44
|
+
val insetRight: Dp,
|
|
45
|
+
val contentWidth: Dp,
|
|
46
|
+
val headerHeight: Dp,
|
|
47
|
+
// 0 when the status bar is off the top edge, so the bar row fills the header and keeps the title centered.
|
|
48
|
+
val headerStatusBarHeight: Dp,
|
|
49
|
+
// iPhone Duo moves back and toolkit into a column in the side safe area; null while they stay in the bar.
|
|
50
|
+
val sideBar: SideBarLayout? = null,
|
|
51
|
+
// Port of RN `device.ts` isIPhoneDuo: the screen stays on the outer display, so Split View still matches.
|
|
52
|
+
val isIPhoneDuoDevice: Boolean = isIPhoneDuo,
|
|
53
|
+
// Portrait iPhone Duo puts the status bar in the side strip, about twice as tall as the strip is wide.
|
|
54
|
+
// Stored so it survives consumeHorizontalInsets().
|
|
55
|
+
val duoTopInset: Dp = insetTop,
|
|
56
|
+
) {
|
|
57
|
+
val headerBarHeight: Dp get() = headerHeight - headerStatusBarHeight
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Measured by momo-kits RN (Layout/utils.ts) on the iOS 27.1 iPhone Duo simulator; keep both in sync.
|
|
61
|
+
internal object DuoMetrics {
|
|
62
|
+
// Inner display is ~1.42, outer ~1.45; every slab iPhone is 1.77 or taller.
|
|
63
|
+
const val MAX_ASPECT_RATIO = 1.6f
|
|
64
|
+
val controlSize = 48.dp
|
|
65
|
+
val itemPitch = 50.dp
|
|
66
|
+
// Portrait: below the camera/clock cluster in the strip (ends near y 151).
|
|
67
|
+
val firstItemTop = 170.dp
|
|
68
|
+
// Inner display in landscape: below its status cluster (ends near y 98).
|
|
69
|
+
val firstItemTopLandscape = 120.dp
|
|
70
|
+
// Window height from which the inner display is in use (outer is 466pt tall in landscape, inner 669pt).
|
|
71
|
+
val innerMinHeight = 600.dp
|
|
72
|
+
// Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
|
|
73
|
+
// lines up with the camera punchout (y 24–66 at the top of the strip).
|
|
74
|
+
val outerHeaderHeight = 76.dp
|
|
75
|
+
val outerCameraCenterY = 45.dp
|
|
76
|
+
// Outer display in landscape has no status bar in the strip: controls keep this far from
|
|
77
|
+
// both ends unless a reserved region (the camera at either end) asks for more.
|
|
78
|
+
val edgeInset = 24.dp
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Widest short side of any single-display iPhone.
|
|
82
|
+
private val IPHONE_MAX_SHORT_SIDE = 440.dp
|
|
83
|
+
|
|
84
|
+
private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
|
|
85
|
+
|
|
86
|
+
// A smaller top inset means the status bar is not on the top edge.
|
|
87
|
+
private val STATUS_BAR_MIN_INSET = 20.dp
|
|
88
|
+
|
|
89
|
+
internal val LocalScreenLayout = compositionLocalOf<ScreenLayout?> { null }
|
|
90
|
+
|
|
91
|
+
// Port of RN `useScreenLayout`; follows fold/unfold and rotation through the window size and safe area.
|
|
92
|
+
@Composable
|
|
93
|
+
fun getScreenLayout(): ScreenLayout =
|
|
94
|
+
LocalScreenLayout.current ?: calculateScreenLayout(getAppStatusBarHeight())
|
|
95
|
+
|
|
96
|
+
@Composable
|
|
97
|
+
internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
|
|
98
|
+
val isPhone = isPhoneIdiom()
|
|
99
|
+
if (parent != null) {
|
|
100
|
+
val (headerHeight, headerStatusBarHeight) =
|
|
101
|
+
headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null)
|
|
102
|
+
return parent.copy(headerHeight = headerHeight, headerStatusBarHeight = headerStatusBarHeight)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
val density = LocalDensity.current
|
|
106
|
+
val layoutDirection = LocalLayoutDirection.current
|
|
107
|
+
val size = LocalWindowInfo.current.containerSize
|
|
108
|
+
val insets = horizontalSafeAreaInsets
|
|
109
|
+
val leftPx = insets.getLeft(density, layoutDirection)
|
|
110
|
+
val rightPx = insets.getRight(density, layoutDirection)
|
|
111
|
+
val bottomPx = WindowInsets.systemBars.getBottom(density)
|
|
112
|
+
val screen = getScreenDimensions()
|
|
113
|
+
val reservedRegions = rememberReservedRegions()
|
|
114
|
+
|
|
115
|
+
return remember(size, leftPx, rightPx, bottomPx, topInset, screen, density, reservedRegions) {
|
|
116
|
+
with(density) {
|
|
117
|
+
val width = size.width.toDp()
|
|
118
|
+
val height = size.height.toDp()
|
|
119
|
+
screenLayoutOf(
|
|
120
|
+
width = width,
|
|
121
|
+
height = height,
|
|
122
|
+
insetLeft = leftPx.toDp(),
|
|
123
|
+
insetRight = rightPx.toDp(),
|
|
124
|
+
topInset = topInset,
|
|
125
|
+
bottomInset = bottomPx.toDp(),
|
|
126
|
+
isPhone = isPhone,
|
|
127
|
+
asymmetricInsets = leftPx != rightPx,
|
|
128
|
+
isDuoDevice = isPhone && maxOf(
|
|
129
|
+
minOf(screen.width, screen.height).dp,
|
|
130
|
+
minOf(width, height),
|
|
131
|
+
) > IPHONE_MAX_SHORT_SIDE,
|
|
132
|
+
reservedRegions = reservedRegions,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Content inside the safe-area padding: the insets are spent and the side bar belongs to the screen.
|
|
139
|
+
internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
|
|
140
|
+
copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
|
|
141
|
+
|
|
142
|
+
private fun headerGeometry(topInset: Dp, isPhone: Boolean, width: Dp, height: Dp, hasSideBar: Boolean): Pair<Dp, Dp> {
|
|
143
|
+
val headerHeight = topInset + HEADER_HEIGHT.dp
|
|
144
|
+
if (!isPhone) return headerHeight to topInset
|
|
145
|
+
if (hasSideBar && topInset < STATUS_BAR_MIN_INSET && height > width && width < DuoMetrics.innerMinHeight) {
|
|
146
|
+
// outer display: the row starts below the top so the title centers on the camera
|
|
147
|
+
return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
|
|
148
|
+
}
|
|
149
|
+
return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private data class StripOcclusion(val top: Dp, val bottom: Dp)
|
|
153
|
+
|
|
154
|
+
// How far system regions reach into a side strip from its top and from its bottom end
|
|
155
|
+
// (e.g. the camera, which sits at either end depending on the rotation).
|
|
156
|
+
private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
|
|
157
|
+
var top = 0.dp
|
|
158
|
+
var bottom = 0.dp
|
|
159
|
+
regions.forEach { region ->
|
|
160
|
+
if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
|
|
161
|
+
if (region.y + region.height / 2 < windowHeight / 2) {
|
|
162
|
+
top = maxOf(top, region.y + region.height)
|
|
163
|
+
} else {
|
|
164
|
+
bottom = maxOf(bottom, windowHeight - region.y)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return StripOcclusion(top, bottom)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
|
|
171
|
+
private fun isIPhoneDuoWindow(width: Dp, height: Dp, asymmetricInsets: Boolean): Boolean {
|
|
172
|
+
val shortSide = minOf(width, height)
|
|
173
|
+
if (shortSide <= 0.dp) return asymmetricInsets
|
|
174
|
+
return maxOf(width, height) / shortSide < DuoMetrics.MAX_ASPECT_RATIO || asymmetricInsets
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun screenLayoutOf(
|
|
178
|
+
width: Dp,
|
|
179
|
+
height: Dp,
|
|
180
|
+
insetLeft: Dp,
|
|
181
|
+
insetRight: Dp,
|
|
182
|
+
topInset: Dp,
|
|
183
|
+
bottomInset: Dp,
|
|
184
|
+
isPhone: Boolean,
|
|
185
|
+
asymmetricInsets: Boolean,
|
|
186
|
+
isDuoDevice: Boolean,
|
|
187
|
+
reservedRegions: List<ReservedRegion>,
|
|
188
|
+
): ScreenLayout {
|
|
189
|
+
val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
|
|
190
|
+
|
|
191
|
+
var sideBar: SideBarLayout? = null
|
|
192
|
+
val sideInset = maxOf(insetLeft, insetRight)
|
|
193
|
+
// narrower strips cannot host a control 12pt from the inner edge
|
|
194
|
+
if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
|
|
195
|
+
// UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
|
|
196
|
+
val top = when {
|
|
197
|
+
topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
|
|
198
|
+
width <= height -> DuoMetrics.firstItemTop
|
|
199
|
+
height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
|
|
200
|
+
// outer display in landscape: no status bar in the strip
|
|
201
|
+
else -> DuoMetrics.edgeInset
|
|
202
|
+
}
|
|
203
|
+
val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
|
|
204
|
+
val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
|
|
205
|
+
// clear of the camera and of the status bar cluster, which grows for calls and Live
|
|
206
|
+
// Activities; region frames already include the system's margin
|
|
207
|
+
val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
|
|
208
|
+
sideBar = SideBarLayout(
|
|
209
|
+
edge = edge,
|
|
210
|
+
width = sideInset,
|
|
211
|
+
top = maxOf(top, occlusion.top),
|
|
212
|
+
bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
val (headerHeight, headerStatusBarHeight) = headerGeometry(topInset, isPhone, width, height, sideBar != null)
|
|
216
|
+
|
|
217
|
+
return ScreenLayout(
|
|
218
|
+
width = width,
|
|
219
|
+
height = height,
|
|
220
|
+
isIPhoneDuo = isIPhoneDuo,
|
|
221
|
+
insetTop = topInset,
|
|
222
|
+
insetBottom = bottomInset,
|
|
223
|
+
insetLeft = insetLeft,
|
|
224
|
+
insetRight = insetRight,
|
|
225
|
+
contentWidth = width - insetLeft - insetRight,
|
|
226
|
+
headerHeight = headerHeight,
|
|
227
|
+
headerStatusBarHeight = headerStatusBarHeight,
|
|
228
|
+
sideBar = sideBar,
|
|
229
|
+
isIPhoneDuoDevice = isDuoDevice,
|
|
230
|
+
duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
|
|
231
|
+
)
|
|
232
|
+
}
|
|
@@ -3,6 +3,7 @@ package vn.momo.kits.layout
|
|
|
3
3
|
import androidx.compose.foundation.layout.Arrangement
|
|
4
4
|
import androidx.compose.foundation.layout.Box
|
|
5
5
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
6
|
+
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
6
7
|
import androidx.compose.foundation.layout.FlowRow
|
|
7
8
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
8
9
|
import androidx.compose.foundation.layout.padding
|
|
@@ -11,27 +12,28 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|
|
11
12
|
import androidx.compose.ui.Modifier
|
|
12
13
|
import androidx.compose.ui.unit.dp
|
|
13
14
|
import vn.momo.kits.components.Image
|
|
15
|
+
import vn.momo.kits.const.Spacing
|
|
14
16
|
|
|
15
17
|
@Composable
|
|
16
|
-
|
|
18
|
+
@OptIn(ExperimentalLayoutApi::class)
|
|
19
|
+
fun Section(
|
|
17
20
|
useMargin: Boolean = true,
|
|
18
21
|
backgroundImage: String? = null,
|
|
19
22
|
modifier: Modifier = Modifier,
|
|
20
23
|
content: @Composable () -> Unit,
|
|
21
24
|
) {
|
|
22
|
-
val columns =
|
|
23
|
-
val
|
|
24
|
-
val margin = if (useMargin)
|
|
25
|
-
Box(modifier =
|
|
25
|
+
val columns = gridColumns()
|
|
26
|
+
val gap = Spacing.M
|
|
27
|
+
val margin = if (useMargin) Spacing.M else 0.dp
|
|
28
|
+
Box(modifier = Modifier.fillMaxWidth().padding(horizontal = margin).then(modifier)) {
|
|
26
29
|
if (backgroundImage != null) {
|
|
27
30
|
Image(source = backgroundImage, modifier = Modifier.matchParentSize())
|
|
28
31
|
}
|
|
29
32
|
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
|
30
|
-
|
|
31
|
-
CompositionLocalProvider(LocalGridContext provides GridContext(columns, gutter, sizePerSpan)) {
|
|
33
|
+
CompositionLocalProvider(LocalGridContext provides gridContext(constraints.maxWidth, columns, gap)) {
|
|
32
34
|
FlowRow(
|
|
33
|
-
horizontalArrangement = Arrangement.spacedBy(
|
|
34
|
-
verticalArrangement = Arrangement.spacedBy(
|
|
35
|
+
horizontalArrangement = Arrangement.spacedBy(gap),
|
|
36
|
+
verticalArrangement = Arrangement.spacedBy(gap),
|
|
35
37
|
) { content() }
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -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
|
) {
|
|
@@ -31,6 +31,8 @@ import vn.momo.kits.const.AppStatusBar
|
|
|
31
31
|
import vn.momo.kits.const.AppTheme
|
|
32
32
|
import vn.momo.kits.const.Theme
|
|
33
33
|
import vn.momo.kits.const.defaultTheme
|
|
34
|
+
import vn.momo.kits.layout.LocalScreenLayout
|
|
35
|
+
import vn.momo.kits.layout.calculateScreenLayout
|
|
34
36
|
import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
|
|
35
37
|
import vn.momo.kits.platform.ProvideOsFontScale
|
|
36
38
|
import vn.momo.kits.utils.getAppStatusBarHeight
|
|
@@ -56,6 +58,7 @@ fun NavigationContainer(
|
|
|
56
58
|
val navigator = remember { Navigator(navController = navController, maxApi = maxApi, registry = registry) }
|
|
57
59
|
val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
|
|
58
60
|
val navigationBarHeight = getNavigationBarHeight()
|
|
61
|
+
val screenLayout = calculateScreenLayout(statusBarHeight, parent = LocalScreenLayout.current)
|
|
59
62
|
|
|
60
63
|
val parentContext = LocalContext.current
|
|
61
64
|
val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
|
|
@@ -101,6 +104,7 @@ fun NavigationContainer(
|
|
|
101
104
|
AppTheme provides resolvedTheme,
|
|
102
105
|
AppStatusBar provides statusBarHeight,
|
|
103
106
|
AppNavigationBar provides navigationBarHeight,
|
|
107
|
+
LocalScreenLayout provides screenLayout,
|
|
104
108
|
LocalContext provides mergedContext,
|
|
105
109
|
LocalDesignSystemConfig provides resolvedDesignSystemConfig,
|
|
106
110
|
LocalLocalize provides resolvedLocalize,
|