(Android/UI) Canvas 그리기 — Path, Paint, Bitmap 조작
1. Canvas와 Paint
- Canvas: “어디에 무엇을” 그릴지 지정하는 그리기 도구 (
drawCircle,drawRect,drawPath…) - Paint: “어떤 색과 스타일로” 그릴지 지정하는 도구 (색, 선 굵기, 안티앨리어싱 …)
class ShapeView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
private val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor("#3F51B5") }
private val stroke = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 8f
color = Color.parseColor("#FF5722")
}
override fun onDraw(canvas: Canvas) {
canvas.drawColor(Color.WHITE) // 배경
canvas.drawRect(40f, 40f, 240f, 160f, fill) // 사각형
canvas.drawRoundRect(RectF(40f, 200f, 240f, 320f), 24f, 24f, fill)
canvas.drawCircle(400f, 100f, 60f, stroke) // 원 (테두리)
canvas.drawLine(40f, 360f, 440f, 360f, stroke)
}
}
2. Paint 핵심 속성
| 속성 | 설명 |
|---|---|
isAntiAlias |
가장자리를 부드럽게 (거의 항상 켬) |
style |
FILL, STROKE, FILL_AND_STROKE |
strokeWidth / strokeCap |
선 굵기 / 끝 모양 (ROUND, SQUARE) |
strokeJoin |
꺾이는 부분 (ROUND, MITER, BEVEL) |
textSize / typeface |
텍스트 크기/서체 |
shader |
그라데이션, 비트맵 채우기 |
pathEffect |
점선(DashPathEffect) 등 |
val gradient = Paint(Paint.ANTI_ALIAS_FLAG).apply {
shader = LinearGradient(0f, 0f, 0f, 400f, Color.CYAN, Color.BLUE, Shader.TileMode.CLAMP)
}
val dashed = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 4f
pathEffect = DashPathEffect(floatArrayOf(20f, 12f), 0f) // 20 그리고 12 비움
}
3. Path — 자유 도형과 곡선
Path는 선과 곡선을 이어 임의의 모양을 정의합니다.
private val path = Path()
override fun onDraw(canvas: Canvas) {
path.reset()
path.moveTo(100f, 300f) // 시작점 이동
path.lineTo(200f, 100f) // 직선
path.quadTo(300f, 0f, 400f, 100f) // 2차 베지어 (제어점, 끝점)
path.cubicTo(450f, 200f, 450f, 300f, 400f, 350f) // 3차 베지어
path.close() // 시작점과 연결
canvas.drawPath(path, fill)
}
// 하트, 별 같은 도형도 Path로 구성 가능. 도형 합치기/빼기는 Path.op()
val result = Path()
result.op(pathA, pathB, Path.Op.DIFFERENCE) // A에서 B 영역 제거
4. 터치로 자유 그리기 (서명 패드)
class DrawingView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 8f
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
color = Color.BLACK
}
private val strokes = mutableListOf<Path>()
private var current: Path? = null
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x; val y = event.y
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
current = Path().apply { moveTo(x, y) }
strokes.add(current!!)
}
MotionEvent.ACTION_MOVE -> current?.lineTo(x, y)
MotionEvent.ACTION_UP -> current = null
}
invalidate()
return true
}
override fun onDraw(canvas: Canvas) {
strokes.forEach { canvas.drawPath(it, paint) }
}
fun clear() { strokes.clear(); invalidate() }
}
실행 취소는
strokes.removeLast()로 간단히 구현됩니다. 점이 매우 많아지면quadTo로 보간해 부드럽게 만들 수 있습니다.
5. Bitmap 그리기와 조작
// 비트맵 그리기
canvas.drawBitmap(bitmap, srcRect, dstRect, paint) // 일부를 잘라 원하는 영역에 맞춰 그림
// 새 비트맵에 직접 그리기 (오프스크린)
val result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val c = Canvas(result) // 비트맵을 캔버스로 사용
c.drawBitmap(background, 0f, 0f, null)
c.drawBitmap(logo, 20f, 20f, null) // 워터마크 합성
원형으로 자르기 (PorterDuff 합성)
fun Bitmap.toCircle(): Bitmap {
val size = minOf(width, height)
val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint) // 마스크
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) // 겹치는 부분만 남김
canvas.drawBitmap(this, (size - width) / 2f, (size - height) / 2f, paint)
return output
}
색 변환 — ColorFilter
// 흑백 처리
val matrix = ColorMatrix().apply { setSaturation(0f) }
paint.colorFilter = ColorMatrixColorFilter(matrix)
canvas.drawBitmap(bitmap, 0f, 0f, paint)
// 단색으로 틴트
paint.colorFilter = PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP)
6. 변환 — save/restore, translate, rotate, scale
canvas.save() // 현재 상태 저장
canvas.translate(200f, 200f) // 원점 이동
canvas.rotate(45f) // 회전 (이동된 원점 기준)
canvas.drawRect(-50f, -50f, 50f, 50f, fill)
canvas.restore() // 변환 되돌리기 (다음 그리기에 영향 없음)
시계 바늘처럼 회전하는 요소를 그릴 때 save → rotate → draw → restore 패턴을 사용합니다.
7. 저장하기
fun View.toBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
draw(Canvas(bitmap)) // 뷰를 비트맵으로 그림
return bitmap
}
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
8. 성능 팁
Paint/Path/Rect는 멤버로 재사용하고Path.reset()/rewind()로 초기화- 큰
Bitmap은inSampleSize로 줄여 로드하고, 사용이 끝나면 참조 해제 (OOM 방지) - 복잡한 정적 배경은 한 번
Bitmap에 그려 두고drawBitmap으로 재사용 setLayerType(LAYER_TYPE_SOFTWARE, null)는 일부 효과가 필요할 때만 (하드웨어 가속을 끄므로 느려짐)
9. 정리
- Canvas는 그릴 대상, Paint는 스타일 — Paint는
onDraw밖에서 생성 Path로 직선/베지어 곡선/자유 도형을 만들고 터치 이벤트로 그리기 구현Bitmap은Canvas(bitmap)으로 오프스크린 합성,PorterDuff로 마스크 처리- 변환은
save/restore로 범위를 제한 - 객체 재사용과 비트맵 메모리 관리가 성능의 핵심