ブラシ: グラデーションとシェーダー

Compose の Brush では、画面上で何かを描画する際の手法を記述します。これは、描画領域(円、四角、経路など)に適用される色を定義するものです。LinearGradient ブラシ、RadialGradient ブラシ、単色の SolidColor ブラシなど、描画で役に立つ組み込みのブラシがいくつか用意されています。

ブラシは、Modifier.background()TextStyleDrawScope の描画呼び出しで使用可能で、描画するコンテンツにペイント スタイルを適用できます。

たとえば、水平グラデーション ブラシを適用して、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 は、領域のサイズがブラシのサイズより大きい場合にのみ、グラデーションをタイル化します。

次のコードの場合、endX50.dp に設定され、サイズが 200.dp に設定されているため、グラデーション パターンが 4 回繰り返されます。

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
            )
        )
)

以下の表は、異なる TileMode が上記の HorizontalGradient の例の場合でどのように機能するかを説明しています。

TileMode 出力
TileMode.Repeated: 最後の色から最初の色までエッジが繰り返されます。 TileMode Repeated
TileMode.Mirror: エッジで反転して、最後の色から最初の色の順で表示されます。 TileMode Mirror
TileMode.Clamp: エッジで最後の色に固定されます。それ以降の領域は、その色に最も近い色で塗りつぶされます。 TileMode Clamp
TileMode.Decal: 境界のサイズまでのみレンダリングされます。TileMode.Decal では、透明な黒を利用して元の境界の外側のコンテンツがサンプリングされますが、TileMode.Clamp ではエッジの色がサンプリングされます。 TileMode Decal

TileMode は、他の方向性のあるグラデーションでも同様に機能します。違いは反復が発生する方向です。

ブラシのサイズを変更

ブラシを適用する領域のサイズがわかっている場合は、上記の TileMode セクションで説明したように、タイル endX を設定できます。DrawScope を開いている場合は、size プロパティを使用して領域のサイズを取得できます。

描画する領域のサイズが不明な場合(たとえば、Brush がテキストに適用される場合)、Shader を拡張して、createShader 関数で描画領域のサイズを利用できます。

この例では、サイズを 4 分の 1 にして、パターンを 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 分の 1 にしたシェーダー サイズ
図 3: 4 分の 1 にしたシェーダー サイズ

円形グラデーションなど、他のグラデーション ブラシのサイズを変更することもできます。サイズと中心を指定しない場合、グラデーションは 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 を内部で再割り当てします。

次のコードは、描画領域のサイズの変化に応じて、シェーダーを異なるサイズで 3 回作成します。

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 ブラシ

AGSL では、GLSL Shader 機能が一部提供されています。Shader を AGSL で記述して、Compose のブラシで使用できます。

Shader ブラシを作成するには、まず AGSL シェーダー文字列として Shader を定義します。

@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()

上のシェーダーは 2 つの入力色を受け取り、描画領域の左下(vec2(0, 1))からの距離を計算して、距離に基づいて 2 つの色間で mix を実行します。これにより、グラデーション効果が得られます。

次に、Shader ブラシを作成し、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 でブラシを使用するその他の例については、以下のリソースをご確認ください。