Android

[Android] Compose TextField 천단위 콤마(,) 설정하기

역삼동개발자D 2024. 2. 25. 15:03
반응형

금액을 입력하거나 각종 숫자 값들을 입력하는 경우, 천단위마다 쉼표(,)를 붙이는 경우가 많다. 아래의 이미지와 같이 Android의 Compose TextField를 활용하여 천단위마다 ,를 붙이고 가장 마지막에 단위까지 표시할 수 있는 방법에 대해 알아보자.

 

아래의 코드는 하단의 링크를 참고하여 작성하였습니다.

 

InputTextField을 작성한다

@Composable
fun NumberInputTextField() {
    var text by remember { mutableStateOf("0") }
    // 천단위마다 콤마(,)를 찍을 수 있도록 decimalFormatter를 사용하였다. 또한 입력값 끝에 표시할 단위를 입력하였다.
    val integerVisualTransformation = rememberIntegerVisualTransformation(DecimalFormat("#,###"), "m")

    TextField(
        value = text,
        onValueChange = {
        //초기값을 0으로 하고, input값이 다 지워졌을 경우에도 0으로 설정한다.
            val newValue = if (it == "") "0" else it

        // 만약 숫자가 아닌값이나, Integer보다 큰 값을 입력할 경우 무시하도록 하였다.
            if (newValue.matches(Regex("[0-9]*")) && newValue.toDouble() <= Int.MAX_VALUE) {
                text = (newValue.toIntOrNull() ?: 0).toString()
            }
        },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        visualTransformation = integerVisualTransformation
    )
}

 

IntegerVisualTransformation을 작성한다

IntegerVisualTransformation은 VisualTransformation 인터페이스를 상속받는다.

먼저 간단하게 VisualTransformation에 대해 살펴보자. 아래는 VisualTransformation의 설명이다.

아래의 설명대로라는 input field의 시각적인 결과를 변경하는데 사용되는 인터페이스이다. 즉, 내가 입력한 값이 보여지는 방법을 변경할 수 있도록 하는 인터페이스이다.

Interface used for changing visual output of the input field.
This interface can be used for changing visual output of the text in the input field. For example, you can mask characters in password field with asterisk with PasswordVisualTransformation.

 

하지만 아쉽게도 기본 구현된 VisualTransformation은 아래의 2개밖에 없다...(아래는 VisualTransformation을 상속받은 클래스 목록이다)

따라서 우리는 천단위마다 콤마(,)를 표시하는 커스텀 VisualTransformation을 작성해야한다.

private class IntegerVisualTransformation(
    val decimalFormat: DecimalFormat,
    val unit: String? = null
) : VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText {
        val newText = text.trim().replace(Regex("[^\\d]"), "")
        // 실제로 input field에 보여질 텍스트는 formattedText이다
        val formattedText = decimalFormat.format(newText.toLong()) + unit?.let { " $it" }

        return TransformedText(AnnotatedString(formattedText), IntegerOffsetMapping(text.text, formattedText))
    }
}

@Composable
fun rememberIntegerVisualTransformation(decimalFormat: DecimalFormat, unit: String) : VisualTransformation {
    return remember(decimalFormat) { IntegerVisualTransformation(decimalFormat, unit)}
}

 

IntegerOffsetMapping을 작성한다

다음으로 OffsetMapping에 대해 알아보자.

아래의 VisualTransformation 코드를 확인해보면 filter 함수는 TransformedText를 반환하는데 이때 text외에도 OffsetMapping을 인자로 넣어줘야한다.

@Immutable
fun interface VisualTransformation {
    fun filter(text: AnnotatedString): TransformedText
    ...
}

class TransformedText(
    val text: AnnotatedString,
    val offsetMapping: OffsetMapping
) {
    ...
}

filter 함수는 입력된 텍스트를 받고 그 결과로 변환된 텍스트와 새로운 커서 위치를 반환한다. 이때 offsetMapping은 변환된 텍스트의 문자가 원본 텍스트의 어느 위치로 매핑되는지를 나타내는 역할을 한다.

 

예를 들어 123에서 가장 마지막에 커서를 둔 상태로 4를 입력한다고 하자. 우리가 원하는 결과값은 1,234이며 이때 커서는 가장 마지막에 위치해야한다. 즉, offset은 4에서 5로 변환되어야한다(콤마가 추가되었으므로).

 

그럼 이제 커스텀 OffsetMapping을 작성해보자.

class IntegerOffsetMapping(private val originalText: String, formattedText: String) : OffsetMapping {
    private val indexes = findDigitIndexes(originalText, formattedText)

    override fun originalToTransformed(offset: Int): Int {
        if (offset >= originalText.length) {
            return indexes.last() + 1
        }
        return indexes[offset]
    }

    override fun transformedToOriginal(offset: Int): Int {
        return indexes.indexOfFirst { it >= offset }.takeIf { it != -1 } ?: originalText.length
    }

    private fun findDigitIndexes(firstString: String, secondString: String): List<Int> {
        val digitIndexes = mutableListOf<Int>()
        var currentIndex = 0
        firstString.forEach { digit ->
            val index = secondString.indexOf(digit, currentIndex)
            if (index != -1) {
                digitIndexes.add(index)
                currentIndex = index + 1
            } else {
                return emptyList()
            }
        }
        return digitIndexes

    }
}

 

 

참고

https://proandroiddev.com/editing-currency-textfields-in-jetpack-compose-b7074b4682ea

반응형