diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/PriceVisualTransformation.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/PriceVisualTransformation.kt new file mode 100644 index 00000000..e1e61b62 --- /dev/null +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/PriceVisualTransformation.kt @@ -0,0 +1,50 @@ +package com.susu.core.designsystem.component.textfield + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import java.text.DecimalFormat + +class PriceVisualTransformation( + private val postfix: String, +) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val amount = text.text + + if (amount.isEmpty()) { + return TransformedText(AnnotatedString(""), OffsetMapping.Identity) + } + + val formatAmount = DecimalFormat("#,###").format(amount.toBigDecimal()) + + return TransformedText( + text = AnnotatedString(formatAmount + postfix), + offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + if (offset <= 1) return offset + + val entireCommaCount = if (amount.length % 3 == 0) amount.length / 3 - 1 else amount.length / 3 + val sliceUntil = if (offset + entireCommaCount <= formatAmount.length) offset + entireCommaCount else formatAmount.length + val commaBeforeOffsetCount = formatAmount.substring(0 until sliceUntil).count { it == ',' } + + return offset + commaBeforeOffsetCount + } + + override fun transformedToOriginal(offset: Int): Int { + return when (offset) { + in 0..1 -> offset + in 2 until formatAmount.length -> { + val entireCommaCount = if (amount.length % 3 == 0) amount.length / 3 - 1 else amount.length / 3 + val sliceUntil = if (offset + entireCommaCount <= formatAmount.length) offset + entireCommaCount else formatAmount.length + val commaBeforeOffsetCount = formatAmount.substring(0 until sliceUntil).count { it == ',' } + offset - commaBeforeOffsetCount + } + + else -> amount.length + } + } + }, + ) + } +} diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/SusuTextField.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/SusuTextField.kt new file mode 100644 index 00000000..efdeb64c --- /dev/null +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/SusuTextField.kt @@ -0,0 +1,125 @@ +package com.susu.core.designsystem.component.textfield + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import com.susu.core.designsystem.R +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray40 +import com.susu.core.designsystem.theme.SusuTheme + +@Composable +fun SusuBasicTextField( + modifier: Modifier = Modifier, + text: String = "", + onTextChange: (String) -> Unit = {}, + placeholder: String = "", + textColor: Color = Gray100, + placeholderColor: Color = Gray40, + enabled: Boolean = true, + textStyle: TextStyle = SusuTheme.typography.title_xl, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + maxLines: Int = 1, + minLines: Int = 1, + visualTransformation: VisualTransformation = VisualTransformation.None, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + cursorBrush: Brush = SolidColor(Color.Black), +) { + BasicTextField( + modifier = modifier, + value = text, + onValueChange = onTextChange, + enabled = enabled, + textStyle = textStyle.copy(color = textColor), + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + maxLines = maxLines, + minLines = minLines, + visualTransformation = visualTransformation, + interactionSource = interactionSource, + cursorBrush = cursorBrush, + ) { innerTextField -> + if (text.isEmpty()) { + Text( + text = placeholder, + style = textStyle.copy(color = placeholderColor), + ) + } + innerTextField() + } +} + +@Composable +fun SusuPriceTextField( + modifier: Modifier = Modifier, + text: String = "", + onTextChange: (String) -> Unit = {}, + placeholder: String = "", + textColor: Color = Gray100, + placeholderColor: Color = Gray40, + enabled: Boolean = true, + textStyle: TextStyle = SusuTheme.typography.title_xl, + keyboardActions: KeyboardActions = KeyboardActions.Default, + maxLines: Int = 1, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + cursorBrush: Brush = SolidColor(Color.Black), +) { + val moneyUnitString = stringResource(R.string.money_unit) + SusuBasicTextField( + modifier = modifier, + text = text, + onTextChange = onTextChange, + placeholder = placeholder, + textColor = textColor, + placeholderColor = placeholderColor, + enabled = enabled, + textStyle = textStyle, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + keyboardActions = keyboardActions, + maxLines = maxLines, + minLines = minLines, + interactionSource = interactionSource, + cursorBrush = cursorBrush, + visualTransformation = PriceVisualTransformation(postfix = moneyUnitString), + ) +} + +@Composable +@Preview +fun SusuBasicTextFieldPreview() { + SusuTheme { + var text by remember { mutableStateOf("") } + var price by remember { mutableStateOf("") } + Column { + SusuBasicTextField( + text = text, + onTextChange = { text = it }, + placeholder = "이름을 입력해주세요", + ) + SusuPriceTextField( + text = price, + onTextChange = { price = it }, + placeholder = "금액을 입력해주세요", + ) + } + } +} diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/SusuUnderlineTextField.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/SusuUnderlineTextField.kt new file mode 100644 index 00000000..2eb082e5 --- /dev/null +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfield/SusuUnderlineTextField.kt @@ -0,0 +1,174 @@ +package com.susu.core.designsystem.component.textfield + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.component.util.ClearIconButton +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray30 +import com.susu.core.designsystem.theme.Gray50 +import com.susu.core.designsystem.theme.Red60 +import com.susu.core.designsystem.theme.SusuTheme + +enum class SusuUnderlineTextFieldColor( + val textColor: Color, + val underlineColor: Color, + val limitationColor: Color, + val descriptionColor: Color, +) { + Inactive( + textColor = Gray30, + underlineColor = Gray50, + limitationColor = Gray30, + descriptionColor = Color.Unspecified, + ), + Active( + textColor = Gray100, + underlineColor = Gray100, + limitationColor = Gray30, + descriptionColor = Color.Unspecified, + ), + Error( + textColor = Gray100, + underlineColor = Red60, + limitationColor = Red60, + descriptionColor = Red60, + ), +} + +data class SusuUnderlineTextFieldTextStyle( + val contentTextStyle: TextStyle, + val limitationTextStyle: TextStyle, + val descriptionTextStyle: TextStyle, +) + +object SusuUnderlineTextFieldDefault { + val textStyle: @Composable () -> SusuUnderlineTextFieldTextStyle = { + SusuUnderlineTextFieldTextStyle( + contentTextStyle = SusuTheme.typography.title_l, + limitationTextStyle = SusuTheme.typography.title_xs, + descriptionTextStyle = SusuTheme.typography.text_xxs, + ) + } +} + +@Composable +fun SusuUnderlineTextField( + modifier: Modifier = Modifier, + text: String = "", + onTextChange: (String) -> Unit = {}, + placeholder: String = "", + placeholderColor: Color = Gray30, + description: String? = null, + isError: Boolean = false, + lengthLimit: Int = 20, + onClickClearIcon: () -> Unit = {}, + textStyle: @Composable () -> SusuUnderlineTextFieldTextStyle = SusuUnderlineTextFieldDefault.textStyle, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + cursorBrush: Brush = SolidColor(Color.Black), +) { + val textFieldColor = when { + isError -> SusuUnderlineTextFieldColor.Error + text.isEmpty() -> SusuUnderlineTextFieldColor.Inactive + else -> SusuUnderlineTextFieldColor.Active + } + + with(textStyle()) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_s), + ) { + Row( + modifier = Modifier + .drawBehind { + drawLine( + color = textFieldColor.underlineColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1f, + ) + } + .padding(SusuTheme.spacing.spacing_xxs), + verticalAlignment = Alignment.CenterVertically, + ) { + SusuBasicTextField( + modifier = Modifier.weight(1f), + text = text, + textColor = textFieldColor.textColor, + onTextChange = onTextChange, + placeholder = placeholder, + placeholderColor = placeholderColor, + textStyle = contentTextStyle, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + visualTransformation = visualTransformation, + interactionSource = interactionSource, + cursorBrush = cursorBrush, + ) + if (text.isNotEmpty()) { + Box(modifier = Modifier.padding(horizontal = SusuTheme.spacing.spacing_s)) { + ClearIconButton(iconSize = 24.dp, onClick = onClickClearIcon) + } + } else { + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxs)) + } + Text( + text = "${text.length}/$lengthLimit", + style = limitationTextStyle.copy(color = textFieldColor.limitationColor), + ) + } + description?.let { descriptionText -> + Text(text = descriptionText, style = descriptionTextStyle.copy(color = textFieldColor.descriptionColor)) + } + } + } +} + +@Preview +@Composable +fun SusuUnderlineTextFieldPreview() { + SusuTheme { + var text by remember { mutableStateOf("") } + Column { + SusuUnderlineTextField( + text = text, + onTextChange = { text = it }, + placeholder = "김수수", + ) + SusuUnderlineTextField( + text = text, + onTextChange = { text = it }, + placeholder = "김수수", + isError = true, + description = "에러 메세지를 입력하세요", + ) + } + } +} diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt index a9fb2336..cb4a76e7 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt @@ -42,6 +42,7 @@ import com.susu.core.designsystem.component.textfieldbutton.style.InnerButtonSty import com.susu.core.designsystem.component.textfieldbutton.style.LargeTextFieldButtonStyle import com.susu.core.designsystem.component.textfieldbutton.style.SmallTextFieldButtonStyle import com.susu.core.designsystem.component.textfieldbutton.style.TextFieldButtonStyle +import com.susu.core.designsystem.component.util.ClearIconButton import com.susu.core.designsystem.theme.SusuTheme import com.susu.core.ui.extension.disabledHorizontalPointerInputScroll import com.susu.core.ui.extension.susuClickable @@ -294,13 +295,9 @@ private fun InnerButtons( if (isSaved.not()) { Box(modifier = Modifier.size(clearIconSize)) { if (showClearIcon) { - Image( - modifier = Modifier - .clip(CircleShape) - .size(clearIconSize) - .susuClickable(onClick = onClickClearIcon), - painter = painterResource(id = R.drawable.ic_clear), - contentDescription = "", + ClearIconButton( + iconSize = clearIconSize, + onClick = onClickClearIcon, ) } } diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/util/ClearIconButton.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/util/ClearIconButton.kt new file mode 100644 index 00000000..6caad53e --- /dev/null +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/util/ClearIconButton.kt @@ -0,0 +1,27 @@ +package com.susu.core.designsystem.component.util + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import com.susu.core.designsystem.R +import com.susu.core.ui.extension.susuClickable + +@Composable +fun ClearIconButton( + iconSize: Dp, + onClick: () -> Unit, +) { + Image( + modifier = Modifier + .clip(CircleShape) + .size(iconSize) + .susuClickable(onClick = onClick), + painter = painterResource(id = R.drawable.ic_clear), + contentDescription = "", + ) +} diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index a0872b86..86654a4d 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -2,4 +2,6 @@ 편집 저장 + + %s원