@momo-kits/native-kits 0.160.9-debug → 0.160.10-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.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.160.9-debug"
43
+ version = "0.160.10-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -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
@@ -70,13 +68,17 @@ import androidx.compose.ui.unit.IntOffset
70
68
  import androidx.compose.ui.unit.LayoutDirection
71
69
  import androidx.compose.ui.unit.dp
72
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
73
76
  import kotlinx.coroutines.flow.collectLatest
74
77
  import kotlinx.coroutines.flow.mapNotNull
75
78
  import vn.momo.kits.components.Icon
76
79
  import vn.momo.kits.components.Text
77
80
  import vn.momo.kits.const.AppTheme
78
81
  import vn.momo.kits.const.Colors
79
- import vn.momo.kits.const.Radius
80
82
  import vn.momo.kits.const.Spacing
81
83
  import vn.momo.kits.const.Typography
82
84
  import vn.momo.kits.modifier.conditional
@@ -86,6 +88,30 @@ import vn.momo.kits.modifier.setAutomationId
86
88
  import vn.momo.kits.modifier.shadow
87
89
  import vn.momo.kits.utils.getAppStatusBarHeight
88
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 }
89
115
 
90
116
  @Composable
91
117
  fun LiteScreen(
@@ -114,48 +140,46 @@ fun LiteScreen(
114
140
 
115
141
  val finalScrollState = scrollState ?: rememberScrollState()
116
142
 
117
- 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,
118
157
  modifier = Modifier
119
158
  .fillMaxSize()
120
- .background(color = backgroundColor)
121
159
  .hideKeyboardOnTap(),
160
+ contentModifier = contentModifier,
122
161
  verticalArrangement = verticalArrangement,
123
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,
124
174
  ) {
125
- val contentModifier = remember(scrollable, finalScrollState) {
126
- var res = Modifier.weight(1f)
127
- if (scrollable) {
128
- res = res.verticalScroll(finalScrollState)
129
- }
130
- res
131
- }
132
-
133
- LiteScreenHeader(
134
- scrollState = finalScrollState,
135
- title = title,
136
- headerRight = headerRight,
137
- headerType = headerType,
138
- onGoBack = goBack,
139
- inputSearchProps = inputSearchProps,
140
- titlePosition = titlePosition,
141
- useAnimationSearch = useAnimationSearch,
142
- headerRightData = headerRightData,
143
- tintColor = headerTintColor,
144
- headerBackgroundColor = headerBackgroundColor,
145
- headerSpaceBetween = headerSpaceBetween,
146
- )
147
-
148
- Box(
149
- modifier = contentModifier,
150
- contentAlignment = Alignment.TopCenter,
151
- ) {
152
- content()
153
- }
175
+ content()
154
176
  }
155
177
  }
156
178
 
