브러시: 그라데이션 및 셰이더

Compose의 Brush는 화면에 무언가가 그려지는 방식을 설명합니다. 그리기 영역(원, 정사각형, 경로)에 그려지는 색상을 결정합니다. 그리기에 유용한 몇 가지 기본 제공 브러시가 있습니다. LinearGradient, RadialGradient 또는 일반 SolidColor 브러시를 예로 들 수 있습니다.

브러시는 Modifier.background(), TextStyle 또는 DrawScope 그리기 호출과 함께 사용하여 그리고 있는 콘텐츠에 페인팅 스타일을 적용할 수 있습니다.

예를 들어 가로 그라데이션 브러시는 DrawScope에서 원을 그리는 데 적용할 수 있습니다.

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)

가로 그라데이션으로 그린 원
그림 1: 가로 그라데이션으로 그린 원

그라데이션 브러시

다양한 그라데이션 효과를 내는 데 사용할 수 있는 기본 제공 그라데이션 브러시가 여러 개 있습니다. 이러한 브러시를 사용하면 그라데이션을 만들려는 색상 목록을 지정할 수 있습니다.

제공되는 그라데이션 브러시 및 상응하는 출력 목록은 다음과 같습니다.

그라데이션 브러시 유형 출력
Brush.horizontalGradient(colorList) 가로 그라데이션
Brush.linearGradient(colorList) 선형 그라데이션
Brush.verticalGradient(colorList) 세로 그라데이션
Brush.sweepGradient(colorList)
참고: 색상 간에 부드럽게 전환하려면 마지막 색상을 시작 색상으로 설정하세요.
스윕 그라데이션
Brush.radialGradient(colorList) 방사형 그라데이션

colorStops를 사용하여 색상 분포 변경

색상이 그라데이션으로 표시되는 방식을 맞춤설정하려면 각 색상의 colorStops 값을 조정합니다. colorStops는 0과 1 사이의 분수로 지정해야 합니다. 1보다 큰 값을 선택하면 이러한 색상이 그라데이션의 일부로 렌더링되지 않게 됩니다.

한 가지 색상을 더 적게 또는 더 많이 쓰는 것과 같이 다양한 양이 포함되도록 색상 중지점을 구성할 수 있습니다.

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(Brush.horizontalGradient(colorStops = colorStops))
)

색상은 colorStop 쌍에 정의된 대로 제공된 오프셋에 분산되어 노란색이 빨간색이나 파란색보다 적습니다.

여러 색상 중지점으로 구성된 브러시
그림 2: 여러 색상 중지점으로 구성된 브러시

TileMode로 패턴 반복

각 그라데이션 브러시에는 TileMode를 설정하는 옵션이 있습니다. 그라데이션의 시작점과 끝점을 설정하지 않은 경우 TileMode를 발견하지 못할 수 있습니다. 전체 영역을 채우도록 기본으로 설정되어 있기 때문입니다. TileMode는 영역의 크기가 브러시 크기보다 큰 경우에만 그라데이션을 타일로 처리합니다.

다음 코드는 그라데이션 패턴을 4번 반복합니다. endX50.dp로 설정되어 있고 크기가 200.dp로 설정되어 있기 때문입니다.

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val tileSize = with(LocalDensity.current) {
    50.dp.toPx()
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(
            Brush.horizontalGradient(
                listColors,
                endX = tileSize,
                tileMode = TileMode.Repeated
            )
        )
)

다음 표에서는 위의 HorizontalGradient 예에서 다양한 타일 모드가 하는 작업을 설명합니다.

TileMode 출력
TileMode.Repeated: 가장자리가 마지막 색상에서 첫 번째 색상까지 반복됩니다. TileMode 반복
TileMode.Mirror: 가장자리가 마지막 색상에서 첫 번째 색상으로 미러링됩니다. TileMode 미러링
TileMode.Clamp: 가장자리가 최종 색상으로 고정됩니다. 그런 다음 나머지 영역에 가장 가까운 색상을 색칠합니다. 타일 모드 고정
TileMode.Decal: 경계 크기까지만 렌더링합니다. TileMode.Decal은 투명한 검은색을 활용하여 원래 경계 외부의 콘텐츠를 샘플링하지만 TileMode.Clamp는 가장자리 색상을 샘플링합니다. 타일 모드 데칼

