@momo-kits/native-kits 0.161.2-beta.12 → 0.161.2-beta.13-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/build.gradle.kts +11 -0
- package/compose/build.gradle.kts +187 -0
- package/compose/build.gradle.kts.backup +187 -0
- package/compose/compose.podspec +47 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +123 -0
- package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
- package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +57 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +109 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +201 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +222 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +305 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +927 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +121 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +403 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -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 +85 -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 +340 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +198 -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 +123 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +94 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +136 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +224 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +148 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +116 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +452 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +172 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +255 -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 +233 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +241 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +364 -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 +92 -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 +352 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +103 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +70 -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 +17 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +96 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +348 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +256 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +494 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +131 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +215 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +96 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +531 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +92 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +130 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +214 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +590 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +177 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +205 -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 +239 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +191 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -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 +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +285 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +57 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +68 -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 +14 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +50 -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 +253 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +133 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +104 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +145 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +345 -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 +556 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +161 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +243 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +187 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +315 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +80 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +370 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +131 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +167 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +66 -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 +70 -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 +105 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +33 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +186 -0
- package/gradle/libs.versions.toml +64 -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/local.properties +8 -0
- package/package.json +1 -1
- package/settings.gradle.kts +64 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
package vn.momo.kits.application
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.FloatRange
|
|
4
|
+
import androidx.compose.animation.animateContentSize
|
|
5
|
+
import androidx.compose.foundation.ScrollState
|
|
6
|
+
import androidx.compose.foundation.background
|
|
7
|
+
import androidx.compose.foundation.border
|
|
8
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
9
|
+
import androidx.compose.foundation.interaction.FocusInteraction
|
|
10
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
11
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
12
|
+
import androidx.compose.foundation.layout.Box
|
|
13
|
+
import androidx.compose.foundation.layout.Row
|
|
14
|
+
import androidx.compose.foundation.layout.WindowInsets
|
|
15
|
+
import androidx.compose.foundation.layout.asPaddingValues
|
|
16
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
17
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
18
|
+
import androidx.compose.foundation.layout.ime
|
|
19
|
+
import androidx.compose.foundation.layout.navigationBars
|
|
20
|
+
import androidx.compose.foundation.layout.offset
|
|
21
|
+
import androidx.compose.foundation.layout.padding
|
|
22
|
+
import androidx.compose.foundation.layout.size
|
|
23
|
+
import androidx.compose.foundation.rememberScrollState
|
|
24
|
+
import androidx.compose.foundation.shape.CircleShape
|
|
25
|
+
import androidx.compose.foundation.text.BasicTextField
|
|
26
|
+
import androidx.compose.foundation.text.KeyboardActions
|
|
27
|
+
import androidx.compose.foundation.text.KeyboardOptions
|
|
28
|
+
import androidx.compose.foundation.verticalScroll
|
|
29
|
+
import androidx.compose.runtime.Composable
|
|
30
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
31
|
+
import androidx.compose.runtime.MutableState
|
|
32
|
+
import androidx.compose.runtime.Stable
|
|
33
|
+
import androidx.compose.runtime.State
|
|
34
|
+
import androidx.compose.runtime.derivedStateOf
|
|
35
|
+
import androidx.compose.runtime.getValue
|
|
36
|
+
import androidx.compose.runtime.mutableStateOf
|
|
37
|
+
import androidx.compose.runtime.produceState
|
|
38
|
+
import androidx.compose.runtime.remember
|
|
39
|
+
import androidx.compose.runtime.rememberUpdatedState
|
|
40
|
+
import androidx.compose.runtime.snapshotFlow
|
|
41
|
+
import androidx.compose.ui.Alignment
|
|
42
|
+
import androidx.compose.ui.Modifier
|
|
43
|
+
import androidx.compose.ui.composed
|
|
44
|
+
import androidx.compose.ui.draw.clip
|
|
45
|
+
import androidx.compose.ui.draw.drawBehind
|
|
46
|
+
import androidx.compose.ui.draw.drawWithContent
|
|
47
|
+
import androidx.compose.ui.geometry.Offset
|
|
48
|
+
import androidx.compose.ui.geometry.Rect
|
|
49
|
+
import androidx.compose.ui.graphics.Brush
|
|
50
|
+
import androidx.compose.ui.graphics.Color
|
|
51
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
52
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
53
|
+
import androidx.compose.ui.layout.Layout
|
|
54
|
+
import androidx.compose.ui.layout.Measurable
|
|
55
|
+
import androidx.compose.ui.layout.MeasurePolicy
|
|
56
|
+
import androidx.compose.ui.layout.MeasureResult
|
|
57
|
+
import androidx.compose.ui.layout.MeasureScope
|
|
58
|
+
import androidx.compose.ui.layout.Placeable
|
|
59
|
+
import androidx.compose.ui.layout.layoutId
|
|
60
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
61
|
+
import androidx.compose.ui.platform.LocalFocusManager
|
|
62
|
+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
63
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
64
|
+
import androidx.compose.ui.text.TextStyle
|
|
65
|
+
import androidx.compose.ui.unit.Constraints
|
|
66
|
+
import androidx.compose.ui.unit.Dp
|
|
67
|
+
import androidx.compose.ui.unit.IntOffset
|
|
68
|
+
import androidx.compose.ui.unit.LayoutDirection
|
|
69
|
+
import androidx.compose.ui.unit.dp
|
|
70
|
+
import androidx.compose.ui.unit.sp
|
|
71
|
+
import androidx.compose.runtime.staticCompositionLocalOf
|
|
72
|
+
import androidx.compose.ui.graphics.Color.Companion
|
|
73
|
+
import androidx.compose.ui.graphics.SolidColor
|
|
74
|
+
import androidx.compose.ui.unit.lerp
|
|
75
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
76
|
+
import kotlinx.coroutines.flow.collectLatest
|
|
77
|
+
import kotlinx.coroutines.flow.mapNotNull
|
|
78
|
+
import vn.momo.kits.components.Icon
|
|
79
|
+
import vn.momo.kits.components.Text
|
|
80
|
+
import vn.momo.kits.const.AppTheme
|
|
81
|
+
import vn.momo.kits.const.Colors
|
|
82
|
+
import vn.momo.kits.const.Spacing
|
|
83
|
+
import vn.momo.kits.const.Typography
|
|
84
|
+
import vn.momo.kits.modifier.conditional
|
|
85
|
+
import vn.momo.kits.modifier.kitsAutomationId
|
|
86
|
+
import vn.momo.kits.modifier.noFeedbackClickable
|
|
87
|
+
import vn.momo.kits.modifier.setAutomationId
|
|
88
|
+
import vn.momo.kits.modifier.shadow
|
|
89
|
+
import vn.momo.kits.utils.getAppStatusBarHeight
|
|
90
|
+
import kotlin.math.max
|
|
91
|
+
import kotlin.math.roundToInt
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
data class AnimationOption(
|
|
95
|
+
val targetBounds: Rect,
|
|
96
|
+
val progress: State<Float>,
|
|
97
|
+
@param:FloatRange(from = 0.0, to = 1.0)
|
|
98
|
+
val opacityLimitFraction: Float = 0f,
|
|
99
|
+
) {
|
|
100
|
+
val opacityCap: Float
|
|
101
|
+
get() = 1f - opacityLimitFraction
|
|
102
|
+
|
|
103
|
+
val animateOpacity: Float
|
|
104
|
+
get() {
|
|
105
|
+
if (progress.value < opacityLimitFraction) return 0f
|
|
106
|
+
return androidx.compose.ui.util.lerp(
|
|
107
|
+
0f,
|
|
108
|
+
1f,
|
|
109
|
+
(progress.value - opacityLimitFraction) / (opacityCap),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
val LocalAnimationOption = staticCompositionLocalOf<AnimationOption?> { null }
|
|
115
|
+
|
|
116
|
+
@Composable
|
|
117
|
+
fun LiteScreen(
|
|
118
|
+
scrollable: Boolean = true,
|
|
119
|
+
scrollState: ScrollState? = null,
|
|
120
|
+
headerType: HeaderType = HeaderType.DEFAULT,
|
|
121
|
+
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
|
122
|
+
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
|
123
|
+
backgroundColor: Color = AppTheme.current.colors.background.default,
|
|
124
|
+
/* Begin of header props */
|
|
125
|
+
title: String? = null,
|
|
126
|
+
inputSearchProps: LiteInputSearchProps? = null,
|
|
127
|
+
goBack: (() -> Unit)? = null,
|
|
128
|
+
headerRight: @Composable() (() -> Unit)? = null,
|
|
129
|
+
useAnimationSearch: Boolean = true,
|
|
130
|
+
titlePosition: TitlePosition = TitlePosition.LEFT,
|
|
131
|
+
headerRightData: HeaderRightData? = null,
|
|
132
|
+
headerTintColor: Color? = null,
|
|
133
|
+
headerBackgroundColor: Color? = null,
|
|
134
|
+
headerSpaceBetween: Dp? = null,
|
|
135
|
+
/* End of header props */
|
|
136
|
+
|
|
137
|
+
screenContent: @Composable () -> Unit,
|
|
138
|
+
) {
|
|
139
|
+
val content by rememberUpdatedState(screenContent)
|
|
140
|
+
|
|
141
|
+
val finalScrollState = scrollState ?: rememberScrollState()
|
|
142
|
+
|
|
143
|
+
val contentModifier = remember(
|
|
144
|
+
key1 = scrollable,
|
|
145
|
+
key2 = finalScrollState,
|
|
146
|
+
) {
|
|
147
|
+
var res: Modifier = Modifier
|
|
148
|
+
.background(color = backgroundColor)
|
|
149
|
+
if (scrollable) {
|
|
150
|
+
res = res.verticalScroll(finalScrollState)
|
|
151
|
+
}
|
|
152
|
+
res
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
LiteScreenLayout(
|
|
156
|
+
scrollState = finalScrollState,
|
|
157
|
+
modifier = Modifier
|
|
158
|
+
.fillMaxSize()
|
|
159
|
+
.hideKeyboardOnTap(),
|
|
160
|
+
contentModifier = contentModifier,
|
|
161
|
+
verticalArrangement = verticalArrangement,
|
|
162
|
+
horizontalAlignment = horizontalAlignment,
|
|
163
|
+
title = title,
|
|
164
|
+
headerRight = headerRight,
|
|
165
|
+
headerType = headerType,
|
|
166
|
+
onGoBack = goBack,
|
|
167
|
+
inputSearchProps = inputSearchProps,
|
|
168
|
+
titlePosition = titlePosition,
|
|
169
|
+
useAnimationSearch = useAnimationSearch,
|
|
170
|
+
headerRightData = headerRightData,
|
|
171
|
+
tintColor = headerTintColor,
|
|
172
|
+
headerBackgroundColor = headerBackgroundColor,
|
|
173
|
+
headerSpaceBetween = headerSpaceBetween,
|
|
174
|
+
) {
|
|
175
|
+
content()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private object HeaderId {
|
|
180
|
+
private const val PACKAGE_NAME = "vn.momo.compose.kits"
|
|
181
|
+
const val CONTENT_ID = "${PACKAGE_NAME}.content"
|
|
182
|
+
const val BACKGROUND_ID = "${PACKAGE_NAME}.background"
|
|
183
|
+
const val BACK_ID = "${PACKAGE_NAME}.back"
|
|
184
|
+
const val HEADER_RIGHT_ID = "${PACKAGE_NAME}.headerRight"
|
|
185
|
+
const val INPUT_SEARCH_ID = "${PACKAGE_NAME}.inputSearch"
|
|
186
|
+
const val TITLE_ID = "${PACKAGE_NAME}.title"
|
|
187
|
+
|
|
188
|
+
val EXTENDED_HEADER_HEIGHT = 154.dp
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@Composable
|
|
192
|
+
private fun LiteScreenLayout(
|
|
193
|
+
scrollState: ScrollState?,
|
|
194
|
+
modifier: Modifier = Modifier,
|
|
195
|
+
contentModifier: Modifier = Modifier,
|
|
196
|
+
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
|
197
|
+
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
|
198
|
+
title: String? = null,
|
|
199
|
+
tintColor: Color? = null,
|
|
200
|
+
headerRightData: HeaderRightData? = null,
|
|
201
|
+
headerType: HeaderType = HeaderType.DEFAULT,
|
|
202
|
+
titlePosition: TitlePosition = TitlePosition.LEFT,
|
|
203
|
+
useAnimationSearch: Boolean = true,
|
|
204
|
+
onGoBack: (() -> Unit)? = null,
|
|
205
|
+
headerBackgroundColor: Color? = null,
|
|
206
|
+
headerSpaceBetween: Dp? = null,
|
|
207
|
+
inputSearchProps: LiteInputSearchProps? = null,
|
|
208
|
+
headerRight: @Composable (() -> Unit)? = null,
|
|
209
|
+
content: @Composable () -> Unit,
|
|
210
|
+
) {
|
|
211
|
+
val statusBarHeight = getAppStatusBarHeight()
|
|
212
|
+
val isHeaderNone = remember(headerType) {
|
|
213
|
+
headerType == HeaderType.NONE
|
|
214
|
+
}
|
|
215
|
+
val theme = AppTheme.current
|
|
216
|
+
val density = LocalDensity.current
|
|
217
|
+
|
|
218
|
+
val isHeaderExtend = remember(headerType) {
|
|
219
|
+
headerType == HeaderType.EXTENDED
|
|
220
|
+
}
|
|
221
|
+
val backgroundHeight = remember(isHeaderNone, isHeaderExtend, statusBarHeight) {
|
|
222
|
+
when {
|
|
223
|
+
isHeaderNone -> statusBarHeight
|
|
224
|
+
!isHeaderExtend -> statusBarHeight + HEADER_HEIGHT.dp
|
|
225
|
+
else -> HeaderId.EXTENDED_HEADER_HEIGHT
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
val listGradientColors = remember {
|
|
229
|
+
listOf(
|
|
230
|
+
Color(0xFFFDCADE),
|
|
231
|
+
Color(0x00FDCADE),
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
val headerColor = remember(tintColor, theme) {
|
|
236
|
+
if (tintColor == Colors.black_01) HeaderColor(
|
|
237
|
+
tintIconColor = tintColor,
|
|
238
|
+
backgroundButton = Colors.black_20.copy(alpha = 0.6f),
|
|
239
|
+
borderColor = Colors.black_01.copy(alpha = 0.2f)
|
|
240
|
+
)
|
|
241
|
+
else HeaderColor(
|
|
242
|
+
tintIconColor = tintColor ?: theme.colors.text.default,
|
|
243
|
+
backgroundButton = Colors.black_01.copy(alpha = 0.6f),
|
|
244
|
+
borderColor = Colors.black_20.copy(alpha = 0.2f)
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
val scrollPercentage = produceState(
|
|
249
|
+
initialValue = 0f,
|
|
250
|
+
key1 = useAnimationSearch,
|
|
251
|
+
key2 = scrollState,
|
|
252
|
+
key3 = backgroundHeight,
|
|
253
|
+
) {
|
|
254
|
+
if (!useAnimationSearch) return@produceState
|
|
255
|
+
scrollState ?: return@produceState
|
|
256
|
+
val rangePx = with(density) { backgroundHeight.toPx() }
|
|
257
|
+
snapshotFlow { scrollState.value }.collectLatest {
|
|
258
|
+
value = (it / rangePx).coerceIn(0f, 1f)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
val titleStyle = remember {
|
|
263
|
+
Typography.actionSBold.copy(
|
|
264
|
+
fontSize = 15.sp,
|
|
265
|
+
lineHeight = 22.sp,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
val animationOption = LocalAnimationOption.current
|
|
270
|
+
val animationOpacityModifier = Modifier.graphicsLayer {
|
|
271
|
+
alpha = animationOption?.animateOpacity ?: 1f
|
|
272
|
+
}
|
|
273
|
+
val titleModifier = Modifier
|
|
274
|
+
.kitsAutomationId("title_navigation_header")
|
|
275
|
+
.layoutId(HeaderId.TITLE_ID)
|
|
276
|
+
.graphicsLayer {
|
|
277
|
+
val titleAlpha = if (isHeaderExtend && inputSearchProps != null && useAnimationSearch)
|
|
278
|
+
(1 - scrollPercentage.value * 2).coerceIn(0f, 1f)
|
|
279
|
+
else 1f
|
|
280
|
+
alpha = titleAlpha * (animationOption?.progress?.value ?: 1f)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
val policy = remember(
|
|
284
|
+
useAnimationSearch,
|
|
285
|
+
isHeaderNone,
|
|
286
|
+
isHeaderExtend,
|
|
287
|
+
statusBarHeight,
|
|
288
|
+
titlePosition,
|
|
289
|
+
scrollPercentage,
|
|
290
|
+
headerSpaceBetween,
|
|
291
|
+
animationOption,
|
|
292
|
+
verticalArrangement,
|
|
293
|
+
horizontalAlignment,
|
|
294
|
+
) {
|
|
295
|
+
LiteScreenLayoutPolicy(
|
|
296
|
+
useAnimationSearch = useAnimationSearch,
|
|
297
|
+
isHeaderNone = isHeaderNone,
|
|
298
|
+
isHeaderExtend = isHeaderExtend,
|
|
299
|
+
statusBarHeight = statusBarHeight,
|
|
300
|
+
titlePosition = titlePosition,
|
|
301
|
+
scrollPercentage = scrollPercentage,
|
|
302
|
+
headerSpaceBetween = headerSpaceBetween,
|
|
303
|
+
animationOption = animationOption,
|
|
304
|
+
verticalArrangement = verticalArrangement,
|
|
305
|
+
horizontalAlignment = horizontalAlignment,
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
Layout(
|
|
310
|
+
modifier = modifier
|
|
311
|
+
.animateContentSize(),
|
|
312
|
+
content = {
|
|
313
|
+
Box(
|
|
314
|
+
modifier = animationOpacityModifier
|
|
315
|
+
.layoutId(HeaderId.CONTENT_ID)
|
|
316
|
+
.then(contentModifier),
|
|
317
|
+
contentAlignment = Alignment.TopCenter,
|
|
318
|
+
) {
|
|
319
|
+
content()
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!isHeaderNone) {
|
|
323
|
+
Box(
|
|
324
|
+
modifier = Modifier
|
|
325
|
+
.layoutId(HeaderId.BACKGROUND_ID)
|
|
326
|
+
.then(animationOpacityModifier)
|
|
327
|
+
.drawBehind {
|
|
328
|
+
val headerHeight = max(
|
|
329
|
+
HeaderId.EXTENDED_HEADER_HEIGHT.toPx(),
|
|
330
|
+
size.height,
|
|
331
|
+
)
|
|
332
|
+
headerBackgroundColor?.let {
|
|
333
|
+
drawRect(color = it)
|
|
334
|
+
} ?: run {
|
|
335
|
+
drawRect(color = Colors.black_01)
|
|
336
|
+
drawRect(
|
|
337
|
+
brush = Brush.linearGradient(
|
|
338
|
+
colors = listGradientColors,
|
|
339
|
+
start = Offset.Zero,
|
|
340
|
+
end = Offset(
|
|
341
|
+
x = 0f,
|
|
342
|
+
y = headerHeight * (1 - scrollPercentage.value),
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!isHeaderNone && onGoBack != null) {
|
|
352
|
+
val coreModifier = Modifier
|
|
353
|
+
.layoutId(HeaderId.BACK_ID)
|
|
354
|
+
.then(animationOpacityModifier)
|
|
355
|
+
.clip(CircleShape)
|
|
356
|
+
.noFeedbackClickable(onClick = onGoBack)
|
|
357
|
+
.setAutomationId("btn_navigation_back")
|
|
358
|
+
inputSearchProps?.customBackIcon?.invoke(coreModifier) ?: Box(
|
|
359
|
+
modifier = coreModifier
|
|
360
|
+
.size(28.dp)
|
|
361
|
+
.border(
|
|
362
|
+
width = 0.2.dp,
|
|
363
|
+
color = headerColor.borderColor,
|
|
364
|
+
shape = CircleShape,
|
|
365
|
+
)
|
|
366
|
+
.background(color = headerColor.backgroundButton)
|
|
367
|
+
.padding(Spacing.XS),
|
|
368
|
+
) {
|
|
369
|
+
Icon(
|
|
370
|
+
source = "arrow-back",
|
|
371
|
+
color = headerColor.tintIconColor,
|
|
372
|
+
size = 20.dp,
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!isHeaderNone) {
|
|
378
|
+
Box(
|
|
379
|
+
modifier = Modifier
|
|
380
|
+
.layoutId(HeaderId.HEADER_RIGHT_ID)
|
|
381
|
+
.then(animationOpacityModifier)
|
|
382
|
+
) {
|
|
383
|
+
if (headerRight != null) {
|
|
384
|
+
headerRight()
|
|
385
|
+
} else {
|
|
386
|
+
HeaderRight(
|
|
387
|
+
headerRight = headerRightData,
|
|
388
|
+
tintColor = tintColor,
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!isHeaderNone && inputSearchProps != null) {
|
|
395
|
+
LiteInputSearch(
|
|
396
|
+
modifier = Modifier.layoutId(HeaderId.INPUT_SEARCH_ID),
|
|
397
|
+
inputSearchProps = inputSearchProps,
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
if (!isHeaderNone && title != null && (inputSearchProps == null || isHeaderExtend)) {
|
|
401
|
+
Text(
|
|
402
|
+
text = title,
|
|
403
|
+
color = headerColor.tintIconColor,
|
|
404
|
+
style = titleStyle,
|
|
405
|
+
modifier = titleModifier,
|
|
406
|
+
maxLines = 1,
|
|
407
|
+
overflow = TextOverflow.Ellipsis
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
measurePolicy = policy,
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private class LiteScreenLayoutPolicy(
|
|
416
|
+
private val useAnimationSearch: Boolean,
|
|
417
|
+
private val isHeaderNone: Boolean,
|
|
418
|
+
private val isHeaderExtend: Boolean,
|
|
419
|
+
private val statusBarHeight: Dp,
|
|
420
|
+
private val scrollPercentage: State<Float>,
|
|
421
|
+
private val titlePosition: TitlePosition,
|
|
422
|
+
private val headerSpaceBetween: Dp? = null,
|
|
423
|
+
private val animationOption: AnimationOption?,
|
|
424
|
+
private val verticalArrangement: Arrangement.Vertical,
|
|
425
|
+
private val horizontalAlignment: Alignment.Horizontal,
|
|
426
|
+
) : MeasurePolicy {
|
|
427
|
+
|
|
428
|
+
val searchStartPosition: IntOffset by lazy {
|
|
429
|
+
if (animationOption == null) return@lazy IntOffset.Zero
|
|
430
|
+
val offset = animationOption.targetBounds.topLeft
|
|
431
|
+
IntOffset(
|
|
432
|
+
x = offset.x.roundToInt(),
|
|
433
|
+
y = offset.y.roundToInt(),
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
override fun MeasureScope.measure(
|
|
438
|
+
measurables: List<Measurable>,
|
|
439
|
+
constraints: Constraints
|
|
440
|
+
): MeasureResult {
|
|
441
|
+
val spacing12 = Spacing.M.roundToPx()
|
|
442
|
+
val spaceBetween = headerSpaceBetween?.roundToPx() ?: spacing12
|
|
443
|
+
val statusBarPx = statusBarHeight.roundToPx()
|
|
444
|
+
val scrollPercent = scrollPercentage.value
|
|
445
|
+
|
|
446
|
+
val contentMeasurable = measurables.find { it.layoutId == HeaderId.CONTENT_ID }
|
|
447
|
+
|
|
448
|
+
val realConstraints = constraints.copy(
|
|
449
|
+
minWidth = 0,
|
|
450
|
+
minHeight = 0,
|
|
451
|
+
maxWidth = (constraints.maxWidth - spacing12 * 2)
|
|
452
|
+
.coerceAtLeast(0),
|
|
453
|
+
)
|
|
454
|
+
val backIconPlaceable =
|
|
455
|
+
measurables.find { it.layoutId == HeaderId.BACK_ID }?.measure(realConstraints)
|
|
456
|
+
val headerRightPlaceable =
|
|
457
|
+
measurables.find { it.layoutId == HeaderId.HEADER_RIGHT_ID }?.measure(
|
|
458
|
+
realConstraints.copy(
|
|
459
|
+
maxWidth = realConstraints.maxWidth / 2,
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
val baseInputSearchConstraints = if (isHeaderExtend) {
|
|
463
|
+
val minWidth =
|
|
464
|
+
if (useAnimationSearch) realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
|
|
465
|
+
else realConstraints.maxWidth
|
|
466
|
+
realConstraints.copy(
|
|
467
|
+
maxWidth = (realConstraints.maxWidth * (1 - scrollPercent)).toInt()
|
|
468
|
+
.coerceAtLeast(minWidth)
|
|
469
|
+
)
|
|
470
|
+
} else {
|
|
471
|
+
var spaceConsumed = 0
|
|
472
|
+
if (backIconPlaceable.safeWidth != 0) spaceConsumed += backIconPlaceable.safeWidth + spaceBetween
|
|
473
|
+
if (headerRightPlaceable.safeWidth != 0) spaceConsumed += headerRightPlaceable.safeWidth + spaceBetween
|
|
474
|
+
realConstraints.copy(
|
|
475
|
+
maxWidth = realConstraints.maxWidth - spaceConsumed
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
val inputSearchConstraints = baseInputSearchConstraints.withAnimationTargetBounds()
|
|
479
|
+
val inputSearchPlaceable = measurables.find { it.layoutId == HeaderId.INPUT_SEARCH_ID }
|
|
480
|
+
?.measure(inputSearchConstraints)
|
|
481
|
+
val titlePlaceable = measurables.find { it.layoutId == HeaderId.TITLE_ID }?.measure(
|
|
482
|
+
constraints = realConstraints.copy(
|
|
483
|
+
maxWidth = realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
val firstRowMaxHeight = buildList {
|
|
488
|
+
add(backIconPlaceable.safeHeight)
|
|
489
|
+
add(headerRightPlaceable.safeHeight)
|
|
490
|
+
if (!isHeaderExtend) {
|
|
491
|
+
add(inputSearchPlaceable.safeHeight)
|
|
492
|
+
}
|
|
493
|
+
add(HEADER_HEIGHT.dp.roundToPx())
|
|
494
|
+
if (isHeaderExtend) {
|
|
495
|
+
add(titlePlaceable.safeHeight)
|
|
496
|
+
}
|
|
497
|
+
}.max()
|
|
498
|
+
|
|
499
|
+
var defaultHeight = statusBarPx + spacing12 + firstRowMaxHeight + spacing12
|
|
500
|
+
if (isHeaderExtend) {
|
|
501
|
+
defaultHeight += inputSearchPlaceable.safeHeight + spacing12
|
|
502
|
+
}
|
|
503
|
+
val headerHeight = when {
|
|
504
|
+
isHeaderNone -> statusBarPx
|
|
505
|
+
!useAnimationSearch && !isHeaderExtend -> defaultHeight
|
|
506
|
+
else -> (defaultHeight - scrollPercent * (defaultHeight - statusBarPx - HEADER_HEIGHT.dp.roundToPx())).toInt()
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
val layoutWidth = constraints.maxWidth
|
|
510
|
+
val layoutHeight = constraints.maxHeight
|
|
511
|
+
val contentHeight = (layoutHeight - headerHeight).coerceAtLeast(0)
|
|
512
|
+
val contentPlaceable = contentMeasurable?.measure(
|
|
513
|
+
constraints.copy(
|
|
514
|
+
minWidth = 0,
|
|
515
|
+
minHeight = contentHeight,
|
|
516
|
+
maxHeight = contentHeight,
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
val backgroundPlaceable = measurables.find { it.layoutId == HeaderId.BACKGROUND_ID }
|
|
520
|
+
?.measure(
|
|
521
|
+
Constraints.fixed(
|
|
522
|
+
width = layoutWidth,
|
|
523
|
+
height = headerHeight,
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
val childrenHeights = intArrayOf(
|
|
527
|
+
headerHeight,
|
|
528
|
+
contentPlaceable.safeHeight,
|
|
529
|
+
)
|
|
530
|
+
val childrenY = IntArray(childrenHeights.size)
|
|
531
|
+
with(verticalArrangement) {
|
|
532
|
+
arrange(
|
|
533
|
+
totalSize = layoutHeight,
|
|
534
|
+
sizes = childrenHeights,
|
|
535
|
+
outPositions = childrenY,
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
val headerY = childrenY[0]
|
|
539
|
+
val contentY = childrenY[1]
|
|
540
|
+
|
|
541
|
+
return layout(
|
|
542
|
+
width = layoutWidth,
|
|
543
|
+
height = layoutHeight,
|
|
544
|
+
) {
|
|
545
|
+
val progress = animationOption?.progress?.value ?: 1f
|
|
546
|
+
|
|
547
|
+
val startX = spacing12
|
|
548
|
+
val startY = headerY + statusBarPx
|
|
549
|
+
var curX = startX
|
|
550
|
+
var curY = startY
|
|
551
|
+
|
|
552
|
+
contentPlaceable?.place(
|
|
553
|
+
x = horizontalAlignment.align(
|
|
554
|
+
size = contentPlaceable.width,
|
|
555
|
+
space = layoutWidth,
|
|
556
|
+
layoutDirection = layoutDirection,
|
|
557
|
+
),
|
|
558
|
+
y = contentY,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
backgroundPlaceable?.place(
|
|
562
|
+
x = 0,
|
|
563
|
+
y = headerY,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if (backIconPlaceable != null) {
|
|
567
|
+
backIconPlaceable.place(
|
|
568
|
+
x = startX,
|
|
569
|
+
y = startY + backIconPlaceable.verticalCenterOffset(firstRowMaxHeight),
|
|
570
|
+
)
|
|
571
|
+
curX += backIconPlaceable.safeWidth + spaceBetween
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
headerRightPlaceable?.place(
|
|
575
|
+
x = layoutWidth - spacing12 - headerRightPlaceable.safeWidth,
|
|
576
|
+
y = startY + headerRightPlaceable.verticalCenterOffset(firstRowMaxHeight),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
val titleOffset = IntOffset(
|
|
580
|
+
x = if (titlePosition == TitlePosition.LEFT) curX
|
|
581
|
+
else titlePlaceable.horizontalCenterOffset(
|
|
582
|
+
space = layoutWidth,
|
|
583
|
+
layoutDirection = layoutDirection,
|
|
584
|
+
),
|
|
585
|
+
y = startY + titlePlaceable.verticalCenterOffset(firstRowMaxHeight),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
titlePlaceable?.place(titleOffset)
|
|
589
|
+
|
|
590
|
+
if (backIconPlaceable != null || headerRightPlaceable != null || titlePlaceable != null) {
|
|
591
|
+
curY += firstRowMaxHeight
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
val inputSearchOffset = if (isHeaderExtend) {
|
|
595
|
+
val baseY = curY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight)
|
|
596
|
+
val y = (baseY * (1 - scrollPercent)).toInt().coerceAtLeast(
|
|
597
|
+
startY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight)
|
|
598
|
+
)
|
|
599
|
+
IntOffset(
|
|
600
|
+
x = startX + ((backIconPlaceable.safeWidth + spaceBetween) * (scrollPercent * 2f).coerceIn(
|
|
601
|
+
0f, 1f
|
|
602
|
+
)).toInt(),
|
|
603
|
+
y = y,
|
|
604
|
+
)
|
|
605
|
+
} else {
|
|
606
|
+
IntOffset(
|
|
607
|
+
x = curX,
|
|
608
|
+
y = startY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight),
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
val finalPosition = lerp(
|
|
612
|
+
searchStartPosition,
|
|
613
|
+
inputSearchOffset,
|
|
614
|
+
progress,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
inputSearchPlaceable?.place(
|
|
618
|
+
finalPosition
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private val Placeable?.safeHeight
|
|
624
|
+
get() = this?.height ?: 0
|
|
625
|
+
private val Placeable?.safeWidth
|
|
626
|
+
get() = this?.width ?: 0
|
|
627
|
+
|
|
628
|
+
private fun Constraints.withAnimationTargetBounds(): Constraints {
|
|
629
|
+
val option = animationOption ?: return this
|
|
630
|
+
val progress = option.progress.value
|
|
631
|
+
if (progress >= 1f) return this
|
|
632
|
+
|
|
633
|
+
val targetWidth = option.targetBounds.width.roundToInt().coerceAtLeast(0)
|
|
634
|
+
val targetHeight = option.targetBounds.height.roundToInt().coerceAtLeast(0)
|
|
635
|
+
val animatedMinWidth = lerpPx(targetWidth, minWidth, progress)
|
|
636
|
+
.coerceInBounds(maxWidth)
|
|
637
|
+
val animatedMaxWidth = lerpMaxPx(targetWidth, maxWidth, progress)
|
|
638
|
+
.coerceAtLeast(animatedMinWidth)
|
|
639
|
+
val animatedMinHeight = lerpPx(targetHeight, minHeight, progress)
|
|
640
|
+
.coerceInBounds(maxHeight)
|
|
641
|
+
val animatedMaxHeight = lerpMaxPx(targetHeight, maxHeight, progress)
|
|
642
|
+
.coerceAtLeast(animatedMinHeight)
|
|
643
|
+
|
|
644
|
+
return copy(
|
|
645
|
+
minWidth = animatedMinWidth,
|
|
646
|
+
maxWidth = animatedMaxWidth,
|
|
647
|
+
minHeight = animatedMinHeight,
|
|
648
|
+
maxHeight = animatedMaxHeight,
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private fun lerpPx(
|
|
653
|
+
start: Int,
|
|
654
|
+
stop: Int,
|
|
655
|
+
fraction: Float,
|
|
656
|
+
): Int = androidx.compose.ui.util.lerp(
|
|
657
|
+
start.toFloat(),
|
|
658
|
+
stop.toFloat(),
|
|
659
|
+
fraction,
|
|
660
|
+
).roundToInt()
|
|
661
|
+
|
|
662
|
+
private fun lerpMaxPx(
|
|
663
|
+
start: Int,
|
|
664
|
+
stop: Int,
|
|
665
|
+
fraction: Float,
|
|
666
|
+
): Int {
|
|
667
|
+
if (stop == Constraints.Infinity) return Constraints.Infinity
|
|
668
|
+
return lerpPx(start, stop, fraction)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private fun Int.coerceInBounds(max: Int): Int {
|
|
672
|
+
if (max == Constraints.Infinity) return coerceAtLeast(0)
|
|
673
|
+
return coerceIn(0, max)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private fun Placeable?.verticalCenterOffset(space: Int): Int {
|
|
677
|
+
if (this == null) return 0
|
|
678
|
+
return Alignment.CenterVertically.align(safeHeight, space)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private fun Placeable?.horizontalCenterOffset(
|
|
682
|
+
space: Int,
|
|
683
|
+
layoutDirection: LayoutDirection
|
|
684
|
+
): Int {
|
|
685
|
+
if (this == null) return 0
|
|
686
|
+
return Alignment.CenterHorizontally.align(safeWidth, space, layoutDirection)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private val EMPTY_FUNC = {}
|
|
691
|
+
|
|
692
|
+
@Stable
|
|
693
|
+
data class LiteInputSearchProps(
|
|
694
|
+
val textFieldState: MutableState<String>,
|
|
695
|
+
val onValueChange: (String) -> Unit,
|
|
696
|
+
val onClear: () -> Unit = EMPTY_FUNC,
|
|
697
|
+
val clearCondition: (() -> Boolean)? = null,
|
|
698
|
+
val modifier: Modifier = Modifier,
|
|
699
|
+
val onFocused: (() -> Unit) = EMPTY_FUNC,
|
|
700
|
+
val keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
|
701
|
+
val keyboardActions: KeyboardActions = KeyboardActions.Default,
|
|
702
|
+
val onKeyboardAction: (() -> Unit) = EMPTY_FUNC,
|
|
703
|
+
|
|
704
|
+
val btnText: String? = null,
|
|
705
|
+
val isShowBtnText: Boolean = !btnText.isNullOrEmpty(),
|
|
706
|
+
val onPressBtnText: (() -> Unit) = EMPTY_FUNC,
|
|
707
|
+
|
|
708
|
+
val disabled: Boolean = false,
|
|
709
|
+
|
|
710
|
+
val placeHolder: String? = null,
|
|
711
|
+
|
|
712
|
+
val cursorBrush: Brush? = null,
|
|
713
|
+
val customBackIcon: @Composable ((Modifier) -> Unit)? = null,
|
|
714
|
+
val customSearchIcon: @Composable (() -> Unit)? = null,
|
|
715
|
+
val customPlaceHolder: @Composable (() -> Unit)? = null,
|
|
716
|
+
val customTextStyle: TextStyle? = null,
|
|
717
|
+
val customIconClear: @Composable (() -> Unit)? = null,
|
|
718
|
+
val iconRightTextField: @Composable ((Modifier) -> Unit)? = null,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
@Composable
|
|
722
|
+
private fun LiteInputSearch(
|
|
723
|
+
modifier: Modifier = Modifier,
|
|
724
|
+
inputSearchProps: LiteInputSearchProps? = null,
|
|
725
|
+
) {
|
|
726
|
+
inputSearchProps ?: return
|
|
727
|
+
val theme = AppTheme.current
|
|
728
|
+
|
|
729
|
+
val isFocused = remember { mutableStateOf(false) }
|
|
730
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
731
|
+
val textState by remember { inputSearchProps.textFieldState }
|
|
732
|
+
|
|
733
|
+
val isShowBtnText by remember(inputSearchProps.isShowBtnText, inputSearchProps.btnText) {
|
|
734
|
+
derivedStateOf {
|
|
735
|
+
inputSearchProps.isShowBtnText && !inputSearchProps.btnText.isNullOrEmpty()
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
val inputFieldStyle = remember(theme) {
|
|
739
|
+
Typography.bodyDefaultRegular.copy(
|
|
740
|
+
color = theme.colors.text.default,
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
LaunchedEffect(inputSearchProps.onFocused) {
|
|
745
|
+
interactionSource.interactions.mapNotNull { it as? FocusInteraction }.collectLatest {
|
|
746
|
+
val isFocus = it is FocusInteraction.Focus
|
|
747
|
+
isFocused.value = isFocus
|
|
748
|
+
if (isFocus) inputSearchProps.onFocused()
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
Row(
|
|
753
|
+
modifier = modifier,
|
|
754
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
755
|
+
) {
|
|
756
|
+
val textFieldModifier = remember(inputSearchProps.modifier) {
|
|
757
|
+
inputSearchProps.modifier
|
|
758
|
+
.weight(1f)
|
|
759
|
+
}
|
|
760
|
+
BasicTextField(
|
|
761
|
+
value = textState,
|
|
762
|
+
onValueChange = inputSearchProps.onValueChange,
|
|
763
|
+
enabled = !inputSearchProps.disabled,
|
|
764
|
+
keyboardOptions = inputSearchProps.keyboardOptions,
|
|
765
|
+
keyboardActions = inputSearchProps.keyboardActions,
|
|
766
|
+
modifier = textFieldModifier,
|
|
767
|
+
textStyle = inputSearchProps.customTextStyle ?: inputFieldStyle,
|
|
768
|
+
singleLine = true,
|
|
769
|
+
cursorBrush = inputSearchProps.cursorBrush ?: SolidColor(Color.Black),
|
|
770
|
+
interactionSource = interactionSource,
|
|
771
|
+
decorationBox = { innerTextField ->
|
|
772
|
+
val isShowClear by remember(inputSearchProps.clearCondition, isFocused.value) {
|
|
773
|
+
derivedStateOf {
|
|
774
|
+
inputSearchProps.clearCondition?.invoke() == true || (isFocused.value && textState.isNotEmpty())
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
val placeHolder by produceState<String?>(null, inputSearchProps.placeHolder) {
|
|
778
|
+
if (inputSearchProps.placeHolder.isNullOrEmpty()) return@produceState
|
|
779
|
+
snapshotFlow { textState }.collectLatest {
|
|
780
|
+
value = if (it.isEmpty()) inputSearchProps.placeHolder else null
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
val iconRightModifier = remember {
|
|
784
|
+
Modifier
|
|
785
|
+
.padding(start = Spacing.L)
|
|
786
|
+
.drawWithContent {
|
|
787
|
+
val offsetX = -Spacing.S.toPx()
|
|
788
|
+
drawLine(
|
|
789
|
+
start = Offset(
|
|
790
|
+
x = offsetX,
|
|
791
|
+
y = 0f,
|
|
792
|
+
),
|
|
793
|
+
end = Offset(
|
|
794
|
+
x = offsetX,
|
|
795
|
+
y = size.height,
|
|
796
|
+
),
|
|
797
|
+
color = theme.colors.primary,
|
|
798
|
+
strokeWidth = 1.dp.toPx(),
|
|
799
|
+
)
|
|
800
|
+
drawContent()
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
Row(
|
|
805
|
+
modifier = Modifier
|
|
806
|
+
.padding(
|
|
807
|
+
horizontal = Spacing.M,
|
|
808
|
+
vertical = Spacing.S,
|
|
809
|
+
),
|
|
810
|
+
horizontalArrangement = Arrangement.Start,
|
|
811
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
812
|
+
) {
|
|
813
|
+
inputSearchProps.customSearchIcon?.invoke() ?: Icon(
|
|
814
|
+
source = "navigation_search",
|
|
815
|
+
modifier = Modifier.padding(end = Spacing.XS),
|
|
816
|
+
size = 24.dp,
|
|
817
|
+
color = theme.colors.text.hint
|
|
818
|
+
)
|
|
819
|
+
Box(
|
|
820
|
+
modifier = Modifier.weight(1f),
|
|
821
|
+
contentAlignment = Alignment.CenterStart,
|
|
822
|
+
) {
|
|
823
|
+
if (!placeHolder.isNullOrEmpty()) {
|
|
824
|
+
inputSearchProps.customPlaceHolder?.invoke() ?: Text(
|
|
825
|
+
text = placeHolder ?: "",
|
|
826
|
+
style = inputSearchProps.customTextStyle ?: Typography.bodyDefaultRegular,
|
|
827
|
+
maxLines = 1,
|
|
828
|
+
color = theme.colors.text.hint,
|
|
829
|
+
overflow = TextOverflow.Ellipsis
|
|
830
|
+
)
|
|
831
|
+
}
|
|
832
|
+
innerTextField()
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (isShowClear) {
|
|
836
|
+
inputSearchProps.customIconClear?.invoke() ?: Icon(
|
|
837
|
+
source = "24_navigation_close_circle_full",
|
|
838
|
+
size = 16.dp,
|
|
839
|
+
color = theme.colors.text.hint,
|
|
840
|
+
modifier = Modifier.padding(start = Spacing.XS)
|
|
841
|
+
.noFeedbackClickable(onClick = inputSearchProps.onClear),
|
|
842
|
+
)
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
inputSearchProps.iconRightTextField?.invoke(iconRightModifier)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
if (isShowBtnText) {
|
|
851
|
+
Text(
|
|
852
|
+
text = inputSearchProps.btnText ?: "",
|
|
853
|
+
style = Typography.actionDefaultBold,
|
|
854
|
+
color = theme.colors.text.default,
|
|
855
|
+
modifier = Modifier.padding(start = Spacing.L)
|
|
856
|
+
.noFeedbackClickable(onClick = inputSearchProps.onPressBtnText)
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
@Composable
|
|
863
|
+
private fun footerOffset(
|
|
864
|
+
useAvoidKeyboard: Boolean = true,
|
|
865
|
+
): State<IntOffset> {
|
|
866
|
+
if (!useAvoidKeyboard) return remember { mutableStateOf(IntOffset.Zero) }
|
|
867
|
+
val density = LocalDensity.current
|
|
868
|
+
val navPaddingValue = WindowInsets.navigationBars.asPaddingValues()
|
|
869
|
+
val keyboardSize = keyboardSizeState()
|
|
870
|
+
|
|
871
|
+
return produceState(IntOffset.Zero, density, navPaddingValue) {
|
|
872
|
+
val navSystemOffset = with(density) { navPaddingValue.calculateBottomPadding().roundToPx() }
|
|
873
|
+
snapshotFlow { keyboardSize.value }.collectLatest {
|
|
874
|
+
value = if (it == 0.dp) IntOffset.Zero
|
|
875
|
+
else IntOffset(
|
|
876
|
+
x = 0, y = with(density) {
|
|
877
|
+
navSystemOffset - it.roundToPx()
|
|
878
|
+
})
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
@Composable
|
|
884
|
+
fun keyboardSizeState(): State<Dp> {
|
|
885
|
+
val bottom = WindowInsets.ime.asPaddingValues()
|
|
886
|
+
return rememberUpdatedState(bottom.calculateBottomPadding())
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
@Composable
|
|
890
|
+
private fun Footer(
|
|
891
|
+
useAvoidKeyboard: Boolean,
|
|
892
|
+
footerContent: (@Composable () -> Unit)?,
|
|
893
|
+
) {
|
|
894
|
+
footerContent ?: return
|
|
895
|
+
val theme = AppTheme.current
|
|
896
|
+
val navPaddingValue = WindowInsets.navigationBars.asPaddingValues()
|
|
897
|
+
val offsetMove = footerOffset(useAvoidKeyboard)
|
|
898
|
+
|
|
899
|
+
Box(
|
|
900
|
+
modifier = Modifier.padding(navPaddingValue).fillMaxWidth().offset {
|
|
901
|
+
offsetMove.value
|
|
902
|
+
}.shadow(
|
|
903
|
+
color = Colors.black_20.copy(alpha = 0.05f),
|
|
904
|
+
blurRadius = 24f,
|
|
905
|
+
offsetX = 0.dp,
|
|
906
|
+
offsetY = (-4).dp
|
|
907
|
+
).background(theme.colors.background.surface).padding(
|
|
908
|
+
start = Spacing.M,
|
|
909
|
+
top = Spacing.M,
|
|
910
|
+
end = Spacing.M,
|
|
911
|
+
)
|
|
912
|
+
) {
|
|
913
|
+
footerContent()
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
fun Modifier.hideKeyboardOnTap() = composed {
|
|
918
|
+
val focusManager = LocalFocusManager.current
|
|
919
|
+
val keyboardManager = LocalSoftwareKeyboardController.current
|
|
920
|
+
|
|
921
|
+
pointerInput(Unit) {
|
|
922
|
+
detectTapGestures {
|
|
923
|
+
keyboardManager?.hide()
|
|
924
|
+
focusManager.clearFocus()
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|