Compose의 그래픽

많은 앱에서 화면에 표시되는 내용을 정확하게 제어할 수 있어야 합니다. 화면의 적절한 위치에 상자나 원을 배치하는 작은 작업일 수도 있고 다양한 스타일의 그래픽 요소를 정교하게 정렬해야 하는 작업일 수도 있습니다.

수정자와 DrawScope를 사용한 기본 그리기

Compose에서 맞춤 항목을 그리는 핵심 방법은 Modifier.drawWithContent, Modifier.drawBehind, Modifier.drawWithCache와 같은 수정자를 사용하는 것입니다.

예를 들어 컴포저블 뒤에 무언가를 그리려면 drawBehind 수정자를 사용하여 그리기 명령어를 실행하면 됩니다.

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

그리기를 실행하는 컴포저블만 필요하다면 Canvas 컴포저블을 사용하면 됩니다. Canvas 컴포저블은 Modifier.drawBehind 주변의 편리한 래퍼입니다. 다른 Compose UI 요소와 동일한 방식으로 레이아웃에 Canvas를 배치합니다. Canvas 내에서 스타일과 위치를 정밀하게 제어하는 요소를 그릴 수 있습니다.

모든 그리기 수정자는 자체 상태를 유지하는 범위가 지정된 그리기 환경인 DrawScope를 노출합니다. 이렇게 하면 그래픽 요소 그룹의 매개변수를 설정할 수 있습니다. DrawScopeDrawScope의 현재 크기를 지정하는 Size 객체인 size와 같은 몇 가지 유용한 필드를 제공합니다.

무언가를 그리려면 DrawScope에서 여러 그리기 함수 중 하나를 사용하면 됩니다. 예를 들어 다음 코드는 화면 왼쪽 상단에 직사각형을 그립니다.

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

흰색 배경에 그려진 화면의 4분의 1을 차지하는 분홍색 직사각형
그림 1. Compose에서 캔버스를 사용하여 그려진 직사각형

다양한 그리기 수정자에 관한 자세한 내용은 그래픽 수정자 문서를 참고하세요.

좌표계

화면에 무언가를 그리려면 항목의 오프셋(xy)과 크기를 알아야 합니다. DrawScope의 대다수 그리기 메서드에서는 위치와 크기가 기본 매개변수 값으로 제공됩니다. 기본 매개변수는 일반적으로 항목을 캔버스의 [0, 0] 지점에 배치하며 전체 그리기 영역을 채우는 기본 size를 제공합니다. 위 예를 보면 직사각형이 왼쪽 상단에 배치된 것을 확인할 수 있습니다. 항목의 크기와 위치를 조정하려면 Compose의 좌표계를 알아야 합니다.

좌표계의 원점([0,0])은 그리기 영역에서 맨 왼쪽 상단 픽셀에 있습니다. x는 오른쪽으로 이동할수록 증가하고 y는 아래쪽으로 이동할수록 증가합니다.

왼쪽 상단[0, 0] 및 오른쪽 하단[너비, 높이]을 보여주는 좌표계를 나타내는 그리드
그림 2. 그리기 좌표계 및 그리기 그리드

예를 들어 캔버스 영역의 오른쪽 상단부터 왼쪽 하단까지 대각선을 그리려면 DrawScope.drawLine() 함수를 사용하고 다음과 같이 상응하는 x, y 위치로 시작 및 끝 오프셋을 지정하면 됩니다.

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

기본 변환

DrawScope는 그리기 명령어 실행 위치나 방식을 변경하는 변환을 제공합니다.

배율

DrawScope.scale()을 사용하여 그리기 작업의 크기를 배율로 늘립니다. scale()과 같은 작업은 상응하는 람다 내 모든 그리기 작업에 적용됩니다. 예를 들어 다음 코드는 scaleX를 10배, scaleY를 15배 늘립니다.

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

원이 균등하지 않게 조정됨
그림 3. 캔버스의 원에 배율 작업 적용

변환

DrawScope.translate()를 사용하여 그리기 작업을 위, 아래, 왼쪽, 오른쪽으로 이동합니다. 예를 들어 다음 코드는 그림을 오른쪽으로 100픽셀, 위로 300픽셀 이동합니다.

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

중심을 벗어난 원
그림 4. 캔버스의 원에 변환 작업 적용

회전

DrawScope.rotate()를 사용하여 피벗 지점을 중심으로 그리기 작업을 회전합니다. 예를 들어 다음 코드는 직사각형을 45도 회전합니다.

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

화면 중앙에 45도 회전된 직사각형이 있는 휴대전화
그림 5. rotate()를 사용하여 현재 그리기 범위에 회전을 적용하여 직사각형을 45도 회전

인셋

DrawScope.inset()을 사용하여 현재 DrawScope의 기본 매개변수를 조정해 그리기 경계를 변경하고 그림을 적절히 변환합니다.

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

이 코드는 사실상 그리기 명령어에 패딩을 추가합니다.

주변에 패딩이 적용된 직사각형
그림 6. 그리기 명령어에 인셋 적용

여러 변환

그림에 여러 변환을 적용하려면 DrawScope.withTransform() 함수를 사용합니다. 이 함수는 원하는 모든 변경사항을 결합하는 단일 변환을 만들어 적용합니다. withTransform()을 사용하면 Compose가 중첩된 각 변환을 계산하고 저장할 필요 없이 모든 변환이 단일 작업으로 함께 실행되므로 개별 변환을 중첩 호출하는 것보다 더 효율적입니다.

예를 들어 다음 코드는 직사각형에 변환과 회전을 모두 적용합니다.

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

화면의 측면으로 이동한 회전된 직사각형이 있는 휴대전화
그림 7. withTransform을 사용하여 회전과 변환을 모두 적용하여 직사각형을 회전하고 왼쪽으로 이동

일반적인 그리기 작업

텍스트 그리기

Compose에서 텍스트를 그리려면 일반적으로 Text 컴포저블을 사용하면 됩니다. 그러나 DrawScope에 있거나 맞춤설정으로 텍스트를 수동으로 그리려면 DrawScope.drawText() 메서드를 사용하세요.

텍스트를 그리려면 rememberTextMeasurer를 사용하여 TextMeasurer를 만들고 이 측정기를 사용하여 drawText를 호출합니다.

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

캔버스에 그려진 Hello
그림 8. 캔버스에 텍스트 그리기

텍스트 측정

텍스트 그리기는 다른 그리기 명령어와 약간 다르게 작동합니다. 일반적으로 그리기 명령어에는 도형/이미지를 그릴 수 있는 크기(너비, 높이)가 제공됩니다. 텍스트의 경우 렌더링된 텍스트의 크기를 제어하는 몇 가지 매개변수(예: 글꼴 크기, 글꼴, 합자, 글자 간격)가 있습니다.

Compose에서는 TextMeasurer를 사용하여 위의 요소에 따라 측정된 텍스트 크기에 액세스할 수 있습니다. 텍스트 뒤에 배경을 그리려면 측정된 정보를 사용하여 텍스트가 차지하는 영역의 크기를 가져올 수 있습니다.

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

다음 코드 스니펫은 텍스트에 분홍색 배경을 생성합니다.

배경 직사각형과 함께 전체 영역의 2⁄3 크기를 차지하는 여러 줄의 텍스트
그림 9. 배경 직사각형과 함께 전체 영역의 2⁄3 크기를 차지하는 여러 줄의 텍스트

제약 조건이나 글꼴 크기, 측정된 크기에 영향을 미치는 모든 속성을 조정하면 새로운 크기가 보고됩니다. widthheight에 모두 고정된 크기를 설정할 수 있습니다. 그러면 텍스트가 설정된 TextOverflow를 따릅니다. 예를 들어 다음 코드는 컴포저블 영역의 1⁄3 높이와 1⁄3 너비로 텍스트를 렌더링하고 TextOverflowTextOverflow.Ellipsis로 설정합니다.

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

이제 생략 부호가 끝에 있는 텍스트가 제약 조건으로 그려집니다.

텍스트를 잘라내는 생략 부호가 있는 텍스트가 분홍색 배경에 그려짐
그림 10. 텍스트 측정에 관한 고정된 제약 조건이 있는 TextOverflow.Ellipsis

이미지 그리기

DrawScopeImageBitmap을 그리려면 ImageBitmap.imageResource()를 사용하여 이미지를 로드한 다음 drawImage를 호출합니다.

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

캔버스에 그려진 강아지 이미지
그림 11. 캔버스에 ImageBitmap 그리기

기본 도형 그리기

DrawScope에는 도형 그리기 함수가 많이 있습니다. 도형을 그리려면 drawCircle과 같은 사전 정의된 그리기 함수 중 하나를 사용합니다.

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

출력

drawCircle()

원 그리기

drawRect()

직사각형 그리기

drawRoundedRect()

둥근 직사각형 그리기

drawLine()

선 그리기

drawOval()

타원형 그리기

drawArc()

원호 그리기

drawPoints()

점 그리기

경로 그리기

경로는 일련의 수학적 명령으로, 실행 후에 그리기가 생성됩니다. DrawScopeDrawScope.drawPath() 메서드를 사용하여 경로를 그릴 수 있습니다.

예를 들어 삼각형을 그리고 싶다고 가정해 보겠습니다. 그리기 영역의 크기를 사용하여 lineTo()moveTo()와 같은 함수로 경로를 생성할 수 있습니다. 그런 다음 새로 만들어진 이 경로로 drawPath()를 호출하여 삼각형을 가져옵니다.

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

Compose에 그려진 거꾸로 된 보라색 경로 삼각형
그림 12. Compose에서 Path 만들기 및 그리기

Canvas 객체에 액세스

DrawScope를 사용하면 Canvas 객체에 직접 액세스할 수 없습니다. DrawScope.drawIntoCanvas()를 사용하여, 함수를 호출할 수 있는 Canvas 객체 자체에 액세스할 수 있습니다.

예를 들어 캔버스에 그리려는 맞춤 Drawable이 있으면 캔버스에 액세스하고 Drawable#draw()를 호출하여 Canvas 객체를 전달할 수 있습니다.

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

전체 크기를 차지하는 타원형 검은색 ShapeDrawable
그림 13. 캔버스에 액세스하여 Drawable 그리기

자세히 알아보기

Compose에서 그리기에 관한 자세한 내용은 다음 리소스를 참고하세요.