[Android] 말풍선 모양 만들기

uuranus·2024년 10월 1일
0
post-thumbnail

포인터 위치 정하기

말풍선에서 누가 말하는 지 알려주는 포인터 부분은 상하좌우에 다 붙을 수 있으니
enum class를 통해서 위치값을 정해주고 위치값에 따라서 각각 말풍선을 그리는 함수를 생성했다.

enum class PointerPosition {
    Top,
    Start,
    End,
    Bottom
}

class ParallelogramShape(
    private val skewed: Float = 0.2f,
    private val cornerStyle: CornerStyle,
    private val topStart: CornerSize,
    private val topEnd: CornerSize,
    private val bottomEnd: CornerSize,
    private val bottomStart: CornerSize,
) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density,
    ): Outline {

        val path = when (cornerStyle) {
            CornerStyle.ROUNDED -> {
                drawRoundedParallelogramShape(
                    size,
                    skewed = skewed,
                    topStart = topStart.toPx(size, density),
                    topEnd = topEnd.toPx(size, density),
                    bottomStart = bottomStart.toPx(size, density),
                    bottomEnd = bottomEnd.toPx(size, density)
                )
            }

            CornerStyle.INNER_ROUNDED -> {
                drawInnerRoundedParallelogramShape(
                    size,
                    skewed = skewed,
                    topStart = topStart.toPx(size, density),
                    topEnd = topEnd.toPx(size, density),
                    bottomStart = bottomStart.toPx(size, density),
                    bottomEnd = bottomEnd.toPx(size, density)
                )
            }

            CornerStyle.CUT -> {
                drawCutParallelogramShape(
                    size,
                    skewed = skewed,
                    topStart = topStart.toPx(size, density),
                    topEnd = topEnd.toPx(size, density),
                    bottomStart = bottomStart.toPx(size, density),
                    bottomEnd = bottomEnd.toPx(size, density)
                )
            }
        }
        return Outline.Generic(path)
    }
}

말풍선 그리기

그리는 방법은 포인터 위치에 상관없이 동일하니까 여기선 Bottom에 붙는 경우를 예를 들어서 설명하겠다.

val pointerHeight = size.height * pointerHeightRatio
val drawPointerHeight = pointerHeight * 1.1f
val pointerWidth = size.width * pointerWidthRatio

val withoutPointerSize = size.copy(height = size.height - pointerHeight)

val topStartPx = if (layoutDirection == LayoutDirection.Ltr) {
    topStart.toPx(withoutPointerSize, density)
} else {
    topEnd.toPx(size, density)
}

val topEndPx = if (layoutDirection == LayoutDirection.Ltr) {
    topEnd.toPx(withoutPointerSize, density)
} else {
    topStart.toPx(size, density)
}

val bottomStartPx = if (layoutDirection == LayoutDirection.Ltr) {
    bottomStart.toPx(withoutPointerSize, density)
} else {
    bottomEnd.toPx(size, density)
}

val bottomEndPx = if (layoutDirection == LayoutDirection.Ltr) {
    bottomEnd.toPx(withoutPointerSize, density)
} else {
    bottomStart.toPx(size, density)
}

우선 포인터의 위치값을 제외하고 실제 말이 들어갈 풍선부분의 위치값에 대해서 cornerRadius값을 측정해야 한다. 그래서 withoutPointerSize를 토대로 px값을 측정해주었다.

포인터 부분은 위치에 상관없이 항상 width, height가 다음을 가리키고 skewed는 양수면 Ltr을 기준으로 오른쪽으로 기울어진다.

그리고 말풍선 경계에 바로 붙이면 말풍선이 곡선이 지면서 위와 같이 떨어지는 틈이 생길 수 있기에 drawPointerHeight를 통해서 좀 더 안쪽에서 시작하도록 하였다.

path.apply {
    arcTo(
        rect = Rect(
            offset = Offset(0f, 0f),
            size = Size(
                topStartPx * 2, topStartPx * 2
            )
        ),
        startAngleDegrees = 180f,
        sweepAngleDegrees = 90f,
        forceMoveTo = false
    )

    arcTo(
        rect = Rect(
            offset = Offset(size.width - topEndPx * 2, 0f),
            size = Size(
                topEndPx * 2, topEndPx * 2
            )
        ),
        startAngleDegrees = 270f,
        sweepAngleDegrees = 90f,
        forceMoveTo = false
    )

    arcTo(
        rect = Rect(
            offset = Offset(
                size.width - bottomEndPx * 2,
                size.height - pointerHeight - bottomEndPx * 2
            ),
            size = Size(
                bottomEndPx * 2, bottomEndPx * 2
            )
        ),
        startAngleDegrees = 0f,
        sweepAngleDegrees = 90f,
        forceMoveTo = false
    )

    arcTo(
        rect = Rect(
            offset = Offset(0f, size.height - pointerHeight - bottomStartPx * 2),
            size = Size(
                bottomStartPx * 2, bottomStartPx * 2
            )
        ),
        startAngleDegrees = 90f,
        sweepAngleDegrees = 90f,
        forceMoveTo = false
    )
}

실제로 그리는 건 arcTo를 이용하여 그렸다.

포인터 그리기

포인터는 위치값과 Ltr에 따라서 start, point, end position을 계산해서 op함수를 통해서 말풍선과 합쳐주었다.

val startPosition = Offset(
    size.width / 2 - pointerWidth / 2,
    size.height - drawPointerHeight
)

val endPosition = Offset(
    size.width / 2 + pointerWidth / 2,
    size.height - drawPointerHeight
)

val pointPosition = if (layoutDirection == LayoutDirection.Ltr) {
    Offset(size.width / 2 + pointerWidth * skewed, size.height)
} else {
    Offset(size.width / 2 - pointerWidth * skewed, size.height)
}

path.apply {
    op(
        path1 = this,
        path2 = Path().apply {
            moveTo(
                startPosition.x,
                startPosition.y
            )
            lineTo(
                pointPosition.x,
                pointPosition.y
            )
            lineTo(
                endPosition.x,
                endPosition.y
            )
        },
        operation = PathOperation.Union
    )
}

최종 결과

깃허브 링크

https://github.com/uuranus/compose-shapes

profile
Frontend Developer

0개의 댓글