Compose의 그래픽

Jetpack Compose를 사용하면 맞춤 그래픽을 더 쉽게 사용할 수 있습니다. 많은 앱에서 화면에 표시되는 내용을 정확하게 제어할 수 있어야 합니다. 화면의 적절한 위치에 상자나 원을 배치하는 작은 작업일 수도 있고 다양한 스타일의 그래픽 요소를 정교하게 정렬해야 하는 작업일 수도 있습니다. Compose의 선언적 방식을 사용하면 모든 그래픽 구성이 메서드 호출과 Paint 도우미 객체 간에 분할되는 대신 한곳에서 발생합니다. Compose가 필요한 객체를 효율적인 방법으로 만들고 업데이트합니다.

Compose를 사용한 선언적 그래픽

Compose는 그래픽도 선언적 접근 방식으로 처리합니다. Compose의 접근방식은 여러 이점을 제공합니다.

  • Compose는 그래픽 요소의 상태를 최소화하므로 상태의 프로그래밍 실수를 피할 수 있습니다.
  • 항목을 그릴 때 모든 옵션이 구성 가능한 함수의 예상되는 위치에 있습니다.
  • Compose의 그래픽 API가 효율적인 방법으로 객체를 만들고 해제합니다.

캔버스

맞춤 그래픽의 핵심 컴포저블은 Canvas입니다. 다른 Compose UI 요소와 동일한 방식으로 레이아웃에 Canvas를 배치합니다. Canvas 내에서 스타일과 위치를 정밀하게 제어하는 요소를 그릴 수 있습니다.

예를 들어 다음 코드는 상위 요소의 모든 가용 공간을 채우는 Canvas 컴포저블을 만듭니다.

Canvas(modifier = Modifier.fillMaxSize()) {
}

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

따라서 예를 들어 캔버스의 오른쪽 상단에서 왼쪽 하단으로 대각선을 그린다고 가정해 보겠습니다. 이렇게 하려면 drawLine 컴포저블을 추가하면 됩니다.

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
    )
}

화면을 가로질러 대각선 방향으로 가는 선이 그려져 있는 휴대전화

그림 1. drawLine를 사용하여 캔버스를 가로질러 선을 그립니다. 이 코드는 선의 색상을 설정하지만 기본 너비를 사용합니다.

다른 매개변수를 사용하여 그림을 맞춤설정할 수 있습니다. 예를 들어 기본적으로 선은 가는 선 굵기로 그려지며 그리기의 배율과 관계없이 1픽셀 너비로 표시됩니다. strokeWidth 값을 설정하여 기본값을 재정의할 수 있습니다.

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,
        strokeWidth = 5F
    )
}

화면을 가로질러 대각선 방향으로 굵은 선이 그려져 있는 휴대전화

그림 2. 기본 너비를 재정의하여 그림 1의 선 수정

drawRect, drawCircle 등 기타 간단한 그리기 함수가 많이 있습니다. 예를 들어 다음 코드는 캔버스 중앙에 지름이 캔버스의 짧은 쪽 크기의 반과 같은 채워진 원을 그립니다.

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawCircle(
        color = Color.Blue,
        center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
        radius = size.minDimension / 4
    )
}

화면 중앙에 파란색 원이 있는 휴대전화

그림 3. drawCircle을 사용하여 캔버스의 중앙에 원을 그립니다. 기본적으로 drawCircle은 채워진 원을 그리므로 이 설정을 명시적으로 지정할 필요가 없습니다.

그리기 함수에는 유용한 기본 매개변수가 있습니다. 예를 들어 기본적으로 drawRectangle()은 전체 상위 범위를 채우고 drawCircle()의 반지름은 상위 요소의 짧은 쪽 크기의 반에 해당합니다. Kotlin과 마찬가지로 기본 매개변수 값을 활용하고 변경해야 하는 매개변수만 설정하면 코드를 훨씬 더 간단하고 명확하게 만들 수 있습니다. 그려진 요소는 상위 범위 설정의 기본 설정을 기반으로 하므로 DrawScope 그리기 메서드에 명시적 매개변수를 제공하여 이 기능을 활용할 수 있습니다.

DrawScope

앞서 언급했듯이 각 Compose Canvas는 범위가 지정된 그리기 환경인 DrawScope를 노출합니다. 이 환경에서 실제로 그리기 명령어를 실행합니다.

예를 들어 다음 코드는 캔버스의 왼쪽 상단에 직사각형을 그립니다.

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

DrawScope.inset() 함수를 사용하여 현재 범위의 기본 매개변수를 조정하고 그리기 경계를 변경하고 그림을 적절히 변환할 수 있습니다. inset()과 같은 작업은 상응하는 람다 내 모든 그리기 작업에 적용됩니다.

val canvasQuadrantSize = size / 2F
inset(50F, 30F) {
    drawRect(
        color = Color.Green,
        size = canvasQuadrantSize
    )
}

DrawScoperotate()와 같이 기타 간단한 변환을 제공합니다. 예를 들어 다음 코드는 캔버스 중앙의 아홉 번째를 채우는 직사각형을 그립니다.

val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRect(
    color = Color.Gray,
    topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
    size = canvasSize / 3F
)

화면 중앙에 채워진 직사각형이 있는 휴대전화

그림 4. drawRect를 사용하여 화면 중앙에 채워진 직사각형 그리기

DrawScope에 회전을 적용하여 직사각형을 회전할 수 있습니다.

rotate(degrees = 45F) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 3F
    )
}

화면 중앙에 45도 회전된 직사각형이 있는 휴대전화

그림 5. rotate()를 사용하여 현재 그리기 범위에 회전을 적용하여 직사각형을 45도 회전

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

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

withTransform({
    translate(left = canvasWidth / 5F)
    rotate(degrees = 45F)
}) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 3F
    )
}

화면의 측면으로 이동한 회전된 직사각형이 있는 휴대전화

그림 6. withTransform을 사용하여 회전과 변환을 모두 적용하여 직사각형을 회전하고 왼쪽으로 이동