Jetpack Compose에서 애니메이션 성별 선택기 만드는 방법

Jetpack Compose에서 애니메이션 성별 선택기 만드는 방법
SeedividendPosted On Aug 3, 202412 min read

안녕하세요 🙋

이 글에서는 캔버스와 사용자 지정 모양을 활용하여 Jetpack Compose에서 멋진 애니메이션 성별 선택기를 만들어보겠습니다.

시작해봅시다 🚀

Gender Selector

도형

우선, 옵션으로 렌더링 될 도형을 정의해야 합니다.

남성 🚹

object MaleShape : Shape {

    override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
        val scale = size.minDimension / 24f

        val path = Path().apply {
            // 머리
            addOval(Rect(Offset(12f * scale, 4f * scale), 2f * scale))

            // 몸과 팔다리
            moveTo(14.0f * scale, 7.0f * scale)
            lineTo(10.0f * scale, 7.0f * scale)
            cubicTo(8.9f * scale, 7.0f * scale, 8.0f * scale, 7.9f * scale, 8.0f * scale, 9.0f * scale)
            lineTo(8.0f * scale, 14.0f * scale)
            cubicTo(8.0f * scale, 14.55f * scale, 8.45f * scale, 15.0f * scale, 9.0f * scale, 15.0f * scale)
            lineTo(10.0f * scale, 15.0f * scale)
            lineTo(10.0f * scale, 21.0f * scale)
            cubicTo(10.0f * scale, 21.55f * scale, 10.45f * scale, 22.0f * scale, 11.0f * scale, 22.0f * scale)
            lineTo(13.0f * scale, 22.0f * scale)
            cubicTo(13.55f * scale, 22.0f * scale, 14.0f * scale, 21.55f * scale, 14.0f * scale, 21.0f * scale)
            lineTo(14.0f * scale, 15.0f * scale)
            lineTo(15.0f * scale, 15.0f * scale)
            cubicTo(15.55f * scale, 15.0f * scale, 16.0f * scale, 14.55f * scale, 16.0f * scale, 14.0f * scale)
            lineTo(16.0f * scale, 9.0f * scale)
            cubicTo(16.0f * scale, 7.9f * scale, 15.1f * scale, 7.0f * scale, 14.0f * scale, 7.0f * scale)
            close()
        }

        return Outline.Generic(path)
    }
}

여성 🚺

object FemaleShape : Shape {

    override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
        val scale = size.minDimension / 24f

        val path = Path().apply {
            // 머리
            addOval(Rect(Offset(12.0f * scale, 4.0f * scale), 2.0f * scale))

            // 몸과 팔다리
            moveTo(16.45f * scale, 14.63f * scale)
            lineTo(13.93f * scale, 8.31f * scale)
            cubicTo(13.61f * scale, 7.52f * scale, 12.85f * scale, 7.01f * scale, 12.0f * scale, 7.0f * scale)
            cubicTo(11.15f * scale, 7.01f * scale, 10.38f * scale, 7.52f * scale, 10.07f * scale, 8.31f * scale)
            lineTo(7.55f * scale, 14.63f * scale)
            cubicTo(7.28f * scale, 15.29f * scale, 7.77f * scale, 16.0f * scale, 8.47f * scale, 16.0f * scale)
            lineTo(10.0f * scale, 16.0f * scale)
            lineTo(10.0f * scale, 21.0f * scale)
            cubicTo(10.0f * scale, 21.55f * scale, 10.45f * scale, 22.0f * scale, 11.0f * scale, 22.0f * scale)
            lineTo(13.0f * scale, 22.0f * scale)
            cubicTo(13.55f * scale, 22.0f * scale, 14.0f * scale, 21.55f * scale, 14.0f * scale, 21.0f * scale)
            lineTo(14.0f * scale, 16.0f * scale)
            lineTo(15.53f * scale, 16.0f * scale)
            cubicTo(16.23f * scale, 16.0f * scale, 16.72f * scale, 15.29f * scale, 16.45f * scale, 14.63f * scale)
            close()
        }

        return Outline.Generic(path)
    }
}

성별 옵션

이제 모양을 정의했으므로, 해당 모양을 옵션 버튼으로 렌더링하는 컴포저를 생성해보겠습니다.

GenderOptionStyle

컴포저블을 만들기 전에 옵션의 스타일을 나타내는 데이터 클래스를 정의해야 합니다:

data class GenderOptionStyle(
    val backgroundColor: Color,
    val fillColor: Color,
    val effectOrigin: TransformOrigin
) {
    companion object {
        val MaleDefault = GenderOptionStyle(Color.LightGray, Color.Blue, TransformOrigin(0f, 0f))
        val FemaleDefault = GenderOptionStyle(Color.LightGray, Color.Red, TransformOrigin(1f, 0f))
    }
}

effectOrigin은 채워지는 효과 애니메이션의 중심점을 결정하며, 성장하는 원 효과의 원점입니다.

옵션 코드

