11 minute read

해당 포스트는 이전 포스트와 연결되므로 이전 포스트를 먼저 보고 오시기를 추천드립니다.

추가적으로 본 글은 Android 그래픽스에 대한 내용을 다루며 아래 포스트들을 참고하시면 내용 이해에 도움을 줄 수 있을 것입니다.

Paint

PaintCanvas에 그림을 그릴때 그림에 대한 스타일을 정의하는 데 사용되는 클래스입니다. Paint는 단순한 선의 굵기 및 색상부터 필터나 그림자등 복잡한 속성들 또한 구현할 수 있도록 지원하고 있습니다.

지난 포스트에서 언급한 CanvasBitmap 처럼 Paint 또한 마찬가지로 실제 기능은 네이티브 영역에 존재하는 Skia엔진에서 처리되며, android.graphics.Paint 클래스에는 해당 기능들을 편리하게 쓸 수 있도록 래핑한 메서드와 JNI들이 존재하고 있습니다. 해당 메서드들을 바탕으로 Java/Kotlin 영역에서 손쉽게 Paint 객체를 조작할 수 있습니다.

...

@CriticalNative
private static native void nReset(long paintPtr);
@CriticalNative
private static native void nSet(long paintPtrDest, long paintPtrSrc);
@CriticalNative
private static native int nGetStyle(long paintPtr);
@CriticalNative
private static native void nSetStyle(long paintPtr, int style);
@CriticalNative
private static native int nGetStrokeCap(long paintPtr);
@CriticalNative
private static native void nSetStrokeCap(long paintPtr, int cap);
@CriticalNative
private static native int nGetStrokeJoin(long paintPtr);

...

Paint 구조에 대한 더 자세한 내용은 [해당 소스]를 참고하시길 바랍니다.

Paint 객체는 여러가지 스타일 속성들을 조합하여 여러 요구사항에 대한 시각적인 요소들을 제어할 수 있습니다. 아래에서부터는 Paint의 주요 스타일 요소들에 대해 자세히 알아보겠습니다.

기본 설정

setColor (색상 설정)

public void setColor(@ColorInt int color) 
public void setColor(@ColorLong long color)
public void setARGB(int a, int r, int g, int b)
public void setAlpha(int a) 

Paint는 스타일의 가장 기본이 되는 요소인 색상을 설정할 수 있습니다. 이때 Alpha 채널을 포함하여 색상을 결정할 수 있도록 지원하며, 색상을 설정할 수 있는 다양한 메서드들이 존재합니다.

setStyle (그리기 스타일 설정)

public void setStyle(Style style)

setStyle() 은 그리기 스타일을 설정해주는 메서드입니다. 이때 스타일은 FILL, STROKE, FILL_AND_STROKE 중 하나를 결정할 수 있으며, 해당 3개의 값은 enum으로 저장되어 있습니다. 스타일이 FILL로 설정된 그림 요소는 선 스타일은 무시한채 모두 채워지고, STROKE는 그림 요소를 채우지 않고 선만 남깁니다. 마지막으로 FILL_AND_STROKE는 선과 채우기를 모두 채택한 방식입니다. Style의 기본 값은 FILL로 설정되어 있습니다.

public enum Style {
    FILL            (0),
    STROKE          (1),
    FILL_AND_STROKE (2);
    ...
}

선 스타일 설정

앞서 Style의 설정 값에는 STROKEFILL_AND_STROKE가 있음을 언급하였습니다. 이때 해당 스타일로 사용되는 선 또한 Paint를 통해 스타일을 변경할 수 있도록 지원하고 있습니다. 아래에서부터는 선에 대한 스타일을 결정하는 속성들에 대해서 다루어 보겠습니다.

strokeWidth

 public void setStrokeWidth(float width)

setStrokeWidth() 메서드는 선의 굵기를 픽셀 단위로 설정하는 메서드입니다.

strokeCap

public void setStrokeCap(Cap cap)

