Shunz Android Dev Note

Compose BasicTextField2 - 꿈의 텍스트필드 [2/2] 본문

Compose

Compose BasicTextField2 - 꿈의 텍스트필드 [2/2]

_Shun_ 2023. 11. 22. 22:10

 

 

2부로 구성된 이 블로그 시리즈에서는 Jetpack Compose 텍스트 필드의 과거, 현재, 미래에 대하여 자세히 다룹니다. 새로 도입된 BasicTextField2에 대해서 알아보세요.

 

Compose 텍스트 팀은 차세대 TextField API를 개발하고 있습니다. 여러분들은 지금 당장 사용(시험)해 볼 수 있습니다. BasicTextField2text2 패키지의 foundation 1.6.0 알파버전에서 이용 가능합니다.

 


1편 요약

1편에서는 다음 방법을 다루었습니다.

  • 상태 동기화 문제를 방지하기 위해 새로운 API로 상태를 관리
  • 데코레이터와 라인 제한을 포함한 기본 스타일링
  • 상태를 관찰하고 비즈니스 규칙을 적용할 때 일반적인 시나리오를 해결
  • TextFieldBuffer를 사용하여 프로그래밍 방식으로 텍스트 필드 값 편집

 

필터링 | 입력 변환

숫자만 허용하는 텍스트 필드를 구현한다고 가정해보겠습니다.

 

예를 들어, 숫자만 받아들이거나 특수문자를 생략하는 등 사용자의 입력을 필터링 하려면 InputTransformation을 정의합니다. 이렇게 하면 텍스트 필드 상태로 저장하기 전에 사용자의 입력이 수정됩니다. 이 작업은 가역적이지 않으므로 변환과 일치하지 않는 사용자의 입력을 잃어버리게 됩니다. 그래서 이 작업을 "filters(필터)"라고 부릅니다.

input & output transformation

 

InputTransformation API의 형태는 다음과 같습니다.

fun interface InputTransformation {

    val keyboardOptions: KeyboardOptions? get() = null

    fun transformInput(
        originalValue: TextFieldCharSequence, 
        valueWithChanges: TextFieldBuffer
    )    
}

 

이 transformInput 메서드는 원래 입력한 텍스트와 TextFieldBuffer 형태로 변경된 값을 포함하고 있습니다. TextFieldBuffer API는 ChangeList 유형의 변경 목록을 제공합니다.

class TextFieldBuffer {
    // other fields and methods

    val changes: ChangeList get()

    interface ChangeList {
        val changeCount: Int
        fun getRange(changeIndex: Int): TextRange
        fun getOriginalRange(changeIndex: Int): TextRange
    }
}

 

변경 사항을 모두 삭제하는 것을 포함하여 모든 작업을 수행할 수 있습니다.

object DigitsOnlyTransformation : InputTransformation {
    override val keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)

    override fun transformInput(
        originalValue: TextFieldCharSequence,
        valueWithChanges: TextFieldBuffer
    ) {
        if (!valueWithChanges.asCharSequence().isDigitsOnly()) {
            valueWithChanges.revertAllChanges()
        }
    }
}

// Compose

BasicTextField2(
    state = state,
    inputTransformation = DigitsOnlyFilter
)

 

InputTransformation 인터페이스를 구현하는 객체를 정의합니다. 먼저 transformInput 메서드를 구현합니다.

이 예제에서, 우리는 TextFieldBuffer에서 변경 사항을 확인합니다. 만약 그것이 숫자만 포함한다면, 우리는 변경 사항을 유지합니다. 하지만 문자가 숫자가 아니라면, 우리는 그 변경 사항을 되돌리게 됩니다. diff가 우리를 위해 내부적으로 행해지기 때문에, 사용하기에(코드가) 매우 간단합니다.

또한, 해당 키보드 유형을 숫자로 설정(KeyboardType.Number)하고 있습니다.

 

InputTransformation는 함수형 인터페이스이기 때문에, 다음과 같이 BasicTextField2 컴포저블로 직접 람다를 전달 할 수 있습니다.

BasicTextField2(
    state = state,
    inputTransformation = { originalValue, valueWithChanges ->
        if (!valueWithChanges.asCharSequence().isDigitsOnly()) {
            valueWithChanges.revertAllChanges()
        }
    },
    
    // 이 경우 keyboardOption을 BasicTextField2에 직접 전달합니다.
    // 이 옵션은 입력된 Transformation의 옵션을 재정의합니다.
    keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number
)

 

