Skip to content

Commit

Permalink
Merge pull request #30 from YAPP-Github/feature/MZ-159-textfield
Browse files Browse the repository at this point in the history
Feature/mz 159 Textfield Component
  • Loading branch information
yangsooplus authored Jan 4, 2024
2 parents 892f54b + 5bf504b commit b9f5737
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
}
},
)
}
}
Original file line number Diff line number Diff line change
@@ -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 = "금액을 입력해주세요",
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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 = "에러 메세지를 입력하세요",
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}
Expand Down
Loading

0 comments on commit b9f5737

Please sign in to comment.