그림자는 UI를 시각적으로 높이고, 사용자에게 상호작용을 나타내며, 사용자 작업에 대한 즉각적인 피드백을 제공합니다. Compose는 앱에 그림자를 통합하는 여러 가지 방법을 제공합니다.
Modifier.shadow(): Material Design 가이드라인을 준수하는 컴포저블 뒤에 고도 기반 그림자를 만듭니다.Modifier.dropShadow(): 컴포저블 뒤에 표시되어 고도가 있는 것처럼 보이게 하는 맞춤설정 가능한 그림자를 만듭니다.Modifier.innerShadow(): 컴포저블의 테두리 내부에 그림자를 만들어 뒤에 있는 표면에 눌린 것처럼 보이게 합니다.
Modifier.shadow() 는 기본 그림자를 만드는 데 적합하며, dropShadow() 및 innerShadow() 수정자는 그림자 렌더링을 더 세부적으로 제어하고 정밀하게 제어할 수 있습니다.
이 페이지에서는 사용자 상호작용 시
그림자를 애니메이션화하는 방법과
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) ) } }
Modifier.shadow()로 만든 고도 기반 그림자그림자 구현
dropShadow() 수정자를 사용하여
콘텐츠 뒤에 정확한 그림자를 그립니다. 이렇게 하면 요소가 고도가 있는 것처럼 보입니다.
Shadow 매개변수를 통해 다음 주요 측면을 제어할 수 있습니다.
radius: 흐림의 부드러움과 확산을 정의합니다.color: 색조의 색상을 정의합니다.offset: x축과 y축을 따라 그림자의 도형을 배치합니다.spread: 그림자 도형의 확장 또는 축소를 제어합니다.
또한 shape 매개변수는 그림자의 전체 모양을 정의합니다.
androidx.compose.foundation.shape 패키지의 모든 도형뿐만 아니라 Material Expressive 도형도 사용할 수 있습니다.
기본 그림자를 구현하려면 반지름, 색상, 확산을 제공하여 컴포저블 체인에 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는 흰색으로 채워집니다.- 배경은 그림자와 동일한 모서리가 둥근 직사각형 모양으로 클리핑됩니다.
결과
내부 그림자 구현
dropShadow()의 역효과를 만들려면
Modifier.innerShadow()를 사용합니다. 이렇게 하면 요소가
움푹 들어가거나 기본 표면에 눌린 것처럼 보이는 착시 현상이 발생합니다.
내부 그림자를 만들 때는 순서가 중요합니다. innerShadow() 수정자는 콘텐츠 위 에 그립니다. 그림자가 표시되도록 하려면 일반적으로 다음 단계를 실행합니다.
- 배경 콘텐츠를 그립니다.
innerShadow()수정자를 적용하여 오목한 모양을 만듭니다.
innerShadow()가 배경 앞에 배치되면 배경이 그림자 위에 그려져 그림자를 완전히 숨깁니다.
다음 예에서는 RoundedCornerShape에 innerShadow()를 적용하는 방법을 보여줍니다.
@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 ) } } }
Modifier.innerShadow()를 적용한 모습사용자 상호작용 시 그림자 애니메이션화
그림자가 사용자 상호작용에 응답하도록 하려면 그림자 속성을 Compose's 애니메이션 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.animateColor및transition.animateFloat를 사용하여 누를 때 애니메이션화할 매개변수의 시작 및 종료 상태를 선언합니다.updateTransition을 사용하고 선택한targetState (targetState = isPressed)을 제공하여 모든 애니메이션이 동기화되었는지 확인합니다.isPressed가 변경될 때마다 전환 객체는 모든 하위 속성의 애니메이션을 현재 값에서 새 타겟 값으로 자동으로 관리합니다.- 전환의 타이밍과 이징을 제어하는
buttonPressAnimation사양을 정의합니다. 400밀리초의 지속 시간과EaseInOut곡선으로tween(중간의 약어)을 지정합니다. 즉, 애니메이션이 느리게 시작되고 중간에 빨라지며 끝에 느려집니다. - 다음과 같은 애니메이션 속성을 모두 적용하여 시각적 요소를 만드는 수정자 함수 체인이 있는
Box를 정의합니다.- .
clickable():Box를 대화형으로 만드는 수정자입니다. .dropShadow(): 두 개의 외부 그림자가 먼저 적용됩니다. 색상 및 알파 속성은 애니메이션 값 (blueDropShadow등)에 연결되어 있으며 초기 상승된 모양을 만듭니다..innerShadow(): 두 개의 내부 그림자가 배경 위에 그려집니다. 속성은 다른 애니메이션 값 집합(innerShadowColor1등)에 연결되어 있으며 들여쓰기된 모양을 만듭니다.
- .
결과
그라데이션 그림자 만들기
그림자는 단색으로 제한되지 않습니다. 그림자 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()를 만들 수 있습니다.
그림자 결합
dropShadow() 및 innerShadow() 수정자를 결합하고 레이어링하여 다양한 효과를 만들 수 있습니다. 다음 섹션에서는 이 기법을 사용하여 뉴모피즘, 네오브루탈리즘, 사실적인 그림자를 만드는 방법을 보여줍니다.
뉴모피즘 그림자 만들기
뉴모피즘 그림자는 배경에서 자연스럽게 나타나는 부드러운 모양이 특징입니다. 뉴모피즘 그림자를 만들려면 다음 단계를 따르세요.
- 배경과 동일한 색상을 공유하는 요소를 사용합니다.
- 두 개의 희미하고 반대되는 그림자를 적용합니다. 한쪽 모서리에는 밝은 그림자를, 반대쪽 모서리에는 어두운 그림자를 적용합니다.
다음 스니펫은 두 개의 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) ) }
네오브루탈리즘 그림자 만들기
네오브루탈리즘 스타일은 대비가 높은 블록형 레이아웃, 생생한 색상, 두꺼운 테두리를 보여줍니다. 이 효과를 만들려면 다음 스니펫과 같이 흐림이 없고 뚜렷한 오프셋이 있는 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 ) } } } }
사실적인 그림자 만들기
사실적인 그림자는 실제 세계의 그림자를 모방합니다. 기본 광원에 의해 조명되는 것처럼 보이며 직접 그림자와 더 확산된 그림자가 모두 생성됩니다. 다음 스니펫과 같이 속성이 다른 여러 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()수정자가 적용되어 구성요소 가장자리 주위에 금속 테두리 효과를 만듭니다.
결과
이전 코드 스니펫은 다음을 생성합니다.