TileMode는 다른 방향의 그라데이션에서도 비슷한 방식으로 작동하며 반복이 발생하는 방향만 다릅니다.

브러시 크기 변경

브러시를 그릴 영역의 크기를 알고 있다면 위 TileMode 섹션에서 본 것처럼 타일 endX를 설정할 수 있습니다. DrawScope에 있다면 size 속성을 사용하여 영역의 크기를 가져올 수 있습니다.

그리기 영역의 크기를 모르는 경우(예: Brush가 텍스트에 할당된 경우) Shader를 확장하고 createShader 함수에서 그리기 영역의 크기를 활용하면 됩니다.

이 예에서는 크기를 4로 나누어 패턴을 4번 반복합니다.

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val customBrush = remember {
    object : ShaderBrush() {
        override fun createShader(size: Size): Shader {
            return LinearGradientShader(
                colors = listColors,
                from = Offset.Zero,
                to = Offset(size.width / 4f, 0f),
                tileMode = TileMode.Mirror
            )
        }
    }
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(customBrush)
)

4로 나눈 셰이더 크기
그림 3: 4로 나눈 셰이더 크기

방사형 그라데이션와 같은 다른 그라데이션의 브러시 크기도 변경할 수 있습니다. 크기와 중심을 지정하지 않으면 그라데이션이 DrawScope의 전체 경계를 차지하며 방사형 그라데이션의 중심이 기본적으로 DrawScope 경계의 중심이 됩니다. 이렇게 하면 방사형 그라데이션의 중심이 더 작은 크기의 중심(너비 또는 높이)으로 표시됩니다.

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(
            Brush.radialGradient(
                listOf(Color(0xFF2be4dc), Color(0xFF243484))
            )
        )
)

크기 변경 없이 설정된 방사형 그라데이션
그림 4: 크기 변경 없이 설정된 방사형 그라데이션

방사형 그라데이션을 변경하여 반경 크기를 최대 크기로 설정하면 더 나은 방사형 그라데이션 효과를 얻을 수 있습니다.

val largeRadialGradient = object : ShaderBrush() {
    override fun createShader(size: Size): Shader {
        val biggerDimension = maxOf(size.height, size.width)
        return RadialGradientShader(
            colors = listOf(Color(0xFF2be4dc), Color(0xFF243484)),
            center = size.center,
            radius = biggerDimension / 2f,
            colorStops = listOf(0f, 0.95f)
        )
    }
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(largeRadialGradient)
)

영역 크기에 따른 방사형 그라데이션의 더 큰 반경
그림 5: 영역 크기에 따른 방사형 그라데이션의 더 큰 반경

셰이더 생성에 전달되는 실제 크기는 호출되는 위치에서 결정된다는 점에 유의해야 합니다. 기본적으로 Brush는 크기가 마지막 Brush 생성과 다르거나 셰이더 생성에 사용된 상태 객체가 변경된 경우 내부적으로 Shader를 재할당합니다.

다음 코드는 그리기 영역의 크기가 변경될 때 크기가 다른 셰이더를 세 번 만듭니다.

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
val brush = Brush.horizontalGradient(colorStops = colorStops)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .drawBehind {
            drawRect(brush = brush) // will allocate a shader to occupy the 200 x 200 dp drawing area
            inset(10f) {
      /* Will allocate a shader to occupy the 180 x 180 dp drawing area as the
       inset scope reduces the drawing  area by 10 pixels on the left, top, right,
      bottom sides */
                drawRect(brush = brush)
                inset(5f) {
        /* will allocate a shader to occupy the 170 x 170 dp drawing area as the
         inset scope reduces the  drawing area by 5 pixels on the left, top,
         right, bottom sides */
                    drawRect(brush = brush)
                }
            }
        }
)

이미지를 브러시로 사용

ImageBitmapBrush로 사용하려면 이미지를 ImageBitmap으로 로드하고 ImageShader 브러시를 만듭니다.

val imageBrush =
    ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.dog)))

// Use ImageShader Brush with background
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(imageBrush)
)

// Use ImageShader Brush with TextStyle
Text(
    text = "Hello Android!",
    style = TextStyle(
        brush = imageBrush,
        fontWeight = FontWeight.ExtraBold,
        fontSize = 36.sp
    )
)

// Use ImageShader Brush with DrawScope#drawCircle()
Canvas(onDraw = {
    drawCircle(imageBrush)
}, modifier = Modifier.size(200.dp))

브러시는 배경, 텍스트, 캔버스 등 몇 가지 유형의 그리기에 적용됩니다. 그러면 다음과 같이 출력됩니다.

다양한 방식으로 사용된 ImageShader 브러시
그림 6: ImageShader 브러시를 사용하여 배경 그리기, 텍스트 그리기, 원 그리기

텍스트도 이제 텍스트의 픽셀을 칠하기 위해 ImageBitmap을 사용하여 렌더링됩니다.

고급 예: 맞춤 브러시

AGSL RuntimeShader 브러시

AGSLGLSL 셰이더 기능의 하위 집합을 제공합니다. 셰이더는 AGSL로 작성할 수 있으며 Compose에서 브러시와 함께 사용할 수 있습니다.

셰이더 브러시를 만들려면 먼저 셰이더를 AGSL 셰이더 문자열로 정의합니다.

@Language("AGSL")
val CUSTOM_SHADER = """
    uniform float2 resolution;
    layout(color) uniform half4 color;
    layout(color) uniform half4 color2;

    half4 main(in float2 fragCoord) {
        float2 uv = fragCoord/resolution.xy;

        float mixValue = distance(uv, vec2(0, 1));
        return mix(color, color2, mixValue);
    }
""".trimIndent()

위의 셰이더는 두 가지 입력 색상을 사용하고 그리기 영역의 왼쪽 하단(vec2(0, 1))으로부터의 거리를 계산하며 거리를 기반으로 두 색상 간에 mix를 실행합니다. 이렇게 하면 그라데이션 효과가 나타납니다.

그런 다음 셰이더 브러시를 만들고 resolution의 유니폼(그리기 영역의 크기, 맞춤 그라데이션에 입력으로 사용하려는 colorcolor2)을 설정합니다.

val Coral = Color(0xFFF3A397)
val LightYellow = Color(0xFFF8EE94)

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
@Preview
fun ShaderBrushExample() {
    Box(
        modifier = Modifier
            .drawWithCache {
                val shader = RuntimeShader(CUSTOM_SHADER)
                val shaderBrush = ShaderBrush(shader)
                shader.setFloatUniform("resolution", size.width, size.height)
                onDrawBehind {
                    shader.setColorUniform(
                        "color",
                        android.graphics.Color.valueOf(
                            LightYellow.red, LightYellow.green,
                            LightYellow
                                .blue,
                            LightYellow.alpha
                        )
                    )
                    shader.setColorUniform(
                        "color2",
                        android.graphics.Color.valueOf(
                            Coral.red,
                            Coral.green,
                            Coral.blue,
                            Coral.alpha
                        )
                    )
                    drawRect(shaderBrush)
                }
            }
            .fillMaxWidth()
            .height(200.dp)
    )
}

이를 실행하면 다음과 같이 렌더링된 화면을 볼 수 있습니다.

Compose에서 실행되는 맞춤 AGSL 셰이더
그림 7: Compose에서 실행되는 맞춤 AGSL 셰이더

모두 수학을 기반으로 하는 계산이므로 그라데이션 외에도 셰이더로 훨씬 더 많은 작업을 할 수 있습니다. AGSL에 관한 자세한 내용은 AGSL 문서를 참고하세요.

추가 리소스

Compose에서 브러시를 사용하는 자세한 예는 다음 리소스를 확인하세요.