XML (EditText) -> Compose (TextField, OutlinedTextField) 두개로 변경이 되었다.

두개로 나눈 이유는 디자인 스타일과 사용자 경험을 다르게 주기 위함이라고 한다.

 

두개의 차이가 무엇인지 확인 해 보자

TextFieldOutlinedTextField

설정 지정 없이 디버깅 했을 때 왼쪽이 TextField, 오른쪽이 OutlinedTextField이다. 

 

왼쪽은 배경에 색상이 들어가있고 오른쪽엔 Outline이 그려져 있는것을 볼 수 있다. 해당 Field중 선호하는것을 사용하면 될 듯하지만

강조하고 싶은 느낌을 줄 때는 TextField, 깔끔하고 가독성이 필요한 경우엔 오른쪽이 적합해 보인다.

 

하지만 우린 과거에도 그랬고 미래에도 그럴 것이다. 

우리는 디자인을 입혀야 하기때문에 개인 앱이 아닌 이상 저대로 사용할리가 만무하다.

그럼 우리가 할일은 무엇일까?

커스텀하기 좋게 평범한 입력창을 만들어야 한다. 

 

자 그럼 어느것을 사용해야 할지 감이 잘 안올것이다.

우린 커스텀을 하기 위해선 백지로 돌아가야한다. 그럼 백지에 가장 가까운 것은 무엇인가?

바로 OutlinedTextField이다.

 

OutlinedTextField는 다음과 같은 이유로 커스텀을 하기가 쉽다.

1. 구조가 단순함 - 배경이 없고 테두리만 있어서, 커스텀 스타일 넣기가 쉽다

2. 테두리 조정 쉬움 - 속성 수정이 명확하기 때문에 원하는 값을 넣어 조정할 수 있다.

3. 배경 간섭이 없다 - TextField는 배경색이 들어가있지만 이 친구는 흰색이다.

4. 구글 샘플 디자인 예시에서도 OutlinedTextField가 더 많이 사용된다. 

 

그럼 OutlinedTextField에 속성에 대해 학습해 보자

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    var text by remember { mutableStateOf(name) }

    Box(modifier = Modifier.fillMaxWidth().fillMaxHeight(),
        contentAlignment = Alignment.Center
    ) {
        OutlinedTextField(
            //현재 입력된 텍스트 값
            value = text,
            //텍스트가 바뀔 때 마다 실행되는 람다
            onValueChange = { text = it },
            //입력창 위에 표시되는 라벨 텍스트
            label = { Text("이름") },
            //입력 전 회색 안내 텍스트 (EditText: hint)
            placeholder = { Text("이름을 입력 해 주세요.") },
            //왼쪽에 아이콘 추가
            leadingIcon = { Icon(Icons.Default.Person, contentDescription = null)},
            //오른쪽에 아이콘 추가
            trailingIcon = {
                if (text.isNotEmpty()) {
                    IconButton(onClick = { text = ""}) {
                        Icon(Icons.Default.Close, contentDescription = "Clear")
                    }
                }
            },
            //에러 상태 여부
            isError = text.isEmpty(),
            //한 줄 입력만 가능
            singleLine = true,
            //테두리 모양 (라운드 설정)
            shape = RoundedCornerShape(4.dp),
            //텍스트 스타일 지정
            textStyle = TextStyle(fontSize = 16.sp, color = Color.Black),
            // 색상 커스터마이징
            colors = OutlinedTextFieldDefaults.colors(
                // 포커스 있을 때 테두리 색
                focusedBorderColor = Color.Blue,
                // 포커스 없을 때 테두리 색
                unfocusedBorderColor = Color.Gray,
                // 에러 상태일 때 테두리 색
                errorBorderColor = Color.Red,
                // 커서 색상
                cursorColor = Color.Blue
            ),
            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
        )
    }
}

텍스트가 비어있으면 에러상태로 설정이 가능하고 XML에 있던 EditText보다 더 유연해진 느낌이다.

그럼 속성값을 분석 해 보자

 

속성 설명
value 초기에 자동으로 입력될 텍스트 값 (이전에 입력된 값을 다시 입력해 줄 때 사용)
onValueChange 텍스트가 바뀔 때 마다 실행되는 람다 (입력되는 값을 변수에 저장할 때 사용)
label 입력 창 위에 표시되는 라벨 텍스트 (설정할 일이 별로 없을 것 같음)
placeholder 입력 전 회색 안내 텍스트(EditText에서 hint역할)
hint color, fontSize등 커스텀 가능

placeholder = { 
Text(
    text="이름을 입력 해 주세요.",
    fontSize = 11.sp,
    color = Color.Blue
    ) 
}

leadingIcon 입력 창 왼쪽에 아이콘 추가
trailingIcon 입력 창 오른쪽에 아이콘 추가
isError 에러 상태 여부 (입력 되면 안되는 값을 넣거나 텍스트가 비었을 때 에러 상태라는 것을 표시해주는 용도로 추정)
singleLine 한 줄 입력만 가능 
shape 테두리 모양 설정 (라운드 설정)
textStyle 입력되는 텍스트 스타일 지정 (폰트 크기 및 색상 등)
colors 색상 커스텀 (포커스 여부에 따른 테두리 색상 변경 및 커서 색상 등)

 

위의 상태에서 테두리도 없고 입력창과 힌트만 있는 TextField를 구현하던가 테두리만 있고 입력창과 힌트만 있는 TextField를 구현해 보자

