(Android/UI) Canvas 그리기 — Path, Paint, Bitmap 조작


1. Canvas와 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. 성능 팁


9. 정리



Related Posts