Pennello: gradienti e ombreggiatori

Un elemento Brush in Scrivi descrive il modo in cui un elemento viene disegnato sullo schermo: determina il colore o i colori disegnati nell'area di disegno (ovvero un cerchio, un quadrato, un percorso). Esistono alcuni pennelli integrati utili per disegnare, ad esempio LinearGradient, RadialGradient o un pennello semplice SolidColor.

I pennelli possono essere utilizzati con chiamate di disegno Modifier.background(), TextStyle o DrawScope per applicare lo stile di disegno ai contenuti disegnati.

Ad esempio, puoi applicare un pennello gradiente orizzontale al disegno di un cerchio in DrawScope:

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

Cerchio disegnato con gradiente orizzontale
Figura 1: cerchio disegnato con gradiente orizzontale

Pennelli sfumati

Esistono molti pennelli sfumatura integrati che possono essere utilizzati per ottenere effetti di gradiente diversi. Questi pennelli consentono di specificare l'elenco di colori da cui creare una sfumatura.

Un elenco di pennelli sfumatura disponibili e l'output corrispondente:

Tipo di pennello sfumato Uscita
Brush.horizontalGradient(colorList) Gradiente orizzontale
Brush.linearGradient(colorList) Gradiente lineare
Brush.verticalGradient(colorList) Gradiente verticale
Brush.sweepGradient(colorList)
Nota: per ottenere una transizione uniforme tra i colori, imposta l'ultimo colore sul colore iniziale.
Sposta gradiente
Brush.radialGradient(colorList) Gradiente radiale

Modifica la distribuzione dei colori con colorStops

Per personalizzare l'aspetto dei colori nel gradiente, puoi modificare il valore colorStops per ciascuno. colorStops deve essere specificato come frazione, compreso tra 0 e 1. Valori superiori a 1 impediscono la visualizzazione dei colori all'interno del gradiente.

Puoi configurare le interruzioni di colore in modo che abbiano quantità diverse, ad esempio più o meno un colore:

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

I colori sono disseminati con l'offset fornito come definito nella coppia colorStop, meno giallo che rosso e blu.

Pennello configurato con interruzioni di colore diverse
Figura 2: pennello configurato con interruzioni di colore diverse

Ripeti un pattern con TileMode

Per ogni pennello sfumatura è possibile impostare un elemento TileMode. Potresti non notare la TileMode se non hai impostato un inizio e una fine per il gradiente, perché riempirà per impostazione predefinita l'intera area. Un TileMode affianca al gradiente solo se le dimensioni dell'area sono maggiori di quelle del pennello.

Il seguente codice ripeterà il pattern del gradiente per quattro volte, poiché endX è impostato su 50.dp e le dimensioni sono impostate su 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
            )
        )
)

Ecco una tabella che descrive in dettaglio cosa fanno le diverse modalità riquadro per l'esempio HorizontalGradient sopra riportato:

Modalità riquadro Uscita
TileMode.Repeated: il bordo viene ripetuto dall'ultimo colore al primo. TileMode ripetuto
TileMode.Mirror: viene eseguito il mirroring del bordo dall'ultimo colore al primo. Specchio TileMode
TileMode.Clamp: il bordo è fissato al colore finale. Verrà applicato il colore più simile per il resto dell'area. Morsetto modalità riquadro
TileMode.Decal: esegui il rendering solo fino alla dimensione dei limiti. TileMode.Decal utilizza il nero trasparente per campionare i contenuti al di fuori dei limiti originali, mentre TileMode.Clamp campiona il colore del bordo. Decalcomania modalità riquadro

TileMode funziona in modo simile per gli altri gradienti direzionali, poiché la differenza è la direzione in cui si verifica la ripetizione.

Cambia dimensioni del pennello

Se conosci le dimensioni dell'area in cui verrà disegnato il tuo pennello, puoi impostare il riquadro endX come abbiamo visto nella sezione TileMode sopra. Se ti trovi in una DrawScope, puoi utilizzare la relativa proprietà size per ottenere le dimensioni dell'area.

Se non conosci le dimensioni dell'area di disegno (ad esempio se l'elemento Brush è assegnato a Testo), puoi estendere Shader e utilizzare le dimensioni dell'area di disegno nella funzione createShader.

In questo esempio, dividi la taglia per 4 e ripeti il motivo 4 volte:

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

Dimensione Shader divisa per 4
Figura 3: dimensione Shader divisa per 4

Puoi anche modificare le dimensioni del pennello di qualsiasi altro gradiente, ad esempio i gradienti radiali. Se non specifichi una dimensione e il centro, il gradiente occuperà tutti i limiti di DrawScope, mentre il centro del gradiente radiale verrà impostato al centro dei limiti DrawScope. In questo modo, il centro del gradiente radiale viene visualizzato come centro della dimensione più piccola (larghezza o altezza):

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

Gradiente radiale impostato senza modifiche alle dimensioni
Figura 4: insieme di gradiente radiale senza modifiche alle dimensioni

Se modifichi il gradiente radiale per impostare la dimensione del raggio sulla dimensione massima, puoi vedere che l'effetto gradiente radiale migliora.

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

Raggio più grande sul gradiente radiale, basato sulle dimensioni dell'area
Figura 5: raggio più grande su gradiente radiale, basato sulle dimensioni dell'area

Vale la pena notare che le dimensioni effettive passate alla creazione dello shader dipendono da dove viene richiamato. Per impostazione predefinita, Brush rialloca il valore Shader internamente se le dimensioni sono diverse dall'ultima creazione dell'elemento Brush o se un oggetto di stato utilizzato nella creazione dello strumento è cambiato.

Il seguente codice crea loshar tre volte diverse con dimensioni diverse, quando le dimensioni dell'area di disegno cambiano:

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

Utilizzare un'immagine come pennello

Per utilizzare un elemento ImageBitmap come Brush, carica l'immagine come ImageBitmap e crea un pennello 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))

Il Pennello viene applicato ad alcuni tipi diversi di disegni: uno sfondo, il Testo e Canvas. L'output restituisce quanto segue:

Pennello ImageShader utilizzato in modi diversi
Figura 6: utilizzo del pennello ImageShader per disegnare uno sfondo, disegnare testo e disegnare un cerchio

Nota che il testo ora viene visualizzato anche usando ImageBitmap per colorare i pixel del testo.

Esempio avanzato: pennello personalizzato

Pennello RuntimeShader AGSL

AGSL offre un sottoinsieme di funzionalità di Shader GLSL. Gli Shader possono essere scritti in AGSL e utilizzati con un pennello in Compose.

Per creare un pennello Shader, definisci prima lo Shader come stringa Shader 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()

Lo Shader in alto utilizza due colori di input, calcola la distanza dal bordo inferiore sinistra (vec2(0, 1)) dell'area di disegno e calcola un mix tra i due colori in base alla distanza. Questo produce un effetto sfumatura.

A questo punto, crea il Pennello Shader e imposta le uniformi per resolution, ovvero le dimensioni dell'area di disegno, e i color e color2 che vuoi utilizzare come input per il gradiente personalizzato:

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

Eseguendo questa operazione, vedrai quanto segue visualizzato sullo schermo:

Shader AGSL personalizzato in esecuzione in Compose
Figura 7: Shader AGSL personalizzato in esecuzione in Compose

Vale la pena notare che con gli Shader si possono fare molto di più che con i gradienti, poiché sono tutti calcoli matematici. Per ulteriori informazioni, consulta la documentazione relativa a AGSL.

Risorse aggiuntive

Per altri esempi sull'utilizzo del Pennello in Compose, consulta le seguenti risorse: