TextField 를 통한 사용자 입력을 받을 때 UI/UX 를 위해 단위 표시를 받는 부분들이 자주 생길 것이다.
하지만 이러한 Unit 을 유저에게 보여주기 위해 Input Value 에 Unit 을 집어넣는다면 코드단에서 다시 Unit 을 지워줘야하므로 불필요한 로직이 발생할 수 있으며, 때로는 버그가 발생할 수도 있다.
이러한 상황을 대비해서 Jetpack Compose TextField 에서는 VisualTransformation 이라는 기능을 통해 실제 값에는 영향이 가지 않는, 사용자에게 시각적으로만 표시가 되어지는 기능을 사용할 수 있다.
VisualTransformation 이란?
- Jetpack Compose에서 TextField의 입력값을 시각적으로 변환하기 위해 사용하는 인터페이스
- 사용자가 입력한 텍스트를 다른 형태로 표시하고자 할 때 활용
- onValueChanged 의 Return 값은 VisualTransformation 이 적용되지 않은 값으로 Return 된다.
- TextField 를 사용할 때 원, 달러나 10세, 19세 등 나이표시와 같은 단위 표시를 할 때 주로 사용
테스트
말로만 설명하면 이해가 쉽게 가지 않을 수 있으니, 테스트를 해보도록 하자.
TextField 의 visuableTransformation 은 기본적으로 VisualTransfomation.None 으로 설정되어 있기 때문에, 개발자가 따로 지정해주지 않는다면 아무런 시각 효과도 제공해주지 않는다.
그러므로 나는 PasswordVisualTransformation 을 사용하여 TextField 를 구현 해보도록 해보겠다.
@Composable
fun Test(modifier: Modifier){
var textFieldValue by remember { mutableStateOf(TextFieldValue()) }
TextField(
modifier = modifier.fillMaxWidth(),
visualTransformation = PasswordVisualTransformation(),
value = textFieldValue,
onValueChange = {
Log.e("확인", "${it.text}")
textFieldValue = it
},
placeholder = {
Text(text = "Type something here")
}
)
}
위와 같이 TextField 에 TextFieldValue 를 설정해주고 키보드 입력이 들어올 때마다 로그와 함께 TextFieldValue 를 업데이트 하도록 설정하였다.
visualTransformation 에는 PasswordVisualTransformation() 을 설정해주었다.
그럼 에뮬레이터를 실행하고 텍스트필드에 숫자를 입력해보자.
숫자 입력 화면
우리가 입력한 값은 숫자임에도 암호화된 것처럼 숫자가 보이지 않고 있다.
그렇다면 Return Value 는 로그에서 어떻게 찍히고 있을까?
보이는 형식과는 입력값이 정상적으로 Return 되고 있다.
위와 같이 설정되는 이유는 무엇일까?
이유는 PasswordVisualTransformation 의 내부 코드를 보면 알 수 있다.
PasswordVisualTransformation 는 VisualTransformation Interface 를 상속받고 있으며, override 한 filter 클래스에서 입력 값인 text.text 의 길이 만큼 \u2022 문자로 변환한 값으로 Transform 하여 TextField 에 전달하기 때문이다.
class PasswordVisualTransformation(val mask: Char = '\u2022') : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return TransformedText(
AnnotatedString(mask.toString().repeat(text.text.length)),
OffsetMapping.Identity
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PasswordVisualTransformation) return false
if (mask != other.mask) return false
return true
}
override fun hashCode(): Int {
return mask.hashCode()
}
}
VisualTransformation 커스텀 방법
- PasswordVisualTransformation 와 동일하게 VisualTransformation 을 상속받는 커스텀 클래스를 구현해준다.
- Filter 함수를 Override 하여 TransformedText 를 Return 해준다.
class CustomVisualTransformation: VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val trimmed = if (text.text.length >= 10) text.text.substring(0..9) else text.text
val annotatedString = AnnotatedString.Builder().apply {
for (i in trimmed.indices) {
append(trimmed[i])
if (i == 2 || i == 5) append("-")
}
}.toAnnotatedString()
val offsetMapping = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 2) return offset
if (offset <= 5) return offset + 1
if (offset <= 9) return offset + 2
return 12
}
override fun transformedToOriginal(offset: Int): Int {
if (offset <= 2) return offset
if (offset <= 6) return offset - 1
if (offset <= 10) return offset - 2
return 10
}
}
return TransformedText(annotatedString, offsetMapping)
}
}
위의 클래스에서 TransformedText 에 설정값들의 설명은 다음과 같다
- annotatedString : 유저에게 시각적으로 보여주기 위한 Text
- offsetMapping : 텍스트 입력 및 삭제 등 이벤트 시 커서의 위치를 설정
- originalToTransformed : 사용자가 원본 텍스트에 커서를 놓았을 때, 변환된 텍스트로 Offset 을 변환.
- transformToOriginal : 사용자가 변환된 텍스트에서 커서를 이동할 때, 현재 커서 위치를 원본 텍스트의 인덱스로 변환하여 원본 데이터에 반영.
OriginalToTransformed 예시
- 0101234567 입력 시 커서를 1에 놓는 경우 Offset 이 4이지만, 하이픈이 추가되었으므로 5를 Return.
TransformToOriginal 예시
- 010-123-5678 에서 커서를 1에 놓는 경우 Offset 이 - 까지 포함되어 5이지만, 하이픈 값을 제거하였으므로 4를 Return