@momo-kits/native-kits 0.300.3 → 0.300.4-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +51 -7
- package/README.md +2 -2
- package/build.gradle.kts +13 -0
- package/compose/build.gradle.kts +197 -0
- package/compose/build.gradle.kts.backup +197 -0
- package/compose/compose.podspec +47 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +20 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +110 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +1071 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +65 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +81 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +357 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +115 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +478 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +265 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +235 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +265 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +308 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +330 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +92 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +560 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +501 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +236 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +180 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +17 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +194 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +287 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +116 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +215 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +404 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +732 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +40 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +233 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +217 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +455 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +103 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +413 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +39 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +133 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +135 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +62 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +42 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +36 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
- package/gradle/libs.versions.toml +68 -0
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/gradle.properties +26 -0
- package/gradlew +252 -0
- package/gradlew.bat +94 -0
- package/ios/Application/ApplicationEnvironment.swift +24 -6
- package/ios/Application/DesignSystemKits.swift +100 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +8 -2
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +171 -148
- package/ios/Input/InputPhoneNumber.swift +10 -9
- package/ios/Input/InputSearch.swift +107 -78
- package/ios/Input/InputTextArea.swift +133 -148
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +32 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +415 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +138 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +7 -3
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +17 -14
- package/ios/Typography/Text.swift +156 -20
- package/ios/Typography/Typography.swift +2 -2
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -3
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
- package/ios-demo/README.md +120 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/publish.sh +19 -2
- package/scripts/gen-ios-icons.mjs +36 -0
- package/settings.gradle.kts +64 -0
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- package/ios/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
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.isPhoneIdiom
|
|
18
|
+
import vn.momo.kits.platform.rememberReservedRegions
|
|
19
|
+
import vn.momo.kits.utils.getAppStatusBarHeight
|
|
20
|
+
import vn.momo.kits.utils.horizontalSafeAreaInsets
|
|
21
|
+
|
|
22
|
+
enum class SideBarEdge { LEFT, RIGHT }
|
|
23
|
+
|
|
24
|
+
@Immutable
|
|
25
|
+
data class SideBarLayout(
|
|
26
|
+
val edge: SideBarEdge,
|
|
27
|
+
val width: Dp,
|
|
28
|
+
// Preferred window y of the first control; the column is still kept above footer and bottom bars.
|
|
29
|
+
val top: Dp,
|
|
30
|
+
// Room kept free above the window bottom, clear of a camera at the bottom end of the strip.
|
|
31
|
+
val bottom: Dp,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// Room the header row keeps from each side for the status bar clock and battery clusters.
|
|
35
|
+
@Immutable
|
|
36
|
+
data class StatusBarClearance(val left: Dp, val right: Dp)
|
|
37
|
+
|
|
38
|
+
@Immutable
|
|
39
|
+
data class ScreenLayout(
|
|
40
|
+
val width: Dp,
|
|
41
|
+
val height: Dp,
|
|
42
|
+
val isIPhoneDuo: Boolean,
|
|
43
|
+
// Raw top / bottom safe area; overlays and the Duo floating tab bar keep clear of them.
|
|
44
|
+
val insetTop: Dp,
|
|
45
|
+
val insetBottom: Dp,
|
|
46
|
+
val insetLeft: Dp,
|
|
47
|
+
val insetRight: Dp,
|
|
48
|
+
val contentWidth: Dp,
|
|
49
|
+
val headerHeight: Dp,
|
|
50
|
+
// 0 when the status bar is off the top edge, so the bar row fills the header and keeps the title centered.
|
|
51
|
+
val headerStatusBarHeight: Dp,
|
|
52
|
+
// iPhone Duo moves back and toolkit into a column in the side safe area; null while they stay in the bar.
|
|
53
|
+
val sideBar: SideBarLayout? = null,
|
|
54
|
+
// The device has a hinge, folded or not, whatever the window size; drives popups, modal and bottom sheet.
|
|
55
|
+
val isIPhoneDuoDevice: Boolean = isIPhoneDuo,
|
|
56
|
+
// Portrait iPhone Duo puts the status bar in the side strip, about twice as tall as the strip is wide.
|
|
57
|
+
// Stored so it survives consumeHorizontalInsets().
|
|
58
|
+
val duoTopInset: Dp = insetTop,
|
|
59
|
+
// iPhone Duo inner display in portrait: the header row shares the status bar band, between its clusters.
|
|
60
|
+
val statusBarClearance: StatusBarClearance? = null,
|
|
61
|
+
) {
|
|
62
|
+
val headerBarHeight: Dp get() = headerHeight - headerStatusBarHeight
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Measured by momo-kits RN (Layout/utils.ts) on the iOS 27.1 iPhone Duo simulator; keep both in sync.
|
|
66
|
+
internal object DuoMetrics {
|
|
67
|
+
// Inner display is ~1.42, outer ~1.45; every slab iPhone is 1.77 or taller.
|
|
68
|
+
const val MAX_ASPECT_RATIO = 1.6f
|
|
69
|
+
val controlSize = 48.dp
|
|
70
|
+
val itemPitch = 50.dp
|
|
71
|
+
// Portrait: below the camera/clock cluster in the strip (ends near y 151).
|
|
72
|
+
val firstItemTop = 170.dp
|
|
73
|
+
// Inner display in landscape: below its status cluster (ends near y 98).
|
|
74
|
+
val firstItemTopLandscape = 120.dp
|
|
75
|
+
// Window height from which the inner display is in use (outer is 466pt tall in landscape, inner 669pt).
|
|
76
|
+
val innerMinHeight = 600.dp
|
|
77
|
+
// Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
|
|
78
|
+
// lines up with the camera punchout (y 24–66 at the top of the strip).
|
|
79
|
+
val outerHeaderHeight = 76.dp
|
|
80
|
+
val outerCameraCenterY = 45.dp
|
|
81
|
+
// Outer display in landscape has no status bar in the strip: controls keep this far from
|
|
82
|
+
// both ends unless a reserved region (the camera at either end) asks for more.
|
|
83
|
+
val edgeInset = 24.dp
|
|
84
|
+
// Inner display in portrait: the clock and connectivity cluster sits at the top right corner
|
|
85
|
+
// (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
|
|
86
|
+
val innerStatusBarCenterY = 47.dp
|
|
87
|
+
val innerStatusBarWidth = 120.dp
|
|
88
|
+
// Host app's iOS 26 tab bar on the inner display in portrait: a 62pt glass pill sized to its tabs,
|
|
89
|
+
// QR as a 62pt platter beside it, ≥ 20pt from the sides, 13pt into the home indicator inset.
|
|
90
|
+
val floatingTabBarHeight = 62.dp
|
|
91
|
+
val floatingTabBarMargin = 20.dp
|
|
92
|
+
val floatingTabBarInsetOverlap = 13.dp
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
|
|
96
|
+
|
|
97
|
+
// A smaller top inset means the status bar is not on the top edge.
|
|
98
|
+
private val STATUS_BAR_MIN_INSET = 20.dp
|
|
99
|
+
|
|
100
|
+
internal val LocalScreenLayout = compositionLocalOf<ScreenLayout?> { null }
|
|
101
|
+
|
|
102
|
+
// Port of RN `useScreenLayout`; follows fold/unfold and rotation through the window size and safe area.
|
|
103
|
+
@Composable
|
|
104
|
+
fun getScreenLayout(): ScreenLayout =
|
|
105
|
+
LocalScreenLayout.current ?: calculateScreenLayout(getAppStatusBarHeight())
|
|
106
|
+
|
|
107
|
+
@Composable
|
|
108
|
+
internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
|
|
109
|
+
val isPhone = isPhoneIdiom()
|
|
110
|
+
if (parent != null) {
|
|
111
|
+
// the parent's row center reproduces its own centered geometry
|
|
112
|
+
val titleCenterY = parent.headerStatusBarHeight + parent.headerBarHeight / 2
|
|
113
|
+
val statusBarClearance = parent.statusBarClearance?.takeIf { topInset >= STATUS_BAR_MIN_INSET }
|
|
114
|
+
val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
|
|
115
|
+
statusBarRowGeometry()
|
|
116
|
+
} else {
|
|
117
|
+
headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null, titleCenterY)
|
|
118
|
+
}
|
|
119
|
+
return parent.copy(
|
|
120
|
+
headerHeight = headerHeight,
|
|
121
|
+
headerStatusBarHeight = headerStatusBarHeight,
|
|
122
|
+
statusBarClearance = statusBarClearance,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
val density = LocalDensity.current
|
|
127
|
+
val layoutDirection = LocalLayoutDirection.current
|
|
128
|
+
val size = LocalWindowInfo.current.containerSize
|
|
129
|
+
val insets = horizontalSafeAreaInsets
|
|
130
|
+
val leftPx = insets.getLeft(density, layoutDirection)
|
|
131
|
+
val rightPx = insets.getRight(density, layoutDirection)
|
|
132
|
+
val bottomPx = WindowInsets.systemBars.getBottom(density)
|
|
133
|
+
val reservedRegions = rememberReservedRegions()
|
|
134
|
+
|
|
135
|
+
return remember(size, leftPx, rightPx, bottomPx, topInset, density, reservedRegions) {
|
|
136
|
+
with(density) {
|
|
137
|
+
val width = size.width.toDp()
|
|
138
|
+
val height = size.height.toDp()
|
|
139
|
+
screenLayoutOf(
|
|
140
|
+
width = width,
|
|
141
|
+
height = height,
|
|
142
|
+
insetLeft = leftPx.toDp(),
|
|
143
|
+
insetRight = rightPx.toDp(),
|
|
144
|
+
topInset = topInset,
|
|
145
|
+
bottomInset = bottomPx.toDp(),
|
|
146
|
+
isPhone = isPhone,
|
|
147
|
+
asymmetricInsets = leftPx != rightPx,
|
|
148
|
+
isDuoDevice = isPhone && reservedRegions.isFoldable,
|
|
149
|
+
reservedRegions = reservedRegions.occlusion,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Content inside the safe-area padding: the insets are spent and the side bar belongs to the screen.
|
|
156
|
+
internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
|
|
157
|
+
copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
|
|
158
|
+
|
|
159
|
+
// titleCenterY: window y the title row centers on for the outer display in landscape.
|
|
160
|
+
private fun headerGeometry(
|
|
161
|
+
topInset: Dp,
|
|
162
|
+
isPhone: Boolean,
|
|
163
|
+
width: Dp,
|
|
164
|
+
height: Dp,
|
|
165
|
+
hasSideBar: Boolean,
|
|
166
|
+
titleCenterY: Dp,
|
|
167
|
+
): Pair<Dp, Dp> {
|
|
168
|
+
val headerHeight = topInset + HEADER_HEIGHT.dp
|
|
169
|
+
if (!isPhone) return headerHeight to topInset
|
|
170
|
+
if (hasSideBar && topInset < STATUS_BAR_MIN_INSET) {
|
|
171
|
+
if (height > width && width < DuoMetrics.innerMinHeight) {
|
|
172
|
+
// outer display: the row starts below the top so the title centers on the camera
|
|
173
|
+
return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
|
|
174
|
+
}
|
|
175
|
+
if (width > height && height < DuoMetrics.innerMinHeight) {
|
|
176
|
+
// reaches 8pt below a side bar control centered on the row
|
|
177
|
+
val centered = maxOf(IPHONE_HEADER_MIN_HEIGHT, titleCenterY + DuoMetrics.controlSize / 2 + Spacing.S)
|
|
178
|
+
return centered to maxOf(0.dp, titleCenterY * 2 - centered)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Centered on the clock digits; the cluster's region center sits above them.
|
|
185
|
+
private fun statusBarRowGeometry(): Pair<Dp, Dp> = DuoMetrics.innerStatusBarCenterY * 2 to 0.dp
|
|
186
|
+
|
|
187
|
+
private data class StripOcclusion(val top: Dp, val topCenter: Dp, val bottom: Dp)
|
|
188
|
+
|
|
189
|
+
// How far system regions reach into a side strip from its top and from its bottom end
|
|
190
|
+
// (e.g. the camera, which sits at either end depending on the rotation), and the center y
|
|
191
|
+
// of the region reaching furthest from the top.
|
|
192
|
+
private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
|
|
193
|
+
var top = 0.dp
|
|
194
|
+
var topCenter = 0.dp
|
|
195
|
+
var bottom = 0.dp
|
|
196
|
+
regions.forEach { region ->
|
|
197
|
+
if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
|
|
198
|
+
val centerY = region.y + region.height / 2
|
|
199
|
+
if (centerY < windowHeight / 2) {
|
|
200
|
+
if (region.y + region.height > top) {
|
|
201
|
+
top = region.y + region.height
|
|
202
|
+
topCenter = centerY
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
bottom = maxOf(bottom, windowHeight - region.y)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return StripOcclusion(top, topCenter, bottom)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// How far the clock and battery clusters of a status bar on the top edge reach in from each side;
|
|
212
|
+
// null without reserved regions there.
|
|
213
|
+
private fun statusBarClusters(regions: List<ReservedRegion>, windowWidth: Dp, topInset: Dp): StatusBarClearance? {
|
|
214
|
+
val midX = windowWidth / 2
|
|
215
|
+
val clusters = regions.filter {
|
|
216
|
+
it.y < topInset && it.y + it.height <= topInset && (it.x + it.width <= midX || it.x >= midX)
|
|
217
|
+
}
|
|
218
|
+
if (clusters.isEmpty()) return null
|
|
219
|
+
var left = 0.dp
|
|
220
|
+
var right = 0.dp
|
|
221
|
+
clusters.forEach {
|
|
222
|
+
if (it.x < midX) left = maxOf(left, it.x + it.width) else right = maxOf(right, windowWidth - it.x)
|
|
223
|
+
}
|
|
224
|
+
return StatusBarClearance(left, right)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
|
|
228
|
+
private fun isIPhoneDuoWindow(width: Dp, height: Dp, asymmetricInsets: Boolean): Boolean {
|
|
229
|
+
val shortSide = minOf(width, height)
|
|
230
|
+
if (shortSide <= 0.dp) return asymmetricInsets
|
|
231
|
+
return maxOf(width, height) / shortSide < DuoMetrics.MAX_ASPECT_RATIO || asymmetricInsets
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private fun screenLayoutOf(
|
|
235
|
+
width: Dp,
|
|
236
|
+
height: Dp,
|
|
237
|
+
insetLeft: Dp,
|
|
238
|
+
insetRight: Dp,
|
|
239
|
+
topInset: Dp,
|
|
240
|
+
bottomInset: Dp,
|
|
241
|
+
isPhone: Boolean,
|
|
242
|
+
asymmetricInsets: Boolean,
|
|
243
|
+
isDuoDevice: Boolean,
|
|
244
|
+
reservedRegions: List<ReservedRegion>,
|
|
245
|
+
): ScreenLayout {
|
|
246
|
+
val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
|
|
247
|
+
val isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight
|
|
248
|
+
val statusBarClearance = if (isIPhoneDuo && isInnerPortrait && topInset >= STATUS_BAR_MIN_INSET) {
|
|
249
|
+
statusBarClusters(reservedRegions, width, topInset)
|
|
250
|
+
?: StatusBarClearance(left = 0.dp, right = DuoMetrics.innerStatusBarWidth + Spacing.S)
|
|
251
|
+
} else {
|
|
252
|
+
null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
var sideBar: SideBarLayout? = null
|
|
256
|
+
var titleCenterY = 0.dp
|
|
257
|
+
val sideInset = maxOf(insetLeft, insetRight)
|
|
258
|
+
// narrower strips cannot host a control 12pt from the inner edge
|
|
259
|
+
if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
|
|
260
|
+
val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
|
|
261
|
+
val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
|
|
262
|
+
// clear of the camera and of the status bar cluster, which grows for calls and Live
|
|
263
|
+
// Activities; region frames already include the system's margin
|
|
264
|
+
val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
|
|
265
|
+
// UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
|
|
266
|
+
val top = when {
|
|
267
|
+
statusBarClearance != null -> statusBarRowGeometry().first + Spacing.M
|
|
268
|
+
topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
|
|
269
|
+
width <= height -> DuoMetrics.firstItemTop
|
|
270
|
+
height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
|
|
271
|
+
// outer display in landscape: no status bar in the strip
|
|
272
|
+
else -> DuoMetrics.edgeInset
|
|
273
|
+
}
|
|
274
|
+
// outer display in landscape: the title row centers on the camera when it sits at the
|
|
275
|
+
// top end, otherwise on the back button
|
|
276
|
+
titleCenterY = if (occlusion.top > top) occlusion.topCenter else top + DuoMetrics.controlSize / 2
|
|
277
|
+
sideBar = SideBarLayout(
|
|
278
|
+
edge = edge,
|
|
279
|
+
width = sideInset,
|
|
280
|
+
top = maxOf(top, occlusion.top),
|
|
281
|
+
bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
|
|
285
|
+
statusBarRowGeometry()
|
|
286
|
+
} else {
|
|
287
|
+
headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return ScreenLayout(
|
|
291
|
+
width = width,
|
|
292
|
+
height = height,
|
|
293
|
+
isIPhoneDuo = isIPhoneDuo,
|
|
294
|
+
insetTop = topInset,
|
|
295
|
+
insetBottom = bottomInset,
|
|
296
|
+
insetLeft = insetLeft,
|
|
297
|
+
insetRight = insetRight,
|
|
298
|
+
contentWidth = width - insetLeft - insetRight,
|
|
299
|
+
headerHeight = headerHeight,
|
|
300
|
+
headerStatusBarHeight = headerStatusBarHeight,
|
|
301
|
+
sideBar = sideBar,
|
|
302
|
+
isIPhoneDuoDevice = isDuoDevice,
|
|
303
|
+
duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
|
|
304
|
+
statusBarClearance = statusBarClearance,
|
|
305
|
+
)
|
|
306
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package vn.momo.kits.layout
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
4
|
+
import androidx.compose.foundation.layout.Box
|
|
5
|
+
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
6
|
+
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
7
|
+
import androidx.compose.foundation.layout.FlowRow
|
|
8
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
9
|
+
import androidx.compose.foundation.layout.padding
|
|
10
|
+
import androidx.compose.runtime.Composable
|
|
11
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
12
|
+
import androidx.compose.ui.Modifier
|
|
13
|
+
import androidx.compose.ui.unit.dp
|
|
14
|
+
import vn.momo.kits.components.Image
|
|
15
|
+
import vn.momo.kits.const.Spacing
|
|
16
|
+
|
|
17
|
+
@Composable
|
|
18
|
+
@OptIn(ExperimentalLayoutApi::class)
|
|
19
|
+
fun Section(
|
|
20
|
+
useMargin: Boolean = true,
|
|
21
|
+
backgroundImage: String? = null,
|
|
22
|
+
modifier: Modifier = Modifier,
|
|
23
|
+
content: @Composable () -> Unit,
|
|
24
|
+
) {
|
|
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)) {
|
|
29
|
+
if (backgroundImage != null) {
|
|
30
|
+
Image(source = backgroundImage, modifier = Modifier.matchParentSize())
|
|
31
|
+
}
|
|
32
|
+
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
|
33
|
+
CompositionLocalProvider(LocalGridContext provides gridContext(constraints.maxWidth, columns, gap)) {
|
|
34
|
+
FlowRow(
|
|
35
|
+
horizontalArrangement = Arrangement.spacedBy(gap),
|
|
36
|
+
verticalArrangement = Arrangement.spacedBy(gap),
|
|
37
|
+
) { content() }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package vn.momo.kits.modifier
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.Modifier
|
|
4
|
+
import androidx.compose.ui.node.ModifierNodeElement
|
|
5
|
+
import androidx.compose.ui.node.SemanticsModifierNode
|
|
6
|
+
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
|
|
7
|
+
import androidx.compose.ui.semantics.contentDescription
|
|
8
|
+
import androidx.compose.ui.semantics.semantics
|
|
9
|
+
import androidx.compose.ui.semantics.testTag
|
|
10
|
+
import vn.momo.kits.platform.getPlatformName
|
|
11
|
+
|
|
12
|
+
fun Modifier.setAutomationId(accessibilityId: String, mergeDescendants: Boolean = false): Modifier {
|
|
13
|
+
return if (getPlatformName() == "Android") {
|
|
14
|
+
semantics {
|
|
15
|
+
contentDescription = accessibilityId
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
semantics(mergeDescendants = mergeDescendants) {
|
|
19
|
+
testTag = accessibilityId
|
|
20
|
+
}
|
|
21
|
+
}}
|
|
22
|
+
|
|
23
|
+
fun Modifier.kitsAutomationId(accessibilityId: String, label: String? = null) =
|
|
24
|
+
this then KitsAutomationIdElement(accessibilityId, label)
|
|
25
|
+
|
|
26
|
+
private data class KitsAutomationIdElement(
|
|
27
|
+
private val accessibilityId: String,
|
|
28
|
+
private val label: String? = null,
|
|
29
|
+
) : ModifierNodeElement<KitsAutomationIdNode>() {
|
|
30
|
+
override fun create(): KitsAutomationIdNode {
|
|
31
|
+
return KitsAutomationIdNode(accessibilityId, label)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override fun update(node: KitsAutomationIdNode) {
|
|
35
|
+
node.accessibilityId = accessibilityId
|
|
36
|
+
node.label = label
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private class KitsAutomationIdNode(
|
|
41
|
+
var accessibilityId: String,
|
|
42
|
+
var label: String?,
|
|
43
|
+
) : Modifier.Node(), SemanticsModifierNode {
|
|
44
|
+
override fun SemanticsPropertyReceiver.applySemantics() {
|
|
45
|
+
if (getPlatformName() == "Android") {
|
|
46
|
+
contentDescription = accessibilityId
|
|
47
|
+
} else {
|
|
48
|
+
testTag = accessibilityId
|
|
49
|
+
contentDescription = label ?: accessibilityId
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package vn.momo.kits.modifier
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.animateFloatAsState
|
|
4
|
+
import androidx.compose.foundation.clickable
|
|
5
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
6
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
7
|
+
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
8
|
+
import androidx.compose.runtime.getValue
|
|
9
|
+
import androidx.compose.runtime.remember
|
|
10
|
+
import androidx.compose.ui.Modifier
|
|
11
|
+
import androidx.compose.ui.composed
|
|
12
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
13
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
14
|
+
import androidx.compose.ui.platform.LocalFocusManager
|
|
15
|
+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
16
|
+
import androidx.compose.ui.semantics.Role
|
|
17
|
+
|
|
18
|
+
fun Modifier.noFeedbackClickable(
|
|
19
|
+
enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onClick: () -> Unit
|
|
20
|
+
): Modifier = composed {
|
|
21
|
+
clickable(
|
|
22
|
+
interactionSource = remember { MutableInteractionSource() },
|
|
23
|
+
indication = null,
|
|
24
|
+
enabled,
|
|
25
|
+
onClickLabel,
|
|
26
|
+
role,
|
|
27
|
+
onClick
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun Modifier.activeOpacityClickable(
|
|
32
|
+
activeOpacity: Float = 0.2f,
|
|
33
|
+
enabled: Boolean = true,
|
|
34
|
+
onClickLabel: String? = null,
|
|
35
|
+
role: Role? = null,
|
|
36
|
+
onClick: () -> Unit
|
|
37
|
+
): Modifier = composed {
|
|
38
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
39
|
+
val isPressed by interactionSource.collectIsPressedAsState()
|
|
40
|
+
val animatedAlpha by animateFloatAsState(
|
|
41
|
+
targetValue = if (isPressed) activeOpacity else 1f,
|
|
42
|
+
label = "ActiveOpacity"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
this
|
|
46
|
+
.graphicsLayer { alpha = animatedAlpha }
|
|
47
|
+
.clickable(
|
|
48
|
+
interactionSource = interactionSource,
|
|
49
|
+
indication = null,
|
|
50
|
+
enabled,
|
|
51
|
+
onClickLabel,
|
|
52
|
+
role,
|
|
53
|
+
onClick
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
fun Modifier.hideKeyboardOnTap() = composed {
|
|
57
|
+
val focusManager = LocalFocusManager.current
|
|
58
|
+
val keyboardManager = LocalSoftwareKeyboardController.current
|
|
59
|
+
|
|
60
|
+
pointerInput(Unit) {
|
|
61
|
+
detectTapGestures {
|
|
62
|
+
keyboardManager?.hide()
|
|
63
|
+
focusManager.clearFocus()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package vn.momo.kits.modifier
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.LinearEasing
|
|
4
|
+
import androidx.compose.animation.core.RepeatMode
|
|
5
|
+
import androidx.compose.animation.core.animateFloat
|
|
6
|
+
import androidx.compose.animation.core.infiniteRepeatable
|
|
7
|
+
import androidx.compose.animation.core.rememberInfiniteTransition
|
|
8
|
+
import androidx.compose.animation.core.tween
|
|
9
|
+
import androidx.compose.runtime.Composable
|
|
10
|
+
import androidx.compose.runtime.getValue
|
|
11
|
+
import androidx.compose.ui.Modifier
|
|
12
|
+
import androidx.compose.ui.draw.drawWithContent
|
|
13
|
+
import androidx.compose.ui.geometry.Offset
|
|
14
|
+
import androidx.compose.ui.graphics.Color
|
|
15
|
+
import androidx.compose.ui.graphics.drawscope.clipRect
|
|
16
|
+
import androidx.compose.ui.text.TextStyle
|
|
17
|
+
import androidx.compose.ui.text.drawText
|
|
18
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
19
|
+
import androidx.compose.ui.text.rememberTextMeasurer
|
|
20
|
+
import androidx.compose.ui.unit.sp
|
|
21
|
+
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
22
|
+
import vn.momo.kits.const.Colors
|
|
23
|
+
|
|
24
|
+
@Suppress("ExperimentalAnnotationRetention")
|
|
25
|
+
@RequiresOptIn(
|
|
26
|
+
message = "This is internal API of native-kits, become internal in next sprint",
|
|
27
|
+
level = RequiresOptIn.Level.WARNING,
|
|
28
|
+
)
|
|
29
|
+
@Retention(AnnotationRetention.BINARY)
|
|
30
|
+
@Target(
|
|
31
|
+
AnnotationTarget.CLASS,
|
|
32
|
+
AnnotationTarget.FUNCTION,
|
|
33
|
+
AnnotationTarget.PROPERTY,
|
|
34
|
+
)
|
|
35
|
+
annotation class InternalApi
|
|
36
|
+
|
|
37
|
+
@Composable
|
|
38
|
+
fun DeprecatedModifier(text: String = "Deprecated component"): Modifier = debugMarker(
|
|
39
|
+
label = text,
|
|
40
|
+
background = Colors.red_04.copy(alpha = 0.7f),
|
|
41
|
+
textColor = Colors.red_01,
|
|
42
|
+
animationLabel = "deprecatedMarquee",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@Composable
|
|
46
|
+
private fun debugMarker(
|
|
47
|
+
label: String,
|
|
48
|
+
background: Color,
|
|
49
|
+
textColor: Color,
|
|
50
|
+
animationLabel: String,
|
|
51
|
+
): Modifier {
|
|
52
|
+
if (!IsShowBaseLineDebug) return Modifier
|
|
53
|
+
|
|
54
|
+
val textMeasurer = rememberTextMeasurer()
|
|
55
|
+
val marquee = rememberInfiniteTransition(label = animationLabel)
|
|
56
|
+
val progress by marquee.animateFloat(
|
|
57
|
+
initialValue = 0f,
|
|
58
|
+
targetValue = 1f,
|
|
59
|
+
animationSpec = infiniteRepeatable(
|
|
60
|
+
animation = tween(durationMillis = 4000, easing = LinearEasing),
|
|
61
|
+
repeatMode = RepeatMode.Restart,
|
|
62
|
+
),
|
|
63
|
+
label = "${animationLabel}Progress",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return Modifier.drawWithContent {
|
|
67
|
+
drawContent()
|
|
68
|
+
drawRect(color = background)
|
|
69
|
+
|
|
70
|
+
if (size.width <= 0f || size.height <= 0f) return@drawWithContent
|
|
71
|
+
|
|
72
|
+
val fontPx = (size.width * 0.1f).coerceIn(12.sp.toPx(), 20.sp.toPx())
|
|
73
|
+
val style = TextStyle(
|
|
74
|
+
color = textColor,
|
|
75
|
+
fontSize = fontPx.toSp(),
|
|
76
|
+
fontWeight = FontWeight.Bold,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
val intrinsic = textMeasurer.measure(
|
|
80
|
+
text = label,
|
|
81
|
+
style = style,
|
|
82
|
+
maxLines = 1,
|
|
83
|
+
softWrap = false,
|
|
84
|
+
)
|
|
85
|
+
val textW = intrinsic.size.width
|
|
86
|
+
val textH = intrinsic.size.height
|
|
87
|
+
val y = (size.height - textH) / 2f
|
|
88
|
+
|
|
89
|
+
if (textW <= size.width) {
|
|
90
|
+
drawText(
|
|
91
|
+
textLayoutResult = intrinsic,
|
|
92
|
+
topLeft = Offset((size.width - textW) / 2f, y),
|
|
93
|
+
)
|
|
94
|
+
} else {
|
|
95
|
+
clipRect(right = size.width, bottom = size.height) {
|
|
96
|
+
val gap = textW * 0.5f
|
|
97
|
+
val travel = textW + gap
|
|
98
|
+
val x = -progress * travel
|
|
99
|
+
drawText(textLayoutResult = intrinsic, topLeft = Offset(x, y))
|
|
100
|
+
drawText(textLayoutResult = intrinsic, topLeft = Offset(x + travel, y))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package vn.momo.kits.modifier
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.Modifier
|
|
4
|
+
import androidx.compose.ui.draw.drawBehind
|
|
5
|
+
import androidx.compose.ui.graphics.Color
|
|
6
|
+
import androidx.compose.ui.graphics.Paint
|
|
7
|
+
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
|
8
|
+
import androidx.compose.ui.unit.Dp
|
|
9
|
+
import androidx.compose.ui.unit.dp
|
|
10
|
+
import vn.momo.kits.platform.setColor
|
|
11
|
+
import vn.momo.kits.platform.setMaskFilter
|
|
12
|
+
fun Modifier.shadow(
|
|
13
|
+
color: Color = Color.Black,
|
|
14
|
+
borderRadius: Dp = 0.dp,
|
|
15
|
+
blurRadius: Float = 0f,
|
|
16
|
+
offsetY: Dp = 0.dp,
|
|
17
|
+
offsetX: Dp = 0.dp,
|
|
18
|
+
spread: Dp = 0f.dp,
|
|
19
|
+
modifier: Modifier = Modifier
|
|
20
|
+
) = this.then(
|
|
21
|
+
modifier.drawBehind {
|
|
22
|
+
this.drawIntoCanvas {
|
|
23
|
+
val paint = Paint()
|
|
24
|
+
val frameworkPaint = paint.asFrameworkPaint()
|
|
25
|
+
val spreadPixel = spread.toPx()
|
|
26
|
+
val leftPixel = (0f - spreadPixel) + offsetX.toPx()
|
|
27
|
+
val topPixel = (0f - spreadPixel) + offsetY.toPx()
|
|
28
|
+
val rightPixel = (this.size.width + spreadPixel)
|
|
29
|
+
val bottomPixel = (0f + spreadPixel) - offsetY.toPx()
|
|
30
|
+
|
|
31
|
+
if (blurRadius != 0f) {
|
|
32
|
+
frameworkPaint.setMaskFilter(blurRadius)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
frameworkPaint.setColor(color)
|
|
36
|
+
|
|
37
|
+
it.drawRoundRect(
|
|
38
|
+
left = leftPixel,
|
|
39
|
+
top = topPixel,
|
|
40
|
+
right = rightPixel,
|
|
41
|
+
bottom = bottomPixel,
|
|
42
|
+
radiusX = borderRadius.toPx(),
|
|
43
|
+
radiusY = borderRadius.toPx(),
|
|
44
|
+
paint
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package vn.momo.kits.modifier
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.State
|
|
5
|
+
import androidx.compose.ui.Modifier
|
|
6
|
+
import androidx.compose.ui.layout.Measurable
|
|
7
|
+
import androidx.compose.ui.layout.MeasureResult
|
|
8
|
+
import androidx.compose.ui.layout.MeasureScope
|
|
9
|
+
import androidx.compose.ui.node.LayoutModifierNode
|
|
10
|
+
import androidx.compose.ui.node.ModifierNodeElement
|
|
11
|
+
import androidx.compose.ui.unit.Constraints
|
|
12
|
+
import androidx.compose.ui.unit.IntOffset
|
|
13
|
+
|
|
14
|
+
@Composable
|
|
15
|
+
fun Modifier.dynamicSize(
|
|
16
|
+
widthState: State<Int>? = null,
|
|
17
|
+
heightState: State<Int>? = null,
|
|
18
|
+
) = this then DynamicSizeElement(widthState, heightState)
|
|
19
|
+
|
|
20
|
+
private data class DynamicSizeElement(
|
|
21
|
+
val widthState: State<Int>?,
|
|
22
|
+
val heightState: State<Int>?,
|
|
23
|
+
) : ModifierNodeElement<DynamicSizeNode>() {
|
|
24
|
+
override fun create(): DynamicSizeNode {
|
|
25
|
+
return DynamicSizeNode(widthState, heightState)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun update(node: DynamicSizeNode) {
|
|
29
|
+
node.widthState = widthState
|
|
30
|
+
node.heightState = heightState
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private class DynamicSizeNode(
|
|
36
|
+
var widthState: State<Int>?,
|
|
37
|
+
var heightState: State<Int>?,
|
|
38
|
+
) : Modifier.Node(), LayoutModifierNode {
|
|
39
|
+
override fun MeasureScope.measure(
|
|
40
|
+
measurable: Measurable,
|
|
41
|
+
constraints: Constraints
|
|
42
|
+
): MeasureResult {
|
|
43
|
+
val p = measurable.measure(constraints)
|
|
44
|
+
return layout(
|
|
45
|
+
width = widthState?.value ?: p.width,
|
|
46
|
+
height = heightState?.value ?: p.height,
|
|
47
|
+
) {
|
|
48
|
+
p.place(IntOffset.Zero)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|