setStrokeCap() 선의 Cap을 정하는 메서드입니다. 여기서 Cap은 선의 양쪽 끝이 어떻게 보일지를 결정하는 스타일 입니다. 스타일은 BUTT, ROUND, SQUARE가 있으며, 기본 값인 BUTT는 선의 끝이 좌표에서 정확하게 잘립니다. 비슷한 속성으로 SQUARE가 존재하는데, SQUARE는 strokeWidth 속성에 따라 선의 끝이 strokeWidth의 절반만큼 확장되어 실제 좌표보다 길게 그려지게 됩니다. ROUND는 SQUARE처럼 strokeWidth의 절반만큼 확장되지만, 둥근 모양으로 그려진다는 특징이 존재합니다.

public enum Cap {
    BUTT(0),
    ROUND(1),
    SQUARE(2);
    ...
}

실제로 동일한 길이의 선에 대해서 Cap 속성을 다르게 부여할경우 아래와 같이 길이가 차이나는것을 확인할 수 있습니다.

strokeJoin

public void setStrokeJoin(Join join) 

setStrokeJoin() 은 두개 이상 이어진 선분이 만나는 모시리 또는 꺾이는 지점이 어떻게 보일지를 결정하는 메서드입니다. setStrokeJoin() 을 결정하기 위해서는 Paint.Style.Join enum을 사용하여야 하며, 종류는 아래와 같습니다.

public enum Join {
    MITER   (0),
    ROUND   (1),
    BEVEL   (2);
    ...
}

Paint.Join은 3가지의 값이 존재하며 MITER는 기본값으로 선분들이 만나는 모서리가 뾰족하게 각진 형태로 연결되도록 구성합니다. ROUND에 경우는 모서리를 둥글게 처리하는 속성이며, BEVEL은 모서리를 비스듬하게 잘린 형태로 연결시킵니다.

아래는 왼쪽부터 MITER BEVEL ROUND 순 입니다. 가운데 BEVEL속성은 연결되는 부분이 잘려있는것을 확인하실 수 있습니다.

strokeMiter

public void setStrokeMiter(float miter)

setStrokeMiter()Stroke.Join의 속성이 MITER일 경우에만 적용되는 속성으로 MITER 조인으로 생성되는 모서리의 뾰족함을 제어할 수 있습니다. 이는 MITER을 적용하였을 때, 두 선이 만나는 각도가 작아질수록 모서리가 길게 튀어나게 되는 현상을 예방하기 위해 사용됩니다. miter 값이 클수록 모서리가 길게 튀어나오도록 허용하지만, 반대로 miter값이 작을 수록 모서리가 길게 튀오는 부분에 대해 BEVEL 형태로 변환이 일어납니다.

miter의 값은 0보다 커야하며, 기본값은 10f 입니다. 이는 miter 길이가 선 두께의 10배를 초과하면 BEVEL 조인으로 전환한다는 의미입니다.

아래 예시는 동일한 경로에서 stroke miter를 각각 10f과 1f로 설정한 결과입니다.

경로 효과 설정

public PathEffect setPathEffect(PathEffect effect)

Paint는 선의 경로에 대해서 점선, 지그재그와 같은 다양한 스타일을 만들 수 있도록 속성을 제공하고 있습니다. 해당 스타일을 적용하기 위해서는 setPathEffect() 사용하여야 합니다. setPathEffect()PathEffect라는 객체를 인자로 받아 수행하는데, Android에서는 주로 사용되는 PathEffect 스타일을 서브클래스로 만들어 기본적으로 제공해주고 있습니다.

CornerPathEffect

public CornerPathEffect(float radius)

경로에 존재하는 모든 모서리들을 둥글게 처리하는 스타일입니다. 파라미터인 radius가 클수록 선분 사이의 각도가 둥글게 되도록 처리됩니다.

DashPathEffect

public DashPathEffect(float intervals[], float phase)

경로를 점선 스타일로 처리합니다. 파리미터로 주어지는 intervals는 float 배열로 선 패턴의 길이를 정의합니다. 이때 배열에 따른 패턴은 on off on off 식으로 전환되는 패턴으로 구성됩니다. 예를 들어, [5f, 10f] 5픽셀 길이의 선과 10픽셀 길이의 공백이 교차하며 반복됩니다.

phase는 패턴 시작의 오프셋을 지정하는 파라미터입니다. 패턴을 시작점에서 얼마나 이동시켜 시작할지를 결정합니다.

PathDashPathEffect

public PathDashPathEffect(Path shape, float advance, float phase, Style style)

경로를 특정 모양을 반복하여 그리는 스타일로 처리합니다.

파라미터인 shape는 경로를 따라 반복될 작은 모양을 의미합니다. shapePath 타입을 사용해야만하며, Path를 통해 경로를 그려 모양을 구성할 수 있습니다. advance 파라미터는 shape 인스턴스간의 간격을 의미하며, phase는 앞서 DashPathEffect에서 언급한것과 동일하게 시작 오프셋을 지정합니다.

PathDashPathEffect에는 style이라는 파라미터가 존재하며, 이는 경로가 꺾이는 지점등이 있을 때 어떻게 변형해야할지 알려주는 속성입니다. 해당 스타일은 TRANSLATE, ROTATE, MORPH 속성이 존재하는 enum으로 구성되어 있습니다. 가장 먼저 TRANSLATE 속성은 코너와 같은 지점에서도 어떠한 변형도 없이 그대로 복사되어 배치됩니다. ROTATE의 경우 경로를 따라 이동하면서 경로의 회전 방향에 맞춰 자동으로 회전되는 속성이며, MORPH의 경우 경로의 곡률이나 너비변화에 따라 모양 자체가 변형되도록 하는 속성입니다.

public enum Style {
    TRANSLATE(0), 
    ROTATE(1),
    MORPH(2);   
    ...
}

아래 예시는 다이아몬드 패턴을 만들어서 사각형에 PathEffect를 부여한 결과입니다.

val diamondPath = Path().apply {
    moveTo(0f, 5f);
    lineTo(5f, 0f);
    lineTo(10f, 5f);
    lineTo(5f, 10f);
    close();
}

val paint = Paint().apply {
    color = Color.rgb(0, 128, 255)
    strokeWidth = 20f
    pathEffect = PathDashPathEffect(
        diamondPath, // 앞서 정의한 Path를 패턴으로 적용
        10f, //shape 간의 간격
        0f, // 초기 오프셋
        PathDashPathEffect.Style.TRANSLATE
    )
    style = Paint.Style.STROKE
}

canvas.drawRect(
	(width / 2) - 200f,
	(height / 2) - 200f,
	(width / 2) + 200f,
	(height / 2) + 200f,
	paint
)  // 중앙에 사각형을 그리도록 배치

DiscretePathEffect

public DiscretePathEffect(float segmentLength, float deviation)

DiscretePathEffect 는 원래의 경로를 따라 선들을 쪼개고 그 선들에게 무작위로 떨림을 부여하여 그리는 효과입니다. 파라미터로 주어지는 segmentLength 는 원래 경로를 얼마나 작은 선분으로 나눌지를 결정하는 속성으로 해당 값이 작아질수록 선분이 많아져 떨림효과가 더 많이 적용되게 됩니다. deviation 은 각 선분이 원래 경로에서 수직방향으로 얼마나 더 벗어날 수 있을지를 의미합니다. 이 값은 클수록 선이 벗어날 수 있는 경로가 커져 심하게 흔들리는 효과를 부여할 수 있습니다. 이때 흔들리는 방식은 무작위로 결정됩니다.

아래는 segmentLength를 10f deviation을 5f로 설정했을 때 나타나나는 결과입니다.

ComposePathEffect

public ComposePathEffect(PathEffect outerpe, PathEffect innerpe)

ComposePathEffect는 여러 PathEffect를 결합하여 하나의 새로운 PathEffect를 만드는데 사용되는 효과입니다. 파라미터로 주어지는 innerpe 의 경우 가장 먼저 원본 경로에 적용되는 효과이고, innerpe가 적용된 경로 위에 outerpe 파라미터로 전달된 효과가 적용됩니다. innerpeDiscretePathEffect 를 적용하고 outerpePathDashPathEffect 를 적용할 경우 아래와 같은 결과가 나타나게 됩니다.

SumPathEffect

public SumPathEffect(PathEffect first, PathEffect second)

SumPathEffectComposePathEffect와 다르게 각자 Effect를 적용한뒤 생성되는 경로를 합쳐서 그리는 효과입니다. 이 효과는 두가지 다른 스타일의 선을 동시에 표시하거나 패턴을 중첩할 때 사용됩니다.

Shader

public Shader setShader(Shader shader)

Shader는 도형 내부에 색상 대신 그라데이션, 패턴, 이미지 등을 채우는 데 사용하는 스타일 클래스입니다. Shader는 LinearGradient, RadialGradient, BitmapShader등과 같이 Android에서 제공하는 기본적인 하위클래스 들이 존재하며, 이러한 Shader를 사용하여 특수한 형태의 UI를 구현할 수 있게 됩니다.

Shader 는 또한 Paint,Bitmap,…등과 마찬가지로 네이티브 영역 Skia 엔진 내에 SKShader라는 클래스가 존재하며, Java/Kotlin에서 제공하는 메서드를 사용할 경우 JNI를 통해 내부적으로 전달되게 됩니다.

이러한 Shader를 사용하기 위해 Paint 에서는 setShader() 라는 메서드를 제공하고 있으며, 이를 통해 더 이상 setColor() 를 통한 단색으로 채우는 방식이 아닌, 특수한 스타일을 적용하는 방식으로 구성할 수 있게 지원합니다.

TileMode

Shader에 대해 설명하기전 Shader에서 사용되는 TileMode 에 대한 개념 먼저 살펴보겠습니다. TileMode는 Shader가 적용되는 영역이 넘어갔을 때 그 이후에 영역을 어떻게 채울지에 대한 방식을 지정하는 속성입니다.

public enum TileMode {
    CLAMP(0),
    REPEAT(1),
    MIRROR(2),
    DECAL(3);
    ...
}

각 속성은 CLAMP, REPEAT, MIRROR, DECAL 로 enum으로 저장되어 있으며, 속성의 기본 값은 CLAMP 입니다. CLAMP는 가장자리의 색상을 고려하여 확장하는 방식으로 그라디언트 Shader의 경우 끝점의 색상이 이어지는 형태로 나아갑니다. REPEAT 는 패턴을 반복하며 채우고, MIRROR의 경우 다음 패턴은 반전되어서 표시하는 설정입니다. 마지막 DECAL 속성은 셰이더의 영역이 끝날경우 그 부분은 별도의 추가처리를 하는것이 아닌 그대로 둡니다. 아래는 4가지의 속성의 결과를 나열한 것으로 어떤식으로 Shader가 배치되는지 이해하는데 도움이 될 것 입니다.

LinearGradiant

public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int[] colors,
            @Nullable float[] positions, @NonNull TileMode tile)

두 포인트를 바탕으로 시작점과 끝점을 선언한 뒤, 두 포인트 사이에 선형으로 색상을 변환하는 효과를 만듭니다. 이때 colors 파라미터를 통해 그라디언트로 사용할 색상의 목록을 정하며, 0.0 ~ 1.0 값을 positions 배열에 넣어 색상이 나타날 위치를 정할 수 있습니다. (positions 가 null 일 경우 균등하게 색상을 분배합니다.)

RadialGradient

public RadialGradient(float centerX, float centerY, float radius,
        @NonNull @ColorInt int[] colors, @Nullable float[] stops,
        @NonNull TileMode tileMode) 

방사형 그라디언트로 안에서 바깥쪽으로 원형으로 색상이 퍼져나가도록 하는 효과를 만듭니다. 그라디언트의 시작점은 centerxcentery 를 통해 지정하며, radius 는 그라디언트가 적용될 원의 반경을 의미합니다.

SweepGradient

public SweepGradient(float cx, float cy, @NonNull @ColorInt int[] colors,
            @Nullable float[] positions)

회전용 그라디언트로 중심점을 기준으로 360도를 회전하면서 색상이 전환되는 효과를 만듭니다. 그라디언트의 중심점은 cx, cy 로 표시하며, colorspositions으로 그라디언트에 사용될 색상배열 및 위치를 지정할 수 있습니다.

BitmapShader

public BitmapShader(@NonNull Bitmap bitmap, @NonNull TileMode tileX, @NonNull TileMode tileY)

비트맵을 사용하여 패턴을 만듭니다. 이미지를 반복하는 패턴으로 스타일을 구성할 수 있습니다. 파라미터로 주어지는 tileXtileY 를 통해 다양한 비트맵 패턴을 구성할 수 있습니다.

ComposeShader

 public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull PorterDuff.Mode mode)

ComposeShader 는 Shader를 결합하여 복잡한 스타일을 만들어내는 Shader 입니다. ComposeShader를 구성하기 위해서는 파라미터로 2가지 Shader를 전달해야합니다. 이때 shaderB가 먼저 적용이 되며, 그 이후 shaderA가 그 위에 적용되는 방식입니다. 마지막 인자로 전달되는 mode는 두 Shader를 어떻게 혼합할지 결정되는 규칙이며, 다양한 혼합방식이 존재합니다.

예를 들어, 아래 예시와 같이 BitmapShader와 SweepGradiant Shader를 결합하여 새로운 Shader를 만들어 낼 수 있습니다.

혼합모드

public void setBlendMode(@Nullable BlendMode blendmode) 

Paint 객체는 혼합모드를 설정할 수 있습니다. 혼합모드란 그래픽상에서 두 레이어간의 색상을 어떻게 혼합할지를 결정하는 방식이며, 혼합모드를 설정한 Paint 객체를 특정 요소에 입힐 경우 이미 그려져있는 다른 Canvas 요소들과 픽셀이 혼합되어서 그려지게됩니다. 혼합모드 설정은 setBlendMode() 메서드를 통해 진행되며, blendMode 파라미터에 원하는 혼합 방식인 BlendMode를 넣어 결정할 수 있습니다.

BlendMode는 일반 이미지 편집 프로그램에서 존재하는 기능들처럼 다양한 유형이 존재하며, 자세한 방식들에 대한 설명은 분량 상 해당 포스트에서 다루지 않기 때문에 [해당 문서]를 통해 어떠한 방식이 있고 어떻게 적용되는지 참고하시길 바랍니다.

추가적으로 앞서 ComposeShader에 인자로 포함된 PorterDuff.Mode 또한 BlendMode와 같은 역할을 하지만, 이는 오래된 혼합방식 API이며, BlendMode 는 이러한 PorterDuff.Mode 에서 제공하는 기능들을 포함하여 설계된 클래스입니다.

텍스트 스타일

이전 포스트에서 Canvas를 바탕으로 텍스트를 그릴 수 있다는 부분을 다루었습니다. Paint 객체에는 이에 따라 텍스트의 스타일을 관리할 수 있는 기능들이 있으며 텍스트와 관련한 다양한 속성 설정을 바탕으로 크기, 정렬방식 등 다양한 요구사항을 구현해 낼 수 있습니다. 추가적으로 텍스트 스타일은 도형 및 경로 스타일과 마찬가지로 Shader 및 Color를 적용할 수 있습니다.

앞서 사용된 ComposeShader를 바탕으로 텍스트 그리기를 시도할 경우 위처럼 텍스트에 Shader효과가 적용됩니다.

아래에서부터는 다양한 Text 스타일에 대해서 알아보겠습니다.

textSize, textAlign, letterSpacing

public void setTextSize(float textSize)
public void setTextAlign(Align align)
public void setLetterSpacing(float letterSpacing)

위 3가지 메서드는 각각 텍스트의 사이즈, 정렬, 자간을 정의하는 속성입니다. setTextSize() 는 글자의 크기를 지정하는 속성으로 기본 단위는 픽셀입니다.

setTextAlign()는 텍스트를 그릴때 텍스트가 어떻게 정렬될지를 결정하는 속성이며 LEFT, CENTER, RIGHT 를 지정할 수 있습니다.

public enum Align {
    LEFT    (0),
    CENTER  (1),
    RIGHT   (2);
    ...
}

마지막으로 setLetterSpacing() 의 경우 자간을 조절하는 메서드로 양수일 경우 간격을 늘리고, 반대로 음수일 경우 간격을 줄입니다. 예를 들어 값을 0.1f로 설정할 경우 자간을 10% 늘리는 것을 의미합니다.

typeface

public Typeface setTypeface(Typeface typeface)

