이 Codelab은 Kotlin 기반 Android 고급 교육 과정의 일부입니다. Codelab을 순서대로 진행하는 경우 학습 효과를 극대화할 수 있지만 순서를 바꿔 진행해도 괜찮습니다. 모든 교육 과정 Codelab은 Kotlin 기반 고급 Android Codelab 방문 페이지에 나열되어 있습니다.
소개
Android에서는 뷰에 맞춤 2D 그래픽과 애니메이션을 구현하는 데 사용할 수 있는 여러 기법이 있습니다.
드로어블을 사용하는 것 외에도 Canvas 클래스의 그리기 메서드를 사용하여 2D 그림을 만들 수 있습니다. Canvas는 그리기를 위한 메서드를 제공하는 2D 그리기 표면입니다. 이는 사용자가 보는 내용이 시간이 지남에 따라 변경되므로 앱이 정기적으로 자체적으로 다시 그려야 하는 경우에 유용합니다. 이 Codelab에서는 View에 표시되는 캔버스를 만들고 그리는 방법을 알아봅니다.
캔버스에서 수행할 수 있는 작업 유형은 다음과 같습니다.
- 캔버스 전체를 색상으로 채웁니다.
Paint객체에 정의된 스타일로 사각형, 호, 경로와 같은 도형을 그립니다.Paint객체는 기하학적 구조 (예: 선, 직사각형, 타원, 경로)를 그리는 방법이나 텍스트의 서체에 관한 스타일 및 색상 정보를 보유합니다.- 변환(예: 변환, 크기 조절, 맞춤 변환)을 적용합니다.
- 캔버스에 모양이나 경로를 적용하여 표시되는 부분을 정의합니다.

Android 그리기 방식 (매우 단순화됨)
Android 또는 기타 최신 시스템에서의 그리기는 하드웨어까지 추상화 및 최적화 레이어가 포함된 복잡한 프로세스입니다. Android의 그리기 방식은 많은 글이 작성된 흥미로운 주제이며, 세부사항은 이 Codelab의 범위를 벗어납니다.
이 Codelab과 전체 화면 뷰에 표시하기 위해 캔버스에 그리는 앱의 맥락에서 다음과 같이 생각할 수 있습니다.

- 그리는 내용을 표시하는 뷰가 필요합니다. Android 시스템에서 제공하는 뷰 중 하나일 수 있습니다. 또는 이 Codelab에서는 앱의 콘텐츠 뷰 역할을 하는 맞춤 뷰 (
MyCanvasView)를 만듭니다. - 이 뷰는 모든 뷰와 마찬가지로 자체 캔버스 (
canvas)가 함께 제공됩니다. - 뷰의 캔버스에 그리는 가장 기본적인 방법은
onDraw()메서드를 재정의하고 캔버스에 그리는 것입니다. - 그림을 빌드할 때는 이전에 그린 내용을 캐시해야 합니다. 데이터를 캐시하는 방법에는 여러 가지가 있습니다. 비트맵 (
extraBitmap)에 캐시하는 방법도 있고, 그린 내용을 좌표와 명령어로 저장하는 방법도 있습니다. - 캔버스 그리기 API를 사용하여 캐싱 비트맵 (
extraBitmap)에 그리려면 캐싱 비트맵의 캐싱 캔버스 (extraCanvas)를 만듭니다. - 그런 다음 캐싱 캔버스 (
extraCanvas)에 그리면 캐싱 비트맵 (extraBitmap)에 그려집니다. - 화면에 그려진 모든 항목을 표시하려면 뷰의 캔버스 (
canvas)에 캐싱 비트맵 (extraBitmap)을 그리라고 지시합니다.
기본 요건
- 활동과 기본 레이아웃이 있는 앱을 만들고 Android 스튜디오를 사용하여 실행하는 방법
- 이벤트 핸들러를 뷰와 연결하는 방법
- 맞춤 뷰를 만드는 방법
학습할 내용
- 사용자 터치에 응답하여
Canvas를 만들고 그리는 방법
실습할 내용
- 사용자가 화면을 터치하면 화면에 선을 그리는 앱을 만듭니다.
- 동작 이벤트를 캡처하고 이에 대한 응답으로 화면의 전체 화면 맞춤 뷰에 표시되는 캔버스에 선을 그립니다.
MiniPaint 앱은 아래 스크린샷과 같이 사용자 터치에 대한 응답으로 선을 표시하기 위해 맞춤 뷰를 사용합니다.

1단계: MiniPaint 프로젝트 만들기
- Empty Activity 템플릿을 사용하는 MiniPaint라는 새 Kotlin 프로젝트를 만듭니다.
app/res/values/colors.xml파일을 열고 다음 두 색상을 추가합니다.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>styles.xml열기- 지정된
AppTheme스타일의 상위 요소에서DarkActionBar을NoActionBar로 바꿉니다. 이렇게 하면 작업 표시줄이 삭제되므로 전체 화면을 그릴 수 있습니다.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">2단계: MyCanvasView 클래스 만들기
이 단계에서는 그리기용 맞춤 뷰 MyCanvasView를 만듭니다.
app/java/com.example.android.minipaint패키지에서MyCanvasView이라는 New > Kotlin File/Class를 만듭니다.MyCanvasView클래스가View클래스를 확장하고context: Context를 전달하도록 합니다. 추천 가져오기를 수락합니다.
import android.content.Context
import android.view.View
class MyCanvasView(context: Context) : View(context) {
}3단계: MyCanvasView를 콘텐츠 뷰로 설정
MyCanvasView에 그릴 내용을 표시하려면 MainActivity의 콘텐츠 뷰로 설정해야 합니다.
strings.xml를 열고 뷰의 콘텐츠 설명에 사용할 문자열을 정의합니다.
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
Drag your fingers to draw. Rotate the phone to clear.</string>MainActivity.kt열기onCreate()에서setContentView(R.layout.activity_main)을 삭제합니다.MyCanvasView의 인스턴스를 생성하세요.
val myCanvasView = MyCanvasView(this)- 그 아래에서
myCanvasView레이아웃의 전체 화면을 요청합니다.myCanvasView에서SYSTEM_UI_FLAG_FULLSCREEN플래그를 설정하여 이 작업을 실행합니다. 이렇게 하면 뷰가 화면을 완전히 채웁니다.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN- 콘텐츠 설명을 추가합니다.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)- 그 아래에서 콘텐츠 뷰를
myCanvasView로 설정합니다.
setContentView(myCanvasView)- 앱을 실행합니다. 캔버스에 크기가 없고 아직 아무것도 그리지 않았으므로 완전히 흰색 화면이 표시됩니다.
1단계: onSizeChanged() 재정의
onSizeChanged() 메서드는 뷰의 크기가 변경될 때마다 Android 시스템에 의해 호출됩니다. 뷰는 크기가 없는 상태로 시작하므로 활동이 처음 뷰를 만들고 확장한 후 뷰의 onSizeChanged() 메서드도 호출됩니다. 따라서 이 onSizeChanged() 메서드는 뷰의 캔버스를 만들고 설정하는 데 적합합니다.
MyCanvasView에서 클래스 수준으로 캔버스와 비트맵의 변수를 정의합니다.extraCanvas및extraBitmap를 호출합니다. 이러한 항목은 이전에 그려진 항목을 캐싱하기 위한 비트맵과 캔버스입니다.
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap- 캔버스의 배경색에 관한 클래스 수준 변수
backgroundColor을 정의하고 앞서 정의한colorBackground로 초기화합니다.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)MyCanvasView에서onSizeChanged()메서드를 재정의합니다. 이 콜백 메서드는 변경된 화면 크기, 즉 새로운 너비와 높이 (변경할 대상) 및 이전 너비와 높이 (변경할 대상)를 사용하여 Android 시스템에 의해 호출됩니다.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
}onSizeChanged()내에서 화면 크기인 새 너비와 높이로Bitmap인스턴스를 만들고extraBitmap에 할당합니다. 세 번째 인수는 비트맵 색상 구성입니다.ARGB_8888는 각 색상을 4바이트로 저장하며 권장됩니다.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)extraBitmap에서Canvas인스턴스를 만들고extraCanvas에 할당합니다.
extraCanvas = Canvas(extraBitmap)extraCanvas를 채울 배경색을 지정합니다.
extraCanvas.drawColor(backgroundColor)onSizeChanged()를 살펴보면 함수가 실행될 때마다 새 비트맵과 캔버스가 생성됩니다. 크기가 변경되었으므로 새 비트맵이 필요합니다. 하지만 이는 메모리 누수로, 이전 비트맵이 남아 있습니다. 이 문제를 해결하려면super호출 바로 뒤에 이 코드를 추가하여 다음extraBitmap를 만들기 전에 재활용하세요.
if (::extraBitmap.isInitialized) extraBitmap.recycle()2단계: onDraw() 재정의
MyCanvasView의 모든 그리기 작업은 onDraw()에서 이루어집니다.
먼저 캔버스를 표시하여 onSizeChanged()에서 설정한 배경색으로 화면을 채웁니다.
onDraw()를 재정의하고 캐시된extraBitmap의 콘텐츠를 뷰와 연결된 캔버스에 그립니다.drawBitmap()Canvas메서드는 여러 버전으로 제공됩니다. 이 코드에서는 비트맵, 왼쪽 상단 모서리의 x 및 y 좌표 (픽셀 단위),Paint의null를 제공합니다.null는 나중에 설정할 예정입니다.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}onDraw()에 전달되고 시스템에서 비트맵을 표시하는 데 사용되는 캔버스는 onSizeChanged() 메서드에서 생성되고 비트맵에 그리는 데 사용되는 캔버스와 다릅니다.
- 앱을 실행합니다. 지정된 배경색으로 전체 화면이 채워져야 합니다.

