Compose를 사용하면 다각형으로 구성된 도형을 만들 수 있습니다. 예를 들어 다음과 같은 모양을 만들 수 있습니다.

Compose에서 맞춤 둥근 다각형을 만들려면 app/build.gradle
에 graphics-shapes
종속 항목을 추가합니다.
implementation "androidx.graphics:graphics-shapes:1.0.1"
이 라이브러리를 사용하면 다각형으로 구성된 도형을 만들 수 있습니다. 다각형 모양에는 직선 모서리와 날카로운 모서리만 있지만 이러한 모양에서는 선택적으로 둥근 모서리를 사용할 수 있습니다. 이를 통해 두 가지 다른 모양 사이에서 간단하게 변형할 수 있습니다. 임의의 모양 간의 모핑은 어렵고 디자인 타임 문제가 되는 경향이 있습니다. 하지만 이 라이브러리는 유사한 다각형 구조를 가진 이러한 도형 간에 변형하여 간단하게 만듭니다.
다각형 만들기
다음 스니펫은 드로잉 영역 중앙에 점 6개가 있는 기본 다각형 모양을 만듭니다.
Box( modifier = Modifier .drawWithCache { val roundedPolygon = RoundedPolygon( numVertices = 6, radius = size.minDimension / 2, centerX = size.width / 2, centerY = size.height / 2 ) val roundedPolygonPath = roundedPolygon.toPath().asComposePath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Blue) } } .fillMaxSize() )

이 예시에서 라이브러리는 요청된 모양을 나타내는 지오메트리를 보유하는 RoundedPolygon
를 만듭니다. Compose 앱에서 이 도형을 그리려면 Path
객체를 가져와 Compose에서 그리는 방법을 아는 형식으로 도형을 가져와야 합니다.
다각형의 모서리를 둥글게 만들기
다각형의 모서리를 둥글게 하려면 CornerRounding
매개변수를 사용합니다. 이 함수는 radius
및 smoothing
의 두 매개변수를 사용합니다. 각 둥근 모서리는 1~3개의 3차 곡선으로 구성되며, 가운데는 원호 모양이고 양쪽 곡선은 모양의 가장자리에서 가운데 곡선으로 전환됩니다.
반경
radius
은 꼭짓점을 둥글게 만드는 데 사용되는 원의 반지름입니다.
예를 들어 다음 둥근 모서리 삼각형은 다음과 같이 만들어집니다.


r
은 둥근 모서리의 원형 둥근 크기를 결정합니다.부드러움
스무딩은 모서리의 원형 라운딩 부분에서 가장자리까지 도달하는 데 걸리는 시간을 결정하는 요소입니다. 0의 평활화 계수(평활화되지 않음, CornerRounding
의 기본값)는 순전히 원형 모서리 라운딩을 생성합니다. 0이 아닌 평활화 계수 (최대 1.0)를 사용하면 세 개의 별도 곡선으로 모서리가 둥글게 처리됩니다.


예를 들어 아래 스니펫은 스무딩을 0으로 설정하는 것과 1로 설정하는 것의 미묘한 차이를 보여줍니다.
Box( modifier = Modifier .drawWithCache { val roundedPolygon = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2, centerX = size.width / 2, centerY = size.height / 2, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val roundedPolygonPath = roundedPolygon.toPath().asComposePath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Black) } } .size(100.dp) )

크기 및 위치
기본적으로 도형은 중심 (0, 0
)을 기준으로 반경이 1
인 상태로 생성됩니다. 이 반경은 도형이 기반으로 하는 다각형의 중심과 외부 꼭짓점 사이의 거리를 나타냅니다. 모서리를 둥글게 하면 둥근 모서리가 둥글게 처리되는 꼭짓점보다 중심에 더 가까워지므로 도형이 더 작아집니다. 다각형의 크기를 조정하려면 radius
값을 조정합니다. 위치를 조정하려면 다각형의 centerX
또는 centerY
을 변경하세요.
또는 DrawScope#translate()
와 같은 표준 DrawScope
변환 함수를 사용하여 객체를 변환하여 크기, 위치, 회전을 변경합니다.
모양 변환
Morph
객체는 두 다각형 모양 사이의 애니메이션을 나타내는 새로운 모양입니다. 두 도형 사이를 모핑하려면 두 개의 RoundedPolygons
와 이 두 도형을 사용하는 Morph
객체를 만듭니다. 시작 모양과 종료 모양 사이의 모양을 계산하려면 0과 1 사이의 progress
값을 제공하여 시작 (0) 모양과 종료 (1) 모양 사이의 모양을 결정하세요.
Box( modifier = Modifier .drawWithCache { val triangle = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val square = RoundedPolygon( numVertices = 4, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f ) val morph = Morph(start = triangle, end = square) val morphPath = morph .toPath(progress = 0.5f).asComposePath() onDrawBehind { drawPath(morphPath, color = Color.Black) } } .fillMaxSize() )
위의 예에서 진행률은 두 도형(둥근 삼각형과 정사각형)의 정확히 중간에 있으므로 다음 결과가 생성됩니다.

