๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ’ป/Nutri Capture

Nutri Capture ํ”„๋ก ํŠธ์—”๋“œ - Dimens์™€ ํ•จ๊ป˜ ChatBar ๋งŒ๋“ค๊ธฐ

interfacer_han 2024. 12. 31. 01:25

#1 ๊ฐœ์š”

#1-1 NutrientChatBar()

...

class MainActivity : ComponentActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        setContent {
            NutricapturenewTheme {
                ...

                Scaffold(
                    ...
                    bottomBar = {
                        when(currentRoute) {
                            Destination.NutrientScreen.route -> NutrientChatBar()
                            else -> MainNavigationBar(navController)
                        }
                    },
                    ...
                ) { ...
                    ...
            }
        }
    }

    @Composable
    private fun MainNavigationBar(navController: NavHostController) {
        ...
    }

    @Composable
    fun NutrientChatBar() {
        // TODO
    }
}

...

์›๋ž˜ BottomAppBar()๊ฐ€ ์žˆ๋˜ ์ž๋ฆฌ์—, ์ปค์Šคํ…€ ์ปดํฌ์ €๋ธ”์ธ NutrientChatBar()๋ฅผ ๋„ฃ๋Š”๋‹ค. ๋งŽ์€ ์‹œํ–‰์ฐฉ์˜ค ๋์— ๊ธฐ๋ณธ ์ œ๊ณต๋˜๋Š” BottomBar์ธ BottomAppBar()๋Š” ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋Š” ํŒ๋‹จ์„ ๋‚ด๋ ธ๋‹ค. ํŒ๋‹จ์˜ ๊ฐ€์žฅ ์ฃผ์š”ํ•œ ๊ทผ๊ฑฐ๋Š” BottomAppBar()์— ๊ฑธ๋ ค ์žˆ๋Š” ๋†’์ด ์ œํ•œ์ด๋‹ค. ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์จ ๋ดค์ง€๋งŒ, 80.dp์„ ์ดˆ๊ณผํ•ด ๋†’์•„์ง€๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์—†์—ˆ๋‹ค. ํ˜น์‹œ๋ผ๋„ ์–ด๋–ป๊ฒŒํ•ด์„œ๋“  ๋†’์ด ์ œํ•œ์„ ์—†์•จ ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋„, ์ด์™€ ๊ฐ™์€ ์“ธ ๋ฐ ์—†๋Š” ๋˜ ๋‹ค๋ฅธ ์ œ์•ฝ์กฐ๊ฑด์ด ๋ช‡ ๊ฐœ ์กด์žฌํ•  ๊ฒƒ์ด๋ž€ ์˜ˆ๊ฐ์ด ๋“ค์—ˆ๋‹ค. ์ด๋Ÿด ๋ฐ”์—” ๊ทธ๋ƒฅ ์ปค์Šคํ…€ BottomBar๋ฅผ ์“ฐ๊ธฐ๋กœ ํ•œ ๊ฒƒ์ด๋‹ค. ๋˜, BottomAppBar() ์ž์ฒด๊ฐ€ ์ฑ„ํŒ… ๊ธฐ๋Šฅ์„ ์œ„ํ•ด ์กด์žฌํ•˜๋Š” ์ปดํฌ์ €๋ธ”๋„ ์•„๋‹ˆ๋‹ค. ์šฉ๋„๊ฐ€ ๋‹ค๋ฅด๋‹ˆ ๊ตณ์ด BottomAppBar()๋ฅผ ๊ณ ์ง‘ํ•  ํ•„์š”๋„ ์—†๋‹ค.

 

#1-2 ๋จธํ„ฐ๋ฆฌ์–ผ ๋””์ž์ธ ๊ฐ€์ด๋“œ

 

Components – Material Design 3

Discover over 30 Material Design UI components and their functions. Understand how to use them to design intuitive and visually appealing user experiences.

m3.material.io

NutrientChatBar()์˜ ๋ชจ์–‘ ๊ทธ๋ฆฌ๊ณ  ์•ž์œผ๋กœ ์ ์šฉ๋  ๋ชจ๋“  ์ปดํฌ์ €๋ธ” ํ•จ์ˆ˜์˜ ์ˆ˜์น˜๋Š” ์œ„ ๋งํฌ์— ์žˆ๋Š” ๋จธํ„ฐ๋ฆฌ์–ผ ๋””์ž์ธ ๊ฐ€์ด๋“œ์— ๊ธฐ๋ฐ˜ํ•  ๊ฒƒ์ด๋‹ค.

 

#1-3 object Dimens { ... }

// package com.example.nutri_capture_new.ui.theme

object Dimens {

}

