안드로이드 개발을 하다 보면 기본 뷰 컴포넌트로는 표현하기 어려운 UI를 구현해야 할 때가 있다. 커스텀 그래프를 그리거나, 독특한 모양의 버튼을 만들거나, 사용자가 직접 그림을 그릴 수 있는 기능을 구현해야 하는 경우다. 이럴 때 필요한 것이 바로 Canvas다.
Canvas는 화면에 직접 그림을 그릴 수 있게 해주는 안드로이드의 그리기 도구다. 종이에 그림을 그리듯이, Canvas를 통해 도형, 선, 텍스트, 이미지 등을 프로그래밍 방식으로 그릴 수 있다.
Canvas는 Bitmap이나 View의 surface 위에 그림을 그리는 인터페이스를 제공한다. 이때 Paint 객체와 함께 사용되는데, Canvas가 "어디에 무엇을" 그릴지 결정하고, Paint는 "어떻게" 그릴지(색상, 두께, 스타일 등)를 결정한다.
val paint = Paint().apply {
color = Color.RED
strokeWidth = 5f
style = Paint.Style.STROKE
}
canvas.drawCircle(100f, 100f, 50f, paint) // 원 그리기
canvas.drawLine(0f, 0f, 200f, 200f, paint) // 선 그리기
canvas.drawText("Hello", 50f, 50f, paint) // 텍스트 그리기
Canvas가 제공하는 기본적인 그리기 메서드들은 다음과 같다.
// 기본 도형 그리기
canvas.drawCircle(x, y, radius, paint) // 원
canvas.drawRect(left, top, right, bottom, paint) // 사각형
canvas.drawLine(startX, startY, endX, endY, paint) // 선
canvas.drawPath(path, paint) // 복잡한 경로
canvas.drawText("텍스트", x, y, paint) // 텍스트
canvas.drawBitmap(bitmap, x, y, paint) // 이미지
특정 영역만 그리고 싶을 때는 클리핑을 사용할 수 있다.
canvas.clipRect(left, top, right, bottom) // 사각형 영역만 그려짐
canvas.clipPath(path) // 경로 영역만 그려짐
Canvas의 강력한 기능 중 하나는 좌표계를 변환할 수 있다는 점이다. 이를 통해 복잡한 그리기 작업을 훨씬 간단하게 만들 수 있다.
// 이동
canvas.translate(dx, dy) // 좌표계 이동
// 회전
canvas.rotate(degrees, pivotX, pivotY) // 특정 점 기준 회전
// 크기 조정
canvas.scale(scaleX, scaleY, pivotX, pivotY) // 확대/축소
// 기울이기
canvas.skew(sx, sy) // 비틀기
변환 작업을 할 때는 save()와 restore()를 사용하여 Canvas의 상태를 관리하는 것이 중요하다.
canvas.save() // 현재 상태 저장
// 변환 작업 수행
canvas.rotate(45f)
canvas.drawRect(...)
canvas.restore() // 이전 상태로 복원
이렇게 하면 특정 영역에만 변환을 적용하고, 다른 영역은 원래 상태를 유지할 수 있다.
Canvas를 가장 많이 사용하는 경우는 커스텀 뷰를 만들 때다. onDraw() 메서드를 오버라이드하여 원하는 UI를 직접 그릴 수 있다.
class CustomView(context: Context) : View(context) {
private val paint = Paint().apply {
color = Color.BLUE
strokeWidth = 10f
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 여기서 Canvas를 사용해 원하는 그림 그리기
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
}
실제 프로젝트에서 Canvas를 사용하는 대표적인 사례들을 정리해보았다.
커스텀 프로그레스 바
일반적인 ProgressBar로는 표현하기 어려운 원형 프로그레스를 구현할 때 Canvas를 사용한다.
class CircularProgressView : View {
private var progress = 0f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawArc(rect, -90f, progress * 3.6f, false, paint)
}
}
그래프와 차트
데이터 시각화를 위한 막대 그래프, 파이 차트 등을 라이브러리 없이 직접 구현할 수 있다. 완전한 커스터마이징이 가능하다는 장점이 있다.
이미지 편집
Bitmap에 Canvas를 연결하여 이미지에 워터마크를 추가하거나 필터를 적용할 수 있다.
val bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawText("© 2024", x, y, paint)
실시간 드로잉 앱
Paint 앱처럼 사용자가 자유롭게 그림을 그릴 수 있는 기능을 구현할 때 Canvas는 필수다.
애니메이션 효과
파티클 효과, 리플 애니메이션 등 복잡한 애니메이션을 구현할 때도 Canvas를 활용한다. invalidate()를 호출하여 화면을 계속 다시 그리면서 움직이는 효과를 만들 수 있다.
Canvas 작업은 메인 스레드에서 실행되므로 복잡한 그리기 작업이 많으면 성능에 영향을 줄 수 있다. 이럴 때는 SurfaceView나 TextureView를 사용하여 별도 스레드에서 그리기 작업을 수행하는 것이 좋다.
또한 onDraw() 메서드 안에서는 객체 생성을 피해야 한다. Paint 객체 등은 미리 생성해두고 재사용하는 것이 성능에 유리하다.
Jetpack Compose를 사용하는 프로젝트라면 전통적인 Canvas 대신 Compose의 Canvas API를 사용하는 것이 더 적합하다. 선언적 방식으로 그리기 작업을 수행할 수 있어 코드가 더 간결해진다.
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(
color = Color.Red,
radius = 50.dp.toPx()
)
}
Canvas는 "직접 픽셀을 제어하고 싶을 때" 사용하는 도구다. XML로 표현하기 어렵거나, 동적으로 계속 변하는 UI를 만들 때 특히 유용하다. 기본 뷰 컴포넌트의 한계를 느낄 때, Canvas를 사용하면 원하는 UI를 자유롭게 구현할 수 있다.
처음에는 좌표 계산이나 변환 작업이 복잡하게 느껴질 수 있지만, 몇 번 사용해보면 강력하고 유연한 도구라는 것을 알게 될 것이다.