Nozioni di base sulle app

Migliora le tue app con le ultime funzionalità grafiche di Jetpack Compose e Android

Altoparlanti

Informazioni

Informazioni sulla presentazione

Vuoi trasformare la UI di Android da "funziona" a "è un'opera d'arte"?

Unisciti a noi per un tour delle ultime funzionalità grafiche di Jetpack Compose. Ti mostreremo come creare nuovi effetti visivi, dai gradienti mesh ai blur progressivi, utilizzando le nuove potenti funzionalità grafiche di Android. Ti mostreremo anche come migliorare le prestazioni delle funzionalità grafiche, senza sacrificare la durata della batteria o la frequenza fotogrammi.

Riepilogo

  • Compose 1.12 introduce i gradienti mesh per sfumature multicolore ricche su una griglia 2D di vertici. Su Android 14 e versioni successive, puoi utilizzare l'API drawMesh nativa per creare effetti visivi personalizzati basati sulla geometria.
  • Compose supporta ampie gamme di colori per fornire una fusione delle sfumature più fluida sull'hardware supportato. Attiva la modalità con ampia gamma di colori nell'attività o nella finestra solo quando mostri contenuti multimediali avanzati.
  • Compose 1.13 introduce le API native PolygonShape e MorphPolygonShape per le geometrie personalizzate. MorphPolygonShape legge l'avanzamento durante la fase di risoluzione del contorno, aggiornando le forme frame per frame senza attivare la ricomposizione.
  • Compose 1.13 aggiunge la sfocatura progressiva, mentre il livello API 37.2 introduce RenderNode.setBackdropRenderEffect per il campionamento dei contenuti di sfondo. Gli sviluppatori possono concatenare gli effetti di sfondo per creare effetti di vetro smerigliato o di ingrandimento.
  • Ottimizza le prestazioni grafiche evitando operazioni non necessarie tra i frame. Decodifica solo i pixel immagine necessari fuori dal thread principale e inserisci gli effetti di animazione in un graphicsLayer.

Risorse

Gemini Notebook

Argomenti

  • jetpack-compose
  • android-graphics
  • mesh-gradients
  • shader
  • sfocatura
  • color-gamut
  • prestazioni

Trascrizione

Scarica la trascrizione

Introduzione alle funzionalità grafiche in Jetpack Compose

  • Ciao a tutti e benvenuti alla nostra sessione, Migliora le tue app con le ultime funzionalità grafiche di Compose e Android.

  • Mi chiamo Rebecca Franks e oggi sul palco con me c'è…

  • Sono Aayush Chaudhary.

  • Ciao, sono Rahul Ray.

  • Stiamo assistendo a una tendenza emergente nel modo in cui tutti voi state ampliando i confini di ciò che è possibile fare su Android e Compose, utilizzando l'AI per dare vita alle vostre incredibili idee.

  • Siamo felici di vedere app che offrono un'esperienza premium, nativa e piacevole.

  • Sappiamo che le app che piacciono e ricevono costantemente valutazioni eccellenti dagli utenti sono quelle che generalmente sfruttano le ultime modifiche alle API a livello di sistema, hanno continuità tra le piattaforme con elementi condivisi e sfocatura, si adattano perfettamente ai fattori di forma, sono efficienti, non lasciano gli utenti nel dubbio e spingono i limiti di ciò che è possibile fare con il dispositivo.

  • In questo intervento, vi mostreremo alcune delle funzionalità più recenti che vi aiutano a sfruttare Android, dalla soddisfazione degli utenti con i gradienti mesh, all'abilitazione del supporto di un'ampia gamma di colori, alle transizioni piacevoli con forme poligonali, alle novità della sfocatura e, infine, vedremo come potete pensare alle prestazioni quando aggiungete nuove funzionalità grafiche alle vostre app.

  • Tuttavia, nel corso della presentazione di oggi, parleremo di alcune delle nuove API a diversi livelli dello stack.

  • Sebbene il toolkit UI delle visualizzazioni legacy sia ora in modalità di manutenzione, continuiamo a investire nei motori di rendering hardware della grafica Android sottostanti che supportano Jetpack Compose.

  • Puoi pensare alla grafica in Android in tre diversi livelli: le API Compose al primo livello, ed è qui che si trovano tutte le nuove API grafiche dell'interfaccia utente di alto livello, come i painter di gradienti mesh, le forme poligonali e le API di sfocatura progressiva.

  • Poi abbiamo il livello intermedio, in cui DrawScope di Compose, i nodi dei modificatori di disegno e AGSL ti consentono di creare effetti personalizzati.

  • Infine, abbiamo la piattaforma Android e Compose può sempre accedere a un Canvas nativo e utilizzare primitive di rendering del sistema operativo completamente nuove direttamente nei modificatori di Compose.

  • Oggi vi mostreremo alcuni esempi di come farlo.

  • Iniziamo.

