BasicTextField를 활용한 Custom Compose TextField(EditText)

 

Compose의 EditText

compose에서는 editText로 TextField를 사용합니다.

하지만 TextField는 내부 padding 값이 임의로 정해져 있으며, 커스텀하기 어렵다는 단점이 있습니다.

따라서 BasicTextField를 사용한 Custom Compose TextField를 제작했습니다.

 

 

BasicTextField

https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/package-summary#BasicTextField(kotlin.String,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.text.TextStyle,androidx.compose.foundation.text.KeyboardOptions,androidx.compose.foundation.text.KeyboardActions,kotlin.Boolean,kotlin.Int,kotlin.Int,androidx.compose.ui.text.input.VisualTransformation,kotlin.Function1,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Brush,kotlin.Function1)

 

androidx.compose.foundation.text  |  Android Developers

androidx.appsearch.builtintypes.properties

developer.android.com

Compose에서 사용자의 키보드 입력을 받는 Composable Field 입니다.

BasicTextField는 text를 보여주고 사용자 입력을 처리하기 위한 필드이며, 공식 문서에서는  TextField를 사용하는 것을 권장합니다.

 

하지만 독립적인 디자인을 구축하기 위해서는 Meterial Design의 가이드라인을 따르지 않은 필드를 사용해야 합니다.

TextField는 Meterial Design의 가이드라인을 따르는 BasicTextField의 업그레이드 버전입니다.

 

BasicTextField의 경우 다양한 매개변수를 받습니다.

TextField와의 차이점으로는 placeHolder를 통한 hint text의 지원을 하지 않기 때문에, 자체적으로 값을 전달해야 합니다.

@Composable
fun BasicTextField(
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> 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 = remember { MutableInteractionSource() },
    cursorBrush: Brush = SolidColor(Color.Black),
    decorationBox: @Composable (@Composable innerTextField: () -> Unit) -> Unit = @Composable { innerTextField -> innerTextField() }
): Unit

 

Custom BasicTextField

우선 디자인 요구사항은 아래와 같습니다.

패딩 값이 적용 된 검색 필드와 검색 필드 내부의 hint를 보여줍니다.

검색어를 입력할 경우, 검색 아이콘 좌측에 검색어 삭제 아이콘을 추가해야합니다.

 

해당 디자인을 구현하기 위해서는 2가지의 아이콘을 모두 보여주는 코드를 작성해야 하며,

검색어 입력 시 아이콘을 on/off하는 형식을 구현해야 합니다.

 

우선 Composable 함수를 제작해서 커스텀 뷰에 대한 파라미터를 지정합니다.

hintText : "검색어를 입력하세요."

visualCloseIcon : 위의 검색어 삭제 아이콘

searchText : 검색어 

searchData : 검색 아이콘 클릭 이벤트 

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SearchTextLayer(
    hintText : String,
    visualCloseIcon : MutableState<Boolean>,
    searchText : MutableState<TextFieldValue>,
    searchData : () -> Unit
) {

}

 

그 다음, Composable한 trailingIconView를 생성해서 searchText에 따른 아이콘 표시 여부를 지정합니다.

 

    // Composable icon
    val trailingIconView = @Composable{
        IconButton(onClick = {}) {
            Row (
                Modifier
                    .height(24.dp)
                    .padding(end = 8.dp)
            ){
                // close box
                if (visualCloseIcon.value) {
                    Box(
                        Modifier
                            .size(24.dp)
                            .clickable {
                                searchText.value = TextFieldValue("")
                                visualCloseIcon.value = false
                            }
                    ) {
                        Image(
                            painter = painterResource(id = R.drawable.search_data_delete_24),
                            contentDescription = null,
                            modifier = Modifier.fillMaxSize(),
                        )
                    }
                }else{ Box(Modifier.size(24.dp)) }
                Spacer(modifier = Modifier.width(4.dp))
                // search box
                Box(
                    Modifier
                        .size(24.dp)
                        .clickable {
                            searchData()
                        }
                ) {
                    Icon(
                        painter = painterResource(id = R.drawable.search_24),
                        contentDescription = null,
                        tint = colorResource(id = R.color.blue_gray_4)
                    )
                }
            }
        }

    }

 

visualCloseIcon이 False 상태인 경우, searchText가 비어있는 상태가 됩니다.

따라서 빈 Box를 적용해서 위치값만 지정합니다.

그 외의 경우에는 텍스트 삭제 아이콘을 보여주게 합니다.

 

아이콘 생성 이후에는 MutableInteractionSource를 생성합니다.

val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }

https://www.google.com/search?q=MutableInteractionSource&rlz=1C5CHFA_enKR1045KR1045&oq=MutableInteractionSource&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIGCAEQABgeMgcIAhAAGIAEMggIAxAAGAUYHtIBBzE5OWowajeoAgCwAgA&sourceid=chrome&ie=UTF-8 

 

🔎 MutableInteractionSource: Google 검색

 

www.google.com

 

MutableInteractionSource는 구성 요소에서 발생하는 이벤트에 해당하는 스트림을 나타냅니다.

BasicTextField와 그 안의 내부 설정을 담당하는 TextFieldDecorationBox와 같은

interactionSorce를 공유하게 하기 위해서remember 변수로 감싼 후 선언해 주어야 합니다.

 

    Row {
        Surface(
            modifier = Modifier.fillMaxSize(),
            shape = RoundedCornerShape(108.dp),
            color = colorResource(id = R.color.tab_background)
        ){
            BasicTextField(
                value = searchText.value,
                singleLine = true,
                textStyle = TextStyle(
                    color = Color.White,
                    fontSize = 14.sp,
                    fontFamily = pretendardRegular
                ),
                onValueChange = {searchText.value = it},
                modifier = Modifier.fillMaxSize(),
                interactionSource = interactionSource,
                cursorBrush = SolidColor(colorResource(id = R.color.secondary_1))
            ){
                TextFieldDefaults.TextFieldDecorationBox(
                    value = searchText.value.text,
                    innerTextField = it,
                    singleLine = true ,
                    enabled = true,
                    visualTransformation = VisualTransformation.None,
                    trailingIcon = trailingIconView,
                    placeholder = {
                        Text(
                            text = hintText,
                            color = colorResource(id = R.color.blue_gray_3),
                            fontSize = 14.sp,
                        )
                    },
                    interactionSource = interactionSource,
                    contentPadding = TextFieldDefaults.textFieldWithoutLabelPadding(
                        start = 12.dp, end = 8.dp, top = 4.dp,bottom = 4.dp
                    ),
                    colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White),
                )
            }
        }

 

BasicTextField 내부에는 TextFieldDecorationBox를 통해서 커스텀을 진행하였습니다.

https://developer.android.com/reference/kotlin/androidx/compose/material/TextFieldDefaults#TextFieldDecorationBox(kotlin.String,kotlin.Function0,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.text.input.VisualTransformation,androidx.compose.foundation.interaction.InteractionSource,kotlin.Boolean,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.material.TextFieldColors,androidx.compose.foundation.layout.PaddingValues)

 

TextFieldDefaults  |  Android Developers

androidx.appsearch.builtintypes.properties

developer.android.com

 

BasicTextField는 TextField와 달리 palceholder에 대한 decoration을 적용해주지 않습니다.

따라서 placeholder의 역할을 하는 TextFieldDecorationBox를 사용했습니다.

이를 통해서 패딩값을 지정할 수 있고, hint 텍스트와 trailingIconView를 적용할 수 있습니다.

 

 

 

Compose Custom TextField에 대해서 알아보았습니다.