@momo-kits/native-kits 0.89.18 → 0.89.20
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 +18 -14
- 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 +79 -26
- 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 +6 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +6 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +40 -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 +23 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PromotionPopup.kt +8 -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/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/composeResources/drawable/test.jpg +0 -0
- 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
|
@@ -3,6 +3,7 @@ plugins {
|
|
|
3
3
|
id("com.android.library")
|
|
4
4
|
id("org.jetbrains.compose")
|
|
5
5
|
id("maven-publish")
|
|
6
|
+
alias(libs.plugins.kotlinx.serialization)
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
val libGroup = "vn.momo.kits"
|
|
@@ -35,32 +36,29 @@ kotlin {
|
|
|
35
36
|
implementation(libs.ktor.client.core)
|
|
36
37
|
implementation(libs.jetbrains.coroutines.core)
|
|
37
38
|
implementation(libs.ktor.client.core)
|
|
38
|
-
implementation("com.github.skydoves:landscapist-coil3:2.3.2")
|
|
39
39
|
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")
|
|
40
|
+
implementation(libs.navigation.compose)
|
|
41
|
+
implementation(libs.kotlinx.serialization.json)
|
|
42
|
+
implementation(libs.navigation.compose)
|
|
43
|
+
implementation(libs.coil.compose)
|
|
44
|
+
implementation(libs.coil.compose.core)
|
|
45
|
+
implementation(libs.coil.network.ktor)
|
|
46
|
+
implementation(libs.coil.mp)
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
val androidMain by getting {
|
|
51
50
|
dependencies {
|
|
52
|
-
val voyagerVersion = "1.1.0-alpha03"
|
|
53
51
|
implementation(libs.ktor.client.okhttp)
|
|
54
52
|
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
53
|
}
|
|
60
54
|
}
|
|
61
55
|
val iosX64Main by getting
|
|
62
56
|
val iosArm64Main by getting
|
|
63
57
|
val iosSimulatorArm64Main by getting
|
|
58
|
+
|
|
59
|
+
iosMain.dependencies {
|
|
60
|
+
implementation(libs.ktor.client.darwin)
|
|
61
|
+
}
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
task("testClasses")
|
|
@@ -104,3 +102,9 @@ android {
|
|
|
104
102
|
jvmToolchain(17)
|
|
105
103
|
}
|
|
106
104
|
}
|
|
105
|
+
|
|
106
|
+
dependencies {
|
|
107
|
+
implementation(libs.androidx.animation.android)
|
|
108
|
+
implementation(libs.androidx.navigation.compose)
|
|
109
|
+
implementation(libs.androidx.activity.ktx)
|
|
110
|
+
}
|
|
@@ -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,25 +10,33 @@ 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
|
|
32
|
+
import androidx.compose.ui.semantics.contentDescription
|
|
33
|
+
import androidx.compose.ui.semantics.semantics
|
|
34
|
+
import androidx.compose.ui.semantics.testTag
|
|
28
35
|
import androidx.compose.ui.text.style.TextAlign
|
|
29
36
|
import androidx.compose.ui.unit.dp
|
|
30
37
|
import androidx.compose.ui.zIndex
|
|
38
|
+
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
39
|
+
import kotlinx.coroutines.flow.map
|
|
31
40
|
import vn.momo.kits.components.Icon
|
|
32
41
|
import vn.momo.kits.components.Image
|
|
33
42
|
import vn.momo.kits.components.Options
|
|
@@ -38,26 +47,25 @@ import vn.momo.kits.const.Colors
|
|
|
38
47
|
import vn.momo.kits.const.Spacing
|
|
39
48
|
import vn.momo.kits.const.Typography
|
|
40
49
|
import vn.momo.kits.utils.bottomBorder
|
|
41
|
-
import vn.momo.kits.utils.getPlatformName
|
|
42
50
|
|
|
43
51
|
enum class TitlePosition {
|
|
44
52
|
LEFT, CENTER,
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
|
|
56
|
+
data class AnimatedHeader(
|
|
57
|
+
val type: HeaderType = HeaderType.DEFAULT,
|
|
58
|
+
val composable: @Composable (scrollState: Int) -> Unit = {}
|
|
59
|
+
)
|
|
60
|
+
|
|
47
61
|
@Composable
|
|
48
62
|
fun HeaderImage(
|
|
49
63
|
headerType: HeaderType = HeaderType.DEFAULT,
|
|
50
64
|
headerBackground: String?,
|
|
51
65
|
) {
|
|
52
|
-
val statusBarHeight = if (getPlatformName() == "iOS") {
|
|
53
|
-
WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
|
54
|
-
} else {
|
|
55
|
-
AppStatusBar.current
|
|
56
|
-
}
|
|
57
|
-
|
|
58
66
|
when (headerType) {
|
|
59
67
|
HeaderType.DEFAULT -> {
|
|
60
|
-
val height =
|
|
68
|
+
val height = AppStatusBar.current + 52.dp
|
|
61
69
|
Image(
|
|
62
70
|
source = headerBackground ?: "",
|
|
63
71
|
modifier = Modifier.fillMaxWidth().height(height),
|
|
@@ -77,6 +85,18 @@ fun HeaderImage(
|
|
|
77
85
|
)
|
|
78
86
|
}
|
|
79
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
|
+
|
|
80
100
|
else -> {
|
|
81
101
|
Box(modifier = Modifier.fillMaxWidth())
|
|
82
102
|
}
|
|
@@ -91,6 +111,9 @@ fun Header(
|
|
|
91
111
|
isBack: Boolean,
|
|
92
112
|
headerRight: @Composable (() -> Unit)? = null,
|
|
93
113
|
goBack: (() -> Unit) = { },
|
|
114
|
+
opacity: Float? = null,
|
|
115
|
+
animatedHeader: AnimatedHeader? = null,
|
|
116
|
+
scrollState: Int = 0
|
|
94
117
|
) {
|
|
95
118
|
var tintIconColor = Colors.black_01
|
|
96
119
|
var backgroundButton = Colors.black_20.copy(alpha = 0.4f)
|
|
@@ -98,20 +121,31 @@ fun Header(
|
|
|
98
121
|
tintIconColor = AppTheme.current.colors.text.default
|
|
99
122
|
backgroundButton = Color.Transparent
|
|
100
123
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
134
|
}
|
|
106
135
|
|
|
136
|
+
val tintIconColorAnim by animateColorAsState(
|
|
137
|
+
targetValue = animateColor(
|
|
138
|
+
Colors.black_01,
|
|
139
|
+
AppTheme.current.colors.text.default,
|
|
140
|
+
colorFraction
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
107
144
|
if (headerType != HeaderType.NONE) {
|
|
108
145
|
BoxWithConstraints(
|
|
109
|
-
Modifier.height(
|
|
146
|
+
Modifier.height(AppStatusBar.current + 52.dp)
|
|
110
147
|
.fillMaxWidth()
|
|
111
|
-
.
|
|
112
|
-
strokeWidth = if (headerType == HeaderType.SURFACE) 2.dp else 0.dp,
|
|
113
|
-
color = AppTheme.current.colors.border.default
|
|
114
|
-
),
|
|
148
|
+
.zIndex(10f),
|
|
115
149
|
contentAlignment = Alignment.BottomCenter
|
|
116
150
|
) {
|
|
117
151
|
Row(
|
|
@@ -145,20 +179,25 @@ fun Header(
|
|
|
145
179
|
indication = rememberRipple(),
|
|
146
180
|
onClick = goBack
|
|
147
181
|
)
|
|
148
|
-
.padding(Spacing.XS)
|
|
182
|
+
.padding(Spacing.XS)
|
|
183
|
+
.semantics { contentDescription = "btn_navigation_back"; testTag = "btn_navigation_back" },
|
|
149
184
|
contentAlignment = Alignment.Center
|
|
150
185
|
) {
|
|
151
186
|
Icon(
|
|
152
187
|
source = "ic_back_ios",
|
|
153
|
-
color = tintIconColor,
|
|
188
|
+
color = if (headerType == HeaderType.SURFACE && animatedHeader !== null) tintIconColorAnim else tintIconColor,
|
|
154
189
|
size = 20.dp,
|
|
155
190
|
)
|
|
156
191
|
}
|
|
157
192
|
}
|
|
158
|
-
Box {
|
|
159
|
-
|
|
193
|
+
Box(Modifier.graphicsLayer {
|
|
194
|
+
alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
|
|
195
|
+
}) {
|
|
196
|
+
headerRight?.invoke()
|
|
197
|
+
}
|
|
160
198
|
}
|
|
161
199
|
|
|
200
|
+
// Title
|
|
162
201
|
Row(
|
|
163
202
|
modifier = Modifier.height(52.dp)
|
|
164
203
|
.fillMaxWidth()
|
|
@@ -169,12 +208,18 @@ fun Header(
|
|
|
169
208
|
Spacer(Modifier.width(40.dp))
|
|
170
209
|
}
|
|
171
210
|
Box(
|
|
172
|
-
Modifier.weight(1f)
|
|
211
|
+
Modifier.weight(1f).graphicsLayer {
|
|
212
|
+
alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
|
|
213
|
+
},
|
|
173
214
|
contentAlignment = if (titlePosition == TitlePosition.LEFT) Alignment.TopStart else Alignment.Center
|
|
174
215
|
) {
|
|
175
216
|
HeaderTitle(
|
|
176
217
|
title = title,
|
|
177
|
-
color = tintIconColor,
|
|
218
|
+
color = if (headerType == HeaderType.SURFACE && animatedHeader !== null) tintIconColorAnim else tintIconColor,
|
|
219
|
+
modifier = Modifier.semantics {
|
|
220
|
+
contentDescription = "title_navigation_header"
|
|
221
|
+
testTag = "title_navigation_header"
|
|
222
|
+
}
|
|
178
223
|
)
|
|
179
224
|
}
|
|
180
225
|
if (isBack) {
|
|
@@ -200,5 +245,13 @@ fun HeaderTitle(
|
|
|
200
245
|
color = color,
|
|
201
246
|
maxLines = 1
|
|
202
247
|
)
|
|
248
|
+
}
|
|
203
249
|
|
|
250
|
+
fun animateColor(start: Color, stop: Color, fraction: Float): Color {
|
|
251
|
+
return Color(
|
|
252
|
+
red = start.red + fraction * (stop.red - start.red),
|
|
253
|
+
green = start.green + fraction * (stop.green - start.green),
|
|
254
|
+
blue = start.blue + fraction * (stop.blue - start.blue),
|
|
255
|
+
alpha = start.alpha + fraction * (stop.alpha - start.alpha)
|
|
256
|
+
)
|
|
204
257
|
}
|
|
@@ -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
|
+
}
|