๐Ÿ“ฑJetpack Compose๋กœ ๋ฐฐ์šฐ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๊ทธ๋ž˜ํ”ฝ

HBยท2025๋…„ 5์›” 13์ผ

๐Ÿ’ ์‹œ๊ฐ์  ์• ๋‹ˆ๋ฉ”์ด์…˜

AnimateVisibility๋ž€?

  • ํŠน์ • UI ์š”์†Œ๋ฅผ ๋ณด์ด๊ฑฐ๋‚˜ ์ˆจ๊ธธ ๋•Œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋ฅผ ์ฃผ๊ธฐ ์œ„ํ•œ ์ปดํฌ์ €๋ธ”
  • ๋‹จ์ˆœํ•œ if ๋ฌธ์œผ๋กœ ์กฐ๊ฑด์„ ๋‚˜๋ˆ„๋Š” ๋Œ€์‹ , ๋ณด์ž„/์ˆจ๊น€ ์ƒํƒœ๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ๋ธŒ๋ฆฟ์ง€ ์—ญํ• 
  • ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
var isVisible by remember { mutableStateOf(true) }

Column {
    Button(onClick = { isVisible = !isVisible }) {
        Text("ํ† ๊ธ€")
    }

    AnimatedVisibility(visible = isVisible) {
        Text("๋ณด์ด๊ฑฐ๋‚˜ ์ˆจ๊ฒจ์ง€๋Š” ํ…์ŠคํŠธ์ž…๋‹ˆ๋‹ค.")
    }
}

=> isVisible์˜ ๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด Text์˜ ๋“ฑ์žฅ๊ณผ ์‚ฌ๋ผ์ง์ด ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ์ฒ˜๋ฆฌ, ๋‹จ์ˆœํžˆ View๋ฅผ ์ถ”๊ฐ€&์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋ถ€๋“œ๋Ÿฌ์šด UX๋ฅผ ๋งŒ๋“ค์–ด์คŒ

์ง„์ž… / ์ดํƒˆ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ •์˜

  • AnimatedVisibility๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ fade ํšจ๊ณผ๊ฐ€ ์ ์šฉ๋˜์ง€๋งŒ, ๋” ํ’๋ถ€ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ง์ ‘ ์ง€์ • ๊ฐ€๋Šฅ
  • ๋‹ค์–‘ํ•œ UI ํšจ๊ณผ
AnimatedVisibility(
    visible = isVisible,
    enter = fadeIn() + slideInVertically(),
    exit = fadeOut() + slideOutHorizontally()
) {
    Text("๋‹ค์–‘ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ ์šฉ๋œ ํ…์ŠคํŠธ")
}

-> ์ง„์ž… ์‹œ ์œ„์—์„œ ์Šฌ๋ผ์ด๋“œ + ํŽ˜์ด๋“œ ์ธ
-> ์ดํƒˆ ์‹œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ + ํŽ˜์ด๋“œ ์•„์›ƒ


์• ๋‹ˆ๋ฉ”์ด์…˜ ์ŠคํŽ™๊ณผ ์ด์ง•

  • ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์†๋„์™€ ํ๋ฆ„ ์ œ์–ด ๊ฐ€๋Šฅ
  • ex) tween, spring, keyframes ๋“ฑ์„ ์‚ฌ์šฉํ•ด ์„ธ๋ถ€์ ์ธ ์ œ์–ด ๊ฐ€๋Šฅ
val slideSpec = tween(
    durationMillis = 1000,
    easing = FastOutSlowInEasing
)
  • tween: ์„ ํ˜• ์• ๋‹ˆ๋ฉ”์ด์…˜

  • spring: ํƒ„๋ ฅ ์žˆ๊ฒŒ ํŠ€๋Š” ๋А๋‚Œ

  • keyframes: ์‹œ๊ฐ„๋งˆ๋‹ค ๊ฐ’์„ ์ˆ˜๋™ ์ง€์ •

  • easing: ๊ฐ€์†/๊ฐ์† ๊ณก์„  (UI์˜ ์ž์—ฐ์Šค๋Ÿฌ์›€์— ํฐ ์˜ํ–ฅ)

