@momo-kits/native-kits 0.161.1-beta.15-debug → 0.161.2-beta.1-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.
Files changed (35) hide show
  1. package/compose/build.gradle.kts +9 -3
  2. package/compose/build.gradle.kts.backup +8 -2
  3. package/compose/compose.podspec +1 -1
  4. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +9 -2
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +8 -14
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +324 -117
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +4 -3
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +4 -0
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +5 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +5 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +19 -14
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +17 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +27 -12
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +1 -28
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +63 -49
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +62 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +23 -2
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +12 -4
  22. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +33 -0
  23. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +18 -8
  24. package/gradle/libs.versions.toml +3 -1
  25. package/gradle.properties +1 -1
  26. package/ios/Application/ApplicationEnvironment.swift +2 -6
  27. package/ios/Input/Input.swift +50 -21
  28. package/ios/Input/InputPhoneNumber.swift +17 -17
  29. package/ios/StatusBarTap/StatusBarTap.h +13 -0
  30. package/ios/StatusBarTap/StatusBarTap.m +75 -0
  31. package/ios/Typography/Text.swift +19 -14
  32. package/ios/Typography/Typography.swift +22 -1
  33. package/ios/native-kits.podspec +2 -1
  34. package/package.json +1 -1
  35. package/settings.gradle.kts +15 -3
@@ -1,5 +1,6 @@
1
1
  package vn.momo.kits.application
2
2
 
3
+ import androidx.annotation.FloatRange
3
4
  import androidx.compose.animation.animateContentSize
4
5
  import androidx.compose.foundation.ScrollState
5
6
  import androidx.compose.foundation.background
@@ -9,22 +10,18 @@ import androidx.compose.foundation.interaction.FocusInteraction
9
10
  import androidx.compose.foundation.interaction.MutableInteractionSource
10
11
  import androidx.compose.foundation.layout.Arrangement
11
12
  import androidx.compose.foundation.layout.Box
12
- import androidx.compose.foundation.layout.Column
13
13
  import androidx.compose.foundation.layout.Row
14
14
  import androidx.compose.foundation.layout.WindowInsets
15
15
  import androidx.compose.foundation.layout.asPaddingValues
16
16
  import androidx.compose.foundation.layout.fillMaxSize
17
17
  import androidx.compose.foundation.layout.fillMaxWidth
18
- import androidx.compose.foundation.layout.height
19
18
  import androidx.compose.foundation.layout.ime
20
19
  import androidx.compose.foundation.layout.navigationBars
21
20
  import androidx.compose.foundation.layout.offset
22
21
  import androidx.compose.foundation.layout.padding
23
22
  import androidx.compose.foundation.layout.size
24
- import androidx.compose.foundation.layout.sizeIn
25
23
  import androidx.compose.foundation.rememberScrollState
26
24
  import androidx.compose.foundation.shape.CircleShape
27
- import androidx.compose.foundation.shape.RoundedCornerShape
28
25
  import androidx.compose.foundation.text.BasicTextField
29
26
  import androidx.compose.foundation.text.KeyboardActions
30
27
  import androidx.compose.foundation.text.KeyboardOptions
@@ -48,6 +45,7 @@ import androidx.compose.ui.draw.clip
48
45
  import androidx.compose.ui.draw.drawBehind
49
46
  import androidx.compose.ui.draw.drawWithContent
50
47
  import androidx.compose.ui.geometry.Offset
48
+ import androidx.compose.ui.geometry.Rect
51
49
  import androidx.compose.ui.graphics.Brush
52
50
  import androidx.compose.ui.graphics.Color
53
51
  import androidx.compose.ui.graphics.graphicsLayer
@@ -63,27 +61,57 @@ import androidx.compose.ui.platform.LocalDensity
63
61
  import androidx.compose.ui.platform.LocalFocusManager
64
62
  import androidx.compose.ui.platform.LocalSoftwareKeyboardController
65
63
  import androidx.compose.ui.text.style.TextOverflow
64
+ import androidx.compose.ui.text.TextStyle
66
65
  import androidx.compose.ui.unit.Constraints
67
66
  import androidx.compose.ui.unit.Dp
68
67
  import androidx.compose.ui.unit.IntOffset
69
68
  import androidx.compose.ui.unit.LayoutDirection
