@momo-kits/native-kits 0.89.19 → 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 +71 -25
- 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/Icon.kt +6 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +36 -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/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/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,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(
|
|
@@ -154,15 +185,19 @@ fun Header(
|
|
|
154
185
|
) {
|
|
155
186
|
Icon(
|
|
156
187
|
source = "ic_back_ios",
|
|
157
|
-
color = tintIconColor,
|
|
188
|
+
color = if (headerType == HeaderType.SURFACE && animatedHeader !== null) tintIconColorAnim else tintIconColor,
|
|
158
189
|
size = 20.dp,
|
|
159
190
|
)
|
|
160
191
|
}
|
|
161
192
|
}
|
|
162
|
-
Box {
|
|
163
|
-
|
|
193
|
+
Box(Modifier.graphicsLayer {
|
|
194
|
+
alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
|
|
195
|
+
}) {
|
|
196
|
+
headerRight?.invoke()
|
|
197
|
+
}
|
|
164
198
|
}
|
|
165
199
|
|
|
200
|
+
// Title
|
|
166
201
|
Row(
|
|
167
202
|
modifier = Modifier.height(52.dp)
|
|
168
203
|
.fillMaxWidth()
|
|
@@ -173,12 +208,14 @@ fun Header(
|
|
|
173
208
|
Spacer(Modifier.width(40.dp))
|
|
174
209
|
}
|
|
175
210
|
Box(
|
|
176
|
-
Modifier.weight(1f)
|
|
211
|
+
Modifier.weight(1f).graphicsLayer {
|
|
212
|
+
alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
|
|
213
|
+
},
|
|
177
214
|
contentAlignment = if (titlePosition == TitlePosition.LEFT) Alignment.TopStart else Alignment.Center
|
|
178
215
|
) {
|
|
179
216
|
HeaderTitle(
|
|
180
217
|
title = title,
|
|
181
|
-
color = tintIconColor,
|
|
218
|
+
color = if (headerType == HeaderType.SURFACE && animatedHeader !== null) tintIconColorAnim else tintIconColor,
|
|
182
219
|
modifier = Modifier.semantics {
|
|
183
220
|
contentDescription = "title_navigation_header"
|
|
184
221
|
testTag = "title_navigation_header"
|
|
@@ -208,4 +245,13 @@ fun HeaderTitle(
|
|
|
208
245
|
color = color,
|
|
209
246
|
maxLines = 1
|
|
210
247
|
)
|
|
248
|
+
}
|
|
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
|
+
)
|
|
211
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
|
+
}
|