@Composable
fun GenderOption(
    shape: Shape,
    selected: Boolean,
    style: GenderOptionStyle,
    modifier: Modifier = Modifier,
    animationSpec: AnimationSpec<Float> = tween(400, easing = LinearEasing)
) {
    // 선택 상태의 진행률을 애니메이션
    val progress by animateFloatAsState(
        targetValue = if (selected) 1f else 0f,
        animationSpec = animationSpec
    )

    Canvas(
        modifier = modifier.size(48.dp)
    ) {
        // 모양을 아웃라인으로 변환
        val outline = shape.createOutline(size, layoutDirection, this)
        // 아웃라인을 기반으로 경로 생성
        val path = Path().apply { addOutline(outline) }

        // 캔버스 크기의 대각선 길이 계산
        val diagonal = sqrt(size.width.pow(2) + size.height.pow(2))
        // 선택 진행률에 따른 채우기 원의 반경 계산
        val radius = diagonal * progress
        // effectOrigin에 따라 채우기 효과의 중심 위치 결정
        val circleCenter = Offset(
            x = size.width * style.effectOrigin.pivotFractionX,
            y = size.height * style.effectOrigin.pivotFractionY
        )

        // 도형 경로로 그리기 영역 자르기
        clipPath(path) {
            // 배경 색상 그리기
            drawRect(style.backgroundColor)
            // 채우기 원 그리기
            drawCircle(
                color = style.fillColor,
                radius = radius,
                center = circleCenter
            )
        }
    }
}

성별 선택기

이 섹션에서는 선택기 Composable을 구성합니다.

옵션

셀렉터를 구현하기 전에 사용 가능한 옵션을 나타내는 enum 클래스를 정의해 봅시다:

enum class Gender { MALE, FEMALE }

유틸리티

코드를 더 깔끔하게 만들기 위해, clickableGenderOption modifier를 정의해보겠습니다. 이 modifier는 시각적인 표시 없이 성별 옵션을 클릭하는 이벤트를 처리할 것입니다:

fun Modifier.clickableGenderOption(
    gender: Gender,
    onGenderChange: (newGender: Gender) -> Unit
) = composed {
    this.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null,
        onClick = { onGenderChange(gender) }
    )
}

Selector Code

@Composable
fun GenderSelector(
    selected: Gender,
    modifier: Modifier = Modifier,
    iconSize: Dp = 48.dp,
    maleStyle: GenderOptionStyle = GenderOptionStyle.MaleDefault,
    femaleStyle: GenderOptionStyle = GenderOptionStyle.FemaleDefault,
    animationSpec: AnimationSpec<Float> = tween(400, easing = LinearEasing),
    onGenderChange: (newGender: Gender) -> Unit
) {
    // Arrange the gender options in a horizontal row
    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically
    ) {
        // Female gender option
        GenderOption(
            shape = FemaleShape,
            style = femaleStyle,
            selected = (selected == Gender.FEMALE),
            animationSpec = animationSpec,
            modifier = Modifier
                .size(iconSize)
                .clickableGenderOption(Gender.FEMALE, onGenderChange)
        )
        // Male gender option
        GenderOption(
            shape = MaleShape,
            style = maleStyle,
            selected = (selected == Gender.MALE),
            animationSpec = animationSpec,
            modifier = Modifier
                .size(iconSize)
                .clickableGenderOption(Gender.MALE, onGenderChange)
        )
    }
}

축하드립니다 🥳! 성공적으로 구축했습니다 👏. GitHub Gist에서 전체 코드를 찾을 수 있어요 🧑‍💻. 사용 방법을 살펴보죠 👇

실용적인 사용 💁‍♂️

다양한 effectOrigin 조합이 흥미로울 수 있어요. 몇 가지를 살펴보죠 👀

상태

모든 예시는 이 상태를 사용합니다:

val currentGender by remember { mutableStateOf(Gender.MALE) }

기본

GenderSelector(
    selected = currentGender,
    iconSize = 100.dp,
    onGenderChange = {
        currentGender = it
    }
)

이미지 태그의 Markdown 형식으로 변경하겠습니다.

Bottom Rise

GenderSelector(
    selected = currentGender,
    iconSize = 100.dp,
    femaleStyle = GenderOptionStyle.FemaleDefault.copy(
        effectOrigin = TransformOrigin(0.5f, 1f)
    ),
    maleStyle = GenderOptionStyle.MaleDefault.copy(
        effectOrigin = TransformOrigin(0.5f, 1f)
    ),
    onGenderChange = {
        currentGender = it
    }
)

이미지 가로 사이즈를 800px로 조정한 미디어 링크를 첨부합니다:

측면 이동

GenderSelector(
    selected = currentGender,
    iconSize = 100.dp,
    femaleStyle = GenderOptionStyle.FemaleDefault.copy(
        effectOrigin = TransformOrigin(1f, 0.5f)
    ),
    maleStyle = GenderOptionStyle.MaleDefault.copy(
        effectOrigin = TransformOrigin(0f, 0.5f)
    ),
    onGenderChange = {
        currentGender = it
    }
)

아래 내용도 맘에 드실 수 있어요 👇

이 글을 읽어주셔서 감사합니다! ❤️ 만약 즐겁고 가치 있는 정보를 얻으셨다면 👏 클랩(박수)을 보내주시고 더 많은 흥미로운 글을 위해 Kappdev를 팔로우해주세요 😊

🔔 제 최신 콘텐츠를 업데이트 받으시려면 👉 이메일 알림을 구독해주세요.

즐거운 코딩 되세요!