원문 : https://medium.com/over-engineering/android-canvas-drawing-useful-graphics-classes-operations-2803e435e848


안드로이드 캔버스에 뭔가 그리는건 상당히 힘든 작업입니다. 많은 클래스와 개념들이 뭔가 그릴 때 이해를 돕기 위해 존재 합니다. 만약 이전 포스트를 읽지 않았다면꼭 먼저 참고해보시기 바랍니다.

이 포스트에서는 안드로이드 프레임워크에 포함되어있고 캔버스를 사용할 때 유용한 몇몇 클래스들에 대해서 알아 보겠습니다.

Rect / RectF

top, left, right, bottom 이 4가지 정보를 담고 있는 직사각형 클래스 입니다.
이 클래스들은 캔버스에 직접적으로 사각형을 그리거나 또는 그리고 싶은 객체의 사이즈 정보를 저장 해두는 용도로 사용됩니다.

Rect와 RectF의 차이점은 Rect는 Integer 값을 저장하고, RectF는 float 값을 저장한다는 것입니다.

val rect = RectF(100.0f, 200.0f, 300.0f, 400.0f)

KTX라이브러리르 사용하면 유용한 그래픽스 엑스텐션 함수들을 사용할 수 있는 장점이 있습니다. 예를 들어 Rect 와 RectF의 객체를 여러 변수로 분리하는 것입니다. (Destructuring Declarations, 비구조화 선언)

val rect = RectF(100.0f, 200.0f, 300.0f, 400.0f)
val (left, top, right, bottom) = rect
// left = 100.0f, top = 200.0f, right = 300.0f, bottom = 400.0f

Rect클래스로 다른 작업 작업도 할 수 있는데, 예를 들어 Rect 2개를 병합 할 수도 있습니다. 이것은 KTX가 없어도 기본적으로 Rect/RectF에 포함된 기능인데, 더 큰 직사강형을 만들 수 있는 쪽으로 병합하여 값을 반환합니다. 이러한 기능을 위한 몇몇 익스텐션 함수가 있지만 익스텐션 없이도 가능합니다.

val rect = RectF(100.0f, 200.0f, 300.0f, 400.0f)
val otherRect = RectF(50.0f, 400.0f, 150.0f, 500.0f)
rect.union(otherRect)
// rect = RectF(50.0, 200.0, 300.0, 500.0)
// 위에것 대신에
val combinedRect = rect + otherRect
// 또는 이렇게
val combinedRect = rect or otherRect
// combinedRect = RectF(50.0, 200.0, 300.0, 500.0)

Rect에서 사용할수 있는 다른 기능으로 and, xor, or 도 있습니다.

Point / PointF

Point는 x 그리고 y 좌표를 저장하고 있고 캔버스에서 ‘점’을 표현합니다. Point는 Integer 값을 가지고 있으며 PointF는 Float형 값을 가지고 있습니다.

val point = PointF(200.0f, 300.0f)

KTX를 사용한다면 몇몇 익스텐션을 사용할수 있는데요. Point를 좀 더 쉽게 사용할 수 있게 됩니다. 예를들면, + 같은 두개의 값을 더하고 빼는 연산자가 포함되어있습니다.

val start = PointF(100.0f, 30.0f)
val end = PointF(20.0f, 10.0f)
val difference = start - end
val together = start + end
// together = Point(120.0f, 40.0f)

이 클래스들을 위한 비구조화 선언 또한 존재합니다. 그렇기 때문에 쉽게 x 또는 y 값을 꺼내어 쓸 수 있게 됩니다.

val start = PointF(100.0f, 30.0f)
val end = PointF(20.0f, 10.0f)
val (x, y) = start - end
// x = 80.0f y = 20.0f

Matrix

3 x 3 행렬 에서는 캔버스를 변형시킬 수 있는 정보를 담고 있습니다. Matrix는 다음과 같은 형태를 저장할 수 있습니다.

  • 확대/축소 비율
  • 왜곡
  • 회전
  • 이동

아래의 예제들은 Matrix를 사용하여 Bitmap을 변형하여 캔버스에 그린것입니다.

그림을 그릴때 Matrix를 사용하기 위해서는 다음과 같이 할 수 있습니다.

val customMatrix = Matrix()
// in onDraw()
customMatrix.postRotate(20.0f)
canvas.withMatrix(customMatrix) {
    drawBitmap(bitmap, null, rect, paint)
}

위의 코드는 캔버스위에 비트맵을 그리고 이를 20도 회전한 것입니다. Matrix에는 확대를 하거나 회전을 하거나 왜곡을 시킨다거나 하는 기능들도 있습니다. Matrix를 사용하는데 좋은점은 Canvas에서 변형을 자유자재로 할 수 있다는 것입니다. Matrix는 적용된 변형에 누적된 정보들을 가지고 있습니다.