์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฐ˜๋ณตํ•˜๊ธฐ

  • InfiniteTransition์„ ์ด์šฉํ•ด ํ…์ŠคํŠธ์˜ ํˆฌ๋ช…๋„๋ฅผ ๋ฐ˜๋ณต์ ์œผ๋กœ ๋ณ€ํ™”์‹œํ‚ค๋Š” ์˜ˆ์‹œ
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(1000),
        repeatMode = RepeatMode.Reverse
    )
)

=> ๋ฐ˜๋ณต ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋‚˜ ํ˜ธํกํ•˜๋Š” ๋ฒ„ํŠผ ํšจ๊ณผ ๋“ฑ์— ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ

์ž์‹๋ณ„๋กœ ๊ฐ๊ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉํ•˜๊ธฐ

  • AnimatedVisibility์˜ ๋‚ด๋ถ€ ์š”์†Œ๋“ค์— ๊ฐ๊ฐ ๋‹ค๋ฅธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ถ€์—ฌ ๊ฐ€๋Šฅ
AnimatedVisibility(visible = isVisible) {
    Column {
        Text("์ฒซ ๋ฒˆ์งธ", modifier = Modifier.animateEnterExit(
            enter = fadeIn(), exit = fadeOut()
        ))
        Text("๋‘ ๋ฒˆ์งธ", modifier = Modifier.animateEnterExit(
            enter = slideInHorizontally(), exit = slideOutHorizontally()
        ))
    }
}

=> ํ•˜๋‚˜์˜ AnimatedVisibility ์•ˆ์—์„œ๋„ ๊ฐ ์š”์†Œ๋งˆ๋‹ค ๋‹ค์–‘ํ•œ ํšจ๊ณผ๋ฅผ ๋„ฃ์„ ์ˆ˜ ์žˆ์–ด์„œ ํ‘œํ˜„๋ ฅ ์ฆ๊ฐ€

์ž๋™์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์ž‘ํ•˜๊ธฐ

  • ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ์™€ ํ•จ๊ป˜ ๋“ฑ์žฅํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๋ฉด LaunchedEffect ์‚ฌ์šฉ
var isVisible by remember { mutableStateOf(false) }

LaunchedEffect(Unit) {
    isVisible = true
}

=> ์•ฑ ์‹œ์ž‘ ์‹œ ๋กœ๊ณ , ์ธํŠธ๋กœ ํ…์ŠคํŠธ ๋“ฑ์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ ํ™œ์šฉ

Crossfade โ€“ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜

  • ์„œ๋กœ ๋‹ค๋ฅธ ํ™”๋ฉด์ด๋‚˜ ์ƒํƒœ ์ „ํ™˜์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์—ฐ๊ฒฐ
Crossfade(targetState = screen) { state ->
    when (state) {
        "Home" -> HomeScreen()
        "Profile" -> ProfileScreen()
    }
}

=> ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ํ™ˆ โ†” ํ”„๋กœํ•„ ํ™”๋ฉด ์ „ํ™˜ ์‹œ ํŽ˜์ด๋“œ ํšจ๊ณผ๋กœ ์‹œ์„ ์ด ๋Š๊ธฐ์ง€ ์•Š๋„๋ก ๋„์™€์คŒ


๐Ÿ’ ์ƒํƒœ ์ฃผ๋„ ์• ๋‹ˆ๋ฉ”์ด์…˜

animateFloatAsState โ€“ ํšŒ์ „ ํšจ๊ณผ

val angle by animateFloatAsState(targetValue = if (clicked) 360f else 0f)

Image(
    painter = painterResource(id = R.drawable.logo),
    contentDescription = null,
    modifier = Modifier.rotate(angle)
)

=> ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ด๋ฏธ์ง€ ํšŒ์ „
=> rotate()์— ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒํƒœ ๊ฐ’์„ ๋ฐ”์ธ๋”ฉํ•˜๋Š” ๋ฐฉ์‹


animateColorAsState โ€“ ์ƒ‰์ƒ ๋ณ€๊ฒฝ

  • ๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ ์ƒ‰์ƒ์ด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ „ํ™˜
val color by animateColorAsState(targetValue = if (clicked) Color.Red else Color.Blue)

Box(modifier = Modifier.size(100.dp).background(color))

animateDpAsState โ€“ ์œ„์น˜ ์ด๋™

  • ์š”์†Œ๊ฐ€ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ด๋™ํ•˜๋ฉด์„œ ์›€์ง์ด๋Š” ์ธํ„ฐ๋ž™์…˜
val offset by animateDpAsState(
    targetValue = if (isClicked) 200.dp else 0.dp
)
Box(modifier = Modifier.offset(x = offset))

spring, keyframes๋กœ ์ •๊ตํ•˜๊ฒŒ ์ œ์–ด

  • spring์€ ๋ง๋ž‘๋ง๋ž‘ํ•œ ํŠ€๋Š” ํšจ๊ณผ
  • keyframes๋Š” ์‹œ๊ฐ„๋Œ€๋ณ„ ํฌ๊ธฐ, ์œ„์น˜๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ง€์ •
val scale by animateFloatAsState(
    targetValue = 1.5f,
    animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy)
)



๐Ÿ’ Canvas ๊ทธ๋ž˜ํ”ฝ ๊ทธ๋ฆฌ๊ธฐ

Canvas๋ž€?

  • ์ง์ ‘ 2D ๊ทธ๋ž˜ํ”ฝ์„ ๊ทธ๋ฆด ์ˆ˜ ์žˆ๋Š” ์ปดํฌ์ €๋ธ”
  • ๋‹ค์–‘ํ•œ ๋„ํ˜•, ์ด๋ฏธ์ง€, ์„  ๋“ฑ์„ ์ˆ˜๋™์œผ๋กœ ๊ทธ๋ฆด ์ˆ˜ ์žˆ์–ด UI๋ฅผ ์ •๋ฐ€ํ•˜๊ฒŒ ์กฐ์ž‘ ๊ฐ€๋Šฅ

๊ธฐ๋ณธ ๋„ํ˜• ๊ทธ๋ฆฌ๊ธฐ

  • UI์— ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”, ์ฐจํŠธ, ๋„ํ˜• ๋””์ž์ธ ๋“ฑ์„ ํ‘œํ˜„ํ•  ๋•Œ ๋งค์šฐ ์œ ์šฉ
    • drawLine: ์„ 
    • drawRect: ์‚ฌ๊ฐํ˜•
    • drawCircle: ์›
Canvas(modifier = Modifier.size(200.dp)) {
    drawLine(Color.Black, Offset(0f, 0f), Offset(100f, 100f))
    drawRect(Color.Green, Offset(50f, 50f), Size(80f, 80f))
    drawCircle(Color.Blue, center = Offset(100f, 100f), radius = 40f)
}

์ ์„  ๊ทธ๋ฆฌ๊ธฐ

  • PathEffect.dashPathEffect()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„ ์„ ์ ์„  ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ
drawLine(
    color = Color.Red,
    start = Offset(0f, 50f),
    end = Offset(200f, 50f),
    pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
)

์ด๋ฏธ์ง€ ๊ทธ๋ฆฌ๊ธฐ

  • ๊ทธ๋ž˜ํ”„๋‚˜ ๋‹ค์ด์–ด๊ทธ๋žจ ์œ„์— ์ด๋ฏธ์ง€๋ฅผ ์˜ค๋ฒ„๋ ˆ์ด ํ•  ๋•Œ
val image = ImageBitmap.imageResource(id = R.drawable.sample)

Canvas(modifier = Modifier.size(200.dp)) {
    drawImage(image)
}

0๊ฐœ์˜ ๋Œ“๊ธ€