대부분의 시나리오에서 모핑은 정적 렌더링이 아닌 애니메이션의 일부로 실행됩니다. 이 두 가지 사이를 애니메이션으로 처리하려면 Compose의 표준 애니메이션 API를 사용하여 시간이 지남에 따라 진행률 값을 변경하면 됩니다. 예를 들어 다음과 같이 두 도형 간의 모핑을 무한대로 애니메이션으로 만들 수 있습니다.
val infiniteAnimation = rememberInfiniteTransition(label = "infinite animation") val morphProgress = infiniteAnimation.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( tween(500), repeatMode = RepeatMode.Reverse ), label = "morph" ) Box( modifier = Modifier .drawWithCache { val triangle = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val square = RoundedPolygon( numVertices = 4, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f ) val morph = Morph(start = triangle, end = square) val morphPath = morph .toPath(progress = morphProgress.value) .asComposePath() onDrawBehind { drawPath(morphPath, color = Color.Black) } } .fillMaxSize() )

다각형을 클립으로 사용
Compose에서는 컴포저블이 렌더링되는 방식을 변경하고 클리핑 영역 주위에 그려지는 그림자를 활용하기 위해 clip
수정자를 사용하는 것이 일반적입니다.
fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) } class RoundedPolygonShape( private val polygon: RoundedPolygon, private var matrix: Matrix = Matrix() ) : Shape { private var path = Path() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { path.rewind() path = polygon.toPath().asComposePath() matrix.reset() val bounds = polygon.getBounds() val maxDimension = max(bounds.width, bounds.height) matrix.scale(size.width / maxDimension, size.height / maxDimension) matrix.translate(-bounds.left, -bounds.top) path.transform(matrix) return Outline.Generic(path) } }
그런 다음 다음 스니펫과 같이 다각형을 클립으로 사용할 수 있습니다.
val hexagon = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val clip = remember(hexagon) { RoundedPolygonShape(polygon = hexagon) } Box( modifier = Modifier .clip(clip) .background(MaterialTheme.colorScheme.secondary) .size(200.dp) ) { Text( "Hello Compose", color = MaterialTheme.colorScheme.onSecondary, modifier = Modifier.align(Alignment.Center) ) }
이로 인해 다음과 같은 결과가 발생합니다.

이전 렌더링과 크게 다르지 않을 수 있지만 Compose의 다른 기능을 활용할 수 있습니다. 예를 들어 이 기법을 사용하여 이미지를 클리핑하고 클리핑된 영역 주위에 그림자를 적용할 수 있습니다.
val hexagon = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val clip = remember(hexagon) { RoundedPolygonShape(polygon = hexagon) } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .graphicsLayer { this.shadowElevation = 6.dp.toPx() this.shape = clip this.clip = true this.ambientShadowColor = Color.Black this.spotShadowColor = Color.Black } .size(200.dp) ) }

클릭 시 변신 버튼
graphics-shape
라이브러리를 사용하여 누르면 두 모양 사이에서 변신하는 버튼을 만들 수 있습니다. 먼저 Shape
를 확장하는 MorphPolygonShape
를 만들어 적절하게 크기를 조정하고 변환합니다. 모양이 애니메이션으로 표시될 수 있도록 진행률을 전달하는 데 유의하세요.
class MorphPolygonShape( private val morph: Morph, private val percentage: Float ) : Shape { private val matrix = Matrix() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. matrix.scale(size.width / 2f, size.height / 2f) matrix.translate(1f, 1f) val path = morph.toPath(progress = percentage).asComposePath() path.transform(matrix) return Outline.Generic(path) } }
이 변신 모양을 사용하려면 shapeA
및 shapeB
의 두 다각형을 만듭니다. Morph
를 만들고 기억합니다. 그런 다음 누를 때 interactionSource
를 애니메이션의 원동력으로 사용하여 모핑을 클립 윤곽선으로 버튼에 적용합니다.
val shapeA = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val shapeB = remember { RoundedPolygon.star( 6, rounding = CornerRounding(0.1f) ) } val morph = remember { Morph(shapeA, shapeB) } val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val animatedProgress = animateFloatAsState( targetValue = if (isPressed) 1f else 0f, label = "progress", animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium) ) Box( modifier = Modifier .size(200.dp) .padding(8.dp) .clip(MorphPolygonShape(morph, animatedProgress.value)) .background(Color(0xFF80DEEA)) .size(200.dp) .clickable(interactionSource = interactionSource, indication = null) { } ) { Text("Hello", modifier = Modifier.align(Alignment.Center)) }
상자를 탭하면 다음과 같은 애니메이션이 표시됩니다.