만약 Matrix를 이동하고, 회전하고, 확대한 다음 다시 이동한다면 마지막 이동한 값은 처음에 적용했던 값과는 조금은 다릅니다.  만약 일반적인 Canvas의 이동 확대 등의 기능을 직접 수행한다면 직접 이를 계산해야합니다. 

preRotate vs postRotate vs setRotate

행렬 곱셉은 교환법칙이 성립되지 않기 때문에, 세 메소드가 존재하고 결과값이 다릅니다.

  • preRotate : M’ = 기존 행열에 회전 행렬을 곱합니다. M * R(degrees)
  • postRotate : M’ = 회전 행렬에 기존 행렬을 곱합니다. R(degrees) * M
  • setRotate : 기존 행렬의 회전값을 (0,0) 리셋하고, 회전 행렬 값을 적용합니다.

Matrix를 사용한 원근법 그리기

Matrix객체는 원근법 그리기를 가능하게 해주는데, Canvas의 변형 API를 이용해서는 불가능합니다. 캔버스의 투시도 또는 기울기를 허용하는 함수는 Matrix.setPolyToPoly() 입니다.  이 메소드는 처음에는 약간 혼란스럽지만 작동방식에 대해 곰곰히 생각해보면 그다지 어렵진 않습니다.

setPolyToPoly 메소드를 사용하여 비트맵을 왜곡한 예제 입니다.

setPolyToPoly 메소드는 입력된 (src) Point와 그리고 지정된 출력 (dst) Point에 맵핑합니다. Point라고는 했지만 일반적으로 생각하는 점이 아닙니다. 그저 연산에 필요한 float 배열 값일 뿐이므로 그래픽스에 이제 입문했다면 아직 익숙치 않을 겁니다.

아래의 src 배열에서 처음 두 값은 이미지의 왼쪽 상단 지점을 나타내고 두번째 두 값은 오른쪽 상단 지점 등을 나타냅니다. 이 점들은 임의의 순서가 될 수 있지만 dst 배열에서 매핑하려는 해당 점과 일치해야합니다.

val src = floatArrayOf(
    0f, 0f, // top left point
    width, 0f, // top right point
    width, height, // bottom right point
    0f, height // bottom left point
)
val dst = floatArrayOf(
    50f, -200f, // top left point
    width, -200f, // top right point
    width, height +200f, // bottom right point
    0f, height // bottom left point
)
val pointCount = 4 // number of points
// 두번째 그리고 네번째 인자는 src 와 dest 배열에서 포인트가 매핑되기 시작하는 인덱스를 나타냅니다.
newMatrix.setPolyToPoly(src, 0, dst, 0, pointCount)
canvas.withMatrix(newMatrix) {
   drawBitmap(bitmap, null, rect, paint)
}

이 예제에서 오른쪽 아래점은 [width, height] 지점에서 [width, height + 200f] 지점으로 매핑됩니다. 예제에서 Matrix가 매우 강력하고 흥미로운 일들을 하고 있음을 알 수 있습니다.

Tip : Matrix에 대해 좀 더 알기 위해서 이전 포스트를 참고해보세요

하나의 뷰를 다루는데 두개의 다른 좌표계를 가지고 있는 경우, Matrix 클래스를 활용하면 두 좌표계를 매핑하는데 도움이 됩니다.

예를 들어, 화면의 width, height 내에서 터치이벤트 좌표가 발생하고, 해당 좌표계 내에서 화면에 그리는 이미지 내부의 그 지점이 어떤 Point 좌표인지 알고 싶다면 이 두 좌표계 사이에서 Matrix를 사용할 수 있습니다.

화면에 그려진 이미지 내부의 Point 좌표 얻기 위해서는 Matrix.mapPoints() 메소드를 이용할 수 있습니다.

fun mapPoint(point: PointF): PointF {
    computeMatrix.reset()
    // apply the same transformations on the matrix that are applied to the Image
    computeMatrix.postTranslate(20f, 20f)
    computeMatrix.postRotate(20f, x, y)
    // create float array with the points we want to map
    val arrayPoint = floatArrayOf(point.x, point.y)
    // use the map points function to apply the same transformations that the matrix has, onto the input array of coordinates
    computeMatrix.mapPoints(arrayPoint)
    // get the points out from the array, these will now be    transformed by the matrix.
    return PointF(arrayPoint[0], arrayPoint[1])
}

위의 예제에서 입력한 지점은 Android의 터치이벤트이며, computeMatrix에 적용하는 변환 및 회전은 이미지를 그릴 때 적용한 변환 및 회전과 동일합니다. 원래 x 및 y 지점을 포함하는 float배열을 만들고, 만들어진 배열과 함께 mapPoints 메소드를 호출합니다. 그런 다음 적절하게 변환하고 배열에 첫 번째 값과 두 번째 값을 쿼리하면 매핑된 좌표(ImageView 내부의 Point)를 얻을 수 있습니다.

 

카테고리: Graphics

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.