그리려면 그릴 때 스타일이 지정되는 방식을 지정하는 Paint 객체와 그려지는 항목을 지정하는 Path가 필요합니다.
1단계: Paint 객체 초기화
- MyCanvasView.kt에서 파일 수준 상단에 획 너비 상수를 정의합니다.
private const val STROKE_WIDTH = 12f // has to be floatMyCanvasView의 클래스 수준에서 그릴 색상을 보유하는 변수drawColor을 정의하고 이전에 정의한colorPaint리소스로 초기화합니다.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)- 클래스 수준에서 아래에
Paint객체의 변수paint를 추가하고 다음과 같이 초기화합니다.
// Set up the paint with which to draw.
private val paint = Paint().apply {
color = drawColor
// Smooths out edges of what is drawn without affecting shape.
isAntiAlias = true
// Dithering affects how colors with higher-precision than the device are down-sampled.
isDither = true
style = Paint.Style.STROKE // default: FILL
strokeJoin = Paint.Join.ROUND // default: MITER
strokeCap = Paint.Cap.ROUND // default: BUTT
strokeWidth = STROKE_WIDTH // default: Hairline-width (really thin)
}paint의color는 이전에 정의한drawColor입니다.isAntiAlias은 가장자리 다듬기를 적용할지 여부를 정의합니다.isAntiAlias를true로 설정하면 모양에 영향을 주지 않고 그려진 항목의 가장자리가 부드러워집니다.isDither은true일 때 기기보다 정밀도가 높은 색상의 다운샘플링 방식에 영향을 미칩니다. 예를 들어 디더링은 이미지의 색상 범위를 256색 이하로 줄이는 가장 일반적인 방법입니다.style는 기본적으로 선인 획에 적용할 페인팅 유형을 설정합니다.Paint.Style는 그려지는 기본 요소가 색상으로 채워지는지, 윤곽선이 그려지는지 또는 둘 다인지 (동일한 색상)를 지정합니다. 기본값은 페인트가 적용된 객체를 채우는 것입니다. ('채우기'는 도형 내부를 색칠하고 '획'은 윤곽선을 따릅니다.)Paint.Join의strokeJoin는 획이 적용된 경로에서 선과 곡선 세그먼트가 결합되는 방식을 지정합니다. 기본값은MITER입니다.strokeCap는 선의 끝 모양을 캡으로 설정합니다.Paint.Cap는 획이 그어진 선과 경로의 시작과 끝을 지정합니다. 기본값은BUTT입니다.strokeWidth는 획의 너비를 픽셀 단위로 지정합니다. 기본값은 매우 얇은 헤어라인 너비이므로 앞에서 정의한STROKE_WIDTH상수로 설정됩니다.
2단계: 경로 객체 초기화
Path은 사용자가 그리는 항목의 경로입니다.
MyCanvasView에서 변수path을 추가하고Path객체로 초기화하여 사용자의 화면 터치를 따라 그리는 경로를 저장합니다.Path의android.graphics.Path를 가져옵니다.
private var path = Path()1단계: 디스플레이의 움직임에 응답
사용자가 디스플레이를 터치할 때마다 뷰의 onTouchEvent() 메서드가 호출됩니다.
MyCanvasView에서onTouchEvent()메서드를 재정의하여 전달된event의x및y좌표를 캐시합니다. 그런 다음when표현식을 사용하여 화면을 터치하고, 화면에서 이동하고, 화면에서 터치를 해제하는 모션 이벤트를 처리합니다. 화면에 선을 그리는 데 관심 있는 이벤트입니다. 각 이벤트 유형에 대해 아래 코드와 같이 유틸리티 메서드를 호출합니다. 터치 이벤트의 전체 목록은MotionEvent클래스 문서를 참고하세요.
override fun onTouchEvent(event: MotionEvent): Boolean {
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> touchStart()
MotionEvent.ACTION_MOVE -> touchMove()
MotionEvent.ACTION_UP -> touchUp()
}
return true
}- 클래스 수준에서 현재 터치 이벤트 (
MotionEvent좌표)의 x 및 y 좌표를 캐시하기 위해 누락된motionTouchEventX및motionTouchEventY변수를 추가합니다.0f로 초기화합니다.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0ftouchStart(),touchMove(),touchUp()세 함수의 스텁을 만듭니다.
private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}- 코드가 빌드되고 실행되지만 아직 색상이 지정된 배경과 다른 점은 없습니다.
2단계: touchStart() 구현
이 메서드는 사용자가 화면을 처음 터치할 때 호출됩니다.
- 클래스 수준에서 최신 x 및 y 값을 캐시하는 변수를 추가합니다. 사용자가 움직임을 멈추고 터치를 떼면 다음 경로 (그릴 선의 다음 세그먼트)의 시작점이 됩니다.
private var currentX = 0f
private var currentY = 0f- 다음과 같이
touchStart()메서드를 구현합니다.path를 재설정하고 터치 이벤트의 x-y 좌표 (motionTouchEventX및motionTouchEventY)로 이동하여currentX및currentY을 해당 값에 할당합니다.
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}3단계: touchMove() 구현
- 클래스 수준에서
touchTolerance변수를 추가하고ViewConfiguration.get(context).scaledTouchSlop로 설정합니다.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop경로를 사용하면 모든 픽셀을 그리고 디스플레이 새로고침을 요청할 필요가 없습니다. 대신 훨씬 더 나은 성능을 위해 점 사이의 경로를 보간할 수 있습니다.
- 손가락이 거의 움직이지 않았다면 그릴 필요가 없습니다.
- 손가락이
touchTolerance거리 미만으로 이동한 경우 그리지 않습니다. scaledTouchSlop는 시스템에서 사용자가 스크롤하고 있다고 생각하기 전에 터치가 배회할 수 있는 거리(픽셀)를 반환합니다.
touchMove()메서드를 정의합니다. 이동한 거리 (dx,dy)를 계산하고 두 점 사이의 곡선을 만들어path에 저장하고, 실행 중인currentX및currentY집계를 업데이트하고,path를 그립니다. 그런 다음invalidate()를 호출하여 업데이트된path로 화면을 강제로 다시 그립니다.
private fun touchMove() {
val dx = Math.abs(motionTouchEventX - currentX)
val dy = Math.abs(motionTouchEventY - currentY)
if (dx >= touchTolerance || dy >= touchTolerance) {
// QuadTo() adds a quadratic bezier from the last point,
// approaching control point (x1,y1), and ending at (x2,y2).
path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
currentX = motionTouchEventX
currentY = motionTouchEventY
// Draw the path in the extra bitmap to cache it.
extraCanvas.drawPath(path, paint)
}
invalidate()
}이 메서드를 자세히 설명하면 다음과 같습니다.
- 이동한 거리 (
dx, dy)를 계산합니다. - 움직임이 터치 허용 오차보다 큰 경우 경로에 세그먼트를 추가합니다.
- 다음 세그먼트의 시작점을 이 세그먼트의 엔드포인트로 설정합니다.
lineTo()대신quadTo()를 사용하면 모서리 없이 부드럽게 그려진 선이 만들어집니다. 베지어 곡선을 참고하세요.invalidate()를 호출하여 (결국onDraw()를 호출하고) 뷰를 다시 그립니다.
4단계: touchUp() 구현
사용자가 터치를 떼면 경로가 다시 그려지지 않도록 재설정하기만 하면 됩니다. 그려지는 항목이 없으므로 무효화가 필요하지 않습니다.
touchUp()메서드를 구현합니다.
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}- 코드를 실행하고 손가락으로 화면에 그림을 그립니다. 기기를 회전하면 그리기 상태가 저장되지 않으므로 화면이 지워집니다. 이 샘플 앱에서는 사용자가 화면을 간단하게 지울 수 있도록 의도적으로 이렇게 설계했습니다.