이 특정 변환이 필요한 텍스트 필드가 하나 있는 경우에는 잘 동작합니다. 여러개인 경우에는 변환하는 코드를 고유 객체로 추출하는 것이 합리적입니다.

 

다음으로, 길이가 최대 6자이고 대문자로만 이루어진 입력 필드의 코드를 작성해보겠습니다.

 

이러한 일반적인 사용 사례를 위한 입력 변환 기능이 포함되어 있습니다. 문자열의 길이를 제한하기 위하여 maxLengthInChars를 사용할 수 있고, 문자를 대문자료 표시하기 위하여 allCaps를 사용 할 수 있습니다.

다음과 같이 코드를 작성 할 수 있습니다.

BasicTextField2(
    state = state,
    inputTransformation = InputTransformation.maxLengthInChars(6)
        .then(InputTransformation.allCaps(Locale.current)),
)

 

입력값을 변환 시키기 위하여 then 구문을 사용하면, 필터들이 순차적으로 적용이 됩니다.

 

시각적 변화 | 출력 변환

2023년 11월 초 기준: OutputTransformation API가 개발중입니다. 여기에서 진행 상황을 확인 할 수 있습니다.

 

이제 텍스트 입력 필드 예제에서 사용자가 아직 입력하지 않은 문자를 점으로 대체하고, 그 사이에 공백을 추가하여 다음과 같이 세 글자로 그룹화 하려고 합니다.

 

전화번호나 신용 카드 번호 형식과 같이 텍스트 필드 내용을 형식화해야 하는 경우에 OutputTransformation을 정의해야 합니다. UI에 표시 할 때 내부 상태의 형식을 지정합니다. 적용 결과가 텍스트 필드 상태에 저장되는 InputTransformation과 달리 OutputTransformation에 적용되는 변경사항은 별도의 상태값으로 저장되지 않습니다.

 

OutputTransformation API의 형태는 다음과 같습니다.

fun interface OutputTransformation {

    fun transformOutput(buffer: TextFieldBuffer)
}

 

새로운 API의 가장 큰 장점은 원본 텍스트와 변경된 텍스트의 offset mapping을 제공할 필요가 없다는 것입니다. 텍스트 필드는 이를 암묵적으로 처리합니다.

다음 예제에서는 OutputTransformation 인터페이스를 구체화하고 transformOuptput 메서드도 구체화하고 있습니다.

object VerificationCodeOutputTransformation : OutputTransformation {

    override fun transformOutput(buffer: TextFieldBuffer) {
        // 텍스트가 너무 짧으면 자리 표시자 문자로 표시합니다.
        // ··· ···
        val padCount = 6 - buffer.length
        repeat(padCount) {
            buffer.append('·')
        }

        // 123 456
        if (buffer.length > 3) buffer.insert(3, " ")
    }
}

 

먼저, 아직 타이핑하지 않는 문자의 자리에 점문자(.)를 삽입합니다. 그런 다음 insert 메서드를 호출하여 세 자리 사이에 공백을 추가합니다. 이게 전부입니다. 이전 API에서 사용하던 offset mapping은 혼란만 가중시키고 사용하기에 다소 복잡하기 때문에 훌륭한 코드가 아니었습니다. 정말 달콤하지 않나요? :-)

 

SecureTextField

비밀번호를 입력하는 텍스트 필드를 살펴보겠습니다. 비밀번호 입력 필드는 매우 일반적인 사용 사례이므로 BasicSecureTextField라는 BasicTextField2 위에 구현된 새로운 컴포저블 파일이 있습니다.

 

val password = rememberTextFieldState()
BasicSecureTextField(
    state = password
    textObfuscationMode = TextObfuscationMode.RevealLastTyped
)

 

textObfuscationMode 속성을 설정하는 3가지 유용한 모드가 있습니다. RevealLastTyped는 기본값으로 view system의 EditText가 입력 유형을 textPassword로 구성할 때 수행하는 동작과 동일합니다. 이 동작은 사용자가 다음 문자를 입력하기 전에 마지막에 입력한 문자를 보여주도록 합니다. 그리고 Hidden은 타이핑하는것을 절대 보여주지 않고, Visible은 타이핑하는것을 모두 보여주는 동작을 수행합니다. Visible의 경우 일시적으로 보여주다가 다시 숨기는 동작을 할 때 유용하게 사용 할 수 있습니다.

 