๋จธํ„ฐ๋ฆฌ์–ผ ๊ฐ€์ด๋“œ์— ๊ธฐ๋ฐ˜ํ•œ๋‹ค๋Š” ๊ฒƒ์€, ๋””์ž์ธ ๊ฐ€์ด๋“œ์— ๋ช…์‹œ๋œ ์ˆ˜์น˜๋ฅผ ๊ณ„์† ์žฌ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ ์œ„ํ•ด Dimens ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์„ ์–ธํ•ด์„œ ๊ทธ ์•ˆ์˜ ํ”„๋กœํผํ‹ฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•œ๋‹ค. ์ถ”๊ฐ€๋กœ, ์–ธ์ œ๋‚˜ 100% ๋””์ž์ธ ๊ฐ€์ด๋“œ๋ฅผ ๋”ฐ๋ฅผ ์ˆœ ์—†์„ ๊ฒƒ์ด๋‹ค. ํ”„๋กœ์ ํŠธ์— ๋งž๊ฒŒ ์•ฝ๊ฐ„์˜ ์ˆ˜์น˜ ์กฐ์ •์ด ํ•„์š”ํ•  ๋•Œ๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ๊นŒ์ง€ ๊ณ ๋ คํ•ด Dimens ์† ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค. ํ•œ ๋งˆ๋””๋กœ ๋จธํ„ฐ๋ฆฌ์–ผ ๊ฐ€์ด๋“œ๋ฅผ ๋”ฐ๋ฅด๋“  ๋”ฐ๋ฅด์ง€ ์•Š๋“ , ๋ณธ ํ”„๋กœ์ ํŠธ์— ์“ฐ์ผ ๋ชจ๋“  ์ˆ˜์น˜๋Š” ์—ฌ๊ธฐ์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค.

 

#2 ์ฝ”๋“œ - NutrientChatBar()

#2-1 Dimens

// package com.example.nutri_capture_new.ui.theme

import androidx.compose.ui.unit.dp

object Dimens {

    object ChatBar {
        val minHeight = 80.dp
        val maxHeight = 200.dp
        val paddingTop = 12.dp
        val paddingBottom = 12.dp
        val paddingStart = 12.dp
        val paddingEnd = 12.dp
    }
}

NutrientChatBar๋ฅผ ์œ„ํ•œ ์ˆ˜์น˜๋‹ค. minHeight์™€ padding๋“ค์€ ๋จธํ„ฐ๋ฆฌ์–ผ ๋””์ž์ธ ๊ฐ€์ด๋“œ - Bottom app bar๋ฅผ ์ฐธ์กฐํ–ˆ๋‹ค. maxHeight๋Š” ์šฐ์„  200.dp๋กœ ์ž„์‹œ ์„ค์ •ํ–ˆ๋‹ค. ์ถ”ํ›„ ์„ธ๋ถ€์กฐ์ •ํ•˜๊ฒ ๋‹ค.

 

#2-2 MainActivity

...

class MainActivity : ComponentActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    ...

    @Composable
    fun NutrientChatBar() {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .heightIn(min = Dimens.ChatBar.minHeight, max = Dimens.ChatBar.maxHeight)
                .navigationBarsPadding() // ์—†์œผ๋ฉด ์ด ์ปดํฌ์ €๋ธ”์ด ์‹œ์Šคํ…œ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ” ๊ฐ€๋ฆผ
                .padding(
                    start = Dimens.ChatBar.paddingStart,
                    top = Dimens.ChatBar.paddingTop,
                    end = Dimens.ChatBar.paddingEnd,
                    bottom = Dimens.ChatBar.paddingBottom
                ),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.Bottom
        ) {
            
        }
    }
}

...

 

Modifier.navigationBarsPadding()์€ ์‹œ์Šคํ…œ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”์˜ ์˜์—ญ์„ ์‹œ์Šคํ…œ์œผ๋กœ๋ถ€ํ„ฐ ๋ฐ›๋Š”๋‹ค. ์‚ฌ์šฉ์ž์˜ ํœด๋Œ€ํฐ์ด ์–ด๋–ค ๋†’์ด์˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ์˜์—ญ์„ ๊ฐ€์ง€๋”๋ผ๋„ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

#3 ์ฝ”๋“œ - TextField

#3-1 Dimens

// package com.example.nutri_capture_new.ui.theme

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp

object Dimens {

    object ChatBar {
        ...
    }

    object TextField {
        val minHeight = 56.dp
        val maxHeight = 200.dp
        val roundedCorner = minHeight / 2

        @Composable
        fun textStyle(): TextStyle { // MaterialTheme.typography๋Š” @Composable ํ•จ์ˆ˜์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
            return MaterialTheme.typography.bodyLarge
        }
    }
}