157
179
  private object HeaderId {
158
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"
159
183
  const val BACK_ID = "${PACKAGE_NAME}.back"
160
184
  const val HEADER_RIGHT_ID = "${PACKAGE_NAME}.headerRight"
161
185
  const val INPUT_SEARCH_ID = "${PACKAGE_NAME}.inputSearch"
@@ -165,8 +189,12 @@ private object HeaderId {
165
189
  }
166
190
 
167
191
  @Composable
168
- private fun LiteScreenHeader(
192
+ private fun LiteScreenLayout(
169
193
  scrollState: ScrollState?,
194
+ modifier: Modifier = Modifier,
195
+ contentModifier: Modifier = Modifier,
196
+ verticalArrangement: Arrangement.Vertical = Arrangement.Top,
197
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
170
198
  title: String? = null,
171
199
  tintColor: Color? = null,
172
200
  headerRightData: HeaderRightData? = null,
@@ -178,11 +206,11 @@ private fun LiteScreenHeader(
178
206
  headerSpaceBetween: Dp? = null,
179
207
  inputSearchProps: LiteInputSearchProps? = null,
180
208
  headerRight: @Composable (() -> Unit)? = null,
209
+ content: @Composable () -> Unit,
181
210
  ) {
182
211
  val statusBarHeight = getAppStatusBarHeight()
183
- if (headerType == HeaderType.NONE) {
184
- Box(modifier = Modifier.height(statusBarHeight))
185
- return
212
+ val isHeaderNone = remember(headerType) {
213
+ headerType == HeaderType.NONE
186
214
  }
187
215
  val theme = AppTheme.current
188
216
  val density = LocalDensity.current
@@ -190,9 +218,12 @@ private fun LiteScreenHeader(
190
218
  val isHeaderExtend = remember(headerType) {
191
219
  headerType == HeaderType.EXTENDED
192
220
  }
193
- val backgroundHeight = remember(isHeaderExtend, statusBarHeight) {
194
- if (!isHeaderExtend) statusBarHeight + HEADER_HEIGHT.dp
195
- 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
+ }
196
227
  }
197
228
  val listGradientColors = remember {
198
229
  listOf(
@@ -235,79 +266,107 @@ private fun LiteScreenHeader(
235
266
  )
236
267
  }
237
268
 
238
- val titleModifier = remember {
239
- Modifier
240
- .kitsAutomationId("title_navigation_header")
241
- .layoutId(HeaderId.TITLE_ID)
242
- .graphicsLayer {
243
- alpha = if (isHeaderExtend && inputSearchProps != null && useAnimationSearch)
244
- (1 - scrollPercentage.value * 2).coerceIn(0f, 1f)
245
- else 1f
246
- }
269
+ val animationOption = LocalAnimationOption.current
270
+ val animationOpacityModifier = Modifier.graphicsLayer {
271
+ alpha = animationOption?.animateOpacity ?: 1f
247
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
+ }
248
282
 
249
283
  val policy = remember(
250
284
  useAnimationSearch,
285
+ isHeaderNone,
251
286
  isHeaderExtend,
252
287
  statusBarHeight,
253
288
  titlePosition,
254
289
  scrollPercentage,
255
290
  headerSpaceBetween,
291
+ animationOption,
292
+ verticalArrangement,
293
+ horizontalAlignment,
256
294
  ) {
257
- LiteScreenHeaderPolicy(
295
+ LiteScreenLayoutPolicy(
258
296
  useAnimationSearch = useAnimationSearch,
297
+ isHeaderNone = isHeaderNone,
259
298
  isHeaderExtend = isHeaderExtend,
260
299
  statusBarHeight = statusBarHeight,
261
300
  titlePosition = titlePosition,
262
301
  scrollPercentage = scrollPercentage,
263
302
  headerSpaceBetween = headerSpaceBetween,
303
+ animationOption = animationOption,
304
+ verticalArrangement = verticalArrangement,
305
+ horizontalAlignment = horizontalAlignment,
264
306
  )
265
307
  }
266
308
 
267
309
  Layout(
268
- modifier = Modifier
269
- .animateContentSize()
270
- .drawBehind {
271
- val headerHeight = max(
272
- HeaderId.EXTENDED_HEADER_HEIGHT.toPx(),
273
- size.height,
274
- )
275
- headerBackgroundColor?.let {
276
- drawRect(color = it)
277
- } ?: run {
278
- drawRect(color = Colors.black_01)
279
- drawRect(
280
- brush = Brush.linearGradient(
281
- colors = listGradientColors,
282
- start = Offset.Zero,
283
- end = Offset(
284
- x = 0f,
285
- y = headerHeight * (1 - scrollPercentage.value),
286
- ),
287
- )
288
- )
289
- }
290
- },
310
+ modifier = modifier
311
+ .animateContentSize(),
291
312
  content = {
292
- 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) {
293
323
  Box(
294
324
  modifier = Modifier
295
- .size(28.dp)
296
- .layoutId(HeaderId.BACK_ID)
297
- .clip(CircleShape)
298
- .conditional(inputSearchProps?.customBackIcon == null) {
299
- this.border(
300
- width = 0.2.dp,
301
- color = headerColor.borderColor,
302
- shape = CircleShape,
325
+ .layoutId(HeaderId.BACKGROUND_ID)
326
+ .then(animationOpacityModifier)
327
+ .drawBehind {
328
+ val headerHeight = max(
329
+ HeaderId.EXTENDED_HEADER_HEIGHT.toPx(),
330
+ size.height,
303
331
  )
304
- .background(color = headerColor.backgroundButton)
305
- }
306
- .noFeedbackClickable(onClick = onGoBack)
307
- .setAutomationId("btn_navigation_back")
332
+ headerBackgroundColor?.let {
333
+ drawRect(color = it)
334
+ } ?: run {
335
+ drawRect(color = Colors.black_01)
336
+ drawRect(
337
+ brush = Brush.linearGradient(
338
+ colors = listGradientColors,
339
+ start = Offset.Zero,
340
+ end = Offset(
341
+ x = 0f,
342
+ y = headerHeight * (1 - scrollPercentage.value),
343
+ ),
344
+ )
345
+ )
346
+ }
347
+ },
348
+ )
349
+ }
350
+
351
+ if (!isHeaderNone && onGoBack != null) {
352
+ val coreModifier = Modifier
353
+ .layoutId(HeaderId.BACK_ID)
354
+ .then(animationOpacityModifier)
355
+ .clip(CircleShape)
356
+ .noFeedbackClickable(onClick = onGoBack)
357
+ .setAutomationId("btn_navigation_back")
358
+ inputSearchProps?.customBackIcon?.invoke(coreModifier) ?: Box(
359
+ modifier = coreModifier
360
+ .size(28.dp)
361
+ .border(
362
+ width = 0.2.dp,
363
+ color = headerColor.borderColor,
364
+ shape = CircleShape,
365
+ )
366
+ .background(color = headerColor.backgroundButton)
308
367
  .padding(Spacing.XS),
309
368
  ) {
310
- inputSearchProps?.customBackIcon?.invoke() ?: Icon(
369
+ Icon(
311
370
  source = "arrow-back",
312
371
  color = headerColor.tintIconColor,
313
372
  size = 20.dp,
@@ -315,27 +374,30 @@ private fun LiteScreenHeader(
315
374
  }
316
375
  }
317
376
 
318
- Box(
319
- modifier = Modifier
320
- .layoutId(HeaderId.HEADER_RIGHT_ID)
321
- ) {
322
- if (headerRight != null) {
323
- headerRight()
324
- } else {
325
- HeaderRight(
326
- headerRight = headerRightData,
327
- tintColor = tintColor,
328
- )
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
+ }
329
391
  }
330
392
  }
331
393
 
332
- if (inputSearchProps != null) {
394
+ if (!isHeaderNone && inputSearchProps != null) {
333
395
  LiteInputSearch(
334
396
  modifier = Modifier.layoutId(HeaderId.INPUT_SEARCH_ID),
335
397
  inputSearchProps = inputSearchProps,
336
398
  )
337
399
  }
338
- if (title != null && (inputSearchProps == null || isHeaderExtend)) {
400
+ if (!isHeaderNone && title != null && (inputSearchProps == null || isHeaderExtend)) {
339
401
  Text(
340
402
  text = title,
341
403
  color = headerColor.tintIconColor,
@@ -350,15 +412,28 @@ private fun LiteScreenHeader(
350
412
  )
351
413
  }
352
414
 
353
- private class LiteScreenHeaderPolicy(
415
+ private class LiteScreenLayoutPolicy(
354
416
  private val useAnimationSearch: Boolean,
417
+ private val isHeaderNone: Boolean,
355
418
  private val isHeaderExtend: Boolean,
356
419
  private val statusBarHeight: Dp,
357
420
  private val scrollPercentage: State<Float>,
358
421
  private val titlePosition: TitlePosition,
359
422
  private val headerSpaceBetween: Dp? = null,
423
+ private val animationOption: AnimationOption?,
424
+ private val verticalArrangement: Arrangement.Vertical,
425
+ private val horizontalAlignment: Alignment.Horizontal,
360
426
  ) : MeasurePolicy {
361
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
+
362
437
  override fun MeasureScope.measure(
363
438
  measurables: List<Measurable>,
364
439
  constraints: Constraints
@@ -368,6 +443,8 @@ private class LiteScreenHeaderPolicy(
368
443
  val statusBarPx = statusBarHeight.roundToPx()
369
444
  val scrollPercent = scrollPercentage.value
370
445
 
446
+ val contentMeasurable = measurables.find { it.layoutId == HeaderId.CONTENT_ID }
447
+
371
448
  val realConstraints = constraints.copy(
372
449
  minWidth = 0,
373
450
  minHeight = 0,
@@ -382,7 +459,7 @@ private class LiteScreenHeaderPolicy(
382
459
  maxWidth = realConstraints.maxWidth / 2,
383
460
  )
384
461
  )
385
- val inputSearchConstraints = if (isHeaderExtend) {
462
+ val baseInputSearchConstraints = if (isHeaderExtend) {
386
463
  val minWidth =
387
464
  if (useAnimationSearch) realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
388
465
  else realConstraints.maxWidth
@@ -398,6 +475,7 @@ private class LiteScreenHeaderPolicy(
398
475
  maxWidth = realConstraints.maxWidth - spaceConsumed
399
476
  )
400
477
  }
478
+ val inputSearchConstraints = baseInputSearchConstraints.withAnimationTargetBounds()
401
479
  val inputSearchPlaceable = measurables.find { it.layoutId == HeaderId.INPUT_SEARCH_ID }
402
480
  ?.measure(inputSearchConstraints)
403
481
  val titlePlaceable = measurables.find { it.layoutId == HeaderId.TITLE_ID }?.measure(
@@ -412,6 +490,7 @@ private class LiteScreenHeaderPolicy(
412
490
  if (!isHeaderExtend) {
413
491
  add(inputSearchPlaceable.safeHeight)
414
492
  }
493
+ add(HEADER_HEIGHT.dp.roundToPx())
415
494
  if (isHeaderExtend) {
416
495
  add(titlePlaceable.safeHeight)
417
496
  }
@@ -421,20 +500,69 @@ private class LiteScreenHeaderPolicy(
421
500
  if (isHeaderExtend) {
422
501
  defaultHeight += inputSearchPlaceable.safeHeight + spacing12
423
502
  }
424
- val height = when {
503
+ val headerHeight = when {
504
+ isHeaderNone -> statusBarPx
425
505
  !useAnimationSearch && !isHeaderExtend -> defaultHeight
426
506
  else -> (defaultHeight - scrollPercent * (defaultHeight - statusBarPx - HEADER_HEIGHT.dp.roundToPx())).toInt()
427
507
  }
428
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
+
429
541
  return layout(
430
- width = constraints.maxWidth,
431
- height = height,
542
+ width = layoutWidth,
543
+ height = layoutHeight,
432
544
  ) {
545
+ val progress = animationOption?.progress?.value ?: 1f
546
+
433
547
  val startX = spacing12
434
- val startY = statusBarPx + spacing12
548
+ val startY = headerY + statusBarPx
435
549
  var curX = startX
436
550
  var curY = startY
437
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
+
438
566
  if (backIconPlaceable != null) {
439
567
  backIconPlaceable.place(
440
568
  x = startX,
@@ -444,14 +572,14 @@ private class LiteScreenHeaderPolicy(
444
572
  }
445
573
 
446
574
  headerRightPlaceable?.place(
447
- x = constraints.maxWidth - spacing12 - headerRightPlaceable.safeWidth,
575
+ x = layoutWidth - spacing12 - headerRightPlaceable.safeWidth,
448
576
  y = startY + headerRightPlaceable.verticalCenterOffset(firstRowMaxHeight),
449
577
  )
450
578
 
451
579
  val titleOffset = IntOffset(
452
580
  x = if (titlePosition == TitlePosition.LEFT) curX
453
581
  else titlePlaceable.horizontalCenterOffset(
454
- space = constraints.maxWidth,
582
+ space = layoutWidth,
455
583
  layoutDirection = layoutDirection,
456
584
  ),
457
585
  y = startY + titlePlaceable.verticalCenterOffset(firstRowMaxHeight),
@@ -460,17 +588,19 @@ private class LiteScreenHeaderPolicy(
460
588
  titlePlaceable?.place(titleOffset)
461
589
 
462
590
  if (backIconPlaceable != null || headerRightPlaceable != null || titlePlaceable != null) {
463
- curY += firstRowMaxHeight + spacing12
591
+ curY += firstRowMaxHeight
464
592
  }
465
593
 
466
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
+ )
467
599
  IntOffset(
468
600
  x = startX + ((backIconPlaceable.safeWidth + spaceBetween) * (scrollPercent * 2f).coerceIn(
469
601
  0f, 1f
470
602
  )).toInt(),
471
- y = (curY * (1 - scrollPercent)).toInt().coerceAtLeast(
472
- startY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight)
473
- ),
603
+ y = y,
474
604
  )
475
605
  } else {
476
606
  IntOffset(
@@ -478,8 +608,15 @@ private class LiteScreenHeaderPolicy(
478
608
  y = startY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight),
479
609
  )
480
610
  }
611
+ val finalPosition = lerp(
612
+ searchStartPosition,
613
+ inputSearchOffset,
614
+ progress,
615
+ )
481
616
 
482
- inputSearchPlaceable?.place(inputSearchOffset)
617
+ inputSearchPlaceable?.place(
618
+ finalPosition
619
+ )
483
620
  }
484
621
  }
485
622
 
@@ -488,6 +625,54 @@ private class LiteScreenHeaderPolicy(
488
625
  private val Placeable?.safeWidth
489
626
  get() = this?.width ?: 0
490
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
+
491
676
  private fun Placeable?.verticalCenterOffset(space: Int): Int {
492
677
  if (this == null) return 0
493
678
  return Alignment.CenterVertically.align(safeHeight, space)
@@ -524,7 +709,8 @@ data class LiteInputSearchProps(
524
709
 
525
710
  val placeHolder: String? = null,
526
711
 
527
- val customBackIcon: @Composable (() -> Unit)? = null,
712
+ val cursorBrush: Brush? = null,
713
+ val customBackIcon: @Composable ((Modifier) -> Unit)? = null,
528
714
  val customSearchIcon: @Composable (() -> Unit)? = null,
529
715
  val customPlaceHolder: @Composable (() -> Unit)? = null,
530
716
  val customTextStyle: TextStyle? = null,
@@ -580,6 +766,7 @@ private fun LiteInputSearch(
580
766
  modifier = textFieldModifier,
581
767
  textStyle = inputSearchProps.customTextStyle ?: inputFieldStyle,
582
768
  singleLine = true,
769
+ cursorBrush = inputSearchProps.cursorBrush ?: SolidColor(Color.Black),
583
770
  interactionSource = interactionSource,
584
771
  decorationBox = { innerTextField ->
585
772
  val isShowClear by remember(inputSearchProps.clearCondition, isFocused.value) {
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.160.9
21
+ version=0.160.10
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.160.9-debug",
3
+ "version": "0.160.10-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},