티스토리 뷰

커스텀 위젯을 만드는 이유

UI를 구성하기 전에 커스텀 위젯을 만드는 것은 반필수적이라고 볼 수 있다.

정의한 커스텀 위젯은 재사용이 가능하기에 UI의 일관성을 유지할 수 있다.

또한 screen을 구성하는 코드가 확연하게 줄어들어 가독성이 향상된다.

 

 

커스텀 위젯 디자인 방식

컴포저블 함수를 선언한 뒤 그 안에 기본위젯을 넣고 그 위젯의

프로퍼티를 활용해  피그마 디자인과 동일하게 UI를 구성한 다음

컴포저블 함수의 프로퍼티로 위젯의 디자인이나 동작을 제어하도록 한다.

 

(버튼 활성화/비활성화 상태라던지, 텍스트필드 입력 모드라던지 등등..)

 

 

커스텀 위젯 네이밍

커스텀 위젯의 이름은 컴포즈에서 기본적으로 제공하는 위젯과

동일한 이름으로 해도 되지만 사용 시 커스텀 위젯이 아닌 기본 위젯이 import되는

번거로운 문제가 생길 수 있어 네이밍을 custom + 위젯이름 이런식으로 하는 걸 추천한다.

 

필자는 이름이 너무 길어지는게 싫어 커스텀 위젯을 기본 위젯과 동일한 이름으로 설정했다.

 

 

Text

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    style: TextStyle = CustomTheme.typography.b4Regular,
    color: Color = CustomTheme.colors.text
) {
    Text(
        text = text,
        modifier = modifier,
        style = style,
        color = color
    )
}

테마 파일에서 기본적인 텍스트 스타일을 지정할 수 있긴 하지만

미리보기에는 적용이 안되기 때문에 따로 기본 텍스트 위젯을 만들어준다.

 

 

Svg

@Composable
fun Svg(
    drawableId: Int,
    size: Dp = 24.dp,
    startPadding: Dp = 0.dp,
    onClick: () -> Unit = {},
    isIcon: Boolean = false,
    iconColor: Color = CustomTheme.colors.icon
) {
    Image(
        imageVector = ImageVector.vectorResource(drawableId),
        contentDescription = null,
        modifier = Modifier
            .size(size)
            .padding(start = startPadding)
            .clickable(onClick = onClick),
        colorFilter = if (isIcon) ColorFilter.tint(iconColor) else null
    )
}

컴포즈에서 벡터 이미지를 사용하려면 이미지 위젯을 활용해야 한다.

Material Icon과 그냥 벡터 이미지 둘 다 사용할 수 있도록 위젯을 구성하였다. 

 

 

Divider

@Composable
fun Divider(width: Dp, color: Color) {
    HorizontalDivider(
        color = color,
        thickness = 1.dp,
        modifier = Modifier.width(width)
    )
}

구분선을 간단하게 사용할 수 있도록 해준다.

 

 

Button UI

OutlinedButton과 Buttton 이렇게 총 2개의 커스텀 위젯을 만들 것이다.

 

 

Button

@Composable
fun Button(
    text: String,
    onClick: () -> Unit,
    color: Color,
    enabled: Boolean = true,
) {
    Button(
        onClick = onClick,
        enabled = enabled,
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp),
        shape = RoundedCornerShape(8.dp),
        colors = ButtonDefaults.buttonColors(
            disabledContainerColor = CustomTheme.colors.disabled,
            containerColor = color
        )
    ) {
        Text(
            style = CustomTheme.typography.b3Regular,
            text = text,
            color = CustomTheme.colors.buttonText
        )
    }
}

 

커스텀 버튼은 버튼 텍스트, 클릭 이벤트, 배경색상, 활성화 여부를 

프로퍼티로 제어할 수 있도록 한다.

 

 

OutlinedButton

@Composable
fun OutlinedButton(
    onClick: () -> Unit,
    text: String,
    color: Color,
    enabled: Boolean = true
) {
    val buttonColor = if (enabled) color else CustomTheme.colors.disabled.copy().copy(alpha = 0.5f)

    OutlinedButton(
        onClick = onClick,
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp),
        contentPadding = PaddingValues(0.dp),
        border = BorderStroke(width = 1.dp, color = buttonColor),
        shape = RoundedCornerShape(4.dp),
        colors = ButtonDefaults.buttonColors(
            containerColor = CustomTheme.colors.background,
            disabledContainerColor = CustomTheme.colors.background.copy(alpha = 0.5f)
        ),
        enabled = enabled,
    ) {
        Text(
            text = text,
            style = CustomTheme.typography.b4Regular.copy(color = buttonColor)
        )
    }
}

OutliendButton은 Button과 동일하나  비활성화 시 모든 요소의 색이 변하도록 설정한다.

 

 

TextField UI

텍스트 필드도 버튼과 마찬가지로 OutlinedTextField와 TextField

이렇게 총 2개의 커스텀 위젯을 만들것이다.

텍스트필드는 비밀번호 입력모드도 추가해야한다.

 

 

TextField