TextField๋ฅผ ์œ„ํ•œ ์ˆ˜์น˜๋‹ค. ๋จธํ„ฐ๋ฆฌ์–ผ ๋””์ž์ธ ๊ฐ€์ด๋“œ - Text fields๋ฅผ ์ฐธ์กฐํ–ˆ๋‹ค. Typography๋Š” @Composable ์˜์—ญ์—์„œ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ธฐ์—, textStyle()๋ฅผ getter ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•  @Composable ํ•จ์ˆ˜์˜ ํ˜•ํƒœ๋กœ ๋‘์—ˆ๋‹ค.

 

#3-2 MainActivity

...

class MainActivity : ComponentActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    ...

    @Composable
    fun NutrientChatBar() {
        Row(
            ...
        ) {
            val inputtedText = remember { mutableStateOf("") }
            TextField(
                value = inputtedText.value,
                onValueChange = { newText -> inputtedText.value = newText },
                modifier = Modifier
                    .weight(1f)
                    .heightIn(min = Dimens.TextField.minHeight, max = Dimens.TextField.maxHeight)
                    .padding() // ์ด TextField()๋ฅผ ๊ฐ์‹ธ๋Š” Row()์˜ padding์ด, TextField์˜ padding ์—ญํ• ์„ ๋Œ€์‹  ์ˆ˜ํ–‰
                    .clip(shape = RoundedCornerShape(Dimens.TextField.roundedCorner)),
                textStyle = Dimens.TextField.textStyle(),
                placeholder = {
                    Text(
                        text = "๋ฉ”์‹œ์ง€ ์ž…๋ ฅ",
                        style = Dimens.TextField.textStyle()
                    )
                },
                colors = TextFieldDefaults.colors().copy(
                    unfocusedIndicatorColor = Color.Transparent, // ๋งจ ํ•˜๋‹จ์— ์žˆ๋Š” ๋ฐ‘์ค„ ํˆฌ๋ช…ํ™”
                    focusedIndicatorColor = Color.Transparent // TextField๊ฐ€ ํฌ์ปค์Šค๋  ๋•Œ ๋งจ ํ•˜๋‹จ ๋ฐ‘์ค„ ์ƒ‰ ํˆฌ๋ช…ํ™”
                )
            )
        }
    }
}

...

colors ์†์„ฑ์—์„œ TextField์— ์กด์žฌํ•˜๋Š” ํ•˜๋‹จ๋ถ€ ๋ฐ‘์ค„์„ ์—†์•ด๋‹ค. ์ด ๋ฐ‘์ค„์€ ๊ทธ ๋ฐ‘์ค„์ด ์†ํ•œ TextField๊ฐ€ ํ˜„์žฌ Focus๋˜์—ˆ๋Š”์ง€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์—ญํ• ์ธ๋ฐ, TextField๊ฐ€ ํ•œ ํ™”๋ฉด์— ๋‹จ ํ•˜๋‚˜๋งŒ ์กด์žฌํ•  ์ƒํ™ฉ์ด๊ธฐ์— ๋ฐ‘์ค„์˜ ์—ญํ• ์„ ํฐ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง€์ง€ ๋ชปํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๊ฑฐ์Šฌ๋ฆฌ๊ธฐ๋งŒ ํ•  ๋ฟ์ด๋‹ค.

 

#4 ์ฝ”๋“œ - Icon ๋ฐ IconButton

#4-1 Dimens

// package com.example.nutri_capture_new.ui.theme

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp

object Dimens {

    object ChatBar {
        ...
    }

    object TextField {
        ...
    }

    object IconButton {
        val iconSize = 24.dp
        val stateLayer = 40.dp
        val targetSize = 48.dp
    }
}

IconButton ๋ฐ ๊ทธ ์† Icon์„ ์œ„ํ•œ ์ˆ˜์น˜๋‹ค. ๋จธํ„ฐ๋ฆฌ์–ผ ๋””์ž์ธ ๊ฐ€์ด๋“œ - Icon buttons๋ฅผ ์ฐธ์กฐํ–ˆ๋‹ค. targetSize๋Š” ํ„ฐ์น˜๊ฐ€ ๋˜๋Š” ์˜์—ญ, stateLayer๋Š” ๋ฒ„ํŠผ์˜ ์‹œ๊ฐ์  ์˜์—ญ์„ ์˜๋ฏธํ•œ๋‹ค. stateLayer๋Š” targetSize์— ์ข…์†

 

#4-2 MainActivity

...