Sfumature mesh e rendering mesh personalizzato

  • Per prima cosa, esaminiamo i gradienti mesh, una nuova API introdotta in Compose 1.12.

  • In precedenza, avevamo sfumature lineari, coniche e radiali, ottime per transizioni semplici, ma se vuoi miscele multicolore ricche come sfondi ambientali o effetti di illuminazione, hai bisogno di una griglia 2D di un sistema in cui posizioni e colori possono fondersi nello spazio.

  • Per creare un gradiente mesh come questo, innanzitutto specifica il numero di righe e colonne di patch che vuoi.

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
    }
    }
  • Poi, all'interno della funzione lambda finale, chiama setVertex e assegna un offset normalizzato compreso tra 0F e 1F, insieme a un colore per ogni intersezione della griglia.

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
       setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
    
    }
    }
  • Per animare questi vertici, aggiungi offset animati per otto vertici.

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
       setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
         // ....
       setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
       // ....
       setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
       // ....
    }
    }
    
    Box(
       modifier = modifier.padding(32.dp)
           .safeContentPadding()
           .aspectRatio(16 / 9f)
           .fillMaxWidth()
           .paint(gradientPainter)
    )
  • Una volta definito il painter, chiamalo semplicemente con qualsiasi composable tramite un modificatore di disegno.

  • Ora, nell'app JetChat, abbiamo aggiunto una funzionalità per mettere Mi piace a un messaggio quando lo tocchi due volte.

  • Per creare questo effetto, impostiamo una mesh 2x3 e posizioniamo i punti di colore nel componente.

    MeshGradientPainter(
       rows = 2,
       columns = 3,
       hasBicubicColor = true,
    ) {
       // Row 0 (top edge, y = 0.0f)
       setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
       setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
       setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
       setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))
    
       // ... remaining vertices for Rows 0, 1, and 2
    }
  • Nota che abbiamo impostato hasBicubicColor su true qui.

  • Per impostazione predefinita, i colori vengono interpolati in modo lineare tra i triangoli, il che a volte può lasciare macchie visibili.

  • Se attivi questa opzione, lo shader passa all'interpolazione bicubica uniforme, in modo che i colori possano fondersi perfettamente l'uno con l'altro.

  • Ora, cosa succede se vuoi creare un bagliore ambientale animato come questo dietro la casella di immissione?

  • Per ottenere un effetto simile con la sfumatura mesh, hai bisogno di due cose.

  • Innanzitutto, un movimento organico fluido per i vertici interni e un modo per ammorbidire i bordi esterni in modo da non ottenere un rettangolo rigido attorno alla scheda.

  • Iniziamo configurando un rememberInfiniteTransition con due angoli di fase che vengono eseguiti con durate leggermente diverse, in modo che il movimento sembri naturale anziché meccanico.

    val transition = rememberInfiniteTransition(label = "userInputGlowMesh")
    
    val phase1 by transition.animateFloat(
       initialValue = 0f,
       targetValue = (2 * PI).toFloat(),
       animationSpec = infiniteRepeatable(
           animation = tween(durationMillis = 2800, easing = LinearEasing),
           repeatMode = RepeatMode.Restart,
       ),
       label = "glowPhase1",
    )
    
    val phase2 by transition.animateFloat(
       initialValue = 0f,
       targetValue = (2 * PI).toFloat(),
       animationSpec = infiniteRepeatable(
           animation = tween(durationMillis = 4200, easing = LinearEasing),
           repeatMode = RepeatMode.Restart,
       ),
       label = "glowPhase2",
    )
  • In questo caso, animiamo i vertici interni e blocchiamo gli anelli esterni, l'anello esterno dei vertici con bordi con alfa 0.

  • Poiché il bordo esterno è fisso e trasparente, il bagliore si sfuma naturalmente.

  • Infine, inseriamo questi offset animati nel nostro MeshGradientPainter.

    MeshGradientPainter(
       rows = 6,
       columns = 5,
       hasBicubicColor = true,
    ) {
       for (r in 0..6) {
           for (c in 0..5) {
                   val offset = if (r in 1..5 && c in 1..4) {
                   val ampX = when (r) {
                       1, 2 -> 0.048f
                       3 -> 0.042f
                       4 -> 0.028f
                       else -> 0.036f
                   }
                   val ampY = when (r) {
                       1, 2 -> 0.028f
                       3 -> 0.020f
                       4 -> 0.022f
                       else -> 0.016f
                   }
                   val d = orbit(c, r, ampX, ampY)
                   Offset(
                       x = (baseU + d.x).coerceIn(0.04f, 0.96f),
                       y = (baseV + d.y).coerceIn(0.05f, 0.95f),
                   )
               } else {
                   Offset(baseU, baseV)
               }
               setVertex(r, c, offset, gridColors[r][c])
           }
       }
    }
  • Il gradiente mesh che hai appena visto viene disegnato utilizzando l'API drawMesh su Android 14 e versioni successive.

  • Diamo quindi un'occhiata più da vicino a questa API creando un effetto da zero.

  • Per la schermata di benvenuto di JetChat, vogliamo disegnare un percorso che assomigli a un nastro.

  • Nota due cose: la larghezza del tratto non è costante e il colore segue ogni tratto del tracciato.

  • Analizziamo la situazione e cerchiamo di capire di cosa abbiamo effettivamente bisogno per creare questo effetto.

  • Per ogni pixel, dobbiamo decidere se disegnarlo o meno e quale colore utilizzare.

  • Per scoprirlo, abbiamo bisogno di due distanze.

  • Il primo valore indica la distanza di un pixel dal centro del tratto.

  • La seconda distanza è la distanza di un pixel lungo il percorso, dall'inizio alla fine.

  • In questo modo, sappiamo quale colore scegliere dal gradiente e, mescolando un po' del primo valore, la curva del colore si estende per tutta la larghezza.

  • Quindi, possiamo disegnare un gradiente lungo un percorso utilizzando solo Path e Paint?

  • Quindi sì, possiamo farlo.

  • Ciò richiede l'appiattimento di un percorso in un elenco di brevi linee rette, che fornisce anche la distanza lungo il percorso.

    val lines = remember {
            path.asAndroidPath().flatten(0.5f)
        }
    //... inside draw scope
    lines.forEach { line ->
    //...
        drawLine(
            brush = Brush.linearGradient(...),
            start = Offset(line.start.x,
    line.start.y),
            end = Offset(line.end.x, line.end.y),
            strokeWidth = 30f,
        )
    //...
    }
  • Poi disegniamo ogni linea con il proprio gradiente.

  • Funziona, ma ogni linea richiede una chiamata di disegno e un oggetto Paint separati e l'intero percorso ha comunque una sola larghezza.

  • L'altra opzione è uno shader di runtime, ma ottiene solo la posizione dei pixel all'interno del confine del container.

  • Non ci dice cosa dobbiamo disegnare per questo nastro.

  • Inoltre, non possiamo calcolarlo facilmente nemmeno dalla posizione dei pixel.

  • Draw mesh ci fornisce il vertex shader per questo.

  • Quindi, cos'è una rete mesh?

  • Una mesh è costituita da triangoli con dati in ogni angolo e ogni angolo è chiamato vertice.

  • Ogni vertice contiene un piccolo record di dati.

  • In genere, questo record contiene una posizione, ma non è obbligatorio.

  • Il vertex shader legge il record e calcola la posizione.

  • Ecco il percorso di un triangolo: il vertex shader viene eseguito per ogni angolo, calcola la posizione degli angoli e qualsiasi valore che vogliamo passare per ogni pixel.

  • Poi il rasterizzatore trova ogni pixel coperto da quel triangolo e interpola i valori di questi tre angoli per ogni pixel.

  • Infine, lo Shading dei frammenti viene eseguito per questi pixel, utilizza i valori interpolati e calcola il colore dei pixel.

  • Vediamo come scrivere vertex shader e fragment shader con un esempio più semplice: una linea retta con un impulso che si muove lungo di essa.

    LineShaders.VERTEX
    
    uniform float2 uStart;
    uniform float2 uEnd;
    uniform float uHalfWidth;
    uniform float uTime;
    
    Varyings main(const Attributes a) {
        Varyings v;
        float2 dir = uEnd - uStart;
        float2 normal = normalize(float2(-dir.y, dir.x));
        float along = a.corner.x * length(dir);
        float pulse = (a.corner.x - uTime) / 0.18;
        float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
        float across = a.corner.y * (radius + 1.0);
        v.position = uStart + dir * a.corner.x + normal * across;
        v.stroke = float3(along, across, radius);
        return v;
    }
  • Il vertex shader accetta un attributo come input e restituisce un valore variabile.

  • Gli attributi sono i dati per un singolo angolo.

  • Ecco solo due numeri: l'avanzamento lungo la linea e il lato della linea in cui si trova.

  • Le variabili vengono quindi interpolate e passate al fragment shader.

  • Calcoliamo la distanza in pixel dall'inizio e, per la larghezza del tratto, utilizziamo il tempo per creare un impulso sul raggio.

  • Poi moltiplichiamo il raggio per il lato dell'angolo, ottenendo la distanza del segno lungo il tratto.

  • Man mano che la GPU esegue l'interpolazione tra questi segni, le linee centrali diventano zero.

  • Infine, calcoliamo la posizione degli angoli, memorizziamo tutti questi valori in un tratto variabile e li passiamo al fragment shader.

  • A differenza dello shader di runtime, uno shader di frammenti di mesh restituisce un colore tramite un parametro e una posizione per il campionamento della pittura, se impostata.

    LineShaders.FRAGMENT
    
    uniform float uLength;
    layout(color) uniform float4 uStartColor;
    layout(color) uniform float4 uEndColor;
    
    float2 main(const Varyings v, out float4 color) {
        float distance = abs(v.stroke.y);
        float coverage = 1.0 - smoothstep(
        v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
        float side = distance / v.stroke.z;
        float4 ink = mix(uStartColor, uEndColor,
            clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
        color = ink * coverage;
        return v.position;
    }
  • Prendendo il valore assoluto della nostra coordinata trasversale, otteniamo la distanza pixel per pixel dalla linea centrale.

  • Confrontiamo questa distanza con il raggio per ottenere la copertura e, per il colore, sfumiamo lungo il percorso della linea e pieghiamo leggermente il gradiente verso il bordo.

  • Moltiplica questa copertura per una copertura e otterrai il colore finale.

  • Sul lato Compose, colleghiamo gli shader e il layout dei dati con l'oggetto MeshSpecification.

    val spec = remember {
        MeshSpecification.make(
            arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
            vertexStride = 8,
            arrayOf(Varying(TYPE_FLOAT3, "stroke")),
            LineShaders.VERTEX, LineShaders.FRAGMENT)
    }
  • Innanzitutto, dichiariamo l'attributo corner come due float che iniziano dal byte 0.

  • Poiché due float occupano 8 byte, ogni passo dell'angolo nel buffer è 8.

  • Poi, dichiariamo le nostre variabili di tratto come tre valori con virgola mobile e passiamo lo shader dei vertici e dei frammenti che abbiamo appena scritto.

  • Creiamo il buffer dei vertici.

    fun lineCorners(segments: Int): ByteBuffer {
        val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
            .order(ByteOrder.nativeOrder())
        for (i in 0..segments) {
            val t = i / segments.toFloat()
            buffer.putFloat(t).putFloat(-1f) // one side
            buffer.putFloat(t).putFloat(1f)  // the other side
        }
        return buffer.rewind() as ByteBuffer
    }
  • In ogni passaggio lungo la linea, completiamo l'avanzamento, calcoliamo l'avanzamento da 0 a 1 e scriviamo due angoli nel buffer.

  • Infine, combiniamo la specifica e il buffer in un oggetto mesh.

    val mesh = Mesh(
        spec,
        mode = Mesh.TRIANGLE_STRIP,
        vertexBuffer = corners,
        vertexCount = (segments + 1) * 2,
        bounds = RectF(0f, 0f, size.width, size.height)
    )
  • Compose canvas non espone ancora drawMesh, quindi passiamo a nativeCanvas e disegniamo la mesh.

    Box(Modifier.fillMaxSize().drawWithCache {
       val mesh = Mesh(...)
       onDrawBehind {
           mesh.setFloatUniform("uTime", time.value)
           drawIntoCanvas {
              it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
           }
       }
    })
  • Poiché controlli la geometria e il pixel, puoi disegnare qualsiasi effetto visivo basato sulla geometria utilizzando la mesh di disegno.

  • Scopri di più su questa API nella documentazione.

Supporto di un'ampia gamma di colori

  • In Compose 1.12, abbiamo introdotto il supporto per ampie gamme di colori.

  • Ma cosa significa in pratica?

  • Sapevi che gli schermi possono riprodurre una gamma di colori diversa rispetto allo spazio dei colori sRGB tradizionale?

  • Ad esempio, se guardi questa immagine, vedrai che il triangolo bianco rappresenta la gamma di colori sRGB standard che la maggior parte degli schermi può visualizzare.

  • Tuttavia, alcuni schermi più recenti supportano una gamma più ampia di colori, come quella rappresentata dal triangolo giallo, con colori più saturi e vivaci.

  • Ma quando rappresentiamo un colore, in realtà lo rappresentiamo con la descrizione di ogni vertice di quel triangolo.

  • Quando esamini questo confronto tra gradienti, è importante distinguere tra gamut del display e interpolazione dei gradienti.

  • Su un display sRGB standard o probabilmente su questo proiettore, lo schermo non può riprodurre fisicamente i colori al di fuori di questa gamma sRGB.

  • Quindi, in realtà è molto difficile fare una demo sul palco in questo momento.

  • Quindi, dovrai fidarti di me quando dico che i colori sono migliori su Android con il supporto di un'ampia gamma di colori.

  • In questo esempio, ciò che vedi effettivamente è l'effetto dell'interpolazione dei punti di colore in uno spazio dei colori più ampio.

  • La fusione dei colori in uno spazio dei colori ampio o lineare modifica il percorso matematico effettivo di questo gradiente, impedendo l'effetto grigio medio e le bande marcate che possono verificarsi quando non si utilizza un'ampia gamma di colori.

  • Con Compose 1.12, puoi usufruire di una fusione dei gradienti più fluida su qualsiasi display, nonché di un vero supporto per l'ampia gamma di colori quando utilizzi un hardware supportato.

  • Per attivare il supporto corretto sul dispositivo, devi assicurarti che l'attività o la finestra siano impostate per attivare la modalità con ampia gamma cromatica.

  • Questa operazione può essere eseguita nel file AndroidManifest.

    <!-- AndroidManifest.xml -->
    <activity
    android:name=".NavActivity"
    android:colorMode="wideColorGamut"
    android:exported="true">
    </activity>
  • In alternativa, puoi farlo in modo programmatico nel tuo componibile.

    // Dynamic toggle in code
    DisposableEffect(window) {
       window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
       onDispose {
           window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
       }
    }
  • Se non imposti colorMode, la finestra rimane in sRGB standard a 8 bit e tutti i colori ad ampia gamma di colori visualizzati dalla GPU verranno ritagliati.

  • Pertanto, è importante evitare di utilizzare colori con un'ampia gamma cromatica se il display non è in grado di supportarli.

  • Per risparmiare batteria e migliorare le prestazioni, devi attivare le ampie gamme di colori solo per le attività o i percorsi del codice che mostrano effettivamente contenuti multimediali, foto o grafica avanzata.

  • Ciò significa che puoi creare colori in diversi spazi dei colori in Compose e questi vengono ora rispettati.

    // 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
    val VividP3Red = Color(
       red = 1.0f,
       green = 0.05f,
       blue = 0.05f,
       alpha = 1.0f,
       colorSpace = ColorSpaces.DisplayP3
    )
    // 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
    val HdrNeonCyan = Color(
       red = 0.0f,
       green = 1.8f,  // Peak brightness > 1.0 in extended gamut
       blue = 2.0f,
       alpha = 1.0f,
       colorSpace = ColorSpaces.ExtendedSrgb
    )
    // 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
    val HdrSunsetOrange = Color(
       red = 1.0f,
       green = 0.45f,
       blue = 0.1f,
       alpha = 1.0f,
       colorSpace = ColorSpaces.Bt2020Hlg
    )
  • Puoi passare il tipo di colorSpace in cui vuoi che venga creato il colore. Se non passi alcun colorSpace, verrà utilizzato lo spazio sRGB standard.

  • Per le bitmap in particolare, puoi controllare l'origine della bitmap utilizzando getColorSpace. Le librerie di caricamento delle immagini più diffuse gestiranno automaticamente gli spazi dei colori corretti della bitmap.

  • Devi utilizzare Bitmap.Config.HARDWARE per conservare le informazioni sui colori, poiché una configurazione come 565 perderà precisione e mostrerà un banding visibile agli utenti.

  • Per scoprire di più sui colori, puoi tornare indietro nel tempo fino al 2017 per guardare Romain Guy parlare di tutto ciò che riguarda i colori in Compose o in Android.

Forme poligonali e morphing

  • Con Compose 1.13, introduciamo il supporto nativo per la forma poligonale.

  • In questo modo, la creazione di elementi UI con forme personalizzate e dall'aspetto organico sarà molto più semplice.

  • In precedenza, se volevi disegnare una geometria complessa come questa, dovevi utilizzare RoundedPolygon per descrivere una geometria che generava un percorso e poi mappare quel percorso a un'API di forma di Compose.

    fun cookiePolygon(): RoundedPolygon {
        val vertices = FloatArray(32)
        for (i in 0 until 16) {
            val radius = if (i % 2 == 0) 1f else 0.84f
    // …
        }
        return RoundedPolygon(
            vertices,
            rounding = CornerRounding(0.12f)
        )
    }
  • Ad esempio, per prima cosa definisci le proporzioni utilizzando un RoundedPolygon e noi impostiamo In questo snippet, impostiamo i punti interni all'84% dal centro rispetto ai punti esterni.

  • Puoi anche applicare l'arrotondamento degli angoli qui, ma questo descrive solo le curve matematiche.

  • Per utilizzare effettivamente questo poligono per il ritaglio o gli sfondi in Compose, devi scrivere un wrapper Shape personalizzato come questo.

    class RoundedPolygonShape(
     polygon: RoundedPolygonShape
    ) : Shape {
        // …
        override fun createOutline(size: Size, layoutDirection: LayoutDirection,
        density: Density,
        ): Outline {
            path = polygon.toPath().asComposePath()
            // …
            matrix.scale(size.width / maxDimension, size.height / maxDimension)
            matrix.translate(-bounds.left, -bounds.top)
            // …
            return Outline.Generic(path)
        }
    }
  • All'interno di questo snippet createOutline, puoi vedere che devi creare il percorso e ridimensionarlo e tradurlo manualmente.

  • Ora, con la nuova API PolygonShape, non hai più bisogno di questo wrapper Shape personalizzato.

    private val AvatarShape = PolygonShape {
        val vertices = List(16) { i ->
            val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
            val angle = i * PI.toFloat() / 8f
            size.center + Offset(cos(angle), sin(angle)) * radius
        }
        polygon(vertices, rounding = CornerRounding(radius = 8.dp))
    }
    
        // …
    Modifier.clip(AvatarShape)
  • Poiché si tratta del builder Compose Shape dedicato, riceve la densità e le dimensioni direttamente out-of-the-box.

  • Ciò significa che puoi calcolare i vertici in modo dinamico e specificare l'arrotondamento degli angoli direttamente in dp o anche come percentuale.

  • Una volta definita, basta passarla a qualsiasi API che accetta una forma.

  • Stiamo anche introducendo l'API MorphPolygonShape, che consente di passare facilmente da una forma poligonale all'altra.

    val recordShape = remember {
        MorphPolygonShape(
            start = PolygonShape.circle(),
            end = PolygonShape.star(
                numPoints = 9,
                innerRadiusRatio = 0.8f,
                outerRounding = CornerRounding(percent = 50),
            ),
        ) { shapeProgress.value }
    }
  • In questo esempio, abbiamo un pulsante di registrazione che si trasforma da un cerchio a una stella quando interagisci con il pulsante.

  • Devi solo fornire la forma iniziale, la forma finale e il valore di avanzamento.

  • L'API MorphPolygonShape legge il valore di avanzamento durante la fase di risoluzione del contorno, ovvero non durante la ricomposizione, non durante la composizione.

  • Ciò significa che, mentre la forma si trasforma tra un frame e l'altro, si risolve semplicemente in un nuovo contorno senza mai attivare una ricomposizione.

Effetti di rendering di sfocatura e sfondo

  • Ora parliamo della sfocatura.

  • Nelle ultime release, abbiamo cercato di rendere l'implementazione delle sfocature molto più semplice ed espressiva.

  • Ogni volta che parliamo di sfocatura, questo è il modificatore di uso comune che vediamo.

    Modifier
       .blur(
           radiusX = 10.dp,
           radiusY = 10.dp,
           edgeTreatment = BlurredEdgeTreatment(
               shape = RoundedCornerShape(8.dp),
           ),
       )
  • Compose dispone di questo modificatore da Android 12.

  • Funziona benissimo quando vuoi una sfocatura uniforme, in cui lo stesso raggio di sfocatura viene applicato a un componibile.

  • Tuttavia, i design delle UI moderne spesso richiedono una transizione fluida da una regione nitida a una sfocata, anziché un taglio netto.

  • E questo ci porta alla sfocatura progressiva, che ora è un'API ufficiale a partire da Compose 1.13.

  • Invece di un singolo raggio costante nell'intero composable, ora puoi variare l'intensità della sfocatura nello spazio.

  • Ecco l'esempio della nostra app JetChat dalla schermata del profilo utente.

  • Il nuovo modificatore di sfocatura accetta una specifica del raggio di sfocatura.

    Modifier.blur {
    blurRadiusSpec = BlurRadiusSpec.verticalGradient(
    listOf(
    BlurStop(0.5f, 0.dp),
    BlurStop(1.0f, 32.dp),
    )
    )
    edgeTreatment = BlurredEdgeTreatment.Unbounded
    }
  • In questo caso utilizziamo un gradiente verticale con due punti di sfocatura, mantenendo la metà superiore della foto completamente nitida a 0 dp e aumentando la sfocatura fino a 32 dp sul bordo inferiore.

  • Tieni presente che abbiamo anche impostato edgeTreatment su unbounded, il che in pratica indica alla sfocatura di campionare anche i pixel trasparenti al di fuori dei limiti del composable.

  • Oltre alle dissolvenze verticali, questa specifica del raggio di sfocatura offre diversi modi integrati per applicare la sfocatura progressiva.

  • Ad esempio, a sinistra puoi vedere un gradiente orizzontale, in cui il raggio iniziale è 0 dp e il raggio finale è determinato dal valore di un cursore.

    BlurRadiusSpec.horizontalGradient(
    startRadius = 0.dp,
    endRadius = sliderValue.dp,
    )
  • A destra, abbiamo una sfumatura radiale, dove al centro il raggio di sfocatura è 0 dp e, man mano che ci si sposta verso fallOffRadius, che in questo caso è 300 dp, la sfocatura aumenta gradualmente fino a 24 dp.

    BlurRadiusSpec.radialGradient(
    center = center,
    fallOffRadius = 300.dp,
    startRadius = 0.dp,
    endRadius = 24.dp,
    )
  • E se tutti questi gradienti verticali, radiali e orizzontali non ti bastano, puoi sempre applicare la sfocatura progressiva utilizzando shader personalizzati.

    val customShader = remember {
       RuntimeShader(
           """
           uniform float2 size;
           uniform float time;
    
           half4 main(float2 coords) {
              // Diagonal wave mask
              float normalizedX = coords.x / size.x;
              float normalizedY = coords.y / size.y;
              float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
              return half4(0.0, 0.0, 0.0, wave);
           }
           """.trimIndent()
       )
    }
    
    val customBlurRadius = remember(customShader) {
       BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
    }
    
    Modifier.blur {
           val w = size.width.toPx()
           val h = size.height.toPx()
           customShader.setFloatUniform("size", w, h)
           customShader.setFloatUniform("time", animatedTime)
           blurRadiusSpec = customBlurRadius
       },
  • Qui scriviamo un piccolo fragment shader che calcola un'onda sinusoidale diagonale animata e restituisce l'intensità in un canale alfa.

  • Quindi, la specifica dello shader mappa direttamente la maschera da 0 a 1 a una maschera da 24 dp maxRadius.

  • Prima di parlare della sfocatura dello sfondo, vediamo la differenza tra i due.

  • Ora, a sinistra, abbiamo una barra dell'app in alto a cui abbiamo applicato la sfocatura del livello.

  • In altre parole, sfoca il componibile stesso.

  • Come puoi vedere, il titolo e le icone delle azioni sono sfocati, ma il componente della chat dietro non lo è affatto.

  • A destra, abbiamo la sfocatura dello sfondo.

  • In questo caso, vengono campionati e sfocati solo i messaggi chat che scorrono dietro la barra superiore.

  • Tuttavia, il testo e le icone sono completamente nitidi e leggibili.

  • A partire da Android 37.2, puoi sfocare lo sfondo utilizzando una nuova API chiamata RenderNode.setBackdropRenderEffect.

  • Ma prima di approfondire i miglioramenti della sfocatura, cerchiamo di capire come funziona Android dietro le quinte per garantire che la sfocatura sia efficace.

  • Android utilizza i RenderNode, che sono contenitori di elenchi di visualizzazione con accelerazione hardware, il che significa che possono compattare e archiviare una serie di comandi di disegno diversi tramite l'inizio e la fine della registrazione.

  • RenderNode contiene informazioni quali traslazioni, rotazioni, alfa, scale e limiti di posizione 3D.

  • In effetti, Compose utilizza RenderNode in background.

  • Quindi, per utilizzare gli effetti di rendering dello sfondo in Compose, possiamo creare un Modifier.Node personalizzato per poter riutilizzare questo modificatore.

    fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
       this then BackdropBlurElement(spec, shape, tint)
    
    private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
       private var renderNode: RenderNode? = null
    
       override fun ContentDrawScope.draw() {
           val node = getOrCreateNode()
           if (isDirty || sizeChanged) {
               node.setPosition(0, 0, widthPx, heightPx)
               node.setBackdropRenderEffect(spec.createRenderEffect(this))
           }
           drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
           drawContent()
       }
    }
  • Manteniamo un riferimento a un RenderNode, che RenderNode viene configurato con gli effetti che vogliamo in un'altra funzione.

  • Ad esempio, qui passiamo un effetto di sfocatura.

  • Poi, chiamiamo drawIntoCanvas, richiamando drawRenderNode sul nostro canvas nativo.

  • Infine, chiamiamo drawContent per disegnare anche i contenuti effettivi non sfocati in alto.

  • Per utilizzare questo modificatore che abbiamo appena creato, chiamiamo Modifier.backdropBlur e passiamo una specifica del raggio di sfocatura.

    JetchatAppBar(
        modifier = modifier
            .backdropBlur(
                tint = MaterialTheme.colorScheme
                    .surface.copy(alpha = 0.5f),
                shape = RectangleShape,
                spec = BlurRadiusSpec.uniform(5.dp)
            )
    )
  • Tieni presente che con questo approccio non è necessario inserire nulla nel livello sottostante.

  • RenderNode.setBackdropRenderEffect sta campionando automaticamente i contenuti che si trovano sotto la superficie della barra dell'app JetChat.

  • Il risultato è questa deliziosa sfocatura dello sfondo.

  • Potremmo anche impostare una specifica del raggio di sfocatura progressiva anziché solo una sfocatura progressiva e ottenere una sfocatura verticale anche sui contenuti.

    JetchatAppBar(
        modifier = modifier
            .backdropBlur(
                tint = MaterialTheme.colorScheme
                    .surface.copy(alpha = 0.5f),
                shape = RectangleShape,
                spec = BlurRadiusSpec.verticalGradient(
                    listOf(
                        BlurStop(0.5f, 32.dp),
                        BlurStop(1f, 0.dp),
                    ),
                )
            )
    )
  • Con un gradiente verticale, mostra i contenuti sottostanti sfocati con raggi diversi in frazioni diverse del contenitore.

  • Stiamo lavorando a un modificatore di sfocatura dello sfondo integrato in Compose.

  • Questo è solo un esempio della tecnologia di base su Android con Compose.

  • Questo esempio che ti ho mostrato funzionerebbe solo sul livello API 37.2, ma stiamo lavorando anche a un backport per Compose, quindi continua a seguirci.

  • Un altro esempio divertente con gli effetti di sfondo di RenderNode è che possiamo anche concatenarli per formare UI davvero accattivanti e applicare più effetti diversi.

  • Quindi, per concatenare gli effetti, creiamo questi due effetti.

    val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)
    
    // Boost saturation by 30% and apply blur together
    val matrix = ColorMatrix().apply { setSaturation(1.3f) }
    val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
    val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)
  • In questo esempio, creiamo un effetto di sfocatura e una saturazione del 30%.

  • Chiamiamo createChainEffect applicando prima l'effetto di sfocatura e poi la saturazione.

  • Questo è un esempio in JetChat.

    val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
    val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
    
    val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
    renderNode?.setBackdropRenderEffect(finalEffect)
  • Il pulsante Vai alla fine, che possiamo far sembrare una piccola lente d'ingrandimento combinando alcuni effetti diversi per arrotondare i bordi di questo composable come la lente d'ingrandimento.

  • Ma non solo puoi applicare effetti di ingrandimento, puoi anche concatenare qualsiasi tipo di shader per applicarlo a uno sfondo.

  • Ecco alcuni esempi divertenti che abbiamo implementato di ciò che è possibile fare combinandoli insieme.

  • Ora è possibile ottenere il tradizionale effetto di tipo vetro smerigliato.

  • Ma potremmo anche creare altri effetti divertenti, come un caleidoscopio.

  • Oppure, se volessimo trasformare questa mascotte Android in una sorta di elemento UI che si trasforma e modifica i contenuti sottostanti, potremmo anche applicare questi effetti di rendering dello sfondo per dare l'impressione di guardare attraverso questo piccolo droide.

  • Possiamo farlo anche con gli effetti di rendering dello sfondo.

  • Quindi, non solo la sfocatura dello sfondo e l'effetto vetro smerigliato sono stati sbloccati con questa nuova API, ma ora l'unico limite è la tua immaginazione.

  • Un'altra nuova API che abbiamo introdotto nella piattaforma è SurfaceView.setBlurRegions.

  • Al momento puoi sfocare le visualizzazioni delle texture e con qualsiasi composable utilizzando gli effetti di sfondo Modifier.blur e RenderNode, ma sfocare parti di una SurfaceView è difficile e non fa parte dell'albero RenderNode della visualizzazione dell'app, in quanto crea un foro nella finestra.

  • Pertanto, Compose non può effettivamente campionare il buffer di supporto di SurfaceView, il che rendeva questa operazione impossibile prima dell'API 37.2.

  • Pertanto, setBlurRegions di SurfaceView consente all'app di dichiarare direttamente le regioni di sfocatura.

  • Ad esempio, su questo video player in JetChat, abbiamo aggiunto la sfocatura di SurfaceView quando i controlli vengono mostrati e nascosti.

  • Per utilizzare questa nuova API, puoi accedere a setBlurRegions su un metodo SurfaceView.

    val region = RoundedRectBlurRegion().apply {
       bounds = RectF(left, top, right, bottom)
       setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
       alpha = spec.alpha.coerceIn(0f, 1f)
       blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
    }
    
    surfaceView.setBlurRegions(listOf(region))
    
    surfaceView.setBlurRegions(emptyList()) // Clear
  • Questo metodo accetta l'elenco delle aree dei limiti, il raggio di sfocatura, l'alpha e un elenco di raggi degli angoli.

  • Quando vengono passati, questi limiti devono essere relativi alla SurfaceView sullo schermo.

  • Pertanto, devi tenere traccia delle coordinate di SurfaceView e dell'area che vuoi sfocare.