@Composable
fun TextField(
    value: String,
    onValueChange: (String) -> Unit,
    drawableId: Int,
    hint: String,
    isPassword: Boolean = false,
    isLast: Boolean = false,
    maxLength: Int = 50
) {
    val focusRequester = remember { FocusRequester() }
    var isPasswordHidden by remember { mutableStateOf(true) }

    val visualTransformation =
        if (isPassword && isPasswordHidden) PasswordVisualTransformation() else VisualTransformation.None
    val visibleIcon =
        if (isPasswordHidden) R.drawable.ic_visibility_off else R.drawable.ic_visibility


    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Svg(drawableId = drawableId, startPadding = 4.dp, isIcon = true)
        BasicTextField(
            value = value,
            textStyle = CustomTheme.typography.b4Regular.copy(
                color = CustomTheme.colors.text
            ),
            onValueChange = {
                if (it.length <= maxLength && it.all { c -> !c.isWhitespace() }) {
                    onValueChange(it)
                }
            },
            cursorBrush = SolidColor(CustomTheme.colors.text),
            maxLines = 1,
            modifier = Modifier
                .weight(1f)
                .height(48.dp)
                .focusRequester(focusRequester),
            decorationBox = { innerTextField ->
                Box(
                    modifier = Modifier
                        .weight(1f)
                        .padding(start = 8.dp),
                    contentAlignment = Alignment.CenterStart
                ) {
                    if (value.isEmpty()) {
                        Text(
                            text = hint,
                            style = CustomTheme.typography.b4Regular.copy(
                                color = CustomTheme.colors.hint
                            ),
                        )
                    }
                    innerTextField()
                }
            },
            visualTransformation = visualTransformation,
            keyboardOptions = KeyboardOptions.Default.copy(
                imeAction = if (isLast) ImeAction.Done else ImeAction.Next
            ),
        )
        if (isPassword) {
            Svg(
                drawableId = visibleIcon,
                onClick = { isPasswordHidden = !isPasswordHidden },
                size = 20.dp,
                isIcon = true
            )
        }
        Spacer(modifier = Modifier.width(12.dp))
    }
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(1.dp)
            .background(color = CustomTheme.colors.border)
            .padding(start = 8.dp)
    )
}

기본 텍스트필드로는 원하는 UI를 구성하는 것이 불가했기에

BasicTextField를 활용하여 커스텀 텍스트필드 위젯을 만들었다.

 

비밀번호 입력모드일 때의 텍스트 필드는 입력한 값이 가려진 상태이며

 우측 아이콘을 클릭하여 텍스트 가림 여부를 제어할 수 있다.

 

 

OutlinedTextField

@Composable
fun OutlinedTextField(
    value: String,
    onValueChange: (String) -> Unit,
    hint: String,
    isPassword: Boolean = false,
    isLastField: Boolean = false,
    maxLength: Int = 50,
    enabled: Boolean = true
) {
    var isPasswordHidden by remember { mutableStateOf(true) }
    val focusRequester = remember { FocusRequester() }

    val textColor =
        if (enabled) CustomTheme.colors.text else CustomTheme.colors.disabled.copy(alpha = 0.5f)
    val borderColor =
        if (enabled) CustomTheme.colors.border else CustomTheme.colors.disabled.copy(alpha = 0.5f)

    val visualTransformation =
        if (isPassword && isPasswordHidden) PasswordVisualTransformation() else VisualTransformation.None
    val visibleIcon =
        if (isPasswordHidden) R.drawable.ic_visibility_off else R.drawable.ic_visibility

    Box(
        modifier = Modifier
            .border(width = 1.dp, color = borderColor, shape = RoundedCornerShape(4.dp))
            .background(CustomTheme.colors.background)
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween,
        ) {
            BasicTextField(
                enabled = enabled,
                value = value,
                textStyle = CustomTheme.typography.b4Regular.copy(color = textColor),
                onValueChange = {
                    if (it.length <= maxLength && it.all { c -> !c.isWhitespace() }) {
                        onValueChange(it)
                    }
                },
                maxLines = 1,
                cursorBrush = SolidColor(CustomTheme.colors.text),
                modifier = Modifier
                    .weight(1f)
                    .height(48.dp)
                    .focusRequester(focusRequester),
                decorationBox = { innerTextField ->
                    Box(
                        modifier = Modifier
                            .weight(1f)
                            .padding(start = 12.dp),
                        contentAlignment = Alignment.CenterStart
                    ) {
                        if (value.isEmpty()) {
                            Text(
                                text = hint,
                                style = CustomTheme.typography.b4Regular.copy(
                                    color = CustomTheme.colors.hint
                                ),
                            )
                        }
                        innerTextField()
                    }
                },
                visualTransformation = visualTransformation,
                keyboardOptions = KeyboardOptions.Default.copy(
                    imeAction = if (isLastField) ImeAction.Done else ImeAction.Next
                ),

                )
            if (isPassword) {
                Svg(
                    drawableId = visibleIcon,
                    onClick = { isPasswordHidden = !isPasswordHidden },
                    size = 20.dp,
                    isIcon = true
                )
            }
            Spacer(modifier = Modifier.width(12.dp))
        }
    }
}

기존 커스텀 텍스트필드 위젯에서 테두리를 추가하고, 우측 아이콘 영역을 제거하였다.

 

 

CheckBox UI

체크박스는 활성화 시 띄울 체크 모양 아이콘이 하나 필요하다.

 

 

CheckBox

@Composable
fun Checkbox(
    isChecked: Boolean,
    onClick: (Boolean) -> Unit,
    modifier: Modifier = Modifier
) {
    val checkBoxColor = if (isChecked) CustomTheme.colors.skyBlue else CustomTheme.colors.background

    Box(
        modifier = modifier
            .size(18.dp)
            .clickable { onClick(!isChecked) }
            .background(
                color = checkBoxColor,
                shape = RoundedCornerShape(4.dp)
            )
            .then(
                if (!isChecked) {
                    Modifier.border(1.dp, CustomTheme.colors.hint, RoundedCornerShape(4.dp))
                } else {
                    Modifier
                }
            )
    ) {
        if (isChecked) {
            Icon(
                imageVector = ImageVector.vectorResource(R.drawable.ic_check),
                tint = CustomTheme.colors.buttonText,
                contentDescription = null,
                modifier = Modifier.align(Alignment.Center)
            )
        }
    }
}

체크 여부에 따라 다른 레이아웃을 보여주도록 한다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함