Android의 Canvas — android.graphics 패키지의 클래스로, View에서 2D 렌더링을 위한 API를 제공합니다. Canvas는 캔버스처럼 작동하며 개발자는 다양한 draw* 메서드를 사용하여 기하학적 도형, 텍스트, 이미지 및 경로를 그립니다. Android Developers Documentation(2025)에 따르면 Canvas는 Android에서 사용자 정의 렌더링을 위한 기본 도구이며 스타일과 색상을 정의하기 위해 Paint 클래스와 함께 사용됩니다. 각 onDraw() 호출은 뷰의 비트맵 버퍼와 연결된 준비된 Canvas 인스턴스를 받습니다.
핵심 사항
Canvas — Android의 중앙 2D 그래픽스 클래스로, 평면에 그리기 위한 메서드를 제공합니다. 각 Canvas는 Bitmap에 연결됩니다 — 그리기 작업의 결과가 기록되는 메모리 영역입니다. View가 다시 그려지면 시스템은 onDraw()에 해당 View의 버퍼와 이미 연결된 Canvas를 전달합니다.
Canvas는 (0,0)이 왼쪽 상단 모서리이고, X축은 오른쪽, Y축은 아래쪽인 좌표계에서 작동합니다. 모든 그리기 작업은 변환 행렬, 클리핑 영역 및 Paint 스타일을 포함한 Canvas의 현재 상태를 사용합니다. Android 문서(2025)에 따르면 Canvas API에는 다양한 유형의 그래픽 프리미티브를 위한 30개 이상의 draw* 메서드가 포함되어 있습니다.
Android 3.0(API 11)부터 Canvas는 DisplayList를 통한 하드웨어 가속을 지원합니다. 가속 모드에서 Canvas 작업은 DisplayList에 기록된 후 GPU에서 실행됩니다. 이는 상당한 성능 향상을 제공하지만 제한 사항이 있습니다. 일부 Canvas 작업(비Path 클립이 있는 clipPath)은 가속 모드에서 지원되지 않습니다.
Canvas는 기본 프리미티브를 그리기 위한 메서드 세트를 제공합니다. 각 메서드는 기하학적 매개변수와 시각적 스타일을 정의하는 Paint 객체를 받습니다.
| 메서드 | 목적 | 예제 |
|---|---|---|
| drawRect() | 사각형 그리기 | canvas.drawRect(10f, 10f, 100f, 100f, paint) |
| drawCircle() | 원 그리기 | canvas.drawCircle(50f, 50f, 30f, paint) |
| drawLine() | 선 그리기 | canvas.drawLine(0f, 0f, 100f, 100f, paint) |
| drawText() | 텍스트 그리기 | canvas.drawText(“Hello”, 10f, 20f, paint) |
| drawBitmap() | 이미지 그리기 | canvas.drawBitmap(bitmap, 0f, 0f, paint) |
| drawPath() | 임의의 경로 그리기 | canvas.drawPath(path, paint) |
| drawOval() | 타원 그리기 | canvas.drawOval(rectF, paint) |
| drawArc() | 호 또는 부채꼴 그리기 | canvas.drawArc(rectF, 0f, 90f, true, paint) |
각 메서드는 순서에 관계없이 호출할 수 있습니다 — Canvas는 호출 순서대로 그리며 이전 요소 위에 새 요소를 레이어링합니다. 복잡한 구성의 경우 Canvas 상태 저장 및 복원을 사용한 다중 레이어 렌더링 기술이 사용됩니다.
Paint — Canvas가 어떻게 그릴지 정의하는 클래스입니다. Paint 없이는 Canvas 메서드가 무용지물입니다 — 색상, 선 두께, 채우기 스타일, 텍스트 크기, 효과(그림자, 그라데이션, 흐림) 및 블렌딩 모드를 설정합니다.
Paint의 주요 속성: color(색상), style(FILL, STROKE 또는 FILL_AND_STROKE), strokeWidth(선 두께), textSize(텍스트 크기), isAntiAlias(안티앨리어싱), shader(그라데이션 또는 이미지). Paint 객체 생성은 비용이 많이 드는 작업이므로 한 번 생성하여 재사용해야 합니다.
private val fillPaint = Paint().apply {
color = Color.parseColor("#FF6200EE")
style = Paint.Style.FILL
isAntiAlias = true
}
private val strokePaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 4f
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(100f, 100f, 80f, fillPaint)
canvas.drawCircle(100f, 100f, 80f, strokePaint)
}
이 예제에서는 두 개의 Paint 객체가 사용됩니다. 하나는 원을 보라색으로 채우기 위한 것이고, 다른 하나는 흰색 테두리를 위한 것입니다. 채우기와 테두리를 분리하는 것은 객체를 다시 만들지 않고도 모양을 유연하게 제어할 수 있는 표준 관행입니다.
그라데이션 채우기를 위해 Paint는 Shader를 받습니다 — 하위 클래스 LinearGradient, RadialGradient, SweepGradient. Shader는 setShader() 메서드를 통해 Paint에 바인딩되며 그리기 영역 전체에서 색상이 어떻게 변하는지 정의합니다.
Canvas는 좌표계의 행렬 변환을 지원합니다: translate()(이동), rotate()(회전), scale()(크기 조정), skew()(기울이기). 변환은 누적됩니다. 각 후속 호출은 이전 호출 위에 적용됩니다.
변환 스택 관리는 save()와 restore()를 통해 수행됩니다. save()는 현재 Canvas 상태(행렬, 클립)를 스택에 저장하고 restore()는 마지막으로 저장된 상태를 복원합니다. save()와 restore() 사이에 중첩된 변환을 적용할 수 있습니다.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.save() // Save initial state
canvas.translate(100f, 100f) // Translate origin
// First square
canvas.drawRect(0f, 0f, 50f, 50f, fillPaint)
canvas.save() // Save after translate
canvas.rotate(45f, 25f, 25f) // Rotate around square center
// Rotated square
canvas.drawRect(0f, 0f, 50f, 50f, strokePaint)
canvas.restore() // Restore after rotate
canvas.restore() // Restore initial state
}
변환을 시작하기 전에 항상 save()를 사용하고 변환을 완료한 후에는 restore()를 사용하세요. 불균형한 save/restore는 행렬 누적과 후속 요소의 잘못된 렌더링을 초래합니다.
복잡한 그래픽을 위해 Canvas는 Bitmap 및 Path와 결합됩니다. Bitmap은 이미지 로드 및 렌더링된 프레임 캐싱에 사용됩니다. Path는 임의의 베지어 곡선, 다각형 및 복잡한 윤곽선을 만드는 데 사용됩니다.
오프스크린 렌더링 기술: Bitmap을 만들고, 별도의 Canvas를 통해 복잡한 장면을 그린 다음, 완성된 Bitmap을 drawBitmap()을 통해 onDraw()에서 출력합니다. 이렇게 하면 매 프레임마다 전체를 다시 그리지 않고도 복잡한 그래픽을 업데이트할 수 있습니다.
private var offscreenBitmap: Bitmap? = null
private var offscreenCanvas: Canvas? = null
fun buildOffscreenScene(width: Int, height: Int) {
offscreenBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
offscreenCanvas = Canvas(offscreenBitmap)
// Render scene once
offscreenCanvas!!.drawColor(Color.WHITE)
drawGrid(offscreenCanvas!!)
drawComplexShape(offscreenCanvas!!)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
offscreenBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
}
Path는 복잡한 윤곽선을 만드는 데 사용됩니다: 베지어 곡선(quadTo, cubicTo), 호(arcTo), 선(lineTo). Path는 채워진 모양의 경우 닫히거나(close()) 선의 경우 열릴 수 있습니다. 기본 drawPath() 메서드는 최종 렌더링을 위해 Path와 Paint를 결합합니다.
Canvas 렌더링 성능은 애플리케이션 FPS에 직접적인 영향을 미칩니다. 주요 문제: onDraw()에서의 과도한 할당, 빈번한 invalidate() 호출 및 하드웨어 가속의 비최적 사용.
Paint, Path, Rect, RectF 및 기타 객체는 초기화 프로그램이나 setup() 메서드에서 한 번만 생성해야 합니다. onDraw()에서의 각 할당은 가비지 컬렉터에 부담을 주어 마이크로 랙을 유발합니다. 예외는 불변 객체를 위한 static 또는 companion object입니다.
View의 대부분이 표시 영역 밖에 숨겨져 있는 경우(예: ScrollView에서), 렌더링을 제한하기 위해 canvas.clipRect()를 사용합니다. 이 경우 하드웨어 가속은 보이지 않는 픽셀의 클리핑을 추가로 최적화합니다.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Clip to visible area
canvas.save()
canvas.clipRect(scrollX, scrollY, scrollX + width, scrollY + height)
// Render large scene
renderScene(canvas)
canvas.restore()
}
드물게 업데이트되는 복잡한 Canvas 그래픽이 있는 View에는 LAYER_TYPE_HARDWARE와 함께 View.setLayerType()을 사용합니다. 자주 업데이트되는 애니메이션의 경우 비용이 많이 드는 하드웨어 레이어 업데이트를 피하기 위해 LAYER_TYPE_NONE을 사용합니다. GPU Profile Rendering 및 Android Studio Profiler를 사용하여 프로파일링합니다.
자주 묻는 질문
네, Canvas(Bitmap)을 만들 수 있습니다 — Bitmap을 받는 생성자입니다. 이는 오프스크린 렌더링에 사용됩니다: Bitmap에 그리고 drawBitmap()을 통해 출력합니다. Canvas는 Surface(SurfaceView용) 또는 Picture를 통해서도 만들 수 있습니다.
Canvas — UI 스레드에서 View에 그리기 위한 API입니다. SurfaceView — 자체 Canvas(lockCanvas() / unlockCanvasAndPost())를 통해 백그라운드 스레드에서 업데이트할 수 있는 별도의 Surface입니다. SurfaceView는 게임 및 비디오에 선호됩니다.
가장 일반적인 원인은 불균형한 save()와 restore()입니다. 변환(translate, rotate) 후 restore()가 호출되지 않으면 Canvas가 수정된 좌표계에 남아 이후 요소가 오프셋되어 그려집니다. save()/restore() 쌍을 확인하세요.
수평 중앙 정렬에는 Paint.setTextAlign(Paint.Align.CENTER)를, 수직 정렬에는 Paint.getTextBounds() 또는 Paint.descent() / ascent()를 사용합니다. 중앙 정렬: canvas.drawText(텍스트, centerX, centerY - (ascent + descent) / 2, paint).
Canvas 자체는 애니메이션을 만들지 않습니다 — 현재 상태만 그릴 뿐입니다. 애니메이션은 invalidate()(또는 postInvalidateOnAnimation())의 연속 호출을 통해 이루어지며, 매번 onDraw()에서 새 프레임을 그립니다. 부드러운 애니메이션을 위해 Vsync와 동기화하려면 ValueAnimator 또는 Choreographer를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.