RuntimeColorFilter

  • Un'altra novità di Android 16 e versioni successive è RuntimeColorFilter.

  • In precedenza, con le immagini, potevi solo impostare un filtro colore a matrice statica e applicare effetti come la saturazione.

  • Tuttavia, con Android 16 e versioni successive, ora puoi passare uno shader che accetta un colore in entrata, il pixel di input per quel particolare contenuto, e produce un nuovo colore.

  • Ad esempio, se volessimo applicare un filtro cyberpunk a una foto, potremmo definire un piccolo shader da utilizzare come input per RuntimeColorFilter.

    @Language("AGSL")
    private const val CYBERPUNK_SHADER = """
        vec4 main(half4 in_color) {
            float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
            vec3 shadow = vec3(0.06, 0.04, 0.25);
            vec3 mid = vec3(0.92, 0.12, 0.58);
            vec3 high = vec3(0.18, 0.94, 0.98);
            vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
            return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
        }
    """
  • Questo shader produce un tema cyberpunk, ma puoi creare qualsiasi altro effetto che preferisci.

  • Quindi, per utilizzare lo shader stesso, applichiamo RuntimeColorFilter e lo convertiamo in un filtro colore di Compose.

    AsyncImage(
        model = imageUri,
        contentDescription = filter.displayName,
        colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
                       .asComposeColorFilter(),
        contentScale = ContentScale.Crop,
        modifier = Modifier.fillMaxSize(),
    )
  • Come puoi vedere in questo esempio, ora possiamo creare effetti più dinamici rispetto a prima senza dover approfondire OpenGL, le configurazioni di SurfaceView o cose del genere.

  • Questo può essere applicato anche a qualsiasi composable, quindi non solo alle immagini.