5단계: 스케치 주위에 프레임 그리기
사용자가 화면에 그리면 앱은 경로를 구성하고 비트맵 extraBitmap에 저장합니다. onDraw() 메서드는 뷰의 캔버스에 추가 비트맵을 표시합니다. onDraw()에서 더 많은 그림을 그릴 수 있습니다. 예를 들어 비트맵을 그린 후 도형을 그릴 수 있습니다.
이 단계에서는 사진의 가장자리에 프레임을 그립니다.
MyCanvasView에서Rect객체를 보유하는frame이라는 변수를 추가합니다.
private lateinit var frame: RectonSizeChanged()끝에 인셋을 정의하고 새 크기와 인셋을 사용하여 프레임에 사용할Rect를 만드는 코드를 추가합니다.
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)onDraw()에서 비트맵을 그린 후 직사각형을 그립니다.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)- 앱을 실행합니다. 프레임을 확인합니다.

작업 (선택사항): 경로에 데이터 저장
현재 앱에서 그림 정보는 비트맵에 저장됩니다. 이 방법이 좋은 해결책이긴 하지만 그림 정보를 저장하는 유일한 방법은 아닙니다. 그리기 기록을 저장하는 방법은 앱과 다양한 요구사항에 따라 다릅니다. 예를 들어 도형을 그리는 경우 위치와 크기가 포함된 도형 목록을 저장할 수 있습니다. MiniPaint 앱의 경우 경로를 Path로 저장할 수 있습니다. 시도해 보려면 아래의 일반적인 개요를 참고하세요.
MyCanvasView에서extraCanvas및extraBitmap의 코드를 모두 삭제합니다.- 지금까지의 경로와 현재 그려지고 있는 경로의 변수를 추가합니다.
// Path representing the drawing so far
private val drawing = Path()
// Path representing what's currently being drawn
private val curPath = Path()onDraw()에서 비트맵을 그리는 대신 저장된 경로와 현재 경로를 그립니다.
// Draw the drawing so far
canvas.drawPath(drawing, paint)
// Draw any current squiggle
canvas.drawPath(curPath, paint)
// Draw a frame around the canvas
canvas.drawRect(frame, paint)touchUp()에서 이전 경로에 현재 경로를 추가하고 현재 경로를 재설정합니다.
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()- 앱을 실행하면 아무런 차이가 없습니다.
완료된 Codelab의 코드를 다운로드합니다.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.
Canvas는 그리기를 위한 메서드를 제공하는 2D 그리기 표면입니다.Canvas는 이를 표시하는View인스턴스와 연결될 수 있습니다.Paint객체는 기하학적 구조 (예: 선, 직사각형, 타원, 경로)와 텍스트를 그리는 방법에 관한 스타일 및 색상 정보를 보유합니다.- 캔버스로 작업하는 일반적인 패턴은 맞춤 뷰를 만들고
onDraw()및onSizeChanged()메서드를 재정의하는 것입니다. onTouchEvent()메서드를 재정의하여 사용자 터치를 포착하고 항목을 그려 응답합니다.- 추가 비트맵을 사용하여 시간이 지남에 따라 변경되는 그림의 정보를 캐시할 수 있습니다. 또는 도형이나 경로를 저장할 수 있습니다.
Udacity 과정:
Android 개발자 문서:
Canvas클래스Bitmap클래스View클래스Paint클래스Bitmap.config구성Path클래스- 베지어 곡선 위키백과 페이지
- 캔버스 및 드로어블
- 그래픽 아키텍처 관련 도움말 시리즈 (고급)
- drawables
- onDraw()
- onSizeChanged()
MotionEventViewConfiguration.get(context).scaledTouchSlop
이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.
- 필요한 경우 과제를 할당합니다.
- 과제 제출 방법을 학생에게 알립니다.
- 과제를 채점합니다.
강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.
이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.
질문에 답하세요
질문 1
Canvas을 사용하는 데 필요한 구성요소는 무엇인가요? 해당하는 항목을 모두 선택해 주세요.
▢ Bitmap
▢ Paint
▢ Path
▢ View
질문 2
invalidate() 호출은 어떤 작업을 하나요 (일반적으로)?
▢ 앱을 무효화하고 다시 시작합니다.
▢ 비트맵에서 그림을 지웁니다.
▢ 이전 코드를 실행하면 안 됨을 나타냅니다.
▢ 시스템에 화면을 다시 그려야 한다고 알립니다.
질문 3
Canvas, Bitmap, Paint 객체의 기능은 무엇인가요?
▢ 2D 그리기 표면, 화면에 표시되는 비트맵, 그리기를 위한 스타일 정보
▢ 3D 그리기 표면, 경로를 캐시하는 비트맵, 그리기 스타일 정보
▢ 2D 그리기 노출 영역, 화면에 표시된 비트맵, 뷰 스타일 지정
▢ 그리기 정보, 그릴 비트맵, 그리기 스타일 지정 정보를 위한 캐시
이 과정의 다른 Codelab 링크는 Kotlin 기반 Android 고급 Codelab 방문 페이지를 참고하세요.