Compose를 사용하면 다각형으로 만든 도형을 만들 수 있습니다. 예를 들어 다음과 같은 도형을 만들 수 있습니다.
Compose에서 원형으로 잘린 맞춤 다각형을 만들려면 app/build.gradle
에 graphics-shapes
종속 항목을 추가합니다.
implementation "androidx.graphics:graphics-shapes:1.0.0-rc01"
이 라이브러리를 사용하면 다각형으로 만든 도형을 만들 수 있습니다. 다각형 도형에는 직선 가장자리와 날카로운 모서리만 있지만 이러한 도형에는 둥근 모서리를 사용할 수 있습니다. 이렇게 하면 서로 다른 두 도형 간에 간단하게 모핑할 수 있습니다. 임의의 도형 간에 모핑하는 것은 어렵고 디자인 시간 문제인 경향이 있습니다. 하지만 이 라이브러리를 사용하면 유사한 다각형 구조로 이러한 도형 간에 모핑할 수 있습니다.
다각형 만들기
다음 스니펫은 그리기 영역 중앙에 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개의 입방 곡선으로 구성되며, 이 중 중앙은 원호 모양을 하고 있고, 두 개의 측면 ('측면') 곡선은 도형의 가장자리에서 중앙 곡선으로 전환됩니다.
반경
radius
는 정점을 둥글게 처리하는 데 사용되는 원의 반지름입니다.
예를 들어 다음과 같이 모서리가 둥근 삼각형을 만들 수 있습니다.
부드러움
부드럽게 처리는 모서리의 원형 둥근 부분에서 가장자리까지 이동하는 데 걸리는 시간을 결정하는 요소입니다. 평활화 계수가 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
라이브러리는 임의의 도형에 사용하는 것이 아니라 특히 둥근 다각형을 간편하게 만들고 그 사이의 모프 애니메이션을 단순화하기 위한 것입니다.
추가 리소스
자세한 내용과 예시는 다음 리소스를 참고하세요.