Canvas는 또한 텍스트에 Font, Italic, Bold와 같은 다양한 스타일을 적용할 수 있습니다. 이때 이러한 스타일을 적용하기 위해서는 Typeface 객체를 사용해야 합니다. Typeface는 직접 만들어 구현할 수도 있지만, Android는 Typeface를 간단하게 적용하기 위한 몇 가지의 스타일을 제공하고 있습니다.

...

public static final Typeface DEFAULT = null;

public static final Typeface DEFAULT_BOLD = null;

public static final Typeface SANS_SERIF = null;

public static final Typeface SERIF = null;

public static final Typeface MONOSPACE = null;

...

textSkewX, textScaleX

 public void setTextSkewX(float skewX)
 public void setTextScaleX(float scaleX)

setTextSkewX()는 텍스트를 기울이는 메서드로 기본값은 0 입니다. 양수일 경우 왼쪽으로 기울이고 음수일 경우 오른쪽으로 기울입니다.

setTextScaleX()는 텍스트를 확대 축소하는 메서드로 기본값은 1입니다. 0 아래로는 줄일 수 없으며, 0.5일경우 X축 기준 절반으로 축소되고 2일경우 X축 기준으로 2배 늘어납니다.

fakeBoldText, underlineText, strikeThruText

public void setFakeBoldText(boolean fakeBoldText)
public void setUnderlineText(boolean underlineText)
public void setStrikeThruText(boolean strikeThruText)

기본적으로 UI로 텍스트 스타일을 적용할 때 자주 사용되는 볼드, 밑줄, 취소선을 설정하는 속성이지만, 이 중setFakeBoldText() 이라는 독특한 설정이 존재합니다. 일반적으로 폰트의 볼드 스타일은 FontFamily를 따라가지만, FontFamily자체에 Bold이 없는 케이스가 존재합니다. 이때 fakeBoldText를 설정할 경우 획을 조금 더 두껍게 그려 시각적으로 마치 볼드처럼 보이게 하는 기능입니다. 단 실제 Bold를 적용한 스타일과는 시각적으로 차이가 있을 수 있기 때문에 가급적 Bold 스타일이 있는 폰트를 사용하길 권장합니다.

hinting, antialiasing

public void setHinting(int mode)
public void setAntiAlias(boolean aa)

...

public static final int HINTING_OFF = 0x0;
public static final int HINTING_ON = 0x1;

Paint에서는 텍스트에 대한 가독성을 향상시키기 위한 기능들을 제공하고 있습니다. 힌팅(Hinting)은 낮은 해상도 화면에서 텍스트를 읽기 쉽도록 글자의 형태를 조정하는 기법이며, 안티앨리어싱(Anti-aliasing)의 경우 낮은 해상도에서 발생하는 계단 현상(aliasing이라고 표현합니다.)을 제거하는 기법을 의미합니다. 이때 힌팅 설정의 경우 boolean 플래그가 아닌 별도로 Paint내부에서 제공하는 값을 사용하여 적용해야만합니다.

힌팅(Hinting)은 원본 폰트를 작은 해상도에서 그대로 그릴 경우 획이 너무 얇아지거나 픽셀사이에 간격이 불균형해져 가독성이 떨어지는 문제를 글꼴의 메타데이터를 활용하여 글자 모양을 미세조정하는 기법입니다. 힌팅에 대한 자세한 정보는 [해당 문서]를 통해 확인하실 수 있습니다.

안티앨리어싱(Anti-aliasing)은 계단현상을 제거 하기위해 경계 주변에 중간색상을 채워넣어 부드럽게 하는 기술로 더욱 자세한 정보는 [해당 문서]를 통해 확인하실 수 있습니다. 안티앨리어싱 설정의 경우 텍스트 뿐만 아니라 Canvas에 그려지는 도형에도 적용이 됩니다.


이번 포스트에서는 주로 사용되는 Paint 속성들에 대해 다루어봤으며, 대다수의 프로젝트 요구사항들은 해당 속성들을 활용하여 구현할 수 있습니다. Paint는 이번 포스트에서 다룬 속성 외에도 고급 스타일을 처리하기 위한 Filter 기능등을 제공하고 있으며, 해당 속성에 대한 내용은 이후 포스트에서 다루어보겠습니다.