텍스트 속성 정리 -- (1)

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var lineCount = 0
    var hasVisualOverflow = false
    var sizeWidth = 0
    var sizeHeight = 0
    var lineStartIndex = 0

    Text(
        text = "Hello $name!",                      //텍스트에 들어갈 내용
        fontSize = 60.sp,                           //텍스트 사이즈
        lineHeight = 60.sp,                         //텍스트 라인 높이
        color = Color.Black,                        //텍스트 색상
        textAlign = TextAlign.Center,               //텍스트 정렬
        fontFamily = FontFamily.Serif,              //텍스트 폰트 지정
        letterSpacing = 2.sp,                       //글자간 간격
        overflow = TextOverflow.Ellipsis,           //영역 초과시 말줄임(..)처리
        textDecoration = TextDecoration.Underline,  //텍스트 밑줄 처리
        softWrap = true,                            //줄바꿈 허용
        modifier = modifier                         //modifier객체
            .fillMaxWidth()                         //width를 화면 가득 채움
            .wrapContentHeight()                    //height를 내용만큼만 채움
            .padding(10.dp, 0.dp, 10.dp, 0.dp),     //텍스트 바깥 여백 설정 (Left, Top, Right, Bottom)
        onTextLayout = { result ->                  //텍스트가 그려진 후 호출되는 콜백
            lineCount = result.lineCount            //텍스트가 몇줄로 표시됐는지 리턴
            hasVisualOverflow = result.hasVisualOverflow    //텍스트가 잘렸는지 여부를 리턴 (...이 표시되는 경우 true)
            sizeWidth = result.size.width           //텍스트 width
            sizeHeight = result.size.height         //텍스트 height
            Log.d("onTextLayout","lineCount: ${lineCount}")
            Log.d("onTextLayout","hasVisualOverflow: ${hasVisualOverflow}")
            Log.d("onTextLayout","sizeWidth: ${sizeWidth}")
            Log.d("onTextLayout","sizeHeight: ${sizeHeight}")
        }
    )

onTextLayout -> 로그를 찍어보면 두번 호출되는것을 확인 할 수 있다. 

초기 레이아웃 예측 시 호출 -> 레이아웃 완료 후 호출 총 두번 호출 된다. 

텍스트가 다 그려진 후에 더보기 버튼이나 접기 버튼이 노출되어야 하는 상황이라면 onTextLayout안에서 노출을 시키면 될 듯 한데 동일한 코드가 두번 호출된다는게 비효율적인게 아닌가 싶다... 할거면 다 그려진 다음 한번만 호출하지 왜 두번호출되게 했을까?

두번 호출 하는 이유 

- 한 번만 호출하는 경우 예측이 틀린 경우 레이아웃 깨짐이나 재조정이 발생 할 가능성이 있음

- 두 번 호출함으로써 초기 렌더링 지연을 줄이고 렌더링 이후 정확한 상태 업데이트를 보장함

 

두번 째 호출에서 UI변경사항을 처리하고 싶은 경우에는
상태 플래그 변수를 저장해서 사용하는 방법과 LaunchedEffect를 사용하는 방법이 있는데 해당 방법은 천천히 알아보도록 하자


텍스트 위치 조정하기 -- (2)


화면 전체 가운데 정렬을 위해 Box를 추가하고 Alignment.Center값을 추가해주면 된다.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    Box(modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "Hello $name!",                      //텍스트에 들어갈 내용
            fontSize = 20.sp,                           //텍스트 사이즈
            color = Color.Black,                        //텍스트 색상
        )
    }
}

contentAlignment --> Alignment.CenterStart, Center, BottomCenter 등 속성값을 통해 텍스트의 위치를 제어할 수 있다.

 

안드로이드 앱 화면을 그리는 방법

(1) Xml - ConstarintLayout, LinearLayout, RecyclerView등으로 화면을 그리는게 가능

(2) Compose - 선언형 UI로 새롭게 출시된 뷰를 그리는 방법이다. (2021년 7월 정식 버전 출시)

 

@Composable이란 Annotation을 붙여야만 화면에 그려지는 듯 하다.

그러면 여기서 궁금한게 Compose와 관련된 Annotation은 어떤것들이 있을까?

 

Annotation 종류

@Composable -> 실제 UI를 그리는 함수

@Preview(showBackground = true) -> 미리보기(배경 색 설정) -> 실제 화면에 그려지지 않음(테스트 용도로만 사용)

@Stable ->  값이 자주 바뀌지 않는 안정적인 클래스나 객체라는 것을 컴파일러에게 알려줌

@Immutable -> 변경이 불가능한 DataClass라는 것을 나타냄

@ReadOnlyComposable -> 상태를 변경하지 않고 읽기만 가능한 Composable 함수

 

Compose의 SampleActivity를 하나 만들 면 다음과 같은 코드를 볼 수 있는데 한줄 한줄 해석해 보자 

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeApplicationTheme {
                Scaffold( modifier = Modifier.fillMaxSize() ) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    ComposeApplicationTheme {
        Greeting("Android")
    }
}

 

1) ComposeApplicationTheme 

적용할 테마를 설정하는 함수

 

2) Scaffold (modifier)

기본적인 레이아웃 틀을 만들어주는 함수

Modifier.fillMaxSize() -> 화면 전체 크기만큼 차지하라는 의미

modifier

  -> Compose에서 컴포넌트의 속성값을 체이닝 형식으로 지정가능하게 해주는 객체

  xml에서는 뷰마다 속성값을 다 넣어주었지만 compose에서는 modifier로 공통 속성값들을 정의해서 재사용 가능하게 만들었다.
  체이닝 형식으로 사용 (**체이닝 순서에 따라 결과값이 다르기 때문에 주의해서 사용)

  크기나 위치 조절, 배경 등 설정들 지정 가능 

modifier = Modifier.padding(30.dp)
    .background(Color.White)
    .clickable{print("clicked")}

+ Recent posts