Ottimizzazione delle prestazioni grafiche

  • Grazie.

  • Abbiamo creato un effetto molto carino, ma assicuriamoci che la nostra app sia ancora veloce e fluida.

  • Quando scorriamo la conversazione, tutti gli effetti vengono eseguiti insieme.

  • Iniziamo misurando questo scorrimento.

    @Test
    fun scrollFling() = measureScroll(swipeSteps = 5)
    
    private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
        packageName = TARGET_PACKAGE,
        metrics = defaultComposeScrollingMetrics(),
        compilationMode = CompilationMode.Full(),
        iterations = 5,
        setupBlock = {
            killProcess()
        }
    //...
  • Utilizziamo un benchmark macro che avvia un'app in un nuovo processo ed esegue lo scorrimento della conversazione.

  • Poi, eseguiamo una query sulla traccia per contare i frame che non rispettano le scadenze.

    select count(*) as late_frames
    from actual_frame_timeline_slice
    where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
      and jank_type like '%App Deadline Missed%'
  • In questo modo, abbiamo sei frame in ritardo.

  • Apriamo uno dei frame successivi.

  • Quindi, subito prima di questo frame, Compose esegue il precaricamento del messaggio successivo prima che scorra verso l'alto nella visualizzazione.

  • Il precaricamento dovrebbe avvenire nel breve intervallo tra i frame, ma la preparazione di questo messaggio decodifica anche l'immagine direttamente nel thread principale, che viene eseguito a lungo senza interruzioni.

  • Poiché il thread principale è bloccato nella decodifica di questa immagine, non è possibile avviare nuovi frame.

  • Poi, quando il messaggio scorre effettivamente nella visualizzazione, RenderThread deve allocare memoria e caricare tutti quei pixel sulla GPU, quindi la stessa identica immagine ritarda un secondo frame.

  • Ora vediamo dove è andato il nostro tempo, ma abbiamo davvero bisogno di tutto questo lavoro per disegnare questo frame?

  • E come possiamo capirlo?

  • La traccia ci dice che

  • Ogni volta che una bitmap di grandi dimensioni viene disegnata in un'area molto più piccola sullo schermo, Android registra un indicatore direttamente nella traccia.

  • Mostra due numeri in pixel: la dimensione completa della bitmap e l'area che copre effettivamente sullo schermo.

  • Dividi il risultato per la densità dello schermo e otterrai il valore in DP.

  • Poi, cerchiamo nel nostro codice il valore DP per trovare l'immagine.

  • Ecco cosa troviamo.

  • L'immagine viene caricata con painterResource, che non conosce le dimensioni del contenitore, quindi decodifica l'intera immagine alla risoluzione massima.

    Image(
        painter = painterResource(it),
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(160.dp),
        contentDescription = stringResource(id = R.string.attached_image),
    )
  • Quindi, lo sostituiamo con AsyncImage di Coil.

    AsyncImage(
        model = it,
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(160.dp),
        contentDescription = stringResource(id = R.string.attached_image),
    )
  • Misura prima il contenitore, in modo da decodificare solo il pixel necessario, e svolge questo lavoro su un thread diverso.

  • Eseguendo nuovamente lo stesso benchmark, la decodifica delle immagini sul thread principale è scesa a 0 e il frame in ritardo è sceso a 2.

  • Non eseguiremo il debug di questi due frame qui, ma il concetto è semplice: cerca di non calcolare i pixel che non raggiungeranno mai il frame.

  • Quindi, la decodifica delle immagini era abbastanza grande da ritardare i frame, ma non tutti i costi si presentano come frame persi e ci sono ancora costi minori che possiamo evitare durante la scrittura del codice.

  • Vediamone due.

  • Ogni volta che utilizziamo un RuntimeShader, dobbiamo crearlo una sola volta perché, anche se viene solo compilato e memorizzato nella cache, analizza comunque la stringa di origine ogni volta che ne creiamo uno.

  • Inoltre, ogni volta che un effetto anima ogni frame, possiamo racchiuderlo in un graphicsLayer, in modo che venga registrato nuovamente solo l'elemento animato, anziché ridisegnare la UI circostante.

  • Il pulsante Vai in fondo viene visualizzato ogni volta che scorriamo.

  • Per mostrare questo effetto vengono utilizzati due RuntimeShader e, ogni volta che viene visualizzato il pulsante, ricreiamo entrambi gli shader dalla stringa di origine.

    val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
        setFloatUniform("size", size.width, size.height)
        setFloatUniform("zoom", zoom)
    // …
    val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
    // …
    val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {
  • L'analisi di questi due shader e la creazione dell'effetto richiedono centinaia di microsecondi sul thread principale ogni volta che viene visualizzato il pulsante.

  • Invece, creiamo lo shader una volta per l'intero processo o possiamo memorizzarlo e poi aggiornare la sua uniforme.

    val lensShader = MagnifierShaders.lens.apply {
        setFloatUniform("size", size.width, size.height)
        setFloatUniform("zoom", zoom)
    // …
    private object MagnifierShaders {
        val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
        val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }
  • Con questa modifica, la creazione di questo effetto è quasi 40 volte più veloce e il costo quando viene visualizzato il pulsante diminuisce di 20 volte.

  • Nel bagliore del gradiente della mesh che abbiamo visto in precedenza, il bagliore anima ogni frame all'interno di drawBehind.

    Box(
        modifier = Modifier
            .matchParentSize()
            .drawBehind {
                // Behind the card, scaled past its bounds.
                translate(top = -58.dp.toPx()) {
                    scale(scaleX = 1.38f, scaleY = 2.85f) {
                        with(glowMeshPainter) {
    draw(size, alpha = glowAlpha)
    }
  • Pertanto, la lettura dello stato nella fase di disegno mantiene la composizione e il layout, ma l'elenco di visualizzazione non può aggiornare un singolo comando di disegno in loco.

  • Quindi Compose risale l'albero per trovare il nodo più vicino con il livello grafico, che in questo caso è il contorno rosso, ovvero l'intero schermo, e registra nuovamente l'intero livello per ogni frame.

  • Inserendo un graphicsLayer direttamente sulla casella, il percorso si interrompe in corrispondenza del contorno verde, quindi viene registrata nuovamente solo la casella luminosa e la sezione di disegno del thread principale viene ridotta di due decimi di millisecondo.

    Box(
        modifier = Modifier
            .matchParentSize()
                      .graphicsLayer()
            .drawBehind {
                // Behind the card, scaled past its bounds.
                translate(top = -58.dp.toPx()) {
                    scale(scaleX = 1.38f, scaleY = 2.85f) {
                        with(glowMeshPainter) {
    draw(size, alpha = glowAlpha)
    }
  • Con Perfetto, analizzare cosa non va in una traccia può essere difficile, quindi puoi utilizzare la skill di analisi delle tracce per raccogliere alcuni insight.

  • Se vuoi un aiuto in più per scrivere codice Compose ad alte prestazioni, installa CLI Android e utilizza la skill di prestazioni di Compose per iniziare.

    android skills add perfetto-trace-analysis
    android skills add jetpack-compose-performance

Riepilogo e risorse

  • Se vuoi vedere gli esempi che abbiamo mostrato oggi, dai un'occhiata all'app JetChat, disponibile a questo link.

Codice

Snippet di codice

Snippet di Kotlin alle 02:47 in "Mesh gradients and custom mesh rendering"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}
Snippet Kotlin alle 02:55 in "Mesh gradients and custom mesh rendering"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
   setVertex(0, 0, Offset(0.0f, 0.0f), indigo)

}
}
Snippet Kotlin al minuto 03:05 in "Mesh gradients and custom mesh rendering"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
   setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
     // ....
   setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
   // ....
   setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
   // ....
}
}

Box(
   modifier = modifier.padding(32.dp)
       .safeContentPadding()
       .aspectRatio(16 / 9f)
       .fillMaxWidth()
       .paint(gradientPainter)
)
Snippet di Kotlin alle 03:24 in "Mesh gradients and custom mesh rendering"
MeshGradientPainter(
   rows = 2,
   columns = 3,
   hasBicubicColor = true,
) {
   // Row 0 (top edge, y = 0.0f)
   setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
   setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
   setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
   setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))

   // ... remaining vertices for Rows 0, 1, and 2
}
Snippet Kotlin alle 04:09 in "Mesh gradients and custom mesh rendering"
val transition = rememberInfiniteTransition(label = "userInputGlowMesh")

val phase1 by transition.animateFloat(
   initialValue = 0f,
   targetValue = (2 * PI).toFloat(),
   animationSpec = infiniteRepeatable(
       animation = tween(durationMillis = 2800, easing = LinearEasing),
       repeatMode = RepeatMode.Restart,
   ),
   label = "glowPhase1",
)

val phase2 by transition.animateFloat(
   initialValue = 0f,
   targetValue = (2 * PI).toFloat(),
   animationSpec = infiniteRepeatable(
       animation = tween(durationMillis = 4200, easing = LinearEasing),
       repeatMode = RepeatMode.Restart,
   ),
   label = "glowPhase2",
)
Snippet Kotlin al minuto 04:33 in "Mesh gradients and custom mesh rendering"
MeshGradientPainter(
   rows = 6,
   columns = 5,
   hasBicubicColor = true,
) {
   for (r in 0..6) {
       for (c in 0..5) {
               val offset = if (r in 1..5 && c in 1..4) {
               val ampX = when (r) {
                   1, 2 -> 0.048f
                   3 -> 0.042f
                   4 -> 0.028f
                   else -> 0.036f
               }
               val ampY = when (r) {
                   1, 2 -> 0.028f
                   3 -> 0.020f
                   4 -> 0.022f
                   else -> 0.016f
               }
               val d = orbit(c, r, ampX, ampY)
               Offset(
                   x = (baseU + d.x).coerceIn(0.04f, 0.96f),
                   y = (baseV + d.y).coerceIn(0.05f, 0.95f),
               )
           } else {
               Offset(baseU, baseV)
           }
           setVertex(r, c, offset, gridColors[r][c])
       }
   }
}
Snippet di Kotlin al minuto 05:40 in "Mesh gradients and custom mesh rendering"
val lines = remember {
        path.asAndroidPath().flatten(0.5f)
    }
//... inside draw scope
lines.forEach { line ->
//...
    drawLine(
        brush = Brush.linearGradient(...),
        start = Offset(line.start.x,
line.start.y),
        end = Offset(line.end.x, line.end.y),
        strokeWidth = 30f,
    )
//...
}
Snippet di testo al minuto 07:03 in "Mesh gradients and custom mesh rendering"
LineShaders.VERTEX

uniform float2 uStart;
uniform float2 uEnd;
uniform float uHalfWidth;
uniform float uTime;

Varyings main(const Attributes a) {
    Varyings v;
    float2 dir = uEnd - uStart;
    float2 normal = normalize(float2(-dir.y, dir.x));
    float along = a.corner.x * length(dir);
    float pulse = (a.corner.x - uTime) / 0.18;
    float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
    float across = a.corner.y * (radius + 1.0);
    v.position = uStart + dir * a.corner.x + normal * across;
    v.stroke = float3(along, across, radius);
    return v;
}
Snippet di testo al minuto 08:01 in "Mesh gradients and custom mesh rendering"
LineShaders.FRAGMENT

uniform float uLength;
layout(color) uniform float4 uStartColor;
layout(color) uniform float4 uEndColor;

float2 main(const Varyings v, out float4 color) {
    float distance = abs(v.stroke.y);
    float coverage = 1.0 - smoothstep(
    v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
    float side = distance / v.stroke.z;
    float4 ink = mix(uStartColor, uEndColor,
        clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
    color = ink * coverage;
    return v.position;
}
Snippet di Kotlin alle 08:37 in "Mesh gradients and custom mesh rendering"
val spec = remember {
    MeshSpecification.make(
        arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
        vertexStride = 8,
        arrayOf(Varying(TYPE_FLOAT3, "stroke")),
        LineShaders.VERTEX, LineShaders.FRAGMENT)
}
Snippet Kotlin al minuto 09:06 in "Mesh gradients and custom mesh rendering"
fun lineCorners(segments: Int): ByteBuffer {
    val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
        .order(ByteOrder.nativeOrder())
    for (i in 0..segments) {
        val t = i / segments.toFloat()
        buffer.putFloat(t).putFloat(-1f) // one side
        buffer.putFloat(t).putFloat(1f)  // the other side
    }
    return buffer.rewind() as ByteBuffer
}
Snippet Kotlin al minuto 09:18 in "Mesh gradients and custom mesh rendering"
val mesh = Mesh(
    spec,
    mode = Mesh.TRIANGLE_STRIP,
    vertexBuffer = corners,
    vertexCount = (segments + 1) * 2,
    bounds = RectF(0f, 0f, size.width, size.height)
)
Snippet Kotlin al minuto 09:24 in "Mesh gradients and custom mesh rendering"
Box(Modifier.fillMaxSize().drawWithCache {
   val mesh = Mesh(...)
   onDrawBehind {
       mesh.setFloatUniform("uTime", time.value)
       drawIntoCanvas {
          it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
       }
   }
})
Snippet XML al minuto 11:22 in "Supporto di un'ampia gamma di colori"
<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>
Snippet di Kotlin al minuto 11:24 in "Wide color gamut support"
// Dynamic toggle in code
DisposableEffect(window) {
   window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
   onDispose {
       window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
   }
}
Snippet di Kotlin al minuto 11:49 in "Supporto di un'ampia gamma di colori"
// 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
val VividP3Red = Color(
   red = 1.0f,
   green = 0.05f,
   blue = 0.05f,
   alpha = 1.0f,
   colorSpace = ColorSpaces.DisplayP3
)
// 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
val HdrNeonCyan = Color(
   red = 0.0f,
   green = 1.8f,  // Peak brightness > 1.0 in extended gamut
   blue = 2.0f,
   alpha = 1.0f,
   colorSpace = ColorSpaces.ExtendedSrgb
)
// 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
val HdrSunsetOrange = Color(
   red = 1.0f,
   green = 0.45f,
   blue = 0.1f,
   alpha = 1.0f,
   colorSpace = ColorSpaces.Bt2020Hlg
)
Snippet di Kotlin al minuto 12:45 in "Forme poligonali e morphing"
fun cookiePolygon(): RoundedPolygon {
    val vertices = FloatArray(32)
    for (i in 0 until 16) {
        val radius = if (i % 2 == 0) 1f else 0.84f
// …
    }
    return RoundedPolygon(
        vertices,
        rounding = CornerRounding(0.12f)
    )
}
Snippet Kotlin alle 13:15 in "Forme poligonali e morphing"
class RoundedPolygonShape(
 polygon: RoundedPolygonShape
) : Shape {
    // …
    override fun createOutline(size: Size, layoutDirection: LayoutDirection,
    density: Density,
    ): Outline {
        path = polygon.toPath().asComposePath()
        // …
        matrix.scale(size.width / maxDimension, size.height / maxDimension)
        matrix.translate(-bounds.left, -bounds.top)
        // …
        return Outline.Generic(path)
    }
}
Snippet Kotlin alle 13:30 in "Forme poligonali e morphing"
private val AvatarShape = PolygonShape {
    val vertices = List(16) { i ->
        val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
        val angle = i * PI.toFloat() / 8f
        size.center + Offset(cos(angle), sin(angle)) * radius
    }
    polygon(vertices, rounding = CornerRounding(radius = 8.dp))
}

    // …
Modifier.clip(AvatarShape)
Snippet Kotlin alle 13:53 in "Polygon shapes and morphing"
val recordShape = remember {
    MorphPolygonShape(
        start = PolygonShape.circle(),
        end = PolygonShape.star(
            numPoints = 9,
            innerRadiusRatio = 0.8f,
            outerRounding = CornerRounding(percent = 50),
        ),
    ) { shapeProgress.value }
}
Snippet di Kotlin alle 14:37 in "Blurring and backdrop render effects"
Modifier
   .blur(
       radiusX = 10.dp,
       radiusY = 10.dp,
       edgeTreatment = BlurredEdgeTreatment(
           shape = RoundedCornerShape(8.dp),
       ),
   )
Snippet Kotlin al minuto 15:15 in "Blurring and backdrop render effects"
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}
Snippet Kotlin al minuto 15:42 in "Effetti di rendering di sfocatura e sfondo"
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)
Snippet di Kotlin al minuto 15:53 ​​in "Effetti di rendering di sfocatura e sfondo"
BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)
Snippet Kotlin al minuto 16:07 in "Blurring and backdrop render effects"
val customShader = remember {
   RuntimeShader(
       """
       uniform float2 size;
       uniform float time;

       half4 main(float2 coords) {
          // Diagonal wave mask
          float normalizedX = coords.x / size.x;
          float normalizedY = coords.y / size.y;
          float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
          return half4(0.0, 0.0, 0.0, wave);
       }
       """.trimIndent()
   )
}

val customBlurRadius = remember(customShader) {
   BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
}

Modifier.blur {
       val w = size.width.toPx()
       val h = size.height.toPx()
       customShader.setFloatUniform("size", w, h)
       customShader.setFloatUniform("time", animatedTime)
       blurRadiusSpec = customBlurRadius
   },
Snippet Kotlin al minuto 17:40 in "Effetti di rendering di sfocatura e sfondo"
fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
   this then BackdropBlurElement(spec, shape, tint)

private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
   private var renderNode: RenderNode? = null

   override fun ContentDrawScope.draw() {
       val node = getOrCreateNode()
       if (isDirty || sizeChanged) {
           node.setPosition(0, 0, widthPx, heightPx)
           node.setBackdropRenderEffect(spec.createRenderEffect(this))
       }
       drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
       drawContent()
   }
}
Snippet Kotlin alle 18:09 in "Effetti di rendering di sfocatura e sfondo"
JetchatAppBar(
    modifier = modifier
        .backdropBlur(
            tint = MaterialTheme.colorScheme
                .surface.copy(alpha = 0.5f),
            shape = RectangleShape,
            spec = BlurRadiusSpec.uniform(5.dp)
        )
)
Snippet Kotlin alle 18:30 in "Effetti di rendering di sfocatura e sfondo"
JetchatAppBar(
    modifier = modifier
        .backdropBlur(
            tint = MaterialTheme.colorScheme
                .surface.copy(alpha = 0.5f),
            shape = RectangleShape,
            spec = BlurRadiusSpec.verticalGradient(
                listOf(
                    BlurStop(0.5f, 32.dp),
                    BlurStop(1f, 0.dp),
                ),
            )
        )
)
Snippet Kotlin al minuto 19:10 in "Effetti di rendering di sfocatura e sfondo"
val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)

