@momo-kits/native-kits 0.89.19 → 0.89.21
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/compose/build.gradle.kts +11 -15
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +22 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Components.kt +71 -27
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +200 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +47 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +7 -139
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +89 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +17 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -7
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +6 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +37 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +54 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +155 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +2 -20
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +2 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +65 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +43 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +14 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +28 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +18 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -23
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +33 -0
- package/gradle.properties +1 -1
- package/package.json +1 -1
- package/settings.gradle.kts +1 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/utils/Platform.kt +0 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/utils/SetMaskFilter.kt +0 -8
- package/compose/src/androidMain/kotlin/vn/momo/kits/utils/Utils.android.kt +0 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Platform.kt +0 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/SetMaskFilter.kt +0 -5
- package/compose/src/iosMain/kotlin/vn/momo/kits/utils/Platform.kt +0 -3
- package/compose/src/iosMain/kotlin/vn/momo/kits/utils/SetMaskFilter.kt +0 -9
- package/compose/src/iosMain/kotlin/vn/momo/kits/utils/Utils.ios.kt +0 -18
package/compose/build.gradle.kts
CHANGED
|
@@ -27,7 +27,6 @@ kotlin {
|
|
|
27
27
|
sourceSets {
|
|
28
28
|
val commonMain by getting {
|
|
29
29
|
dependencies {
|
|
30
|
-
val voyagerVersion = "1.1.0-alpha03"
|
|
31
30
|
implementation(compose.runtime)
|
|
32
31
|
implementation(compose.foundation)
|
|
33
32
|
implementation(compose.material)
|
|
@@ -35,32 +34,29 @@ kotlin {
|
|
|
35
34
|
implementation(libs.ktor.client.core)
|
|
36
35
|
implementation(libs.jetbrains.coroutines.core)
|
|
37
36
|
implementation(libs.ktor.client.core)
|
|
38
|
-
implementation("com.github.skydoves:landscapist-coil3:2.3.2")
|
|
39
37
|
implementation(compose.components.resources)
|
|
40
|
-
implementation(
|
|
41
|
-
|
|
42
|
-
implementation(
|
|
43
|
-
implementation(
|
|
44
|
-
implementation(
|
|
45
|
-
implementation(
|
|
46
|
-
implementation(
|
|
47
|
-
implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion")
|
|
38
|
+
implementation(libs.navigation.compose)
|
|
39
|
+
implementation(libs.kotlinx.serialization.json)
|
|
40
|
+
implementation(libs.navigation.compose)
|
|
41
|
+
implementation(libs.coil.compose)
|
|
42
|
+
implementation(libs.coil.compose.core)
|
|
43
|
+
implementation(libs.coil.network.ktor)
|
|
44
|
+
implementation(libs.coil.mp)
|
|
48
45
|
}
|
|
49
46
|
}
|
|
50
47
|
val androidMain by getting {
|
|
51
48
|
dependencies {
|
|
52
|
-
val voyagerVersion = "1.1.0-alpha03"
|
|
53
49
|
implementation(libs.ktor.client.okhttp)
|
|
54
50
|
implementation(libs.jetbrains.coroutines.android)
|
|
55
|
-
implementation("cafe.adriel.voyager:voyager-hilt:$voyagerVersion")
|
|
56
|
-
implementation("cafe.adriel.voyager:voyager-livedata:$voyagerVersion")
|
|
57
|
-
// RxJava integration
|
|
58
|
-
implementation("cafe.adriel.voyager:voyager-rxjava:$voyagerVersion")
|
|
59
51
|
}
|
|
60
52
|
}
|
|
61
53
|
val iosX64Main by getting
|
|
62
54
|
val iosArm64Main by getting
|
|
63
55
|
val iosSimulatorArm64Main by getting
|
|
56
|
+
|
|
57
|
+
iosMain.dependencies {
|
|
58
|
+
implementation(libs.ktor.client.darwin)
|
|
59
|
+
}
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
task("testClasses")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package vn.momo.kits.platform
|
|
2
|
+
|
|
3
|
+
import android.graphics.BlurMaskFilter
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.ui.graphics.NativePaint
|
|
6
|
+
import androidx.compose.ui.platform.LocalConfiguration
|
|
7
|
+
|
|
8
|
+
actual fun getPlatformName(): String = "Android"
|
|
9
|
+
|
|
10
|
+
actual fun disableOverscroll() {}
|
|
11
|
+
|
|
12
|
+
@Composable
|
|
13
|
+
actual fun getScreenDimensions(): ScreenDimension {
|
|
14
|
+
val config = LocalConfiguration.current
|
|
15
|
+
return ScreenDimension(
|
|
16
|
+
config.screenWidthDp, config.screenHeightDp
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
actual fun NativePaint.setMaskFilter(blurRadius: Float) {
|
|
21
|
+
this.maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
|
|
22
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package vn.momo.kits.application
|
|
2
2
|
|
|
3
|
+
import androidx.compose.animation.animateColorAsState
|
|
3
4
|
import androidx.compose.foundation.background
|
|
4
5
|
import androidx.compose.foundation.border
|
|
5
6
|
import androidx.compose.foundation.clickable
|
|
@@ -9,21 +10,24 @@ import androidx.compose.foundation.layout.Box
|
|
|
9
10
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
10
11
|
import androidx.compose.foundation.layout.Row
|
|
11
12
|
import androidx.compose.foundation.layout.Spacer
|
|
12
|
-
import androidx.compose.foundation.layout.WindowInsets
|
|
13
|
-
import androidx.compose.foundation.layout.asPaddingValues
|
|
14
13
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
15
14
|
import androidx.compose.foundation.layout.height
|
|
16
15
|
import androidx.compose.foundation.layout.padding
|
|
17
16
|
import androidx.compose.foundation.layout.size
|
|
18
|
-
import androidx.compose.foundation.layout.systemBars
|
|
19
17
|
import androidx.compose.foundation.layout.width
|
|
20
18
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
21
19
|
import androidx.compose.material.ripple.rememberRipple
|
|
22
20
|
import androidx.compose.runtime.Composable
|
|
21
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
22
|
+
import androidx.compose.runtime.getValue
|
|
23
|
+
import androidx.compose.runtime.mutableStateOf
|
|
23
24
|
import androidx.compose.runtime.remember
|
|
25
|
+
import androidx.compose.runtime.setValue
|
|
26
|
+
import androidx.compose.runtime.snapshotFlow
|
|
24
27
|
import androidx.compose.ui.Alignment
|
|
25
28
|
import androidx.compose.ui.Modifier
|
|
26
29
|
import androidx.compose.ui.graphics.Color
|
|
30
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
27
31
|
import androidx.compose.ui.layout.ContentScale
|
|
28
32
|
import androidx.compose.ui.semantics.contentDescription
|
|
29
33
|
import androidx.compose.ui.semantics.semantics
|
|
@@ -31,6 +35,8 @@ import androidx.compose.ui.semantics.testTag
|
|
|
31
35
|
import androidx.compose.ui.text.style.TextAlign
|
|
32
36
|
import androidx.compose.ui.unit.dp
|
|
33
37
|
import androidx.compose.ui.zIndex
|
|
38
|
+
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
39
|
+
import kotlinx.coroutines.flow.map
|
|
34
40
|
import vn.momo.kits.components.Icon
|
|
35
41
|
import vn.momo.kits.components.Image
|
|
36
42
|
import vn.momo.kits.components.Options
|
|
@@ -41,26 +47,25 @@ import vn.momo.kits.const.Colors
|
|
|
41
47
|
import vn.momo.kits.const.Spacing
|
|
42
48
|
import vn.momo.kits.const.Typography
|
|
43
49
|
import vn.momo.kits.utils.bottomBorder
|
|
44
|
-
import vn.momo.kits.utils.getPlatformName
|
|
45
50
|
|
|
46
51
|
enum class TitlePosition {
|
|
47
52
|
LEFT, CENTER,
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
|
|
56
|
+
data class AnimatedHeader(
|
|
57
|
+
val type: HeaderType = HeaderType.DEFAULT,
|
|
58
|
+
val composable: @Composable (scrollState: Int) -> Unit = {}
|
|
59
|
+
)
|
|
60
|
+
|
|
50
61
|
@Composable
|
|
51
62
|
fun HeaderImage(
|
|
52
63
|
headerType: HeaderType = HeaderType.DEFAULT,
|
|
53
64
|
headerBackground: String?,
|
|
54
65
|
) {
|
|
55
|
-
val statusBarHeight = if (getPlatformName() == "iOS") {
|
|
56
|
-
WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
|
57
|
-
} else {
|
|
58
|
-
AppStatusBar.current
|
|
59
|
-
}
|
|
60
|
-
|
|
61
66
|
when (headerType) {
|
|
62
67
|
HeaderType.DEFAULT -> {
|
|
63
|
-
val height =
|
|
68
|
+
val height = AppStatusBar.current + 52.dp
|
|
64
69
|
Image(
|
|
65
70
|
source = headerBackground ?: "",
|
|
66
71
|
modifier = Modifier.fillMaxWidth().height(height),
|
|
@@ -80,6 +85,18 @@ fun HeaderImage(
|
|
|
80
85
|
)
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
HeaderType.SURFACE -> {
|
|
89
|
+
val height = AppStatusBar.current + 52.dp
|
|
90
|
+
Box(
|
|
91
|
+
modifier = Modifier.fillMaxWidth().height(height)
|
|
92
|
+
.background(AppTheme.current.colors.background.surface)
|
|
93
|
+
.bottomBorder(
|
|
94
|
+
strokeWidth = 2.dp,
|
|
95
|
+
color = AppTheme.current.colors.border.default
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
else -> {
|
|
84
101
|
Box(modifier = Modifier.fillMaxWidth())
|
|
85
102
|
}
|
|
@@ -94,6 +111,9 @@ fun Header(
|
|
|
94
111
|
isBack: Boolean,
|
|
95
112
|
headerRight: @Composable (() -> Unit)? = null,
|
|
96
113
|
goBack: (() -> Unit) = { },
|
|
114
|
+
opacity: Float? = null,
|
|
115
|
+
animatedHeader: AnimatedHeader? = null,
|
|
116
|
+
scrollState: Int = 0
|
|
97
117
|
) {
|
|
98
118
|
var tintIconColor = Colors.black_01
|
|
99
119
|
var backgroundButton = Colors.black_20.copy(alpha = 0.4f)
|
|
@@ -101,20 +121,31 @@ fun Header(
|
|
|
101
121
|
tintIconColor = AppTheme.current.colors.text.default
|
|
102
122
|
backgroundButton = Color.Transparent
|
|
103
123
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
|
|
125
|
+
var colorFraction by remember { mutableStateOf(0f) }
|
|
126
|
+
|
|
127
|
+
LaunchedEffect(scrollState) {
|
|
128
|
+
if (headerType == HeaderType.SURFACE) {
|
|
129
|
+
snapshotFlow { scrollState }
|
|
130
|
+
.map { (it / 50f).coerceIn(0f, 1f) }
|
|
131
|
+
.distinctUntilChanged()
|
|
132
|
+
.collect { fraction -> colorFraction = fraction }
|
|
133
|
+
}
|
|
108
134
|
}
|
|
109
135
|
|
|
136
|
+
val tintIconColorAnim by animateColorAsState(
|
|
137
|
+
targetValue = animateColor(
|
|
138
|
+
Colors.black_01,
|
|
139
|
+
AppTheme.current.colors.text.default,
|
|
140
|
+
colorFraction
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
110
144
|
if (headerType != HeaderType.NONE) {
|
|
111
145
|
BoxWithConstraints(
|
|
112
|
-
Modifier.height(
|
|
146
|
+
Modifier.height(AppStatusBar.current + 52.dp)
|
|
113
147
|
.fillMaxWidth()
|
|
114
|
-
.
|
|
115
|
-
strokeWidth = if (headerType == HeaderType.SURFACE) 2.dp else 0.dp,
|
|
116
|
-
color = AppTheme.current.colors.border.default
|
|
117
|
-
),
|
|
148
|
+
.zIndex(10f),
|
|
118
149
|
contentAlignment = Alignment.BottomCenter
|
|
119
150
|
) {
|
|
120
151
|
Row(
|
|
@@ -144,8 +175,6 @@ fun Header(
|
|
|
144
175
|
RoundedCornerShape(100)
|
|
145
176
|
)
|
|
146
177
|
.clickable(
|
|
147
|
-
interactionSource = remember { MutableInteractionSource() },
|
|
148
|
-
indication = rememberRipple(),
|
|
149
178
|
onClick = goBack
|
|
150
179
|
)
|
|
151
180
|
.padding(Spacing.XS)
|
|
@@ -154,15 +183,19 @@ fun Header(
|
|
|
154
183
|
) {
|
|
155
184
|
Icon(
|
|
156
185
|
source = "ic_back_ios",
|
|
157
|
-
color = tintIconColor,
|
|
186
|
+
color = if (headerType == HeaderType.SURFACE && animatedHeader !== null) tintIconColorAnim else tintIconColor,
|
|
158
187
|
size = 20.dp,
|
|
159
188
|
)
|
|
160
189
|
}
|
|
161
190
|
}
|
|
162
|
-
Box {
|
|
163
|
-
|
|
191
|
+
Box(Modifier.graphicsLayer {
|
|
192
|
+
alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
|
|
193
|
+
}) {
|
|
194
|
+
headerRight?.invoke()
|
|
195
|
+
}
|
|
164
196
|
}
|
|
165
197
|
|
|
198
|
+
// Title
|
|
166
199
|
Row(
|
|
167
200
|
modifier = Modifier.height(52.dp)
|
|
168
201
|
.fillMaxWidth()
|
|
@@ -173,12 +206,14 @@ fun Header(
|
|
|
173
206
|
Spacer(Modifier.width(40.dp))
|
|
174
207
|
}
|
|
175
208
|
Box(
|
|
176
|
-
Modifier.weight(1f)
|
|
209
|
+
Modifier.weight(1f).graphicsLayer {
|
|
210
|
+
alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
|
|
211
|
+
},
|
|
177
212
|
contentAlignment = if (titlePosition == TitlePosition.LEFT) Alignment.TopStart else Alignment.Center
|
|
178
213
|
) {
|
|
179
214
|
HeaderTitle(
|
|
180
215
|
title = title,
|
|
181
|
-
color = tintIconColor,
|
|
216
|
+
color = if (headerType == HeaderType.SURFACE && animatedHeader !== null) tintIconColorAnim else tintIconColor,
|
|
182
217
|
modifier = Modifier.semantics {
|
|
183
218
|
contentDescription = "title_navigation_header"
|
|
184
219
|
testTag = "title_navigation_header"
|
|
@@ -208,4 +243,13 @@ fun HeaderTitle(
|
|
|
208
243
|
color = color,
|
|
209
244
|
maxLines = 1
|
|
210
245
|
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fun animateColor(start: Color, stop: Color, fraction: Float): Color {
|
|
249
|
+
return Color(
|
|
250
|
+
red = start.red + fraction * (stop.red - start.red),
|
|
251
|
+
green = start.green + fraction * (stop.green - start.green),
|
|
252
|
+
blue = start.blue + fraction * (stop.blue - start.blue),
|
|
253
|
+
alpha = start.alpha + fraction * (stop.alpha - start.alpha)
|
|
254
|
+
)
|
|
211
255
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
package vn.momo.kits.application
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.AnimatedVisibility
|
|
4
|
+
import androidx.compose.animation.core.CubicBezierEasing
|
|
5
|
+
import androidx.compose.animation.core.animateDpAsState
|
|
6
|
+
import androidx.compose.animation.core.tween
|
|
7
|
+
import androidx.compose.animation.expandHorizontally
|
|
8
|
+
import androidx.compose.animation.fadeIn
|
|
9
|
+
import androidx.compose.animation.fadeOut
|
|
10
|
+
import androidx.compose.animation.shrinkHorizontally
|
|
11
|
+
import androidx.compose.foundation.ScrollState
|
|
12
|
+
import androidx.compose.foundation.background
|
|
13
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
14
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
15
|
+
import androidx.compose.foundation.layout.Box
|
|
16
|
+
import androidx.compose.foundation.layout.Row
|
|
17
|
+
import androidx.compose.foundation.layout.Spacer
|
|
18
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
19
|
+
import androidx.compose.foundation.layout.height
|
|
20
|
+
import androidx.compose.foundation.layout.offset
|
|
21
|
+
import androidx.compose.foundation.layout.padding
|
|
22
|
+
import androidx.compose.foundation.layout.sizeIn
|
|
23
|
+
import androidx.compose.foundation.layout.width
|
|
24
|
+
import androidx.compose.material3.AlertDialogDefaults.shape
|
|
25
|
+
import androidx.compose.material3.FloatingActionButton
|
|
26
|
+
import androidx.compose.material3.FloatingActionButtonDefaults
|
|
27
|
+
import androidx.compose.runtime.Composable
|
|
28
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
29
|
+
import androidx.compose.runtime.getValue
|
|
30
|
+
import androidx.compose.runtime.mutableStateOf
|
|
31
|
+
import androidx.compose.runtime.remember
|
|
32
|
+
import androidx.compose.runtime.rememberCoroutineScope
|
|
33
|
+
import androidx.compose.runtime.saveable.rememberSaveable
|
|
34
|
+
import androidx.compose.runtime.setValue
|
|
35
|
+
import androidx.compose.ui.Alignment
|
|
36
|
+
import androidx.compose.ui.Modifier
|
|
37
|
+
import androidx.compose.ui.graphics.Color
|
|
38
|
+
import androidx.compose.ui.unit.Dp
|
|
39
|
+
import androidx.compose.ui.unit.dp
|
|
40
|
+
import androidx.compose.ui.zIndex
|
|
41
|
+
import kotlinx.coroutines.delay
|
|
42
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
43
|
+
import kotlinx.coroutines.flow.collectLatest
|
|
44
|
+
import kotlinx.coroutines.launch
|
|
45
|
+
import vn.momo.kits.components.Icon
|
|
46
|
+
import vn.momo.kits.components.Text
|
|
47
|
+
import vn.momo.kits.const.Typography
|
|
48
|
+
|
|
49
|
+
enum class FABSize {
|
|
50
|
+
SMALL,
|
|
51
|
+
DEFAULT,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
enum class FABPosition {
|
|
55
|
+
END,
|
|
56
|
+
CENTER,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
data class FabProps(
|
|
60
|
+
val icon: String,
|
|
61
|
+
val iconColor: Color? = null,
|
|
62
|
+
val label: String? = null,
|
|
63
|
+
val onClick: () -> Unit,
|
|
64
|
+
val size: FABSize = FABSize.DEFAULT,
|
|
65
|
+
val bottom: Dp? = null,
|
|
66
|
+
val scrollState: ScrollState? = null,
|
|
67
|
+
val position: FABPosition? = FABPosition.END,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@Composable
|
|
71
|
+
fun FloatingButton(
|
|
72
|
+
scrollPosition: Int,
|
|
73
|
+
bottom: Dp,
|
|
74
|
+
onClick: () -> Unit,
|
|
75
|
+
containerColor: Color,
|
|
76
|
+
contentColor: Color = containerColor,
|
|
77
|
+
icon: String,
|
|
78
|
+
iconColor: Color? = null,
|
|
79
|
+
text: String? = null,
|
|
80
|
+
elevation: androidx.compose.material3.FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(
|
|
81
|
+
4.dp,
|
|
82
|
+
4.dp,
|
|
83
|
+
4.dp,
|
|
84
|
+
4.dp
|
|
85
|
+
),
|
|
86
|
+
size: FABSize = FABSize.DEFAULT,
|
|
87
|
+
position: FABPosition = FABPosition.END,
|
|
88
|
+
keyboardSize: Dp = 0.dp,
|
|
89
|
+
) {
|
|
90
|
+
val scrollPositionStateFlow = remember { MutableStateFlow(scrollPosition) }
|
|
91
|
+
var lastScrollPosition by rememberSaveable { mutableStateOf(0) }
|
|
92
|
+
val coroutineScope = rememberCoroutineScope()
|
|
93
|
+
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
|
94
|
+
|
|
95
|
+
val startPadding = if (isExpanded) 12.dp else 0.dp
|
|
96
|
+
val endPadding = if (isExpanded) 12.dp else 0.dp
|
|
97
|
+
val widthIn = if ((size === FABSize.SMALL)) 36.dp else 48.dp
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
val width = animateDpAsState(
|
|
101
|
+
targetValue = if (isExpanded && text != null) 80.dp else widthIn,
|
|
102
|
+
animationSpec = tween(
|
|
103
|
+
durationMillis = 200,
|
|
104
|
+
easing = CubicBezierEasing(0.2f, 0.2f, 0.2f, 0.2f),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
LaunchedEffect(text) {
|
|
109
|
+
if (text != null) {
|
|
110
|
+
coroutineScope.launch {
|
|
111
|
+
scrollPositionStateFlow
|
|
112
|
+
.collectLatest { debouncedScrollPosition ->
|
|
113
|
+
isExpanded = debouncedScrollPosition > lastScrollPosition
|
|
114
|
+
delay(300)
|
|
115
|
+
lastScrollPosition = debouncedScrollPosition
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
LaunchedEffect(scrollPosition) {
|
|
123
|
+
scrollPositionStateFlow.value = scrollPosition
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Box(
|
|
127
|
+
Modifier
|
|
128
|
+
.fillMaxSize()
|
|
129
|
+
.padding(end = 12.dp, bottom = bottom)
|
|
130
|
+
.offset(y = -keyboardSize)
|
|
131
|
+
.background(Color.Transparent)
|
|
132
|
+
.zIndex(10f),
|
|
133
|
+
contentAlignment = if (position == FABPosition.END) Alignment.BottomEnd else Alignment.BottomCenter,
|
|
134
|
+
) {
|
|
135
|
+
FloatingActionButton(
|
|
136
|
+
onClick = onClick,
|
|
137
|
+
shape = shape,
|
|
138
|
+
containerColor = containerColor,
|
|
139
|
+
contentColor = contentColor,
|
|
140
|
+
elevation = elevation,
|
|
141
|
+
interactionSource = remember { MutableInteractionSource() },
|
|
142
|
+
modifier = Modifier
|
|
143
|
+
.sizeIn(minWidth = width.value)
|
|
144
|
+
.height(widthIn)
|
|
145
|
+
) {
|
|
146
|
+
Row(
|
|
147
|
+
modifier = Modifier.padding(start = startPadding, end = endPadding),
|
|
148
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
149
|
+
horizontalArrangement = if (isExpanded) Arrangement.Start else Arrangement.Center
|
|
150
|
+
) {
|
|
151
|
+
Icon(
|
|
152
|
+
icon,
|
|
153
|
+
size = if (size === FABSize.SMALL) 12.dp else 24.dp,
|
|
154
|
+
color = iconColor ?: Color.White
|
|
155
|
+
)
|
|
156
|
+
AnimatedVisibility(
|
|
157
|
+
visible = isExpanded,
|
|
158
|
+
enter = ExtendedFabExpandAnimation,
|
|
159
|
+
exit = ExtendedFabCollapseAnimation,
|
|
160
|
+
) {
|
|
161
|
+
Row {
|
|
162
|
+
Spacer(Modifier.width(12.dp))
|
|
163
|
+
Text(
|
|
164
|
+
text ?: "",
|
|
165
|
+
color = Color.White,
|
|
166
|
+
style = Typography.actionDefault
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private val ExtendedFabCollapseAnimation = fadeOut(
|
|
176
|
+
animationSpec = tween(
|
|
177
|
+
durationMillis = 800,
|
|
178
|
+
easing = CubicBezierEasing(0.0f, 0.0f, 1.0f, 1.0f),
|
|
179
|
+
)
|
|
180
|
+
) + shrinkHorizontally(
|
|
181
|
+
animationSpec = tween(
|
|
182
|
+
durationMillis = 500,
|
|
183
|
+
easing = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f),
|
|
184
|
+
),
|
|
185
|
+
shrinkTowards = Alignment.Start,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
private val ExtendedFabExpandAnimation = fadeIn(
|
|
189
|
+
animationSpec = tween(
|
|
190
|
+
durationMillis = 200,
|
|
191
|
+
delayMillis = 100,
|
|
192
|
+
easing = CubicBezierEasing(0.0f, 0.0f, 1.0f, 1.0f),
|
|
193
|
+
),
|
|
194
|
+
) + expandHorizontally(
|
|
195
|
+
animationSpec = tween(
|
|
196
|
+
durationMillis = 500,
|
|
197
|
+
easing = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f),
|
|
198
|
+
),
|
|
199
|
+
expandFrom = Alignment.Start,
|
|
200
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package vn.momo.kits.application
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.animateFloatAsState
|
|
4
|
+
import androidx.compose.foundation.layout.Box
|
|
5
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
6
|
+
import androidx.compose.foundation.layout.height
|
|
7
|
+
import androidx.compose.runtime.Composable
|
|
8
|
+
import androidx.compose.runtime.getValue
|
|
9
|
+
import androidx.compose.ui.Modifier
|
|
10
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
11
|
+
import androidx.compose.ui.unit.dp
|
|
12
|
+
import vn.momo.kits.components.Image
|
|
13
|
+
|
|
14
|
+
@Composable
|
|
15
|
+
fun HeaderAnimated(image: String, scrollState: Int = 0) {
|
|
16
|
+
// Scale animation
|
|
17
|
+
val scale by animateFloatAsState(
|
|
18
|
+
targetValue = when {
|
|
19
|
+
scrollState < 0 -> (-(scrollState / 300f)).coerceIn(1f, 4f)
|
|
20
|
+
scrollState.toFloat() in 0f..300f -> 1f
|
|
21
|
+
else -> 1f
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// Opacity animation
|
|
26
|
+
val opacity by animateFloatAsState(
|
|
27
|
+
targetValue = when {
|
|
28
|
+
scrollState.toFloat() in 0f..150f -> 1f - (scrollState / 300f)
|
|
29
|
+
scrollState.toFloat() in 150f..300f -> 0.5f - ((scrollState - 150f) / 300f)
|
|
30
|
+
else -> 1f
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// Translation Y animation
|
|
35
|
+
|
|
36
|
+
Box(modifier = Modifier.fillMaxWidth().height(200.dp)
|
|
37
|
+
.graphicsLayer {
|
|
38
|
+
scaleX = scale
|
|
39
|
+
scaleY = scale
|
|
40
|
+
}) {
|
|
41
|
+
if (image.isEmpty()) return
|
|
42
|
+
Image(
|
|
43
|
+
source = image,
|
|
44
|
+
modifier = Modifier.fillMaxWidth().height(200.dp)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|