Skip to content

Commit 5416ff4

Browse files
committed
fix(ui/text/animated): attempt to normalize baselines for varying currency symbols
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 5c851a7 commit 5416ff4

6 files changed

Lines changed: 454 additions & 206 deletions

File tree

ui/components/src/main/kotlin/com/getcode/ui/components/text/AmountTextAnimated.kt

Lines changed: 63 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
package com.getcode.ui.components.text
22

3-
import androidx.compose.animation.AnimatedContent
4-
import androidx.compose.animation.EnterTransition
5-
import androidx.compose.animation.ExitTransition
63
import androidx.compose.animation.ExperimentalAnimationApi
7-
import androidx.compose.animation.animateContentSize
8-
import androidx.compose.animation.core.LinearOutSlowInEasing
94
import androidx.compose.animation.core.animateDpAsState
10-
import androidx.compose.animation.core.tween
115
import androidx.compose.animation.expandHorizontally
126
import androidx.compose.animation.expandVertically
137
import androidx.compose.animation.fadeIn
@@ -18,7 +12,6 @@ import androidx.compose.animation.slideInHorizontally
1812
import androidx.compose.animation.slideInVertically
1913
import androidx.compose.animation.slideOutHorizontally
2014
import androidx.compose.animation.slideOutVertically
21-
import androidx.compose.animation.togetherWith
2215
import androidx.compose.foundation.Image
2316
import androidx.compose.foundation.layout.Arrangement
2417
import androidx.compose.foundation.layout.IntrinsicSize
@@ -39,27 +32,31 @@ import androidx.compose.runtime.getValue
3932
import androidx.compose.runtime.mutableStateListOf
4033
import androidx.compose.runtime.mutableStateOf
4134
import androidx.compose.runtime.remember
35+
import androidx.compose.runtime.rememberUpdatedState
4236
import androidx.compose.runtime.setValue
4337
import androidx.compose.ui.Alignment
4438
import androidx.compose.ui.Modifier
4539
import androidx.compose.ui.draw.clip
4640
import androidx.compose.ui.graphics.Color
41+
import androidx.compose.ui.layout.Layout
4742
import androidx.compose.ui.platform.LocalDensity
4843
import androidx.compose.ui.res.painterResource
4944
import androidx.compose.ui.text.TextStyle
45+
import androidx.compose.ui.text.font.FontFamily
5046
import androidx.compose.ui.text.font.FontWeight
47+
import androidx.compose.ui.text.rememberTextMeasurer
5148
import androidx.compose.ui.text.style.TextOverflow
52-
import androidx.compose.ui.unit.Density
5349
import androidx.compose.ui.unit.TextUnit
5450
import androidx.compose.ui.unit.dp
5551
import androidx.compose.ui.unit.sp
5652
import com.getcode.theme.CodeTheme
5753
import com.getcode.ui.components.R
58-
import com.getcode.ui.components.text.AmountSizeStore.remember
5954
import com.getcode.ui.components.text.NumberInputHelper.Companion.DECIMAL_SEPARATOR
6055
import com.getcode.ui.components.text.NumberInputHelper.Companion.GROUPING_SEPARATOR
56+
import com.getcode.ui.components.text.animated.AnimatedPlaceholderDigit
57+
import com.getcode.ui.components.text.animated.Digit
58+
import com.getcode.ui.components.text.animated.NormalizedPrefixText
6159
import java.util.Timer
62-
import kotlin.collections.HashMap
6360
import kotlin.concurrent.schedule
6461

