[Android Compose] Figma의 타원형 Radial Gradient를 구현해보자

2025. 6. 6. 20:47·Android/Compose

🧩 문제 상황: Compose에서 Figma gradient가 구현되지 않는다..!

안녕하세요! 오늘은 컴포즈에서 Gradient 구현에 대해 얘기해보려고 해요!

최근 참여중인 프로젝트 Spoony에서 아래와 같은 디자인이 있었어요!

처음에는 단순히 Brush.radialGradient(...)로 해결되기를 제발 바랬어요,,, 하지만 결과는 너무 처참했어요. 그래서 피그마 스펙을 좀더 자세히 뜯어봤습니다. 중심점이 단순한 원처럼 보이지 않았거든요!

border-radius: 40px;
background: radial-gradient(141.42% 141.42% at 100% 0%, #FF5235 54.5%, #FFCEC6 100%);

위의 ActiveTrack의 CSS스펙이에요. 이걸보고 아..단순한 원은 아니구나 했습니다ㅠ

1. Compose의 한계: radial-gradient는 원형만 지원해요

Jetpack Compose의 Brush.radialGradient()는 중심점(center)과 반지름(radius)을 설정하는 원형 그라디언트만 지원해요. 하지만 위의 Figma 스펙은 타원형(Elliptical)이고, 중심이 오른쪽 상단(Top-right)에 위치해요.

141.42% 141.42% 타원의 x축, y축 반지름 비율 → √2 × 100 ≒ 141.42 = 정사각형 대각선
at 100% 0% 중심점이 오른쪽 상단 (top-right)에 위치

 

CSS의 radial-gradient는 타원형 그라디언트를 지원하지만, Compose의 Brush.radialGradient()는 원형만 지원해요. Compose가 아직 CSS 수준의 복잡한 gradient 표현을 직접 제공하지 않기 때문에 저는 직접 그라디언트를 만들고자 했어요.

2. 직접 그라디언트를 만들자!

원형 radial-gradient를 타원처럼 보이게 만들기

Jetpack Compose는 Canvas 기반의 그래픽스 시스템이기 때문에, Android의 그래픽스 API를 함께 사용할 수 있어요. RadialGradient는 Matrix를 적용해 스케일 변형이 가능하기 때문에 사용해봤어요.

그럼 먼저 원형을 타원형으로 만들어야겠죠? 어떻게 할까요?

  • 원형 radial gradient를 먼저 만들고
  • y축 방향으로 스케일을 적용해 타원처럼 보이게 처리하자
val scaleY = radiusY / radiusX
matrix.setScale(1f, scaleY, cx, cy)
shader.setLocalMatrix(matrix)

이 방식은 OpenGL이나 벡터 그래픽스에서 흔히 사용하는 기법으로, 실제 UI 프레임워크에도 사용되고 있어요.

3. Modifier 확장함수로 Figma gradient 구현

@Composable
fun Modifier.spoonySliderGradient(
    cornerRadius: Dp = 40.dp,
    mainColor: Color = Color(0xFFFF5235),
    secondColor: Color = Color(0xFFFFCEC6)
) = composed {
    this.drawWithCache {
        // 라운드 모서리 형태를 위한 Path 생성
        val roundPath = Path().apply {
            addRoundRect(
                RoundRect(
                    rect = Rect(Offset.Zero, size),
                    cornerRadius = CornerRadius(cornerRadius.toPx())
                )
            )
        }

        // gradient 중심점을 오른쪽 상단으로 지정
        val cx = size.width
        val cy = 0f

        // radial-gradient의 반지름 (x축, y축)을 계산
        val radiusX = size.width * 1.4142f  // √2 × width
        val radiusY = size.height * 1.4142f // √2 × height

        // y축 스케일 비율 계산
        val scaleY = radiusY / radiusX

        // RadialGradient 생성
        val shader = RadialGradient(
            cx,
            cy,
            radiusX,
            intArrayOf(mainColor.toArgb(), secondColor.toArgb()),
            floatArrayOf(0.545f, 1f), // 54.5% ~ 100%
            Shader.TileMode.CLAMP
        ).apply {
            // 원형을 y축 방향으로 눌러서 타원처럼 만들기
            val matrix = Matrix().apply {
                setScale(1f, scaleY, cx, cy)
            }
            setLocalMatrix(matrix)
        }

        // Paint 객체 생성 및 shader 바인딩
        val paint = Paint().asFrameworkPaint().apply {
            isAntiAlias = true
            this.shader = shader
        }

        // 실제 gradient를 그리고 content 위에 출력
        onDrawWithContent {
            clipPath(roundPath) {
                drawIntoCanvas { canvas ->
                    canvas.nativeCanvas.drawPath(roundPath.asAndroidPath(), paint)
                }
            }
            drawContent()
        }
    }
}

4. 어떻게 구현한건데..?

먼저 drawWithCache는 Compose에서 제공하는 성능 최적화 도구인데요, 컴포넌트의 크기나 색상이 변하지 않으면 gradient 계산 결과를 캐싱해 재사용해요. 매 프레임마다 비싼 계산을 반복하지 않게 하기위해 사용했어요.

Figma에서 border-radius: 40px 사양이 주어졌기 때문에, 이를 Compose에서 구현하려면 Path를 사용해 모서리가 둥근 사각형 경로를 만들어야 해요. clipPath(...)로 해당 경로를 기준으로 그라디언트를 잘라내야 정확한 외곽 처리를 반영해요.

그리고 setScale(1f, scaleY, cx, cy)는 다음과 같이 동작해요

  • x축 스케일: 1.0 (변화 없음)
  • y축 스케일: scaleY (압축 또는 확장)
  • 변환 중심점: (cx, cy) - 오른쪽 상단

이로 인해 원형 gradient가 y축 방향으로 압축되어 타원형으로 보이게 돼요!

🧪 사용 예시

Box(
    modifier = Modifier
        .fillMaxWidth(sliderState.value / 100f)
        .height(trackHeight)
        .then(
            // 반지름 0이면 앱 터짐
            if ((sliderState.value / 100f) > 0.01f) {
                Modifier.spoonySliderGradient()
            } else {
                Modifier
            }
        )
        .clip(CircleShape)
)

초기의 목적처럼 슬라이더의 ActiveTrack에 적용했어요. .then으로 분기처리를 한 이유는 슬라이더 조정을 통해서 반지름이 0또는 음수가 되는순간 크래시가 나기 때문이에요.. 적용된 모습은 아래와 같아요!

 

Figma의 GUI
In Android Device

개인적으로 아주 만족스러운 결과물이 나왔습니다ㅠ

그래서 결론.

Figma의 radial-gradient 스펙을 Android Compose에서 어떻게 정확히 해석하고 구현할 수 있는지 작성해보았습니다.

Jetpack Compose는 아직 CSS 수준의 복잡한 gradient 표현을 직접 제공하지는 않지만, Android native API와 수학적 원리를 적절히 활용하면 디자이너의 Figma 스펙을 최대한 반영할 수 있어요. 제 코드가 도움이 되길 바랍니다!

'Android > Compose' 카테고리의 다른 글

[Android Compose] Effect Handlers 딥다이브  (0) 2025.06.20
[Android Compose] 상태 읽기 지연(Defer State Reads)으로 리컴포지션 최적화하기  (0) 2025.06.18
[Android Compose] Figma 그림자를 쉽게 만들어 보자  (0) 2025.05.14
[Android Compose] TopBar 배경색 전환 애니메이션 구현 방법  (0) 2025.02.05
[Android Compose] ImePadding 이중 패딩 문제 해결 방법 (+키보드 영역 조정)  (7) 2025.02.05
'Android/Compose' 카테고리의 다른 글
  • [Android Compose] Effect Handlers 딥다이브
  • [Android Compose] 상태 읽기 지연(Defer State Reads)으로 리컴포지션 최적화하기
  • [Android Compose] Figma 그림자를 쉽게 만들어 보자
  • [Android Compose] TopBar 배경색 전환 애니메이션 구현 방법
한민돌
한민돌
Android 개발자가 되기까지.
  • 한민돌
    미래 가젯 연구소
    한민돌
  • 전체
    오늘
    어제
    • 분류 전체보기 (20) N
      • Android (4)
        • Compose (10)
        • Jetpack (2)
      • Kotlin (2)
        • Kotlin In Action (0)
      • 외부 활동 (2) N
        • 우아한테크코스 8기 (2) N
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • GitHub
  • 인기 글

  • 태그

    rememberupdatedstate
    Multi-module
    compose
    backing property
    OssLicensePlugin
    Android
    API35
    build-logic
    coroutine
    LaunchedEffect
    AboutLibraries
    runcatching
    derivedstateof
    producestate
    Baseline Profile
    Google Recommend Architecture
    sideeffect
    우테코
    jetpack
    custom plugin
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
한민돌
[Android Compose] Figma의 타원형 Radial Gradient를 구현해보자
상단으로

티스토리툴바