70
69
  import androidx.compose.ui.unit.dp
71
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
72
76
  import kotlinx.coroutines.flow.collectLatest
73
77
  import kotlinx.coroutines.flow.mapNotNull
74
78
  import vn.momo.kits.components.Icon
75
79
  import vn.momo.kits.components.Text
76
80
  import vn.momo.kits.const.AppTheme
77
81
  import vn.momo.kits.const.Colors
78
- import vn.momo.kits.const.Radius
79
82
  import vn.momo.kits.const.Spacing
80
83
  import vn.momo.kits.const.Typography
84
+ import vn.momo.kits.modifier.conditional
81
85
  import vn.momo.kits.modifier.kitsAutomationId
82
86
  import vn.momo.kits.modifier.noFeedbackClickable
83
87
  import vn.momo.kits.modifier.setAutomationId
84
88
  import vn.momo.kits.modifier.shadow
85
89
  import vn.momo.kits.utils.getAppStatusBarHeight
86
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 }
87
115
 
88
116
  @Composable
89
117
  fun LiteScreen(
@@ -101,6 +129,9 @@ fun LiteScreen(
101
129
  useAnimationSearch: Boolean = true,
102
130
  titlePosition: TitlePosition = TitlePosition.LEFT,
103
131
  headerRightData: HeaderRightData? = null,
132
+ headerTintColor: Color? = null,
133
+ headerBackgroundColor: Color? = null,
134
+ headerSpaceBetween: Dp? = null,
104
135
  /* End of header props */
105
136
 
106
137
  screenContent: @Composable () -> Unit,
@@ -109,45 +140,46 @@ fun LiteScreen(
109
140
 
110
141
  val finalScrollState = scrollState ?: rememberScrollState()
111
142
 
112
- Column(
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,
113
157
  modifier = Modifier
114
158
  .fillMaxSize()
115
- .background(color = backgroundColor)
116
159
  .hideKeyboardOnTap(),
160
+ contentModifier = contentModifier,
117
161
  verticalArrangement = verticalArrangement,
118
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,
119
174
  ) {
120
- val contentModifier = remember(scrollable, finalScrollState) {
121
- var res = Modifier.weight(1f)
122
- if (scrollable) {
123
- res = res.verticalScroll(finalScrollState)
124
- }
125
- res
126
- }
127
-
128
- LiteScreenHeader(
129
- scrollState = finalScrollState,
130
- title = title,
131
- headerRight = headerRight,
132
- headerType = headerType,
133
- onGoBack = goBack,
134
- inputSearchProps = inputSearchProps,
135
- titlePosition = titlePosition,
136
- useAnimationSearch = useAnimationSearch,
137
- headerRightData = headerRightData,
138
- )
139
-
140
- Box(
141
- modifier = contentModifier,
142
- contentAlignment = Alignment.TopCenter,
143
- ) {
144
- content()
145
- }
175
+ content()
146
176
  }
147
177
  }
148
178
 
149
179
  private object HeaderId {
150
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"
151
183
  const val BACK_ID = "${PACKAGE_NAME}.back"
152
184
  const val HEADER_RIGHT_ID = "${PACKAGE_NAME}.headerRight"
153
185
  const val INPUT_SEARCH_ID = "${PACKAGE_NAME}.inputSearch"
@@ -157,8 +189,12 @@ private object HeaderId {
157
189
  }
158
190
 
159
191
  @Composable
160
- private fun LiteScreenHeader(
192
+ private fun LiteScreenLayout(
161
193
  scrollState: ScrollState?,
194
+ modifier: Modifier = Modifier,
195
+ contentModifier: Modifier = Modifier,
196
+ verticalArrangement: Arrangement.Vertical = Arrangement.Top,
197
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
162
198
  title: String? = null,
163
199
  tintColor: Color? = null,
164
200
  headerRightData: HeaderRightData? = null,
@@ -166,13 +202,15 @@ private fun LiteScreenHeader(
166
202
  titlePosition: TitlePosition = TitlePosition.LEFT,
167
203
  useAnimationSearch: Boolean = true,
168
204
  onGoBack: (() -> Unit)? = null,
205
+ headerBackgroundColor: Color? = null,
206
+ headerSpaceBetween: Dp? = null,
169
207
  inputSearchProps: LiteInputSearchProps? = null,
170
208
  headerRight: @Composable (() -> Unit)? = null,
209
+ content: @Composable () -> Unit,
171
210
  ) {
172
211
  val statusBarHeight = getAppStatusBarHeight()
173
- if (headerType == HeaderType.NONE) {
174
- Box(modifier = Modifier.height(statusBarHeight))
175
- return
212
+ val isHeaderNone = remember(headerType) {
213
+ headerType == HeaderType.NONE
176
214
  }
177
215
  val theme = AppTheme.current
178
216
  val density = LocalDensity.current
@@ -180,9 +218,12 @@ private fun LiteScreenHeader(
180
218
  val isHeaderExtend = remember(headerType) {
181
219
  headerType == HeaderType.EXTENDED
182
220
  }
183
- val backgroundHeight = remember(isHeaderExtend, statusBarHeight) {
184
- if (!isHeaderExtend) statusBarHeight + HEADER_HEIGHT.dp
185
- else HeaderId.EXTENDED_HEADER_HEIGHT
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
+ }
186
227
  }
187
228
  val listGradientColors = remember {
188
229
  listOf(
@@ -225,68 +266,104 @@ private fun LiteScreenHeader(
225
266
  )
226
267
  }
227
268
 
228
- val titleModifier = remember {
229
- Modifier
230
- .kitsAutomationId("title_navigation_header")
231
- .layoutId(HeaderId.TITLE_ID)
232
- .graphicsLayer {
233
- alpha = if (isHeaderExtend && inputSearchProps != null && useAnimationSearch)
234
- (1 - scrollPercentage.value * 2).coerceIn(0f, 1f)
235
- else 1f
236
- }
269
+ val animationOption = LocalAnimationOption.current
270
+ val animationOpacityModifier = Modifier.graphicsLayer {
271
+ alpha = animationOption?.animateOpacity ?: 1f
237
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
+ }
238
282
 
239
283
  val policy = remember(
240
284
  useAnimationSearch,
285
+ isHeaderNone,
241
286
  isHeaderExtend,
242
287
  statusBarHeight,
243
288
  titlePosition,
244
289
  scrollPercentage,
290
+ headerSpaceBetween,
291
+ animationOption,
292
+ verticalArrangement,
293
+ horizontalAlignment,
245
294
  ) {
246
- LiteScreenHeaderPolicy(
295
+ LiteScreenLayoutPolicy(
247
296
  useAnimationSearch = useAnimationSearch,
297
+ isHeaderNone = isHeaderNone,
248
298
  isHeaderExtend = isHeaderExtend,
249
299
  statusBarHeight = statusBarHeight,
250
300
  titlePosition = titlePosition,
251
301
  scrollPercentage = scrollPercentage,
302
+ headerSpaceBetween = headerSpaceBetween,
303
+ animationOption = animationOption,
304
+ verticalArrangement = verticalArrangement,
305
+ horizontalAlignment = horizontalAlignment,
252
306
  )
253
307
  }
254
308
 
255
309
  Layout(
256
- modifier = Modifier
257
- .animateContentSize()
258
- .drawBehind {
259
- val headerHeight = max(
260
- HeaderId.EXTENDED_HEADER_HEIGHT.toPx(),
261
- size.height,
262
- )
263
- drawRect(color = Colors.black_01)
264
- drawRect(
265
- brush = Brush.linearGradient(
266
- colors = listGradientColors,
267
- start = Offset.Zero,
268
- end = Offset(
269
- x = 0f,
270
- y = headerHeight * (1 - scrollPercentage.value),
271
- ),
272
- )
273
- )
274
- },
310
+ modifier = modifier
311
+ .animateContentSize(),
275
312
  content = {
276
- if (onGoBack != null) {
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) {
277
323
  Box(
278
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
279
360
  .size(28.dp)
280
- .layoutId(HeaderId.BACK_ID)
281
- .clip(CircleShape)
282
361
  .border(
283
362
  width = 0.2.dp,
284
363
  color = headerColor.borderColor,
285
364
  shape = CircleShape,
286
365
  )
287
366
  .background(color = headerColor.backgroundButton)
288
- .noFeedbackClickable(onClick = onGoBack)
289
- .setAutomationId("btn_navigation_back")
290
367
  .padding(Spacing.XS),
291
368
  ) {
292
369
  Icon(
@@ -297,27 +374,30 @@ private fun LiteScreenHeader(
297
374
  }
298
375
  }
299
376
 
300
- Box(
301
- modifier = Modifier
302
- .layoutId(HeaderId.HEADER_RIGHT_ID)
303
- ) {
304
- if (headerRight != null) {
305
- headerRight()
306
- } else {
307
- HeaderRight(
308
- headerRight = headerRightData,
309
- tintColor = tintColor,
310
- )
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
+ }
311
391
  }
312
392
  }
313
393
 
314
- if (inputSearchProps != null) {
394
+ if (!isHeaderNone && inputSearchProps != null) {
315
395
  LiteInputSearch(
316
396
  modifier = Modifier.layoutId(HeaderId.INPUT_SEARCH_ID),
317
397
  inputSearchProps = inputSearchProps,
318
398
  )
319
399
  }
320
- if (title != null && (inputSearchProps == null || isHeaderExtend)) {
400
+ if (!isHeaderNone && title != null && (inputSearchProps == null || isHeaderExtend)) {
321
401
  Text(
322
402
  text = title,
323
403
  color = headerColor.tintIconColor,
@@ -332,22 +412,39 @@ private fun LiteScreenHeader(
332
412
  )
333
413
  }
334
414
 
335
- private class LiteScreenHeaderPolicy(
415
+ private class LiteScreenLayoutPolicy(
336
416
  private val useAnimationSearch: Boolean,
417
+ private val isHeaderNone: Boolean,
337
418
  private val isHeaderExtend: Boolean,
338
419
  private val statusBarHeight: Dp,
339
420
  private val scrollPercentage: State<Float>,
340
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,
341
426
  ) : MeasurePolicy {
342
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
+
343
437
  override fun MeasureScope.measure(
344
438
  measurables: List<Measurable>,
345
439
  constraints: Constraints
346
440
  ): MeasureResult {
347
441
  val spacing12 = Spacing.M.roundToPx()
442
+ val spaceBetween = headerSpaceBetween?.roundToPx() ?: spacing12
348
443
  val statusBarPx = statusBarHeight.roundToPx()
349
444
  val scrollPercent = scrollPercentage.value
350
445
 
446
+ val contentMeasurable = measurables.find { it.layoutId == HeaderId.CONTENT_ID }
447
+
351
448
  val realConstraints = constraints.copy(
352
449
  minWidth = 0,
353
450
  minHeight = 0,
@@ -362,9 +459,9 @@ private class LiteScreenHeaderPolicy(
362
459
  maxWidth = realConstraints.maxWidth / 2,
363
460
  )
364
461
  )
365
- val inputSearchConstraints = if (isHeaderExtend) {
462
+ val baseInputSearchConstraints = if (isHeaderExtend) {
366
463
  val minWidth =
367
- if (useAnimationSearch) realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spacing12 * 2
464
+ if (useAnimationSearch) realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
368
465
  else realConstraints.maxWidth
369
466
  realConstraints.copy(
370
467
  maxWidth = (realConstraints.maxWidth * (1 - scrollPercent)).toInt()
@@ -372,17 +469,18 @@ private class LiteScreenHeaderPolicy(
372
469
  )
373
470
  } else {
374
471
  var spaceConsumed = 0
375
- if (backIconPlaceable.safeWidth != 0) spaceConsumed += backIconPlaceable.safeWidth + spacing12
376
- if (headerRightPlaceable.safeWidth != 0) spaceConsumed += headerRightPlaceable.safeWidth + spacing12
472
+ if (backIconPlaceable.safeWidth != 0) spaceConsumed += backIconPlaceable.safeWidth + spaceBetween
473
+ if (headerRightPlaceable.safeWidth != 0) spaceConsumed += headerRightPlaceable.safeWidth + spaceBetween
377
474
  realConstraints.copy(
378
475
  maxWidth = realConstraints.maxWidth - spaceConsumed
379
476
  )
380
477
  }
478
+ val inputSearchConstraints = baseInputSearchConstraints.withAnimationTargetBounds()
381
479
  val inputSearchPlaceable = measurables.find { it.layoutId == HeaderId.INPUT_SEARCH_ID }
382
480
  ?.measure(inputSearchConstraints)
383
481
  val titlePlaceable = measurables.find { it.layoutId == HeaderId.TITLE_ID }?.measure(
384
482
  constraints = realConstraints.copy(
385
- maxWidth = realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spacing12 * 2
483
+ maxWidth = realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
386
484
  )
387
485
  )
388
486
 
@@ -392,6 +490,7 @@ private class LiteScreenHeaderPolicy(
392
490
  if (!isHeaderExtend) {
393
491
  add(inputSearchPlaceable.safeHeight)
394
492
  }
493
+ add(HEADER_HEIGHT.dp.roundToPx())
395
494
  if (isHeaderExtend) {
396
495
  add(titlePlaceable.safeHeight)
397
496
  }
@@ -401,37 +500,86 @@ private class LiteScreenHeaderPolicy(
401
500
  if (isHeaderExtend) {
402
501
  defaultHeight += inputSearchPlaceable.safeHeight + spacing12
403
502
  }
404
- val height = when {
503
+ val headerHeight = when {
504
+ isHeaderNone -> statusBarPx
405
505
  !useAnimationSearch && !isHeaderExtend -> defaultHeight
406
506
  else -> (defaultHeight - scrollPercent * (defaultHeight - statusBarPx - HEADER_HEIGHT.dp.roundToPx())).toInt()
407
507
  }
408
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
+
409
541
  return layout(
410
- width = constraints.maxWidth,
411
- height = height,
542
+ width = layoutWidth,
543
+ height = layoutHeight,
412
544
  ) {
545
+ val progress = animationOption?.progress?.value ?: 1f
546
+
413
547
  val startX = spacing12
414
- val startY = statusBarPx + spacing12
548
+ val startY = headerY + statusBarPx
415
549
  var curX = startX
416
550
  var curY = startY
417
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
+
418
566
  if (backIconPlaceable != null) {
419
567
  backIconPlaceable.place(
420
568
  x = startX,
421
569
  y = startY + backIconPlaceable.verticalCenterOffset(firstRowMaxHeight),
422
570
  )
423
- curX += backIconPlaceable.safeWidth + spacing12
571
+ curX += backIconPlaceable.safeWidth + spaceBetween
424
572
  }
425
573
 
426
574
  headerRightPlaceable?.place(
427
- x = constraints.maxWidth - spacing12 - headerRightPlaceable.safeWidth,
575
+ x = layoutWidth - spacing12 - headerRightPlaceable.safeWidth,
428
576
  y = startY + headerRightPlaceable.verticalCenterOffset(firstRowMaxHeight),
429
577
  )
430
578
 
431
579
  val titleOffset = IntOffset(
432
580
  x = if (titlePosition == TitlePosition.LEFT) curX
433
581
  else titlePlaceable.horizontalCenterOffset(
434
- space = constraints.maxWidth,
582
+ space = layoutWidth,
435
583
  layoutDirection = layoutDirection,
436
584
  ),
437
585
  y = startY + titlePlaceable.verticalCenterOffset(firstRowMaxHeight),
@@ -440,17 +588,19 @@ private class LiteScreenHeaderPolicy(
440
588
  titlePlaceable?.place(titleOffset)
441
589
 
442
590
  if (backIconPlaceable != null || headerRightPlaceable != null || titlePlaceable != null) {
443
- curY += firstRowMaxHeight + spacing12
591
+ curY += firstRowMaxHeight
444
592
  }
445
593
 
446
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
+ )
447
599
  IntOffset(
448
- x = startX + ((backIconPlaceable.safeWidth + spacing12) * (scrollPercent * 2f).coerceIn(
600
+ x = startX + ((backIconPlaceable.safeWidth + spaceBetween) * (scrollPercent * 2f).coerceIn(
449
601
  0f, 1f
450
602
  )).toInt(),
451
- y = (curY * (1 - scrollPercent)).toInt().coerceAtLeast(
452
- startY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight)
453
- ),
603
+ y = y,
454
604
  )
455
605
  } else {
456
606
  IntOffset(
@@ -458,8 +608,15 @@ private class LiteScreenHeaderPolicy(
458
608
  y = startY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight),
459
609
  )
460
610
  }
611
+ val finalPosition = lerp(
612
+ searchStartPosition,
613
+ inputSearchOffset,
614
+ progress,
615
+ )
461
616
 
462
- inputSearchPlaceable?.place(inputSearchOffset)
617
+ inputSearchPlaceable?.place(
618
+ finalPosition
619
+ )
463
620
  }
464
621
  }
465
622
 
@@ -468,6 +625,54 @@ private class LiteScreenHeaderPolicy(
468
625
  private val Placeable?.safeWidth
469
626
  get() = this?.width ?: 0
470
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
+
471
676
  private fun Placeable?.verticalCenterOffset(space: Int): Int {
472
677
  if (this == null) return 0
473
678
  return Alignment.CenterVertically.align(safeHeight, space)
@@ -504,6 +709,12 @@ data class LiteInputSearchProps(
504
709
 
505
710
  val placeHolder: String? = null,
506
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,
507
718
  val iconRightTextField: @Composable ((Modifier) -> Unit)? = null,
508
719
  )
509
720
 
@@ -545,7 +756,6 @@ private fun LiteInputSearch(
545
756
  val textFieldModifier = remember(inputSearchProps.modifier) {
546
757
  inputSearchProps.modifier
547
758
  .weight(1f)
548
- .sizeIn(minHeight = 36.dp)
549
759
  }
550
760
  BasicTextField(
551
761
  value = textState,
@@ -554,8 +764,9 @@ private fun LiteInputSearch(
554
764
  keyboardOptions = inputSearchProps.keyboardOptions,
555
765
  keyboardActions = inputSearchProps.keyboardActions,
556
766
  modifier = textFieldModifier,
557
- textStyle = inputFieldStyle,
767
+ textStyle = inputSearchProps.customTextStyle ?: inputFieldStyle,
558
768
  singleLine = true,
769
+ cursorBrush = inputSearchProps.cursorBrush ?: SolidColor(Color.Black),
559
770
  interactionSource = interactionSource,
560
771
  decorationBox = { innerTextField ->
561
772
  val isShowClear by remember(inputSearchProps.clearCondition, isFocused.value) {
@@ -592,10 +803,6 @@ private fun LiteInputSearch(
592
803
 
593
804
  Row(
594
805
  modifier = Modifier
595
- .background(
596
- color = theme.colors.background.surface,
597
- shape = RoundedCornerShape(Radius.XL),
598
- )
599
806
  .padding(
600
807
  horizontal = Spacing.M,
601
808
  vertical = Spacing.S,
@@ -603,7 +810,7 @@ private fun LiteInputSearch(
603
810
  horizontalArrangement = Arrangement.Start,
604
811
  verticalAlignment = Alignment.CenterVertically,
605
812
  ) {
606
- Icon(
813
+ inputSearchProps.customSearchIcon?.invoke() ?: Icon(
607
814
  source = "navigation_search",
608
815
  modifier = Modifier.padding(end = Spacing.XS),
609
816
  size = 24.dp,
@@ -614,9 +821,9 @@ private fun LiteInputSearch(
614
821
  contentAlignment = Alignment.CenterStart,
615
822
  ) {
616
823
  if (!placeHolder.isNullOrEmpty()) {
617
- Text(
824
+ inputSearchProps.customPlaceHolder?.invoke() ?: Text(
618
825
  text = placeHolder ?: "",
619
- style = Typography.bodyDefaultRegular,
826
+ style = inputSearchProps.customTextStyle ?: Typography.bodyDefaultRegular,
620
827
  maxLines = 1,
621
828
  color = theme.colors.text.hint,
622
829
  overflow = TextOverflow.Ellipsis
@@ -626,7 +833,7 @@ private fun LiteInputSearch(
626
833
  }
627
834
 
628
835
  if (isShowClear) {
629
- Icon(
836
+ inputSearchProps.customIconClear?.invoke() ?: Icon(
630
837
  source = "24_navigation_close_circle_full",
631
838
  size = 16.dp,
632
839
  color = theme.colors.text.hint,
@@ -717,4 +924,4 @@ fun Modifier.hideKeyboardOnTap() = composed {
717
924
  focusManager.clearFocus()
718
925
  }
719
926
  }
720
- }
927
+ }