모양 변형을 무한대로 애니메이션 처리
모핑 도형을 무한대로 애니메이션 처리하려면 rememberInfiniteTransition
를 사용합니다.
아래는 시간이 지남에 따라 모양이 무한히 바뀌는 (회전하는) 프로필 사진의 예입니다. 이 접근 방식에서는 위에 표시된 MorphPolygonShape
를 약간 조정합니다.
class CustomRotatingMorphShape( private val morph: Morph, private val percentage: Float, private val rotation: Float ) : Shape { private val matrix = Matrix() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. matrix.scale(size.width / 2f, size.height / 2f) matrix.translate(1f, 1f) matrix.rotateZ(rotation) val path = morph.toPath(progress = percentage).asComposePath() path.transform(matrix) return Outline.Generic(path) } } @Preview @Composable private fun RotatingScallopedProfilePic() { val shapeA = remember { RoundedPolygon( 12, rounding = CornerRounding(0.2f) ) } val shapeB = remember { RoundedPolygon.star( 12, rounding = CornerRounding(0.2f) ) } val morph = remember { Morph(shapeA, shapeB) } val infiniteTransition = rememberInfiniteTransition("infinite outline movement") val animatedProgress = infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( tween(2000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "animatedMorphProgress" ) val animatedRotation = infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( tween(6000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "animatedMorphProgress" ) Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .clip( CustomRotatingMorphShape( morph, animatedProgress.value, animatedRotation.value ) ) .size(200.dp) ) } }
이 코드는 다음과 같은 재미있는 결과를 제공합니다.

맞춤 다각형
정다각형으로 만든 도형이 사용 사례를 충족하지 않는 경우 꼭짓점 목록을 사용하여 더 맞춤설정된 도형을 만들 수 있습니다. 예를 들어 다음과 같은 하트 모양을 만들 수 있습니다.

x, y 좌표의 부동 소수점 배열을 사용하는 RoundedPolygon
오버로드를 사용하여 이 도형의 개별 꼭짓점을 지정할 수 있습니다.
하트 다각형을 분석해 보면 점을 지정하는 데 극좌표계를 사용하면 직교 (x,y) 좌표계를 사용하는 것보다 더 쉽다는 것을 알 수 있습니다. 여기서 0°
는 오른쪽에서 시작하여 시계 방향으로 진행되고 270°
는 12시 방향에 있습니다.

이제 각 지점에서 중심까지의 각도 (𝜭)와 반지름을 지정하여 더 쉽게 모양을 정의할 수 있습니다.

이제 꼭짓점을 만들어 RoundedPolygon
함수에 전달할 수 있습니다.
val vertices = remember { val radius = 1f val radiusSides = 0.8f val innerRadius = .1f floatArrayOf( radialToCartesian(radiusSides, 0f.toRadians()).x, radialToCartesian(radiusSides, 0f.toRadians()).y, radialToCartesian(radius, 90f.toRadians()).x, radialToCartesian(radius, 90f.toRadians()).y, radialToCartesian(radiusSides, 180f.toRadians()).x, radialToCartesian(radiusSides, 180f.toRadians()).y, radialToCartesian(radius, 250f.toRadians()).x, radialToCartesian(radius, 250f.toRadians()).y, radialToCartesian(innerRadius, 270f.toRadians()).x, radialToCartesian(innerRadius, 270f.toRadians()).y, radialToCartesian(radius, 290f.toRadians()).x, radialToCartesian(radius, 290f.toRadians()).y, ) }
정점은 다음 radialToCartesian
함수를 사용하여 직교 좌표로 변환해야 합니다.
internal fun Float.toRadians() = this * PI.toFloat() / 180f internal val PointZero = PointF(0f, 0f) internal fun radialToCartesian( radius: Float, angleRadians: Float, center: PointF = PointZero ) = directionVectorPointF(angleRadians) * radius + center internal fun directionVectorPointF(angleRadians: Float) = PointF(cos(angleRadians), sin(angleRadians))
위 코드는 하트의 원시 꼭짓점을 제공하지만 선택한 하트 모양을 얻으려면 특정 모서리를 둥글게 해야 합니다. 90°
및 270°
의 모서리는 둥글지 않지만 다른 모서리는 둥급니다. 개별 모서리의 맞춤 둥근 모양을 적용하려면 perVertexRounding
매개변수를 사용하세요.
val rounding = remember { val roundingNormal = 0.6f val roundingNone = 0f listOf( CornerRounding(roundingNormal), CornerRounding(roundingNone), CornerRounding(roundingNormal), CornerRounding(roundingNormal), CornerRounding(roundingNone), CornerRounding(roundingNormal), ) } val polygon = remember(vertices, rounding) { RoundedPolygon( vertices = vertices, perVertexRounding = rounding ) } Box( modifier = Modifier .drawWithCache { val roundedPolygonPath = polygon.toPath().asComposePath() onDrawBehind { scale(size.width * 0.5f, size.width * 0.5f) { translate(size.width * 0.5f, size.height * 0.5f) { drawPath(roundedPolygonPath, color = Color(0xFFF15087)) } } } } .size(400.dp) )
그러면 다음과 같은 분홍색 하트가 표시됩니다.

위의 도형이 사용 사례를 포함하지 않는 경우 Path
클래스를 사용하여 맞춤 도형을 그리거나 디스크에서 ImageVector
파일을 로드하는 것이 좋습니다. graphics-shapes
라이브러리는 임의의 모양에 사용하기 위한 것이 아니라 둥근 다각형과 그 사이의 모핑 애니메이션 생성을 간소화하기 위한 것입니다.
추가 리소스
자세한 내용과 예시는 다음 리소스를 참고하세요.