Canvas (1) - Android Canvas 구조 및 기능 소개
본 글은 Android 그래픽스에 대한 내용을 다루며 아래 포스트들을 참고하시면 내용 이해에 도움을 줄 수 있을 것입니다.
- Bitmap(1) - Android Bitmap Config 및 기본 설정
- Bitmap(2) - Android Bitmap 메모리 구조 및 관리 방식
- Bitmap (3) - Android Bitmap 기능 소개 및 BitmapFactory 소개
- Matrix(1) - Android Matrix 구조 및 2D변환 적용 방식
Canvas
android.graphics.Canvas
는 Android에서 직접 그림을 그리는 도구로 Bitmap, View, … 등의 대상을 바탕으로 텍스트, 이미지, 선, 도형 등 다양한 그래픽 요소를 그릴 수 있게 해주는 클래스입니다. 클래스의 이름이 Canvas인 것처럼 현실 세계에서 그림을 그릴 수 있게 도와주는 캔버스와 유사한 목적과 기능을 가지고 있습니다.
Canvas는 단순히 그림을 그리는 도구의 역할과 더불어 렌더링 명령을 수집하고 처리하는 중간 컨트롤러 역할을 수행합니다. 예를 들어, drawRect()
라는 메서드를 호출하면 Canvas가 내부적으로 Skia 그래픽 엔진에 해당 명령을 전달합니다. 그리고 Skia는 명령을 해석하는 과정을 거쳐 최종적으로 실제 화면에 사각형을 렌더링하게됩니다.
다른 포스트에서 소개된 Bitmap과 마찬가지로 Canavs 또한 마찬가지로 네이티브 메서드를 래핑한 클래스입니다. 실제 기능은 SkCanvas에서 처리되며, android.graphics.Canvas는 Java/Kotlin에서 사용하기 쉽도록 설계되어있는 인터페이스 입니다.
private static native void nDrawBitmap(long nativeCanvas, long bitmapHandle, float left,
float top, long nativePaintOrZero, int canvasDensity, int screenDensity,
int bitmapDensity);
private static native void nDrawBitmap(long nativeCanvas, long bitmapHandle, float srcLeft,
float srcTop,
float srcRight, float srcBottom, float dstLeft, float dstTop, float dstRight,
float dstBottom, long nativePaintOrZero, int screenDensity, int bitmapDensity);
private static native void nDrawBitmap(long nativeCanvas, int[] colors, int offset, int stride,
float x, float y, int width, int height, boolean hasAlpha, long nativePaintOrZero);
private static native void nDrawColor(long nativeCanvas, int color, int mode);
private static native void nDrawColor(long nativeCanvas, long nativeColorSpace,
@ColorLong long color, int mode);
private static native void nDrawPaint(long nativeCanvas, long nativePaint);
private static native void nDrawPoint(long canvasHandle, float x, float y, long paintHandle);
private static native void nDrawPoints(long canvasHandle, float[] pts, int offset, int count,
long paintHandle);
private static native void nDrawLine(long nativeCanvas, float startX, float startY, float stopX,
float stopY, long nativePaint);
private static native void nDrawLines(long canvasHandle, float[] pts, int offset, int count,
long paintHandle);
...
위 코드는 Canvas 내 구현된 네이티브 메서드로, 해당 메서드들은 가장 최상위 클래스인 BaseCanvas
에 존재합니다. BaseCanvas에 대한 자세한 소스는 [이곳]에서 확인하실 수 있습니다.
Canvas의 사용
Canvas는 맥락에 따라 다양한 방식으로 제공되어 사용됩니다. 대표적으로 흔히 사용하는 TextView, RadioButton과 같은 View를 상속한 객체들은 렌더링을 위한 그리기를 처리할 때 onDraw()
라는 메서드가 사용됩니다. onDraw()
는 Canvas 파라미터를 가지고 있어 해당 Canvas 파라미터를 사용하여 여러 요청에 대한 그리기를 처리할 수 있습니다. 물론 TextView 및 RadioButton 같은 위젯 패키지 내 클래스들은 이미 내부에서 그리는 방식이 구현 되어있어 직접적인 onDraw()
로직 제어는 불가능 하지만, CustomView기능을 활용한다면 적극적으로 onDraw()
메서드를 통해 그리기 작업을 제어할 수 있게 됩니다.
protected void onDraw(Canvas canvas) {
restartMarqueeIfNeeded();
// Draw the background for this view
super.onDraw(canvas);
...
}
TextView의 onDraw()
메서드의 선언 부분으로 [해당 소스]에서 자세한 정보를 확인하실 수 있습니다.
Bitmap을 그리는 과정에서 또한 마찬가지로 Canvas 가 사용됩니다. Bitmap은 단지 그림 그리는 대상일 뿐, 실제로 UI에 그리기 위해서는 아래 예시처럼 Canvas의 drawBitmap()
을 사용하여야만 합니다.
public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) {
throwIfCannotDraw(bitmap);
throwIfHasHwFeaturesInSwMode(paint);
nDrawBitmap(mNativeCanvasWrapper, bitmap.getNativeInstance(), left, top,
paint != null ? paint.getNativeInstance() : 0, mDensity, mScreenDensity,
bitmap.mDensity);
}
위의 예시는 BaseCanvas 내 구현된 drawBitmap 메서드 중 일부로 자세한 소스는 [해당 주소]에서 확인하실 수 있습니다.
또 하나의 방식으로 Surface
에서 사용할 수 있는 lockCanvas()
라는 메서드가 존재합니다. lockCanvas()
는 Surface나 SurfaceView에 직접 그림을 그릴 때 사용하는 메서드로 Canavs를 반환합니다. 반환된 Canavs는 lockCanvas()를 요청한 주체만 사용할 수 있도록 잠기게 되며, 잠금 해제를 통해 Canavs에서 그리기를 완료할 수 있습니다.
Surface및lockCanavs()에 대한 추가적인 내용은 포스트를 참고하실 바랍니다. Android Graphics(1) - Surface의 구조와 컴포넌트
구조
Canvas를 통해 다양한 그리기 기능을 수행할 수 있으며, 이러한 기능들은 부모 클래스인 BaseCanvas 기반하에 사용 됩니다. BaseCanvas 는 Canvas 의 그리기 작업을 수행하기 위한 추상 클래스로 JNI 메서드와 JNI를 래핑한 기본 draw…() 류의 메서드들을 포함하고 있습니다.
mNativeCanvasWrapper
는 실제 내부에 존재하는 네이티브 객체를 가리키는 필드입니다. 다른 포스트에서 다룬 Bitmap 과 마찬가지로 Java/Kotlin 레벨에서는 네이티브 객체를 가리키는 포인터 주소만 존재하며, 실제 기능 동작은 mNativeCanvasWrapper 필드에 저장된 주소를 전달하여 네이티브 영역에서 수행하게 됩니다.
public class BaseCanvas {
...
protected long mNativeCanvasWrapper;
...
}
다시 한번 정리하자면, Java/Kotlin 레벨에서의 Canvas
클래스는 JNI 메서드와 외부 개발자가 편하게 사용할 수 있도록 JNI를 래핑한 메서드들이 존재합니다. 해당 메서드들의 실제 동작은 JNI와 연동된 네이티브 영역에서 메서드가 처리하게 됩니다. 이때 그리기 관련 처리는 그래픽 엔진인 Skia가 담당하게 됩니다.
기능
Canvas는 요구사항에 따라 다양한 시각적인 요소를 직접 그려낼 수 있습니다. 기본적으로 선분과 곡선 그리고 도형 그리기부터 시작하여 텍스트와 같이 특수한 객체도 Canvas에서 그려낼 수 있습니다. Canvas의 그리기 메서드들은 draw..()
와 같은 접두사를 가지고 있으며, 대다수는 시각적 효과를 부여할 Paint 객체를 파라미터로 전달할 수 있습니다. draw…()
메서드를 통해 특정 포인트, 경로 또는 도형 등을 정보를 입력하고 Paint 객체에 속성을 부여하여 전달한다면 다양한 요소들을 구현해낼 수 있을 것입니다.
Canvas 내 draw…() 메서드들은 선언 순서를 바탕으로 그리게 됩니다. 예를 들어, drawCircle()
과 같은 메서드를 사용하여 Canvas에 도형을 그렸지만, 그 이후에 drawColor를 사용하여 배경을 칠하게 될 경우 도형들이 배경색에 덮어씌어질 수 있습니다. 따라서 순서를 고려하여 가장 먼저 배경을 칠하고 도형을 그리는 식으로 메서드를 선언해야만 합니다.
해당 포스트에서는 기능의 일부분만 다루게 될 것이며, 더욱 자세한 정보들은 [해당 문서]를 참고하시길 바랍니다. 아래에서부터는 Canvas가 그릴 수 있는 기본적인 요소들을 순차적으로 정리해보겠습니다.
도형그리기
Canvas는 draw…()
접두사가 붙은 다양한 함수들을 통해 사각형, 타원, 원, …등 과 같은 도형들을 자유롭게 그릴 수 있습니다. 이 draw...()
함수들은 각각의 도형에 맞는 매개변수를 가지며, 마찬가지로 파라미터로 제공되는 Paint
객체를 이용하여 다양한 스타일들을 구현해낼 수 있습니다. 구체적인 예시로 drawRect()
, drawCircle()
, drawRoundRect()
등과 같은 다양한 도형 그리기를 제공합니다.
// 사각형 그리기
public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
public void drawRect(@NonNull Rect r, @NonNull Paint paint)
// 원 그리기
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
// 타원 그리기
public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)
// 모서리가 둥근 사각형 그리기
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)
실제로 Paint와 함께 drawRect()
사용할 경우 아래와 같이 작성할 수 있습니다.
canvas.drawRect(
/* left = */ 500f,
/* top = */ 500f,
/* right = */ 100f,
/* bottom = */ 100f,
/* paint = */ Paint().apply {
color = 0x0080FF
alpha = 255
}
)
경로 그리기
Canavs는 도형뿐만 아니라 선, 점 그리고 복잡한 형태의 경로를 그리는 기능도 제공합니다. 이러한 기능들을 사용한다면 복잡한 형태의 도형과 시각적인 효과를 만들어 낼 수 있습니다.
drawLine
Canvas에 직선을 그리는 데 사용되는 메서드입니다. 시작점과 끝점을 포인트를 지정하고, Paint 객체를 통해 선분의 스타일을 지정합니다. 추가적으로 여러 개의 선을 한번의 입력으로 그릴 수 있는 drawLines()
라는 메서드도 지원하고 있습니다.
public void drawLine(float startX, float startY, float stopX, float stopY,
@NonNull Paint paint)
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count,
@NonNull Paint paint)
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint)
drawArc
Canvas에 호 또는 부채꼴을 그릴 수 있도록 도와주는 메서드입니다. 경로에 대한 스타일은 마찬가지로 Paint 객체를 통해 입힐 수 있습니다. drawArc()
를 사용할 때 요구되는 파라미터들이 존재하는데, startAngle은 호가 시작될 각도, sweepAngle은 호를 얼마나 그릴지에 대한 각도를 의미합니다. startAngle은 0을 기준으로 우측 3시방향에서 시작되며 각도가 증가함에 따라 시작 위치가 시계방향으로 이동됩니다. sweepAngle이 양수이면 시계방향, 음수이면 반시계 방향으로 각도를 그리게 됩니다.
추가적으로 drawArc()
에는 useCenter
라는 boolean flag의 파라미터가 존재합니다. 해당 플래그를 true로 설정하면 호의 양 끝점과 타원의 중심이 연결되어 부채꼴 모양으로 형성됩니다.
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
@NonNull Paint paint)
public void drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, @NonNull Paint paint)
drawPoint
Canvas에 점을 그리는 메서드입니다. 점의 좌표 포인트와 Paint 스타일을 설정하여 구현할 수 있습니다. drawLines()
와 마찬가지로 한번에 다수의 점을 그릴 수 있는 drawPoints()
메서드를 제공하고 있습니다.
public void drawPoint(float x, float y, @NonNull Paint paint)
public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count, @NonNull Paint paint)
public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint)
drawPath
앞서 다양한 도형들과 직선을 그리는 기능들에 대해 소개하였지만, 해당 기능들은 복잡한 도형이나 시각효과를 그려내기에 어려움이 존재합니다. 이러한 요구사항을 해결하기 위해 drawPath()
라는 메서드를 제공합니다.
drawPath()
는 선, 곡선, 도형 등의 경로를 그리는 메서드입니다. drawPath()
를 사용하기 위해서는 사전에 Path 객체를 통해 경로에 대한 정보를 저장하고 있어야합니다.
아래는 동일한 Path 객체를 구성하고 Paint 각자의 style을FILL 과 STROKE로 설정한 예시입니다.
canvas.drawPath(
/* path = */ Path().apply {
moveTo(200f, 200f)
lineTo(200f, 300f)
lineTo(300f, 300f)
lineTo(300f, 200f)
lineTo(200f, 200f)
},
/* paint = */ Paint().apply {
color = 0x0080FF
alpha = 255
style = Paint.Style.FILL // style을 STROKE로 할 경우,외각선만 표시됩니다.
}
)
텍스트 그리기
Canvas는 도형과 경로 그리기 외에도 텍스트를 그리는데 필요한 다양한 메서드들을 제공합니다. 텍스트 그리기는 단순한 단일 텍스트 부터 복잡한 스타일을 가진 텍스트까지 다양한 요구사항에 따라 구현할 수 있도록 여러 메서드를 제공하고 있습니다. 텍스트 또한 마찬가지로 Paint
객체를 활용하여 스타일 속성을 제어할 수 있습니다.
drawText
Canvas에 텍스트를 그리는 일반적인 메서드입니다. 텍스트 문자열과 x, y 좌표 그리고 Paint 객체를 파라미터로 받습니다. 이때 y 좌표의 기준은 텍스트의 상단이 아닌 텍스트의 Baseline을 기준으로 설정됩니다.
Baseline은 대부분의 문자 아래를 기준으로 확장되는 선을 의미합니다. Baseline에 대한 자세한 내용은 [참고 자료] 를 확인하시길 바랍니다.
// y좌표 500 위치에 선분 그리기
canvas.drawLine(
/* left = */ 0f,
/* top = */ 500f,
/* right = */ 1000f,
/* bottom = */ 500f,
/* paint = */ Paint().apply {
color = 0x000000
alpha = 255
}
)
// y좌표 500 위치에 텍스트 그리기
canvas.drawText(
/* text = */ "Hello World",
/* x = */ 500f,
/* y = */ 500f,
/* paint = */ Paint().apply {
color = 0x0080FF
alpha = 255
textSize = 50f
}
)
drawText()
메서드들은 아래와 같이 다양한 문자열 입력을 지원하도록 오버로드 함수를 제공하고 있습니다.
public void drawText(@NonNull char[] text, int index, int count, float x, float y,
@NonNull Paint paint)
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
public void drawText(@NonNull String text, int start, int end, float x, float y,
@NonNull Paint paint)
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
@NonNull Paint paint)
drawTextRun
앞서 소개한 drawText()
는 단일 라인 텍스트 그리기에 사용되는 메서드입니다. 한편으로 복잡하고 다양한 요구사항을 처리하기 위해서는 drawTextRun()
메서드를 사용해야만 합니다. drawTextRun()
메서드는 특정 언어의 특성과 문맥에 따라 글자의 형태나 간격 및 순서가 동적으로 변화하는 부분을 처리합니다. 파라미터로 제공되는 contextIndex 또는 contextStart 를 통해 특수한 언어적 규칙을 줄 영역을 지정할 수 있습니다. 추가적으로 drawTextRun()
파라미터에는 isRtl
이라는 플래그가 존재합니다. 해당 플래그를 사용하여 텍스트의 방향을 결정할 수 있습니다.
val text = "Good Night"
canvas.drawTextRun(
text,
0, text.length,
0, text.length,
500f,
500f,
true,
Paint().apply {
color = 0x0080FF
alpha = 255
textSize = 100f
}
)
canvas.drawText(
text,
0, text.length,
500f,
600f,
Paint().apply {
color = 0x0080FF
alpha = 255
textSize = 100f
}
)
RTL(Right to Left)는 아랍어와 같이 특정 언어에서 사용되는 표기형식으로 일반적으로 왼쪽에서 오른쪽으로 텍스트를 표기하는 것이 아닌, 오른쪽에서 왼쪽으로 텍스트를 표기합니다.
해당 메서드는 여러가지 사항을 고려하여 구현해야하기 때문에 직접적으로 사용하기 보다는 StaticLayout 를 바탕으로 텍스트를 빌딩한 뒤, StaticLayout의 내장 메서드인 draw(Canvas c)
를 사용하여 구현하는것을 권장합니다.
public void drawTextRun(@NonNull char[] text, int index, int count, int contextIndex,
int contextCount, float x, float y, boolean isRtl, @NonNull Paint paint)
public void drawTextRun(@NonNull CharSequence text, int start, int end, int contextStart,
int contextEnd, float x, float y, boolean isRtl, @NonNull Paint paint)
public void drawTextRun(@NonNull MeasuredText text, int start, int end, int contextStart,
int contextEnd, float x, float y, boolean isRtl, @NonNull Paint paint)
drawTextOnPath
drawTextOnPath()
메서드는 텍스트를 직선이 아닌 특정한 경로에 따라 그릴 수 있도록 지원해줍니다.
해당 메서드는 경로를 바탕으로 택스트를 그리기 때문에 drawPath()
와 마찬가지로 Path 객체를 정의해야만 합니다. Path 객체를 활용하여 자유롭게 경로를 생성할 수 있기 때문에 물결 또는 원과 같은 형태의 텍스트 배치가 가능해집니다. 파라미터로 주어지는 hOffset 은 Path 객체로 그린 경로의 시작점에서부터 수평 방향으로 얼마나 떨어뜨려 그릴지를 의미하고, vOffset은 텍스트의 기준선을 Path 객체를 통해 그린 경로로 부터 수직 방향으로 얼마나 떨어뜨려 그릴지를 결정합니다.
public void drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path,
float hOffset, float vOffset, @NonNull Paint paint)
public void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset,
float vOffset, @NonNull Paint paint)
예를 들어, 대각선 형태의 경로를 지정하여 drawTextOnPath()
에 주입할 경우, 텍스트는 Path의 경로와 동일한 방향으로 그려지게 됩니다.
val text = "Good Night Good Night"
canvas.drawTextOnPath(
text,
Path().apply {
moveTo(500f, 500f)
lineTo(1000f,1500f)
},
0f,
0f,
Paint().apply {
color = 0x0080FF
alpha = 255
textSize = 100f
}
)
칠하기
Canavs
는 배경을 칠하는 기능 또한 마찬가지로 재공하도 있습니다. 칠하기 관련 메서드를 통해 배경을 단색 패턴 또는 그라디언트 등과 같은 다양한 방식으로 칠할 수 있습니다.
칠하기 메서드들은 배경을 덮어쓰는 방식으로 Canvas를 칠하기 때문에 그리기 순서를 고려하여서 배경을 먼저 칠하고 그외 요소들을 그리는 방식으로 진행하여야 합니다.
drawColor
drawColor()
는 Canvas 전체를 단색으로 칠할 수 있도록 지원해주는 메서드입니다. color값을 직접적으로 넣는 방식과 drawRGB()와 같이 채널 값을 직접 설정하여 배경을 칠할 수도 있습니다.
public void drawColor(@ColorInt int color)
public void drawColor(@ColorLong long color)
public void drawColor(@ColorInt int color, @NonNull BlendMode mode)
public void drawRGB(int r, int g, int b)
drawPaint
drawPaint()
는 Paint 객체를 활용하여 배경을 칠하는 메서드입니다. 이를 통해 그라디언트, 패턴, 비트맵과 같은 특수한 유형으로 배경을 색칠할 수 있습니다.
public void drawPaint(@NonNull Paint paint)
Paint 객체에서 BitmapShader
를 바탕으로 shader를 적용하여 Bitmap 패턴을 구현해 낼 수 있습니다. 아래의 예시에서는 BitmapFactory를 통해 외부 파일을 Bitmap으로 변환한 후, BitmapShader에 적용하였습니다.
val bitmap = BitmapFactory.decodeResource(
resources,
R.drawable.sample,
BitmapFactory.Options().apply {
inSampleSize = 16 // 비트맵 사이즈를 가로 세로 각각 1/16로 줄여줍니다.
}
)
val bitmapShader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
canvas.drawPaint(
Paint().apply {
shader = bitmapShader
}
)
이번 포스트에서는 Canavs에 대한 간략한 소개와 일부 기능들의 사용방법에 대한 내용으로 글을 전개하였습니다. 하지만 Canavs의 추가적인 기능들과 Canavs와 연달아서 같이 사용되는 Paint 와 Path 그리고 Shader와 같은 내용들은 자세하게 다루지 않고 건너뛰었습니다. 해당 부분에 대한 자세한 내용들은 다음 시리즈에 이어서 풀어나가겠습니다.