// Boost saturation by 30% and apply blur together
val matrix = ColorMatrix().apply { setSaturation(1.3f) }
val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)
Snippet di Kotlin alle 19:23 in "Effetti di rendering di sfocatura e sfondo"
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")

val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)
Snippet di Kotlin al minuto 20:53 in "Effetti di rendering di sfocatura e sfondo"
val region = RoundedRectBlurRegion().apply {
   bounds = RectF(left, top, right, bottom)
   setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
   alpha = spec.alpha.coerceIn(0f, 1f)
   blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
}

surfaceView.setBlurRegions(listOf(region))

surfaceView.setBlurRegions(emptyList()) // Clear
Snippet di Kotlin al minuto 21:33 in "RuntimeColorFilter"
@Language("AGSL")
private const val CYBERPUNK_SHADER = """
    vec4 main(half4 in_color) {
        float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
        vec3 shadow = vec3(0.06, 0.04, 0.25);
        vec3 mid = vec3(0.92, 0.12, 0.58);
        vec3 high = vec3(0.18, 0.94, 0.98);
        vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
        return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
    }
"""
Snippet Kotlin alle 21:48 in "RuntimeColorFilter"
AsyncImage(
    model = imageUri,
    contentDescription = filter.displayName,
    colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
                   .asComposeColorFilter(),
    contentScale = ContentScale.Crop,
    modifier = Modifier.fillMaxSize(),
)
Snippet Kotlin al minuto 22:21 in "Ottimizzazione delle prestazioni grafiche"
@Test
fun scrollFling() = measureScroll(swipeSteps = 5)

