Compose에서 그림자 추가

그림자는 UI를 시각적으로 높이고, 사용자에게 상호작용을 나타내며, 사용자 작업에 대한 즉각적인 피드백을 제공합니다. Compose는 앱에 그림자를 통합하는 여러 방법을 제공합니다.

  • Modifier.shadow(): Material Design 가이드라인을 준수하는 컴포저블 뒤에 고도 기반 그림자를 만듭니다.
  • Modifier.dropShadow(): 컴포저블 뒤에 표시되어 컴포저블이 더 높게 보이도록 하는 맞춤설정 가능한 그림자를 만듭니다.
  • Modifier.innerShadow(): 컴포저블의 테두리 내부에 그림자를 만들어 뒤에 있는 표면으로 눌린 것처럼 보이게 합니다.

Modifier.shadow()는 기본 그림자를 만드는 데 적합하며 dropShadowinnerShadow 수정자는 그림자 렌더링에 대한 더 세밀한 제어와 정밀도를 제공합니다.

이 페이지에서는 사용자 상호작용 시 그림자를 애니메이션으로 표시하는 방법, innerShadow()dropShadow() 수정자를 연결하여 그라데이션 그림자, 뉴모피즘 그림자 등을 만드는 방법 등 이러한 각 수정자를 구현하는 방법을 설명합니다.

기본 그림자 만들기

Modifier.shadow()Material Design 가이드라인을 따르는 기본 그림자를 만들어 위에서 오는 광원을 시뮬레이션합니다. 그림자 깊이는 elevation 값을 기반으로 하며 드리워진 그림자는 컴포저블의 모양으로 클리핑됩니다.

@Composable
fun ElevationBasedShadow() {
    Box(
        modifier = Modifier.aspectRatio(1f).fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Box(
            Modifier
                .size(100.dp, 100.dp)
                .shadow(10.dp, RectangleShape)
                .background(Color.White)
        )
    }
}

흰색 직사각형 모양 주위에 회색 그림자가 드리워져 있습니다.
그림 1. Modifier.shadow로 만든 고도 기반 그림자입니다.

그림자 구현

dropShadow() 수정자를 사용하여 콘텐츠 뒤에 정확한 그림자를 그려 요소를 입체적으로 보이게 합니다.

Shadow 매개변수를 통해 다음 주요 측면을 제어할 수 있습니다.

  • radius: 흐림의 부드러움과 확산을 정의합니다.
  • color: 색조의 색상을 정의합니다.
  • offset: x축과 y축을 따라 그림자의 도형을 배치합니다.
  • spread: 그림자 도형의 확장 또는 축소를 제어합니다.

또한 shape 매개변수는 그림자의 전체 모양을 정의합니다. androidx.compose.foundation.shape 패키지의 모든 도형과 Material Expressive shapes를 사용할 수 있습니다.

기본 그림자를 구현하려면 컴포저블 체인에 dropShadow() 수정자를 추가하여 반지름, 색상, 확산을 제공합니다. 그림자 위에 표시되는 purpleColor 배경은 dropShadow() 수정자 뒤에 그려집니다.

@Composable
fun SimpleDropShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(300.dp)
                .dropShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 6.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 4.dp, 4.dp)
                    )
                )
                .align(Alignment.Center)
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
        ) {
            Text(
                "Drop Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

코드에 관한 핵심 사항

  • dropShadow() 수정자는 내부 Box에 적용됩니다. 그림자에는 다음과 같은 특성이 있습니다.
    • 모서리가 둥근 직사각형 모양 (RoundedCornerShape(20.dp))
    • 10.dp의 블러 반경으로 가장자리가 부드럽고 확산됩니다.
    • 6.dp의 확산으로 그림자 크기가 확대되어 그림자를 드리우는 상자보다 커집니다.
    • 0.5f의 알파로 그림자를 반투명하게 만듭니다.
  • 그림자가 정의되면 .background() 수정자가 적용됩니다.
    • Box가 흰색으로 채워져 있습니다.
    • 배경이 그림자와 동일한 둥근 직사각형 모양으로 잘립니다.

결과

흰색 직사각형 모양 주위에 회색 드롭 섀도우가 드리워져 있습니다.
그림 2. 도형 주위에 그려진 그림자

내부 그림자 구현

dropShadow의 반대 효과를 만들려면 Modifier.innerShadow()를 사용하세요. 이 효과는 요소가 기본 표면에 움푹 들어가거나 눌린 듯한 착시를 만듭니다.

내부 그림자를 만들 때는 순서가 중요합니다. 내부 그림자는 콘텐츠의 상단에 그려지므로 일반적으로 다음을 수행해야 합니다.

  1. 배경 콘텐츠를 그립니다.
  2. innerShadow() 수정자를 적용하여 오목한 모양을 만듭니다.

innerShadow()가 배경 앞에 배치되면 배경이 그림자 위에 그려져 그림자가 완전히 숨겨집니다.

다음 예는 RoundedCornerShapeinnerShadow()를 적용한 경우를 보여줍니다.

@Composable
fun SimpleInnerShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
                .innerShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 2.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 6.dp, 7.dp)
                    )
                )

        ) {
            Text(
                "Inner Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

흰색 직사각형 모양 안에 있는 회색 내부 그림자
그림 3. 둥근 모서리 직사각형에 Modifier.innerShadow()을 적용한 모습

사용자 상호작용 시 그림자 애니메이션

사용자 상호작용에 그림자가 반응하도록 하려면 그림자 속성을 Compose의 애니메이션 API와 통합하면 됩니다. 예를 들어 사용자가 버튼을 누르면 그림자가 변경되어 즉각적인 시각적 피드백을 제공할 수 있습니다.

다음 코드는 그림자를 사용하여 '눌림' 효과를 만듭니다 (표면이 화면으로 아래로 밀리는 착시).

@Composable
fun AnimatedColoredShadows() {
    SnippetsTheme {
        Box(Modifier.fillMaxSize()) {
            val interactionSource = remember { MutableInteractionSource() }
            val isPressed by interactionSource.collectIsPressedAsState()

            // Create transition with pressed state
            val transition = updateTransition(
                targetState = isPressed,
                label = "button_press_transition"
            )

            fun <T> buttonPressAnimation() = tween<T>(
                durationMillis = 400,
                easing = EaseInOut
            )

            // Animate all properties using the transition
            val shadowAlpha by transition.animateFloat(
                label = "shadow_alpha",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) 0f else 1f
            }
            // ...

            val blueDropShadow by transition.animateColor(
                label = "shadow_color",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) Color.Transparent else blueDropShadowColor
            }

            // ...

            Box(
                Modifier
                    .clickable(
                        interactionSource, indication = null
                    ) {
                        // ** ...... **//
                    }
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = blueDropShadow,
                            offset = DpOffset(x = 0.dp, -(2).dp),
                            alpha = shadowAlpha
                        )
                    )
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = darkBlueDropShadow,
                            offset = DpOffset(x = 2.dp, 6.dp),
                            alpha = shadowAlpha
                        )
                    )
                    // note that the background needs to be defined before defining the inner shadow
                    .background(
                        color = Color(0xFFFFFFFF),
                        shape = RoundedCornerShape(70.dp)
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 8.dp,
                            spread = 4.dp,
                            color = innerShadowColor2,
                            offset = DpOffset(x = 4.dp, 0.dp)
                        )
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 20.dp,
                            spread = 4.dp,
                            color = innerShadowColor1,
                            offset = DpOffset(x = 4.dp, 0.dp),
                            alpha = innerShadowAlpha
                        )
                    )

            ) {
                Text(
                    "Animated Shadows",
                    // ...
                )
            }
        }
    }
}

코드에 관한 핵심 사항

  • transition.animateColortransition.animateFloat를 사용하여 누를 때 애니메이션을 적용할 매개변수의 시작 및 종료 상태를 선언합니다.
  • updateTransition를 사용하고 선택한 targetState (targetState = isPressed)를 제공하여 모든 애니메이션이 동기화되었는지 확인합니다. isPressed가 변경될 때마다 전환 객체는 현재 값에서 새 타겟 값으로 모든 하위 속성의 애니메이션을 자동으로 관리합니다.
  • 전환의 타이밍과 이징을 제어하는 buttonPressAnimation 사양을 정의합니다. 지속 시간이 400밀리초이고 EaseInOut 곡선이 있는 tween (중간의 약자)을 지정합니다. 즉, 애니메이션이 느리게 시작되고 중간에 빨라지며 끝에 느려집니다.
  • 모든 애니메이션 속성을 적용하여 시각적 요소를 만드는 수정자 함수 체인이 있는 Box를 정의합니다. 여기에는 다음이 포함됩니다.
    • .clickable(): Box를 대화형으로 만드는 수정자입니다.
    • .dropShadow(): 두 개의 외부 그림자가 먼저 적용됩니다. 색상 및 알파 속성은 애니메이션 값 (blueDropShadow 등)에 연결되어 초기 돌출된 모양을 만듭니다.
    • .innerShadow(): 배경 위에 두 개의 내부 그림자가 그려집니다. 속성은 다른 애니메이션 값(innerShadowColor1 등)과 연결되어 들여쓰기된 모양을 만듭니다.

결과

그림 4. 사용자가 누르면 애니메이션이 적용되는 그림자

그라데이션 그림자 만들기

그림자는 단색으로 제한되지 않습니다. 그림자 API는 Brush를 허용하므로 그라데이션 그림자를 만들 수 있습니다.

Box(
    modifier = Modifier
        .width(240.dp)
        .height(200.dp)
        .dropShadow(
            shape = RoundedCornerShape(70.dp),
            shadow = Shadow(
                radius = 10.dp,
                spread = animatedSpread.dp,
                brush = Brush.sweepGradient(
                    colors
                ),
                offset = DpOffset(x = 0.dp, y = 0.dp),
                alpha = animatedAlpha
            )
        )
        .clip(RoundedCornerShape(70.dp))
        .background(Color(0xEDFFFFFF)),
    contentAlignment = Alignment.Center
) {
    Text(
        text = breathingText,
        color = Color.Black,
        style = MaterialTheme.typography.bodyLarge
    )
}

코드에 관한 핵심 사항

  • dropShadow()는 상자 뒤에 그림자를 추가합니다.
  • brush = Brush.sweepGradient(colors)는 사전 정의된 colors 목록을 통해 회전하는 그라데이션으로 그림자를 색칠하여 무지개와 같은 효과를 만듭니다.

결과

브러시를 그림자로 사용하여 '숨쉬는' 애니메이션이 있는 그라데이션 dropShadow()을 만들 수 있습니다.

그림 5. 애니메이션이 적용된 그라데이션 그림자

그림자 결합

dropShadow()innerShadow() 수정자를 결합하고 레이어링하여 다양한 효과를 만들 수 있습니다. 다음 섹션에서는 이 기법을 사용하여 뉴모피즘, 네오브루탈리즘, 사실적인 그림자를 만드는 방법을 보여줍니다.

뉴모피즘 그림자 만들기

뉴모픽 그림자는 배경에서 자연스럽게 나타나는 부드러운 모양이 특징입니다. 뉴모피즘 그림자를 만들려면 다음을 실행하세요.

  1. 배경과 동일한 색상을 공유하는 요소를 사용합니다.
  2. 두 개의 희미한 반대 그림자를 적용합니다. 한쪽 모서리에는 밝은 그림자를, 반대쪽 모서리에는 어두운 그림자를 적용합니다.

다음 스니펫은 두 개의 dropShadow() 수정자를 레이어링하여 뉴모피즘 효과를 만듭니다.

@Composable
fun NeumorphicRaisedButton(
    shape: RoundedCornerShape = RoundedCornerShape(30.dp)
) {
    val bgColor = Color(0xFFe0e0e0)
    val lightShadow = Color(0xFFFFFFFF)
    val darkShadow = Color(0xFFb1b1b1)
    val upperOffset = -10.dp
    val lowerOffset = 10.dp
    val radius = 15.dp
    val spread = 0.dp
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(bgColor)
            .wrapContentSize(Alignment.Center)
            .size(240.dp)
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = lightShadow,
                    spread = spread,
                    offset = DpOffset(upperOffset, upperOffset)
                ),
            )
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = darkShadow,
                    spread = spread,
                    offset = DpOffset(lowerOffset, lowerOffset)
                ),

            )
            .background(bgColor, shape)
    )
}

흰색 배경에 뉴모피즘 효과가 적용된 흰색 직사각형 모양
그림 6. 뉴모피즘 그림자 효과

네오브루탈리즘 그림자 만들기

네오브루탈리스트 스타일은 고대비, 블록형 레이아웃, 선명한 색상, 두꺼운 테두리를 보여줍니다. 이 효과를 만들려면 다음 스니펫과 같이 블러가 0이고 오프셋이 명확한 dropShadow()를 사용하세요.

@Composable
fun NeoBrutalShadows() {
    SnippetsTheme {
        val dropShadowColor = Color(0xFF007AFF)
        val borderColor = Color(0xFFFF2D55)
        Box(Modifier.fillMaxSize()) {
            Box(
                Modifier
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .dropShadow(
                        shape = RoundedCornerShape(0.dp),
                        shadow = Shadow(
                            radius = 0.dp,
                            spread = 0.dp,
                            color = dropShadowColor,
                            offset = DpOffset(x = 8.dp, 8.dp)
                        )
                    )
                    .border(
                        8.dp, borderColor
                    )
                    .background(
                        color = Color.White,
                        shape = RoundedCornerShape(0.dp)
                    )
            ) {
                Text(
                    "Neobrutal Shadows",
                    modifier = Modifier.align(Alignment.Center),
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

노란색 배경에 파란색 그림자가 있는 흰색 직사각형 주위에 빨간색 테두리가 있습니다.
그림 7. 네오브루탈리즘 그림자 효과

사실적인 그림자 만들기

사실적인 그림자는 실제 세계의 그림자를 모방합니다. 기본 광원에 의해 조명되는 것처럼 보여 직접 그림자와 더 확산된 그림자가 모두 생성됩니다. 다음 스니펫과 같이 속성이 다른 dropShadow()innerShadow() 인스턴스를 여러 개 쌓아 현실적인 그림자 효과를 재현할 수 있습니다.

@Composable
fun RealisticShadows() {
    Box(Modifier.fillMaxSize()) {
        val dropShadowColor1 = Color(0xB3000000)
        val dropShadowColor2 = Color(0x66000000)

        val innerShadowColor1 = Color(0xCC000000)
        val innerShadowColor2 = Color(0xFF050505)
        val innerShadowColor3 = Color(0x40FFFFFF)
        val innerShadowColor4 = Color(0x1A050505)
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 40.dp,
                        spread = 0.dp,
                        color = dropShadowColor1,
                        offset = DpOffset(x = 2.dp, 8.dp)
                    )
                )
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 0.dp,
                        color = dropShadowColor2,
                        offset = DpOffset(x = 0.dp, 4.dp)
                    )
                )
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.Black,
                    shape = RoundedCornerShape(100.dp)
                )
// //
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 3.dp,
                        color = innerShadowColor1,
                        offset = DpOffset(x = 6.dp, 6.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 1.dp,
                        color = Color.White,
                        offset = DpOffset(x = 5.dp, 5.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 5.dp,
                        color = innerShadowColor2,
                        offset = DpOffset(x = (-3).dp, (-12).dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 10.dp,
                        color = innerShadowColor3,
                        offset = DpOffset(x = 0.dp, 0.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 9.dp,
                        color = innerShadowColor4,
                        offset = DpOffset(x = 1.dp, 1.dp)
                    )
                )

        ) {
            Text(
                "Realistic Shadows",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 24.sp,
                color = Color.White
            )
        }
    }
}

코드에 관한 핵심 사항

  • 속성이 서로 다른 두 개의 연결된 dropShadow() 수정자가 적용된 후 background 수정자가 적용됩니다.
  • 연결된 innerShadow() 수정자는 구성요소의 가장자리 주위에 금속 테두리 효과를 적용하는 데 사용됩니다.

결과

이전 코드 스니펫은 다음을 생성합니다.

검은색 둥근 모양 주위에 흰색 사실적인 그림자가 있습니다.
그림 8. 사실적인 그림자 효과