class MainActivity : ComponentActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    ...

    @Composable
    fun NutrientChatBar() {
        Row(
            ...
            verticalAlignment = Alignment.Bottom
        ) {
            ...

            Spacer(modifier = Modifier.width(4.dp))

            Box(
                modifier = Modifier.height(Dimens.TextField.minHeight), // TextField์˜ minHeight์— ๋งž์ท„์Œ
                contentAlignment = Alignment.Center
            ) {
                FilledTonalIconButton(
                    onClick = {
                        inputtedText.value = ""
                    },
                    modifier = Modifier
                        .size(Dimens.IconButton.targetSize)
                        .padding((Dimens.IconButton.targetSize - Dimens.IconButton.stateLayer) / 2) // ์ด padding() ์ œ๊ฑฐ ์‹œ, stateLayer๋Š” ์‚ฌ๋ผ์ง€๊ฒŒ ๋จ (= stateLayer๊ฐ€ targetSize์™€ ๋˜‘๊ฐ™์€ ํฌ๊ธฐ๊ฐ€ ๋จ)
                ) {
                    Icon(
                        imageVector = Icons.AutoMirrored.Filled.Send,
                        contentDescription = "์ „์†ก",
                        modifier = Modifier.size(Dimens.IconButton.iconSize)
                    )
                }
            }
        }
    }
}

...

 

IconButton์˜ ๋†’์ด๋Š” 48.dp์ธ๋ฐ, ์ด๋Ÿฌ๋ฉด TextField์˜ ์ตœ์†Œ ๋†’์ด์ธ 56.dp๊ณผ ๊ฐ’์ด ์ฐจ์ด๋‚˜๊ฒŒ ๋œ๋‹ค. Row()์˜ verticalAlignment๊ฐ€ Alignment.Bottom์ด๊ธฐ ๋•Œ๋ฌธ์—, 'TextField์˜ ์ •์ค‘์•™์„ ๊ฐ€๋กœ์ง€๋ฅด๋Š” ์ˆ˜ํ‰์„ '๊ณผ 'IconButton์˜ ์ •์ค‘์•™์„ ๊ฐ€๋กœ์ง€๋ฅด๋Š” ์ˆ˜ํ‰์„ '์ด ์ผ์น˜ํ•˜์ง€ ์•Š๊ฒŒ ๋œ๋‹ค. ๋”ฐ๋ผ์„œ, IconButton์„ Box()๋กœ ๊ฐ์‹ผ๋‹ค. Box()์˜ ๋†’์ด๋ฅผ TextField์˜ minHeight์™€ ๊ฐ™๊ฒŒ ๋‘๊ณ , Box() ์† ์ปจํ…์ธ ์˜ ์ •๋ ฌ์„ ์ค‘์•™ ์ •๋ ฌ๋กœ ๋‘”๋‹ค.

 

padding((Dimens.IconButton.targetSize - Dimens.IconButton.stateLayer) / 2) ๋ถ€๋ถ„์€ targetSize์™€ stateLayer๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋‹ค. ํ•˜์ง€๋งŒ padding์„ ๊ทธ๋ƒฅ ์—†์• ๋ฒ„๋ฆด๊นŒ ์‹ถ๋‹ค. ์ด padding ๋•Œ๋ฌธ์— (= stateLayer ๋•Œ๋ฌธ์—) ๋ฒ„ํŠผ์ด ๊ณผ๋„ํ•˜๊ฒŒ ์ž‘์•„๋ณด์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ํ•˜์ง€๋งŒ padding์„ ์—†์• ๋ฉด stateLayer๋Š” ์‚ฌ๋ผ์ง€๊ฒŒ ๋œ๋‹ค. ์šฐ์„ ์€ ๋ƒ…๋‘๊ณ  ๋‚˜์ค‘์— ์•ฑ์„ ์ „์ฒด์ ์œผ๋กœ ๋‹ค๋“ฌ์„ ๋•Œ ๋‹ค์‹œ ํ™•์ธํ•ด๋ณด๊ฒ ๋‹ค. ์•„๋งˆ ์—†์•จ ํ™•๋ฅ ์ด ๋†’๊ฒ ๋‹ค.

 

#5 ์š”์•ฝ

์ˆ˜์น˜ ์กฐ์ •์„ ์œ„ํ•œ Dimens ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์„ ์–ธํ•˜๊ณ , ์ด์— ๊ธฐ๋ฐ˜ํ•ด ChatBar์˜ ์™ธ๊ด€์„ ๋งŒ๋“ค์—ˆ๋‹ค.

 

#6 ์™„์„ฑ๋œ ์•ฑ

#6-1 ์Šคํฌ๋ฆฐ์ƒท

 

#6-2 ์ด ๊ฒŒ์‹œ๊ธ€ ์‹œ์ ์˜ Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com

 

#6-3 ๋ณธ ํ”„๋กœ์ ํŠธ์˜ ๊ฐ€์žฅ ์ตœ์‹  Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com