6562
interface AmountInputViewModel {
@@ -92,7 +89,6 @@ internal fun AmountTextAnimated(
9289
val density = LocalDensity.current
9390

9491
val staticX5 = CodeTheme.dimens.staticGrid.x5
95-
val staticX8 = CodeTheme.dimens.staticGrid.x8
9692

9793
// Initialize visibility states based on uiModel
9894
val initialAmount = uiModel.amountData.amount
@@ -289,7 +285,7 @@ internal fun AmountTextAnimated(
289285
modifier = Modifier
290286
.fillMaxWidth()
291287
.padding(top = CodeTheme.dimens.grid.x2)
292-
.height(IntrinsicSize.Max),
288+
.height(IntrinsicSize.Min),
293289
horizontalArrangement = Arrangement.Center,
294290
verticalAlignment = Alignment.CenterVertically,
295291
) {
@@ -303,18 +299,14 @@ internal fun AmountTextAnimated(
303299
Image(
304300
modifier = Modifier
305301
.requiredSize(CodeTheme.dimens.grid.x7)
306-
.clip(CircleShape)
307-
.align(Alignment.CenterVertically),
308-
painter = painterResource(
309-
currencyResId
310-
),
302+
.clip(CircleShape),
303+
painter = painterResource(currencyResId),
311304
contentDescription = ""
312305
)
313306
if (isClickable) {
314307
Image(
315308
modifier = Modifier
316-
.width(CodeTheme.dimens.grid.x4)
317-
.align(Alignment.CenterVertically),
309+
.width(CodeTheme.dimens.grid.x4),
318310
painter = painterResource(R.drawable.ic_dropdown),
319311
contentDescription = ""
320312
)
@@ -329,68 +321,71 @@ internal fun AmountTextAnimated(
329321
.width(CodeTheme.dimens.grid.x1)
330322
)
331323

332-
Text(
333-
modifier = Modifier.fillMaxHeight(),
334-
text = amountPrefix,
335-
fontSize = textSize,
336-
fontWeight = FontWeight.Bold
337-
)
338324

339-
AnimatedPlaceholderDigit(
340-
modifier = Modifier.fillMaxHeight(),
341-
text = firstDigit,
342-
digitVisible = digitVisibility[0],
343-
placeholder = placeholder,
344-
placeholderVisible = zeroVisibility,
345-
fontSize = textSize,
346-
density = density,
347-
placeholderEnter = zeroEnter,
348-
placeholderExit = zeroExit,
349-
placeholderColor = Color.White
350-
)
351-
352-
for (i in 1 until maxDigits) {
353-
Digit(
325+
NormalizedPrefixText(
326+
amountPrefix = amountPrefix,
327+
textStyle = textStyle,
328+
textSize = textSize,
329+
) {
330+
AnimatedPlaceholderDigit(
354331
modifier = Modifier.fillMaxHeight(),
355-
isVisible = getComma(i = i),
356-
text = GROUPING_SEPARATOR.toString(),
332+
text = firstDigit,
333+
digitVisible = digitVisibility[0],
334+
placeholder = placeholder,
335+
placeholderVisible = zeroVisibility,
357336
fontSize = textSize,
358337
density = density,
338+
placeholderEnter = zeroEnter,
339+
placeholderExit = zeroExit,
340+
placeholderColor = Color.White
359341
)
360-
Digit(
361-
modifier = Modifier.fillMaxHeight(),
362-
isVisible = digitVisibility[i],
363-
text = getValue(0, i) ?: getLastValue(0, i) ?: "",
364-
fontSize = textSize,
365-
density = density
366-
)
367-
}
368342

369-
Digit(
370-
modifier = Modifier.fillMaxHeight(),
371-
isVisible = decimalPointVisibility,
372-
text = DECIMAL_SEPARATOR.toString(),
373-
fontSize = textSize,
374-
density = density,
375-
enter = decimalEnter,
376-
exit = decimalExit,
377-
)
343+
for (i in 1 until maxDigits) {
344+
Digit(
345+
modifier = Modifier.fillMaxHeight(),
346+
isVisible = getComma(i = i),
347+
text = GROUPING_SEPARATOR.toString(),
348+
fontSize = textSize,
349+
density = density,
350+
)
351+
Digit(
352+
modifier = Modifier.fillMaxHeight(),
353+
isVisible = digitVisibility[i],
354+
text = getValue(0, i) ?: getLastValue(0, i) ?: "",
355+
fontSize = textSize,
356+
density = density
357+
)
358+
}
378359

379-
for (i in 0 until totalDecimals) {
380-
AnimatedPlaceholderDigit(
381-
text = getValue(1, i) ?: getLastValue(1, i) ?: "0",
382-
digitVisible = digitDecimalVisibility[i],
383-
placeholderVisible = digitDecimalZeroVisibility[i],
384-
placeholder = "0",
360+
Digit(
361+
modifier = Modifier.fillMaxHeight(),
362+
isVisible = decimalPointVisibility,
363+
text = DECIMAL_SEPARATOR.toString(),
385364
fontSize = textSize,
386365
density = density,
387-
placeholderEnter = decimalZeroEnter,
388-
placeholderExit = decimalZeroExit,
366+
enter = decimalEnter,
367+
exit = decimalExit,
389368
)
369+
370+
for (i in 0 until totalDecimals) {
371+
AnimatedPlaceholderDigit(
372+
text = getValue(1, i) ?: getLastValue(1, i) ?: "0",
373+
digitVisible = digitDecimalVisibility[i],
374+
placeholderVisible = digitDecimalZeroVisibility[i],
375+
placeholder = "0",
376+
fontSize = textSize,
377+
density = density,
378+
placeholderEnter = decimalZeroEnter,
379+
placeholderExit = decimalZeroExit,
380+
modifier = Modifier.fillMaxHeight(),
381+
)
382+
}
390383
}
391384

392385
Text(
393-
modifier = Modifier.padding(end = CodeTheme.dimens.grid.x3),
386+
modifier = Modifier
387+
.fillMaxHeight()
388+
.padding(end = CodeTheme.dimens.grid.x3),
394389
text = amountSuffix.ifEmpty { " " },
395390
fontSize = textSize,
396391
fontWeight = FontWeight.Bold,
@@ -406,142 +401,4 @@ internal fun AmountTextAnimated(
406401
}
407402
}
408403

409-
private fun defaultDigitEnter(initialOffsetY: Int): EnterTransition {
410-
return (slideInVertically(
411-
initialOffsetY = { initialOffsetY },
412-
animationSpec = tween(
413-
durationMillis = 300,
414-
delayMillis = 80,
415-
easing = LinearOutSlowInEasing
416-
)
417-
) + fadeIn())
418-
}
419-
420-
@Composable
421-
private fun defaultDigitExit(targetOffsetY: Int): ExitTransition {
422-
return fadeOut() + slideOutVertically(
423-
targetOffsetY = { targetOffsetY },
424-
animationSpec = tween(durationMillis = 300, easing = LinearOutSlowInEasing)
425-
)
426-
}
427-
428-
@Composable
429-
private fun Digit(
430-
isVisible: Boolean,
431-
text: String,
432-
fontSize: TextUnit,
433-
density: Density,
434-
modifier: Modifier = Modifier,
435-
enter: EnterTransition? = null,
436-
exit: ExitTransition? = null,
437-
color: Color = Color.White,
438-
) {
439-
val staticX4 = CodeTheme.dimens.staticGrid.x4
440-
val defaultEnter = defaultDigitEnter(initialOffsetY = with(density) { -(staticX4).roundToPx() })
441-
val enterTransition = remember(enter) {
442-
enter ?: defaultEnter
443-
}
444-
445-
val defaultExit = defaultDigitExit(targetOffsetY = with(density) { -(staticX4).roundToPx() })
446-
val exitTransition = remember(exit) {
447-
exit ?: defaultExit
448-
}
449-
450-
Row(
451-
modifier = modifier,
452-
) {
453-
AnimatedContent(
454-
targetState = isVisible,
455-
transitionSpec = { enterTransition togetherWith exitTransition }
456-
) { visible ->
457-
if (visible) {
458-
Text(
459-
text = text,
460-
fontSize = fontSize,
461-
fontWeight = FontWeight.Bold,
462-
color = color
463-
)
464-
} else {
465-
Spacer(modifier = Modifier)
466-
}
467-
}
468-
}
469-
}
470-
471-
472-
private enum class PlaceholderState {
473-
Digit, Placeholder, None
474-
}
475-
476-
@Composable
477-
private fun AnimatedPlaceholderDigit(
478-
text: String,
479-
placeholder: String,
480-
digitVisible: Boolean,
481-
placeholderVisible: Boolean,
482-
fontSize: TextUnit,
483-
density: Density,
484-
modifier: Modifier = Modifier,
485-
placeholderEnter: EnterTransition,
486-
placeholderExit: ExitTransition,
487-
placeholderColor: Color = Color.White.copy(alpha = .2f),
488-
) {
489-
val targetState = when {
490-
digitVisible -> PlaceholderState.Digit
491-
placeholderVisible -> PlaceholderState.Placeholder
492-
else -> PlaceholderState.None
493-
}
494-
495-
val staticX4 = CodeTheme.dimens.staticGrid.x4
496-
val digitEnter = defaultDigitEnter(initialOffsetY = with(density) { -(staticX4).roundToPx() })
497-
val digitExit = defaultDigitExit(targetOffsetY = with(density) { -(staticX4).roundToPx() })
498-
499-
AnimatedContent(
500-
targetState = targetState,
501-
transitionSpec = {
502-
when {
503-
// Placeholder -> Digit: Placeholder exits, Digit enters
504-
initialState == PlaceholderState.Placeholder&& targetState == PlaceholderState.Digit -> {
505-
digitEnter togetherWith placeholderExit
506-
}
507-
// Digit -> Placeholder: Digit exits, Placeholder enters
508-
initialState == PlaceholderState.Digit&& targetState == PlaceholderState.Placeholder -> {
509-
placeholderEnter togetherWith digitExit
510-
}
511-
512-
// None -> Placeholder: None exits, Placeholder enters
513-
initialState == PlaceholderState.None && targetState == PlaceholderState.Placeholder -> {
514-
placeholderEnter togetherWith ExitTransition.None
515-
}
516-
517-
else -> {
518-
EnterTransition.None togetherWith ExitTransition.None
519-
}
520-
}
521-
},
522-
modifier = modifier
523-
) { state ->
524-
when (state) {
525-
PlaceholderState.Digit -> {
526-
Text(
527-
text = text,
528-
fontSize = fontSize,
529-
fontWeight = FontWeight.Bold,
530-
color = Color.White
531-
)
532-
}
533-
PlaceholderState.Placeholder -> {
534-
Text(
535-
text = placeholder,
536-
fontSize = fontSize,
537-
fontWeight = FontWeight.Bold,
538-
color = placeholderColor
539-
)
540-
}
541-
542-
PlaceholderState.None -> Unit
543-
}
544-
}
545-
}
546-
547404
private val AnimationMaxY = 120.dp

0 commit comments

Comments
 (0)