private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
    packageName = TARGET_PACKAGE,
    metrics = defaultComposeScrollingMetrics(),
    compilationMode = CompilationMode.Full(),
    iterations = 5,
    setupBlock = {
        killProcess()
    }
//...
Snippet SQL alle 22:32 in "Ottimizzazione delle prestazioni grafiche"
select count(*) as late_frames
from actual_frame_timeline_slice
where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
  and jank_type like '%App Deadline Missed%'
Snippet Kotlin al minuto 23:54 in "Graphics performance optimization"
Image(
    painter = painterResource(it),
    contentScale = ContentScale.Fit,
    modifier = Modifier.size(160.dp),
    contentDescription = stringResource(id = R.string.attached_image),
)
Snippet di Kotlin al minuto 24:02 in "Ottimizzazione delle prestazioni grafiche"
AsyncImage(
    model = it,
    contentScale = ContentScale.Fit,
    modifier = Modifier.size(160.dp),
    contentDescription = stringResource(id = R.string.attached_image),
)
Snippet di Kotlin al minuto 25:14 in "Graphics performance optimization"
val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
    setFloatUniform("size", size.width, size.height)
    setFloatUniform("zoom", zoom)
// …
val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
// …
val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {
Snippet Kotlin al minuto 25:32 in "Graphics performance optimization"
val lensShader = MagnifierShaders.lens.apply {
    setFloatUniform("size", size.width, size.height)
    setFloatUniform("zoom", zoom)
// …
private object MagnifierShaders {
    val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
    val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }
Snippet di Kotlin al minuto 25:49 in "Ottimizzazione delle prestazioni grafiche"
Box(
    modifier = Modifier
        .matchParentSize()
        .drawBehind {
            // Behind the card, scaled past its bounds.
            translate(top = -58.dp.toPx()) {
                scale(scaleX = 1.38f, scaleY = 2.85f) {
                    with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}
Snippet di Kotlin al minuto 26:15 in "Graphics performance optimization"
Box(
    modifier = Modifier
        .matchParentSize()
                  .graphicsLayer()
        .drawBehind {
            // Behind the card, scaled past its bounds.
            translate(top = -58.dp.toPx()) {
                scale(scaleX = 1.38f, scaleY = 2.85f) {
                    with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}
Snippet di shell al minuto 26:37 in "Graphics performance optimization"
android skills add perfetto-trace-analysis
android skills add jetpack-compose-performance