BasicSecureTextField가 별도의 컴포저블로 분리됨에 따라 매우 강력한 이점을 제공합니다. 이를 통해 팀은 보안을 최적화하여 필드 콘텐츠가 메모리에 필요 이상으로 지속되지 않도록 하여 memory spoofing과 같은 것들을 방지 할 수 있습니다. 마스킹 및 텍스트 난독화 모드와 함께 사전에 정의된 UI, 텍스트 도구 모음의 변경과 같은 명시적인 동작도 함께 제공됩니다. (비밀번호 필드의 내용을 자르거나 복사할 수 없음.)

사용자는 비밀번호 필드를 복사하거나 잘라낼 수 없습니다.

 

 

그리고 좀 더...

말하고 싶은게 매우 많지만, 세가지만 더 설명해보려고 합니다.

 

새로운 BasicTextField2 API는 내부 스크롤 상태에 접근하는것을 허용합니다. LazyLayout과 같은 다른 합성 가능한 것처럼 스크롤 상태를 호이스팅합니다. BasicTextField2로 전달하면 이제 다른 합성 가능한 것으로 필드를 프로그래밍 방식으로 스크롤 할 수 있습니다. 예를 들어 텍스트 필드의 스크롤 막대 역할을 하는 수직 슬라이더:

val scrollState = rememberScrollState()

BasicTextField2(
    state = state,
    scrollState = scrollState,
    // ...
)

Slider(
    value = scrollState.value.toFloat(),
    onValueChange = {
        coroutineScope.launch { scrollState.scrollTo(it.roundToInt()) }
    },
    valueRange = 0f..scrollState.maxValue.toFloat()
)

 

TextField의 스크롤을 프로그래밍으로 컨트롤 할 수 있습니다.

 

 

Compose 개발팀은 단어를 선택하기 위해 두 번 두드리는 것과 같은 더 많은 제스처에 대한 기능을 추가했습니다.

 

마지막으로, TextFieldState는 UndoState 클래스에 접근할 수 있도록 해주고 있습니다. 이 클래스는 TextFieldBuffer의 ChangeList 위에 구현된 상태의 기록된 값과 이들의 변경을 취소하거나 다시 실행하는 데 유용한 기능을 제공합니다.

몇줄도 안되는 코드로 개발자는 undo/redo 기능을 구현 할 수 있습니다.

 

val state: TextFieldState = rememberTextFieldState()

Button(
    onClick = { state.undoState.undo() },
    enabled = state.undoState.canUndo
) {
    Text("Undo")
}

Button(
    onClick = { state.undoState.clearHistory() },
    enabled = state.undoState.canUndo || state.undoState.canRedo
) {
    Text("Clear History")
}

 

 

매우 강력한 API들이고, 즉시 사용이 가능합니다. 

 


 

조금 더 많은 정보를 원하시면, 구글에서 Compose Text 의 메인 개발자로 일하고 있는 Zach의 이야기를 들어보세요.

 

 

Reimagining text fields in Compose - droidcon

The Compose Text team is completely rethinking the text field APIs from scratch. Come learn why, how we're approaching the process, and get a sneak peak at what the future might look like.

www.droidcon.com

 

 

지금까지 여러 새로운 기법들을 알아보았습니다.
  • InputTransformation과 함께 필터를 적용 해보았습니다.
  • OutputTransformation과 함께 visual transformation을 적용 해 보았습니다.
  • BasicSecureTextField와 함께 패스워드 입력 필드를 작성 해보았습니다.
또한 새로운 API를 사용하면 편집 과정의 명시적인 변경 사항을 확인하고, 텍스트 필드 스크롤 상태에 접근할 수 있는 등 현재는 불가능한 작업을 수행할 수 있습니다.

 

 

미래

BasicTextField2는 현재 개발중에 있습니다. Compose 1.6.0 알파 버전을 사용 해보고 개발팀에 피드백도 주세요 :-)

피드백 채널은 다음과 같습니다.

 

BasicTextField2는 별도의 패키지인 text2에 있어서 명확히 구분을 할 수 있습니다. 이는 단지 BasicTextField2 API가 안정화되는 동안에만 사용하는 임의 패키지입니다.

다음으로 개발 로드맵상, OutputTransformation API들을 완성할 것입니다. 다중 텍스트 스타일 편집은 요청이 높은 기능이고, 이 새로운 API로 가능합니다.

 

BasicTextField2의 정식 출시를 계속해서 지켜 봐주시기 바랍니다.

 


 

본 포스팅은 이 게시글을 번역한 글입니다.

해당 게시글은 droidcon 컨퍼런스를 토대로 작성되었습니다.