Pennello: gradienti e ombreggiatori

Un Brush in Compose descrive come viene disegnato qualcosa sullo schermo: determina i colori che vengono disegnati nell'area di disegno (ad es. un cerchio, un quadrato, un percorso). Esistono alcuni pennelli integrati utili per il disegno, come LinearGradient, RadialGradient o un semplice pennello SolidColor.

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

Ad esempio, un pennello con gradiente orizzontale può essere applicato per disegnare 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 sfumatura orizzontale

Pennelli sfumati

Esistono molti pennelli gradienti integrati che possono essere utilizzati per ottenere sfumatura. Questi pennelli ti consentono di specificare l'elenco di colori da cui vuoi creare un gradiente.

Un elenco dei pennelli gradiente disponibili e il relativo output:

Tipo di pennello sfumato Output
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.
Gradiente dinamico
Brush.radialGradient(colorList) Sfumatura radiale

Cambia la distribuzione dei colori con colorStops

Per personalizzare la visualizzazione dei colori nel gradiente, puoi modificare il valore colorStops per ciascuno. colorStops deve essere specificato come frazione, tra 0 e 1. I valori superiori a 1 fanno sì che i colori non vengano visualizzati come parte del gradiente.

Puoi configurare le interruzioni di colore in modo che abbiano quantità diverse, ad esempio minori o più di 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 sparsi nell'offset fornito, come definito in colorStop meno gialla che rossa e blu.

Pennello configurato con diversi valori di colore
Figura 2: pennello configurato con diversi valori di colore

Ripeti una sequenza con TileMode

Per ogni pennello gradiente è possibile impostare un TileMode. Non puoi nota l'TileMode se non hai impostato un inizio e una fine per la sfumatura, riempirà per impostazione predefinita l'intera area. Un TileMode suddividerà in riquadri solo il gradiente se la dimensione dell'area è maggiore di quella 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
            )
        )
)

Di seguito è riportata una tabella che descrive in dettaglio le funzioni delle diverse modalità dei riquadri HorizontalGradient esempio sopra:

TileMode Output
TileMode.Repeated: il bordo viene ripetuto dall'ultimo colore al primo. TileMode ripetuta
TileMode.Mirror: il bordo viene specchiato dall'ultimo colore al primo. Specchio TileMode
TileMode.Clamp: il bordo è bloccato sul colore finale. Successivamente, dipinge il colore più vicino al resto dell'area. Morsetto per la modalità Riquadri
TileMode.Decal: visualizza 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. Adesivo per la modalità Riquadro

TileMode funziona in modo simile per gli altri gradienti direzionali, differenza, ossia la direzione in cui avviene la ripetizione.

Modifica dimensione pennello

Se conosci le dimensioni dell'area in cui verrà disegnato il pennello, puoi impostare il riquadro endX come abbiamo visto sopra nella sezione TileMode. Se ti trovi in un 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 Brush è assegnato a Testo), puoi estendere Shader e utilizzare le dimensioni dell'area di disegno nella funzione createShader.

In questo esempio, dividi le dimensioni per 4 per ripetere il pattern 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)
)

Dimensioni shader divise per 4
Figura 3: dimensioni degli shader divise per 4

Puoi anche modificare la dimensione del pennello di qualsiasi altro gradiente, ad esempio quelli radiali. Se non specifichi una dimensione e un centro, il gradiente occuperà il valore limiti completi di DrawScope e il centro del gradiente radiale predefinito al centro dei limiti di DrawScope. Questo determina il gradiente radiale 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: sfumatura radiale impostata senza modifiche delle dimensioni

Quando la sfumatura radiale viene modificata per impostare la dimensione del raggio sulla dimensione massima, produce un migliore effetto gradiente radiale:

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 maggiore per la sfumatura radiale, in base alle dimensioni dell'area
Figura 5: raggio maggiore per la sfumatura radiale, in base alle dimensioni dell'area

Vale la pena notare che le dimensioni effettive passate alla creazione dell'elemento viene determinato da dove viene richiamato. Per impostazione predefinita, Brush rialloca il suo Shader internamente se la dimensione è diversa dall'ultima la creazione dell'elemento Brush oppure se un oggetto di stato utilizzato nella creazione dello streamr ha è cambiato.

Il seguente codice crea lo streamr tre volte diverse con valori dimensioni, man mano che cambiano le dimensioni dell'area di disegno:

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 usare un'immagine 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 a diversi tipi di disegno: uno sfondo, il testo e la tela. L'output è il seguente:

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

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

Esempio avanzato: pennello personalizzato

Spazzola RuntimeShader AGSL

AGSL offre un sottoinsieme di funzionalità di Shader GLSL. Gli Shader possono essere scritte in AGSL e utilizzate con un Pennello in Compose.

Per creare un pennello shader, definisci innanzitutto 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 shaker riportato sopra accetta due colori di input e calcola la distanza dal basso a sinistra (vec2(0, 1)) dell'area di disegno e inserisce un mix tra i due colori in base alla distanza. Questo produce un effetto gradiente.

Quindi, crea il Pennello Shader e imposta le uniformi per resolution, la dimensione dell'area di disegno e i color e i color2 che vuoi utilizzare come input 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 questo comando, sullo schermo viene visualizzato quanto segue:

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

Vale la pena notare che con gli shader puoi fare molto di più che creare gradienti, poiché si tratta di calcoli matematici. Per ulteriori informazioni su AGSL, consulta la documentazione AGSL.

Risorse aggiuntive

Per altri esempi di utilizzo di Pennello in Compose, consulta le seguenti risorse: