Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

TextField 씹고 뜯고 맛보고 즐기고

HyunWoo Lee
June 10, 2024
340

TextField 씹고 뜯고 맛보고 즐기고

DroidKnights 2024에서 진행한 TextField 씹고 뜯고 맛보고 즐기고 발표의 Speaker Deck입니다.

HyunWoo Lee

June 10, 2024
Tweet

Transcript

  1. Index ❏ First of First: TextField와 BasicTextField ❏ Overview: BasicTextField

    ❏ String vs TextFieldValue ❏ “Right” onTextChange ❏ VisualTransformation: 주민등록번호 TextField 만들어보기 ߊ಴ ଵҊ Repository
  2. @Composable fun TextField( value: String, onValueChange: (String) -> Unit, modifier:

    Modifier = Modifier, // etc ) { // etc CompositionLocalProvider( LocalTextSelectionColors provides colors.textSelectionColors ) { BasicTextField( value = value, modifier = modifier .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, minHeight = TextFieldDefaults.MinHeight ), }
  3. @Composable fun TextField( value: String, onValueChange: (String) -> Unit, modifier:

    Modifier = Modifier, // etc ) { // etc CompositionLocalProvider( LocalTextSelectionColors provides colors.textSelectionColors ) { BasicTextField( value = value, modifier = modifier .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, minHeight = TextFieldDefaults.MinHeight ), }
  4. @Immutable object TextFieldDefaults { val MinHeight = 56.dp val MinWidth

    = 280.dp } TextFieldח ୭ࣗ ց࠺/֫੉о Ҋ੿
  5. @Composable fun BasicTextField( value: String, onValueChange: (String) -> Unit, modifier:

    Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = TextStyle.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, visualTransformation: VisualTransformation = VisualTransformation.None, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource? = null, cursorBrush: Brush = SolidColor(Color.Black), decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit = @Composable { innerTextField -> innerTextField() } ) {
  6. @Composable fun BasicTextField( value: String, onValueChange: (String) -> Unit, modifier:

    Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle =
  7. minLines: Int = 1, visualTransformation: VisualTransformation = VisualTransformation.None, decorationBox: @Composable

    (innerTextField: @Composable () -> Unit) -> Unit = @Composable { innerTextField -> innerTextField() }
  8. ࢶ঱ഋ UIীࢲ੄ ࢚క ҙܻ Ӓۧ׮ݶ TextFieldܳ ੜ ഝਊೞӝ ਤ೧ࢲ ঌইঠ

    ೞח ೙ࣻ ૑धੋ Compose੄ ࢚కҙܻ ী ؀೧ рۚ൤ ੉ঠӝ ೞҊ TextFieldী ؀೧ ؊ Ө੉ ׮ܞࠁب۾ ೞѷणפ׮.
  9. ࢶ঱ഋ UIীࢲ੄ ؘ੉ఠ(࢚క) ҙܻ ❏ ӝઓ XML ӝ߈੄ EditText৬ח ׳ܻ,

    BasicTextFieldח ࢎਊ੗о ૒੽ ੑ/୹۱غ ח ч(࢚క)ਸ ҙܻ ❏ TextField ࢎਊ੗о ҙܻೞח ߸ࣻ੄ чਸ ୹۱ೞח ԇؘӝ ৉ೡ ❏ value: ࢎਊ੗о ҙܻೞח ߸ࣻ ❏ onValueChange: ৻ࠗ ੑ۱੢஖ܳ ా೧ ٜযয়ח чਸ ഝਊೞৈ ߸ࣻ੄ чਸ ߸ ҃दఆ ࣻ ੓ח ۈ׮ ೣࣻ ❏ Composeܳ ࠺܃ೠ ׮নೠ ࢶ঱ഋ UI੄ ӝࠄ੸ੋ ࢚క ҙܻ ߑध
  10. ࢶ঱ഋ UIীࢲ੄ ؘ੉ఠ(࢚క) ҙܻ ❏ ӝઓ XML ӝ߈੄ EditText৬ח ׳ܻ,

    BasicTextFieldח ࢎਊ੗о ૒੽ ੑ/୹۱غ ח ч(࢚క)ਸ ҙܻ ❏ TextField ࢎਊ੗о ҙܻೞח ߸ࣻ੄ чਸ ୹۱ೞח ԇؘӝ ৉ೡ ❏ value: ࢎਊ੗о ҙܻೞח ߸ࣻ ❏ onValueChange: ৻ࠗ ੑ۱੢஖ܳ ా೧ ٜযয়ח чਸ ഝਊೞৈ ߸ࣻ੄ чਸ ߸ ҃दఆ ࣻ ੓ח ۈ׮ ೣࣻ ❏ Composeܳ ࠺܃ೠ ׮নೠ ࢶ঱ഋ UI੄ ӝࠄ੸ੋ ࢚క ҙܻ ߑध •ஹನૉܳ ׮ܖ׮ࠁݶ о੢ ݢ੷ ݅աѱ غח ҙ ޙ਷ ߄۽ ࢚క ҙܻੑפ׮. ࠭ী ࠁৈ૑ח ؘ ੉ఠܳ ҙܻೞח ߑध੉ ӝઓҗח ௼ѱ ׮ؘܲ ਃ •XML ӝ߈੄ EditTextীࢲח EditTextо о૑ Ҋ ੓ח valueܳ ѐߊ੗о Viewё୓ী ੽Ӕೞ ৈ ૒੽ ߸҃ਸ दெঠ ೮૑݅, ஹನૉীࢲח ੉ valueܳ ࢎਊ੗о ૒੽ ߸ࣻ۽ ҙܻೡ ࣻ ੓ ѱ ٜ݅য઎णפ׮. •BasicTextField ղীࢲ чਸ ߸҃दఃח ݫழ פ્੉ ইפӝী, ੉ઁ ੷൞ח TextField੄ ࢚ కܳ о૑ח чҗ ৻ࠗ ੑ۱੢஖ܳ ా೧ ੑ۱ ػ чਸ ഝਊ೧ чਸ ߸҃दఆ ࣻ ੓ח ௒ߔ ೣ ࣻܳ ഝਊೞৈ ࢚కܳ ҙܻೡ ࣻ ੓णפ׮.
  11. @Composable fun SampleTextField() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = it } ) } ৘दܳ э੉ ࠁदભ ੿݈ рױೠ TextField ഝਊ৘ઁܳ оઉ৬ࠌणפ׮.
  12. @Composable fun SampleTextField() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = it } ) } BasicTextField੄ valueী ٜযоח ߸ࣻо ਤীࢲ৬ э੉ State੄ ഋక۽ ҙܻغҊ ੓ח Ѫਸ ࠅ ࣻ ੓णפ׮. ੉ ч਷ value ಁ۞޷ఠܳ ా೧ BasicTextField੄ ч੉ غҊ, ఃࠁ٘ܳ ా೧ ࢜۽਍ ޙ੗ٜ੉ ੑ۱ػ׮ݶ ੉ח onValueChangeܳ ా೧ ࢜۽਍ ч੉ ٜযয়ѱ غҊ ੉ܳ ഝਊೞৈ textܳ ߸҃दఆ ࣻ ੓ח ҳઑ۽ ࢸ҅о غয੓णפ׮.
  13. @Composable fun SampleTextField() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = it } ) } TextField੄ ࢚కܳ ѐߊ੗о “੗ਬ܂ѱ” ҙܻ ӝઓ Viewࠁ׮ ѐߊ੗ٜ੉ ؊਌ ೡ ੌ੉ ݆ই૑ӟ ೮૑݅ যڌѱ ࠁݶ EditTextࠁ׮ ఫझ౟ ೙ఠ݂җ э਷ ӝמਸ ҳഅೞӝীח ؊਌ ए਍ ҳઑ۽ ѐࢶغ঻Ҋ TextFieldղ੄ чҗ ࢎਊ ੗о ࣗਬೞח ߸ࣻо э਷ чਵ۽ य௼о غӝ ٸޙী ോݢ ী۞ܳ ഻ঁ ઴ੌ ࣻ ੓ח ҳઑ۽ ѐࢶ੉ غ঻णפ׮.
  14. @Immutable class TextFieldValue constructor( val annotatedString: AnnotatedString, selection: TextRange =

    TextRange.Zero, composition: TextRange? = null ) AnnotatedString ఫझ౟߹/ޙױ߹ झఋੌਸ ૘য֍ਸ ࣻ ੓ѱ ೧઱ח ؘ੉ఠ ҳઑ
  15. @Immutable class TextFieldValue constructor( val annotatedString: AnnotatedString, selection: TextRange =

    TextRange.Zero, composition: TextRange? = null ) buildAnnotatedString { append(“Hello”) pushStyle(SpanStyle(color = Color.Green)) append(" World”) pop() append(“!") addStyle( SpanStyle(color = Color.Red), "Hello World”.length, this.length ) toAnnotatedString() }
  16. @Immutable class TextFieldValue constructor( val annotatedString: AnnotatedString, selection: TextRange =

    TextRange.Zero, composition: TextRange? = null ) buildAnnotatedString { append(“Hello”) pushStyle(SpanStyle(color = Color.Green)) append(" World”) pop() append(“!") addStyle( SpanStyle(color = Color.Red), "Hello World”.length, this.length ) toAnnotatedString() }
  17. @Immutable class TextFieldValue constructor( val annotatedString: AnnotatedString, selection: TextRange =

    TextRange.Zero, composition: TextRange? = null ) selection ఫझ౟о ࢶఖغয੓ח ৔৉ ઁҕ (range = [द੘ index, ՘ index))
  18. @Immutable class TextFieldValue constructor( val annotatedString: AnnotatedString, selection: TextRange =

    TextRange.Zero, composition: TextRange? = null ) composition OS(IME)ীࢲ ੗୓੸ਵ۽ ҙܻೞח ч. ੗ز৮ࢿ/੗ز߸ജ੉ ೙ਃೠ ৔৉ ૑੿
  19. When to use? ❏ String ❏ ੑ۱ೠ чਸ Ӓ؀۽ ࠁৈ઱ӝ݅

    ೞݶ غ ח TextField ❏ TextFieldValue ❏ TextField ղী ׮নೠ झఋੌਸ ੸ਊ ೧ঠೞח ҃਋ ❏ ࢶఖೠ ৔৉੄ ఫझ౟ ഑਷ ੉৻੄ ৔৉ ੄ ఫझ౟ী যڃ ୊ܻܳ о೧ঠೞח ҃ ਋
  20. When to use? ❏ TextFieldState ❏ ࣽର੸ਵ۽ ੑ۱غח ੑ۱ч(state)ਸ ഝਊೞৈ

    ࠺زӝ ۽૒ਸ ୊ܻೞҊ੗ ೡ ٸ Ӓ Ѩૐчਸ “ઁ؀۽ ࢸ੿ೞѱ” ೞӝ ਤೠ BasicTextField੄ ࢜۽਍ ࢚క੿੄ ❏ compose 1.7.0-alphaࠗఠ BasicTextField2ח BasicTextField۽ ੉ܴ੉ ߄ Շ঻णפ׮.
  21. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = it } ) }
  22. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = it } ) }
  23. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { if (it.length <= MAX_LENGTH) { text = it } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗)
  24. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { if (it.length <= MAX_LENGTH) { text = it } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗) җো જ਷ UXܳ ઁҕೡө?
  25. ❏ TextFieldח 7Ӗ੗ ઁೠ੉ Ѧ۰੓਺ ❏ ׮ܲ ಕ੉૑ীࢲ ఫझ౟ܳ ࠂࢎೣ

    ❏ ੉ܳ TextFieldী ࠢৈ֍ਵ۰Ҋ ೣ ׮਺җ э਷ Caseܳ ࢤп೧ࠁ੗ DroidKnights ୭Ҋ~
  26. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { if (it.length <= MAX_LENGTH) { text = it } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗)
  27. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { if (it.length <= MAX_LENGTH) { text = it } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗)
  28. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = /* TODO */ } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗)
  29. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = when { it.length <= MAX_LENGTH -> it else -> it.substring(0, MAX_LENGTH) } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗)
  30. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = when { it.length <= MAX_LENGTH -> it else -> it.substring(0, MAX_LENGTH) } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗)
  31. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = when { it.length <= MAX_LENGTH -> it else -> it.substring(0, MAX_LENGTH) } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗)
  32. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = when { it.length <= MAX_LENGTH -> it else -> it.substring(0, MAX_LENGTH) } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗)
  33. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = when { it.length <= MAX_LENGTH -> it else -> it.substring(0, MAX_LENGTH) } } } Ӗ੗ ӡ੉ ઁೠਸ Ѧ ٸ (7੗) ੉ѱ ୭ࢶੌө?
  34. Ӗ੗ࣻח ׮নೠ ߑधਵ۽ ੍൧ ࣻ ੓णפ׮. ❏ UTF-16 Bit Encoding

    (UTF-16BE) ❏ Javaীࢲ੄ primitive char৬ 1؀ 1 ؀਽ೡ ࣻ ੓ח ҳઑ۽ ޙ੗ܳ ੋ௏٬ ❏ String.length ❏ Grapheme Cluster ❏ ੋр੉ ੋधೞח ޙ੗ ױਤܳ ೞա੄ ޙ ੗۽ ؀਽ ❏ ೞա੄ Grapheme਷ Nѐ੄ ਬפ௏٘ ۽ ҳࢿ оמ ❏ java.text.BreakIterator
  35. @Composable fun TextFilterSample() { var text by remember { mutableStateOf(“”)

    } BasicTextField( value = text, onValueChange = { text = when { it.length <= MAX_LENGTH -> it else -> it.substring(0, MAX_LENGTH) } } }
  36. onValueChange = { text = when { it.length <= MAX_LENGTH

    -> it else -> { val breakIterator = BreakIterator.getInstance() breakIterator.setText(it) var end = 0 while(true) { val newEnd = breakIterator.next() if (newEnd == BreakIterator.DONE || newEnd > MAX_LENGTH) break end = newEnd } it.subString(0, end) } }
  37. onValueChange = { text = when { it.length <= MAX_LENGTH

    -> it else -> { val breakIterator = BreakIterator.getInstance() breakIterator.setText(it) var end = 0 while(true) { val newEnd = breakIterator.next() if (newEnd == BreakIterator.DONE || newEnd > MAX_LENGTH) break end = newEnd } it.subString(0, end) } }
  38. else -> { val breakIterator = BreakIterator.getInstance() breakIterator.setText(it) var end

    = 0 while(true) { val newEnd = breakIterator.next() if (newEnd == BreakIterator.DONE || newEnd > MAX_LENGTH) break end = newEnd } it.subString(0, end) } }
  39. var end = 0 while(true) { val newEnd = breakIterator.next()

    if (newEnd == BreakIterator.DONE || newEnd > MAX_LENGTH) break end = newEnd } it.subString(0, end) } }
  40. onValueChange = { text = when { it.length <= MAX_LENGTH

    -> it else -> { val breakIterator = BreakIterator.getInstance() breakIterator.setText(it) var end = 0 while(true) { val newEnd = breakIterator.next() if (newEnd == BreakIterator.DONE || newEnd > MAX_LENGTH) break end = newEnd } it.subString(0, end) } }
  41. VisualTransformation ❏ valueܳ ੓ח Ӓ؀۽ ࠁৈ઱૑ ঋҊ ౠ߹ೠ ୊ܻܳ оೞৈ

    ࠁৈ઻ঠ ೞח ҃਋ী ࢎਊ ❏ ࠺޻ߣഐ ੑ۱ द ‘•’ܳ ࠁৈ઴ ࣻ ੓ח Ѫب VisualTransformationਸ ഝਊೣ
  42. VisualTransformation: filter @Immutable fun interface VisualTransformation { fun filter(text: AnnotatedString):

    TransformedText } text ਗࠄ ఫझ౟о AnnotatedString ఋੑਵ۽ ٜয১ ਗࠄ ఫझ౟ ୶୹ೞ۰ݶ text.text۽ оઉয়ݶ ؽ
  43. VisualTransformation: filter @Immutable fun interface VisualTransformation { fun filter(text: AnnotatedString):

    TransformedText } TransformedText ਗࠄ ఫझ౟੄ ߸ഋਸ оೠ ఫझ౟৬, ఫझ౟ ղ੄ ழࢲ ઑ੺ ੿଼ਸ ੿੄ೞח ఋੑ
  44. TransformedText class TransformedText( val text: AnnotatedString, val offsetMapping: OffsetMapping )

    text TextFieldী ࠁৈ઱Ҋ र਷ ఫझ౟۽ ਗࠄ ఫझ౟ܳ ߸ഋೠ റ ֍যળ׮.
  45. TransformedText class TransformedText( val text: AnnotatedString, val offsetMapping: OffsetMapping )

    offsetMapping ߸ഋػ ఫझ౟ীࢲ੄ ழࢲ ਤ஖ҙ҅ܳ ੿੄ೞӝ ਤ೧ ೙ਃೠ ੗ܐҳઑ.
  46. class IdentifierTransformation : VisualTransformation { override fun filter(text: AnnotatedString): TransformedText

    { if (text.text.isEmpty()) { // TODO: ੑ۱ػ ఫझ౟о হח ҃਋ } else { // TODO: ੑ۱ػ ఫझ౟о ੓ח ҃਋ } }
  47. else { val originalText = text.text val transformedText = buildAnnotatedString

    { append(originalText.take(6)) append("-") if (originalText.length >= 7) { append(originalText.drop(6).take(1)) } else { withStyle(SpanStyle(color = Color.LightGray)) { append("•") } } append("•".repeat(6)) } }
  48. else { val originalText = text.text val transformedText = buildAnnotatedString

    { append(originalText.take(6)) append("-") if (originalText.length >= 7) { append(originalText.drop(6).take(1)) } else { withStyle(SpanStyle(color = Color.LightGray)) { append("•") } } append("•".repeat(6)) } }
  49. else { val originalText = text.text val transformedText = buildAnnotatedString

    { append(originalText.take(6)) append("-") if (originalText.length >= 7) { append(originalText.drop(6).take(1)) } else { withStyle(SpanStyle(color = Color.LightGray)) { append("•") } } append("•".repeat(6)) } } ઱޹١۾ߣഐ খ੗ܻ
  50. else { val originalText = text.text val transformedText = buildAnnotatedString

    { append(originalText.take(6)) append("-") if (originalText.length >= 7) { append(originalText.drop(6).take(1)) } else { withStyle(SpanStyle(color = Color.LightGray)) { append("•") } } append("•".repeat(6)) } } ઱޹١۾ߣഐ ّ੗ܻ
  51. else { val originalText = text.text val transformedText = /*

    AS IMPLMENTED */ return TrasnformedText( transformedText, object: OffsetMapping { override fun originalToTransformed(offset: Int): Int { return if (offset <= 5) offset else offset + 1 } override fun transformedToOriginal(offset: Int): Int { return originalText.length } } ) }
  52. else { val originalText = text.text val transformedText = /*

    AS IMPLMENTED */ return TrasnformedText( transformedText, object: OffsetMapping { override fun originalToTransformed(offset: Int): Int { return if (offset <= 5) offset else offset + 1 } override fun transformedToOriginal(offset: Int): Int { return originalText.length } } ) }
  53. OffsetMapping ❏ originalToTransformed ❏ ఫझ౟о ߸ഋؼ ٸ੄ ழࢲ ਤ஖ܳ ઑ੿ೞӝ

    ਤ೧ ࢎਊ ❏ transformedToOriginal ❏ ߸ഋػ ఫझ౟ীࢲ੄ ழࢲ ਤ஖ܳ ׮द ਗࠄ ഋక੄ ੋؙझ۽ ߸ജ೧ঠೞח ҃਋ ❏ Text ࢶఖೞѢա ъઑೡ ٸ ഝਊؽ
  54. object: OffsetMapping { override fun originalToTransformed(offset: Int): Int { return

    if (offset <= 5) offset else offset + 1 } override fun transformedToOriginal(offset: Int): Int { return originalText.length } }
  55. object: OffsetMapping { override fun originalToTransformed(offset: Int): Int { return

    if (offset <= 5) offset else offset + 1 } override fun transformedToOriginal(offset: Int): Int { return originalText.length } }
  56. object: OffsetMapping { override fun originalToTransformed(offset: Int): Int { return

    if (offset <= 5) offset else offset + 1 } override fun transformedToOriginal(offset: Int): Int { return originalText.length } }
  57. object: OffsetMapping { override fun originalToTransformed(offset: Int): Int { return

    if (offset <= 5) offset else offset + 1 } override fun transformedToOriginal(offset: Int): Int { return originalText.length } }
  58. 정리 ❏ BasicTextField의 핵심 패러미터들을 알아보았습니다 ❏ 상황에 따라서 적절한

    Input 타입을 고를 수 있습니다 ❏ 다양한 요구사항에 대응하기 위한 onValueChange를 구현해보았습니다 ❏ 마개조(?)가 필요한 상황에도 VisualTransformation을 활용하여 BasicTextField를 구현해볼 수 있습니다.
  59. ୹୊ ❏ DroidKaigi 2023 - [JA] Compose ΀ Text Field

    ΤሑḑͫΝ | Albert Chang ❏ Line Engineering - Ӗ੗ࣻܳ ࣁח 7о૑ ߑߨ