Apps foundation

Melhore seus apps com os recursos gráficos mais recentes do Jetpack Compose e do Android

Alto-falantes

  • Rebecca Franks

    Engenheiro de relações com desenvolvedores

  • Aayush Chaudhary

    Engenheiro de software

  • Rahul Ray

    Engenheiro de relações com desenvolvedores

Sobre

Sobre a palestra

Tudo pronto para transformar sua interface do Android de "funciona" em "é uma obra de arte"?

Confira os recursos gráficos mais recentes do Jetpack Compose. Vamos mostrar como criar novos efeitos visuais, desde gradientes de malha incríveis até desfoques progressivos, usando os novos recursos gráficos avançados do Android. Também vamos mostrar como melhorar o desempenho dos recursos gráficos sem sacrificar a duração da bateria ou as taxas de frames.

Resumo

  • O Compose 1.12 apresenta gradientes de malha para misturas multicoloridas em uma grade 2D de vértices. No Android 14 e versões mais recentes, é possível usar a API drawMesh nativa para criar efeitos visuais personalizados baseados em geometria.
  • O Compose oferece suporte a gamas de cores amplas para proporcionar uma mistura de gradiente mais suave em hardware compatível. Ative o modo de gama ampla de cores na sua atividade ou janela apenas ao exibir rich media.
  • O Compose 1.13 apresenta as APIs nativas PolygonShape e MorphPolygonShape para geometrias personalizadas. MorphPolygonShape lê o progresso durante a fase de resolução do contorno, atualizando as formas frame a frame sem acionar a recomposição.
  • O Compose 1.13 adiciona o desfoque progressivo, enquanto o nível 37.2 da API apresenta RenderNode.setBackdropRenderEffect para amostragem de conteúdo em segundo plano. Os desenvolvedores podem encadear efeitos de plano de fundo para criar efeitos de vidro fosco ou de ampliação.
  • Otimize o desempenho gráfico evitando trabalho desnecessário em frames. Decodifique apenas os pixels de imagem necessários fora da linha de execução principal e envolva os efeitos de animação em um graphicsLayer.

Recursos

Gemini Notebook

Tópicos

  • jetpack-compose
  • android-graphics
  • mesh-gradients
  • shaders
  • desfoque
  • color-gamut
  • desempenho

Transcrição

Introdução aos recursos gráficos no Jetpack Compose

  • Olá, pessoal. Sejam bem-vindos à nossa sessão "Como melhorar seus apps com os recursos gráficos mais recentes do Compose e do Android".

  • Meu nome é Rebecca Franks, e hoje estou aqui com…

  • Meu nome é Aayush Chaudhary.

  • Olá, meu nome é Rahul Ray.

  • Estamos vendo uma tendência emergente em como vocês estão ultrapassando os limites do que é possível no Android e no Compose, usando a IA para dar vida às suas ideias incríveis.

  • E adoramos ver apps que parecem premium, nativos e agradáveis.

  • Sabemos que os apps que agradam e recebem avaliações consistentemente boas dos usuários são aqueles que geralmente aproveitam as mudanças mais recentes na API do sistema, têm continuidade entre as plataformas com elementos compartilhados e desfoque, se adaptam de forma agradável a todos os formatos, têm bom desempenho, não deixam os usuários em dúvida e vão além do que é possível com o dispositivo.

  • Nesta palestra, vamos mostrar alguns dos recursos mais recentes que ajudam você a aproveitar o Android, desde encantar os usuários com gradientes de malha, ativar o suporte a uma ampla gama de cores, transições incríveis com formas poligonais, as novidades em desfoque e, por fim, vamos falar sobre como você pode pensar no desempenho ao adicionar novos recursos gráficos aos seus apps.

  • Mas, ao longo da palestra de hoje, vamos falar sobre algumas das novas APIs em diferentes níveis da pilha.

  • Embora o kit de ferramentas de interface de visualização legado esteja em modo de manutenção, continuamos investindo nos mecanismos de renderização de hardware gráfico do Android que alimentam o Jetpack Compose.

  • Os gráficos no Android podem ser divididos em três camadas: as APIs do Compose na primeira camada, que é onde ficam todas as novas APIs de gráficos de interface de alto nível, como as APIs de pintura de gradiente de malha, formas poligonais e desfoque progressivo.

  • Depois, temos a camada intermediária, em que o DrawScope do Compose, os nós modificadores de desenho e a AGSL permitem criar efeitos personalizados.

  • Por fim, temos a plataforma Android, e o Compose sempre pode acessar um Canvas nativo e usar as novas primitivas de renderização do SO diretamente nos modificadores do Compose.

  • Vamos mostrar alguns exemplos de como fazer isso hoje.

  • Vamos lá.

Gradientes de malha e renderização de malha personalizada

  • Primeiro, vamos analisar os gradientes de malha, uma nova API introduzida no Compose 1.12.

  • Antes, tínhamos gradientes lineares, de varredura e radiais, que são ótimos para transições simples. No entanto, se você quiser misturas multicoloridas ricas, como planos de fundo ambientais ou efeitos de iluminação, precisará de uma grade 2D de sistema em que as posições e as cores possam se misturar no espaço.

  • Para criar um gradiente de malha como esse, primeiro especifique quantas linhas e colunas de patches você quer.

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
    }
    }
  • Em seguida, dentro da lambda final, chame setVertex e atribua um deslocamento normalizado que varia de 0F a 1F, além de uma cor para cada interseção da grade.

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
       setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
    
    }
    }
  • Para animar esses vértices, adicione deslocamentos animados para oito vértices.

    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)
    )
  • Depois que o pintor for definido, basta chamá-lo com qualquer elemento combinável usando um modificador de pintura.

  • Agora, no app JetChat, adicionamos um recurso para marcar uma mensagem com "Gostei" ao tocar duas vezes nela.

  • Para criar isso, configuramos uma malha de 2 por 3 e colocamos as paradas de cor no 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
    }
  • Observe que definimos hasBicubicColor como verdadeiro aqui.

  • Por padrão, as cores são interpoladas linearmente entre os triângulos, o que às vezes pode deixar manchas visíveis.

  • Ativar essa opção muda o sombreador para uma interpolação bicúbica suave, de modo que as cores se misturem perfeitamente.

  • E se você quiser criar um brilho ambiente animado como este atrás da caixa de entrada?

  • Para criar um efeito como esse com o gradiente de malha, você precisa de duas coisas.

  • Primeiro, um movimento orgânico suave para os vértices internos e uma maneira de suavizar as bordas externas para que você não acabe com um retângulo rígido ao redor do card.

  • Então, começamos configurando um rememberInfiniteTransition com dois ângulos de fase em durações ligeiramente diferentes para que o movimento pareça orgânico, não mecânico.

    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",
    )
  • Aqui, animamos os vértices internos e fixamos os anéis externos, o anel externo dos vértices com arestas com alfa 0.

  • Como o limite externo é fixo e transparente, o brilho se espalha naturalmente.

  • Por fim, alimentamos esses deslocamentos animados no 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])
           }
       }
    }
  • O gradiente de malha que você acabou de ver é desenhado usando a API drawMesh no Android 14 e versões mais recentes.

  • Vamos analisar essa API mais de perto criando um efeito do zero.

  • Para a tela inicial do JetChat, queremos desenhar um caminho que se pareça com uma fita.

  • Observe duas coisas aqui: a largura do traço não é constante e a cor acompanha cada traço do caminho.

  • Vamos analisar isso e entender o que realmente precisamos para criar esse efeito.

  • Para cada pixel, precisamos decidir se ele será desenhado ou não e qual cor usar.

  • Para descobrir isso, precisamos de duas distâncias.

  • O primeiro valor é a distância de um pixel em relação ao centro do traço.

  • A segunda distância é o quanto um pixel está ao longo do caminho, do início ao fim.

  • Isso informa qual cor escolher do gradiente, e misturar um pouco do primeiro valor faz com que a cor se curve ao longo da largura.

  • Podemos desenhar um gradiente ao longo de um caminho usando apenas Path e Paint?

  • Então, sim, podemos.

  • Isso requer a simplificação de um trajeto em uma lista de linhas retas curtas, o que também fornece a distância ao longo do trajeto.

    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,
        )
    //...
    }
  • Em seguida, desenhamos cada linha com o próprio gradiente.

  • Isso funciona, mas cada linha requer uma chamada de desenho separada e um objeto de pintura, e todo o caminho ainda tem uma única largura.

  • A outra opção é um sombreador de ambiente de execução, mas ele só recebe a posição dos pixels dentro do limite do contêiner.

  • Ele não diz o que precisamos para desenhar essa fita.

  • E não podemos calculá-lo facilmente a partir da posição dos pixels.

  • O Draw mesh nos dá um sombreador de vértice para isso.

  • Então, o que é uma malha?

  • Uma malha é composta por triângulos com dados em cada canto, e cada canto é chamado de vértice.

  • Cada vértice carrega um pequeno registro de dados.

  • Normalmente, esse registro mantém uma posição, mas isso não é obrigatório.

  • O sombreador de vértice lê esse registro e calcula a posição com base nele.

  • Este é o caminho que um triângulo segue: o sombreador de vértice é executado para cada canto, calcula a posição dos cantos e qualquer valor que queiramos transmitir para cada pixel.

  • Em seguida, o rasterizador encontra cada pixel coberto por esse triângulo e interpola os valores desses três cantos para cada pixel.

  • Por fim, o sombreador de fragmentos é executado para esses pixels, usa esses valores interpolados e calcula a cor dos pixels.

  • Vamos ver como escrever um sombreador de vértice e fragmento com um exemplo mais simples: uma linha reta com um pulso se movendo ao longo dela.

    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;
    }
  • O sombreador de vértice recebe um atributo como entrada e retorna uma variável.

  • Os atributos são os dados de um único canto.

  • Aqui estão apenas dois números: o progresso ao longo da linha e de que lado da linha ele está.

  • As variáveis são interpoladas e transmitidas para o sombreador de fragmento.

  • Calculamos a distância em pixels do início e, para a largura do traço, usamos o tempo para criar um pulso no raio.

  • Em seguida, multiplicamos esse raio pelo lado do canto, o que nos dá a distância do sinal ao longo do traço.

  • À medida que a GPU interpola entre esses sinais, as linhas centrais se tornam zero.

  • Por fim, calculamos a posição dos cantos, armazenamos todos esses valores em uma variável de traço e os transmitimos ao sombreador de fragmentos.

  • Ao contrário do sombreador de ambiente de execução, um sombreador de fragmento de malha gera uma cor por um parâmetro e retorna uma posição para amostrar a pintura, se houver uma definida.

    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;
    }
  • O valor absoluto da coordenada transversal fornece a distância em pixels da linha central.

  • Comparamos essa distância com o raio para obter a cobertura e, para a cor, misturamos ao longo do caminho da linha e curvamos o gradiente ligeiramente em direção à borda.

  • Multiplique essa cobertura por uma cobertura, e essa será nossa cor final.

  • No Compose, vinculamos nossos shaders e o layout de dados ao objeto MeshSpecification.

    val spec = remember {
        MeshSpecification.make(
            arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
            vertexStride = 8,
            arrayOf(Varying(TYPE_FLOAT3, "stroke")),
            LineShaders.VERTEX, LineShaders.FRAGMENT)
    }
  • Primeiro, declaramos nosso atributo de canto como dois pontos flutuantes que começam no byte 0.

  • Como dois floats ocupam 8 bytes, cada stride de canto no buffer é 8.

  • Em seguida, declaramos as variações de traço como três pontos flutuantes e transmitimos os sombreadores de vértice e de fragmento que acabamos de escrever.

  • Criamos o buffer de vértice.

    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
    }
  • Em cada etapa da linha, concluímos o progresso, calculamos o progresso de 0 a 1 e gravamos dois cantos no buffer.

  • Por fim, combinamos a especificação e o buffer em um objeto de malha.

    val mesh = Mesh(
        spec,
        mode = Mesh.TRIANGLE_STRIP,
        vertexBuffer = corners,
        vertexCount = (segments + 1) * 2,
        bounds = RectF(0f, 0f, size.width, size.height)
    )
  • A tela do Compose ainda não expõe drawMesh. Por isso, vamos usar nativeCanvas e desenhar a malha.

    Box(Modifier.fillMaxSize().drawWithCache {
       val mesh = Mesh(...)
       onDrawBehind {
           mesh.setFloatUniform("uTime", time.value)
           drawIntoCanvas {
              it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
           }
       }
    })
  • Como você controla a geometria e o pixel, é possível desenhar qualquer efeito visual orientado por geometria usando a malha de desenho.

  • Saiba mais sobre essa API na documentação.

Suporte a uma ampla gama de cores

  • No Compose 1.12, introduzimos o suporte a gamas de cores amplas.

  • O que isso significa?

  • Você sabia que as telas podem reproduzir uma gama de cores diferente do espaço de cores sRGB tradicional?

  • Por exemplo, se você olhar para esta imagem, vai notar que o triângulo branco é o intervalo de cores sRGB padrão que a maioria das telas pode exibir.

  • No entanto, as telas mais novas são compatíveis com uma variedade maior de cores, como as representadas pelo triângulo amarelo, algumas mais saturadas e vibrantes.

  • Mas, quando representamos uma cor, na verdade estamos fazendo isso com a descrição de cada vértice desse triângulo.

  • Ao analisar essa comparação de gradientes, é importante distinguir entre a gama de exibição e a interpolação de gradientes.

  • Em uma tela sRGB padrão ou neste projetor, provavelmente, a tela não consegue renderizar fisicamente as cores fora dessa gama sRGB.

  • É muito difícil demonstrar isso para vocês no palco agora.

  • Então, você vai ter que confiar em mim quando eu digo que as cores ficam melhores no Android com suporte a uma ampla gama de cores.

  • Neste exemplo, o que você está vendo é o efeito de interpolar as paradas de cor em um espaço de cores mais amplo.

  • A mistura de cores em um espaço de cores amplo ou linear muda o caminho matemático real desse gradiente, evitando o acinzentamento e as faixas ásperas que podem acontecer quando você faz isso sem uma ampla gama de cores.

  • Com o Compose 1.12, você tem o benefício de uma mistura de gradiente mais suave em qualquer tela, além de suporte real a uma ampla gama de cores ao executar em um hardware compatível.

  • Para ativar o suporte correto no dispositivo, verifique se a atividade ou janela está configurada para ativar o modo de gama ampla de cores.

  • Isso pode ser feito no AndroidManifest.

    <!-- AndroidManifest.xml -->
    <activity
    android:name=".NavActivity"
    android:colorMode="wideColorGamut"
    android:exported="true">
    </activity>
  • Ou de forma programática no elemento combinável.

    // Dynamic toggle in code
    DisposableEffect(window) {
       window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
       onDispose {
           window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
       }
    }
  • Sem definir o colorMode, a janela permanece no sRGB padrão de 8 bits, e todas as cores de ampla gama renderizadas pela GPU são cortadas.

  • Por isso, é importante evitar o uso de cores de gama ampla se a tela não for compatível.

  • Para economizar bateria e melhorar o desempenho, só ative as gamas amplas de cores em atividades ou caminhos de código que realmente mostrem mídia, fotos ou gráficos avançados.

  • Isso significa que você pode criar cores em diferentes espaços de cores no Compose, e elas serão respeitadas.

    // 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
    )
  • É possível transmitir o tipo de colorSpace em que você quer que a cor seja criada. Se você não transmitir nenhum colorSpace, o sRGB padrão será usado.

  • Para Bitmaps especificamente, é possível verificar qual é a origem do Bitmap usando getColorSpace, e as bibliotecas de carregamento de imagens mais usadas vão processar automaticamente os espaços de cores corretos do Bitmap para você.

  • Use Bitmap.Config.HARDWARE para preservar as informações de cor, já que uma configuração como 565 perde precisão e tem faixas visíveis para os usuários.

  • Para saber mais sobre cores, você pode voltar no tempo até 2017 para assistir Romain Guy falar sobre cores no Compose ou no Android.

Formas poligonais e transformação

  • Com o Compose 1.13, estamos introduzindo suporte nativo para formas poligonais.

  • Isso vai facilitar muito a criação de formas personalizadas e elementos da interface com aparência orgânica.

  • Antes, se você quisesse desenhar uma geometria complexa como essa, precisava usar RoundedPolygon para descrever uma geometria que gerasse um caminho e, em seguida, mapear esse caminho para uma API de forma do 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)
        )
    }
  • Por exemplo, primeiro você define as proporções usando um RoundedPolygon e, neste snippet, definimos os pontos internos como 84% do centro em comparação com os pontos externos.

  • Também é possível aplicar o arredondamento de cantos aqui, mas isso só descreve as curvas matemáticas.

  • Para usar esse polígono para recorte ou planos de fundo no Compose, é necessário escrever um wrapper de forma personalizado como este.

    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)
        }
    }
  • E, dentro desse snippet createOutline, você pode ver que é preciso criar o caminho e dimensionar e traduzir manualmente.

  • Agora, com a nova API PolygonShape, você não precisa mais desse wrapper de forma personalizado.

    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)
  • Como esse é o builder dedicado do Compose Shape, ele recebe a densidade e o tamanho diretamente.

  • Isso significa que você pode calcular os vértices de forma dinâmica e especificar o arredondamento de cantos diretamente em dp ou até mesmo como uma porcentagem.

  • Depois de definido, basta transmiti-lo para qualquer API que use um Shape.

  • Também estamos lançando a API MorphPolygonShape, que permite fazer a transição entre diferentes formas de polígono.

    val recordShape = remember {
        MorphPolygonShape(
            start = PolygonShape.circle(),
            end = PolygonShape.star(
                numPoints = 9,
                innerRadiusRatio = 0.8f,
                outerRounding = CornerRounding(percent = 50),
            ),
        ) { shapeProgress.value }
    }
  • Neste exemplo, temos um botão de gravação que se transforma de um círculo em uma estrela quando você interage com ele.

  • Basta fornecer a forma inicial, a forma final e o valor de progresso.

  • A API MorphPolygonShape lê o valor de progresso durante a fase de resolução do contorno, ou seja, não durante a recomposição, não durante a composição.

  • Isso significa que, à medida que a forma se transforma entre os frames, ela simplesmente se resolve em um novo contorno sem acionar uma recomposição.

Efeitos de renderização de desfoque e plano de fundo

  • Agora vamos falar sobre o desfoque.

  • Nas últimas versões, tentamos tornar a implementação de desfoques muito mais fácil e expressiva.

  • Sempre que falamos sobre desfoque, esse é o modificador usado com frequência.

    Modifier
       .blur(
           radiusX = 10.dp,
           radiusY = 10.dp,
           edgeTreatment = BlurredEdgeTreatment(
               shape = RoundedCornerShape(8.dp),
           ),
       )
  • O Compose tem esse modificador desde o Android 12.

  • Ele funciona muito bem quando você quer um desfoque uniforme, em que o mesmo raio de desfoque é aplicado em todo um elemento combinável.

  • No entanto, os designs de interface modernos geralmente exigem uma transição suave de regiões nítidas para desfocadas, em vez de um corte abrupto.

  • E isso nos leva ao desfoque progressivo, que agora é uma API oficial a partir do Compose 1.13.

  • Em vez de um único raio constante em todo o elemento combinável, agora é possível variar a intensidade do desfoque no espaço.

  • Este é o exemplo do nosso app JetChat na tela do perfil do usuário.

  • O novo modificador de desfoque usa uma especificação de raio de desfoque.

    Modifier.blur {
    blurRadiusSpec = BlurRadiusSpec.verticalGradient(
    listOf(
    BlurStop(0.5f, 0.dp),
    BlurStop(1.0f, 32.dp),
    )
    )
    edgeTreatment = BlurredEdgeTreatment.Unbounded
    }
  • Aqui, usamos um gradiente vertical com dois pontos de desfoque, mantendo a metade de cima da foto completamente nítida em 0 dp e aumentando para um desfoque de 32 dp na borda de baixo.

  • Observe que também definimos edgeTreatment como ilimitado, o que basicamente diz ao desfoque para usar pixels transparentes fora dos limites do elemento combinável.

  • Além dos fade-outs verticais, essa especificação de raio de desfoque oferece várias maneiras integradas de aplicar o desfoque progressivo.

  • Por exemplo, à esquerda, você pode ver o gradiente horizontal, em que o raio inicial é de 0 dp e o raio final é determinado por um valor de controle deslizante.

    BlurRadiusSpec.horizontalGradient(
    startRadius = 0.dp,
    endRadius = sliderValue.dp,
    )
  • À direita, temos um gradiente radial, em que o raio de desfoque é de 0 dp no centro e, conforme você se move em direção ao fallOffRadius, que é de 300 dp neste caso, o desfoque aumenta gradualmente até 24 dp.

    BlurRadiusSpec.radialGradient(
    center = center,
    fallOffRadius = 300.dp,
    startRadius = 0.dp,
    endRadius = 24.dp,
    )
  • E se todos esses gradientes verticais, radiais e horizontais não forem suficientes, você sempre poderá usar sombreadores personalizados para criar o desfoque progressivo.

    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
       },
  • Aqui, escrevemos um pequeno sombreador de fragmento que calcula uma onda senoidal diagonal animada e gera essa intensidade em um canal Alfa.

  • Em seguida, a especificação do sombreador mapeia essa máscara de 0 a 1 diretamente para uma máscara de 24 dp maxRadius.

  • Antes de falar sobre o desfoque de plano de fundo, vamos ver a diferença entre os dois.

  • Agora, à esquerda, temos uma barra de apps de cima em que aplicamos o desfoque de camada.

  • Ou seja, ele desfoca o próprio elemento combinável.

  • Como você pode ver, o título e os ícones de ação estão desfocados, mas o componente de chat atrás deles não está.

  • À direita, temos o desfoque do plano de fundo.

  • Aqui, apenas as mensagens de chat que passam por trás da barra superior são amostradas e desfocadas.

  • No entanto, o texto e os ícones estão completamente nítidos e legíveis.

  • No Android 37.2, é possível desfocar o plano de fundo usando uma nova API chamada RenderNode.setBackdropRenderEffect.

  • Mas, antes de entender como o desfoque está melhorando, vamos entender como o Android funciona nos bastidores para garantir que o desfoque seja eficiente.

  • O Android usa RenderNodes, que são contêineres de lista de exibição acelerados por hardware. Isso significa que eles podem compactar e armazenar vários comandos de desenho diferentes usando as funções beginRecording e endRecording.

  • O RenderNode contém informações como translações 3D, rotações, alfa, escalas e limites de posição.

  • O Compose usa o RenderNode em segundo plano.

  • Para usar os efeitos de renderização de plano de fundo no Compose, podemos criar um Modifier.Node personalizado para reutilizar esse modificador.

    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()
       }
    }
  • Mantemos uma referência a um RenderNode, que RenderNode é configurado com os efeitos que queremos em outra função.

  • Por exemplo, passamos um efeito de desfoque aqui.

  • Em seguida, chamamos drawIntoCanvas, que chama drawRenderNode na tela nativa.

  • Por fim, chamamos drawContent para desenhar o conteúdo real sem desfoque na parte de cima.

  • Para usar esse modificador que acabamos de criar, chamamos Modifier.backdropBlur e transmitimos uma especificação de raio de desfoque.

    JetchatAppBar(
        modifier = modifier
            .backdropBlur(
                tint = MaterialTheme.colorScheme
                    .surface.copy(alpha = 0.5f),
                shape = RectangleShape,
                spec = BlurRadiusSpec.uniform(5.dp)
            )
    )
  • Observe que não precisamos colocar nada na camada de base aqui com essa abordagem.

  • O RenderNode.setBackdropRenderEffect está automaticamente fazendo uma amostragem do conteúdo que fica abaixo da superfície da barra de apps do JetChat.

  • O resultado é esse desfoque de fundo incrível.

  • Também podemos definir uma especificação de raio de desfoque progressivo em vez de apenas um progressivo e para conseguir um desfoque vertical no conteúdo.

    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),
                    ),
                )
            )
    )
  • Com um gradiente vertical, ele mostra o conteúdo abaixo desfocado com raios diferentes em diferentes frações do contêiner.

  • Estamos trabalhando em um modificador de desfoque de fundo integrado no Compose.

  • Esse é apenas um exemplo da tecnologia subjacente no Android com o Compose.

  • Esse exemplo que mostrei só funcionaria no nível 37.2 da API, mas também estamos trabalhando em uma retrocompatibilidade para o Compose. Fique de olho.

  • Outro exemplo divertido com os efeitos de plano de fundo do RenderNode é que podemos encadeá-los para formar interfaces muito bonitas e que realizam vários efeitos diferentes.

  • Para encadear efeitos, criamos esses dois efeitos.

    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)
  • Neste exemplo, criamos um efeito de desfoque e uma saturação de 30%.

  • Chamamos createChainEffect com o efeito de desfoque sendo aplicado primeiro e a saturação depois.

  • Este é um exemplo no JetChat.

    val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
    val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
    
    val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
    renderNode?.setBackdropRenderEffect(finalEffect)
  • O botão de pular para o final, que pode parecer uma pequena lupa ao combinar alguns efeitos diferentes para que as bordas desse elemento combinável fiquem arredondadas como a lupa.

  • Mas não é só isso: qualquer tipo de sombreador pode ser encadeado para ser aplicado a um plano de fundo.

  • Confira alguns exemplos divertidos que implementamos do que é possível fazer ao combinar os dois.

  • Agora é possível usar o efeito tradicional de vidro fosco.

  • Mas também podemos fazer outros efeitos divertidos, como um caleidoscópio.

  • Ou, se quiséssemos transformar esse mascote do Android em um tipo de elemento da interface que se transforma e muda o conteúdo abaixo, também poderíamos aplicar esses efeitos de renderização de plano de fundo para que pareça que estamos olhando através desse pequeno droid.

  • Também podemos fazer isso com os efeitos de renderização do plano de fundo.

  • Portanto, não apenas o desfoque de fundo e o vidro fosco foram desbloqueados com essa nova API, mas agora o único limite é sua imaginação.

  • Outra nova API que introduzimos na plataforma é a SurfaceView.setBlurRegions.

  • Atualmente, é possível desfocar visualizações de textura e qualquer elemento combinável usando os efeitos de plano de fundo Modifier.blur e RenderNode. No entanto, desfocar partes de uma SurfaceView é complicado e não faz parte da árvore RenderNode da visualização do app, porque, na verdade, ela abre um buraco na janela.

  • Portanto, o Compose não pode fazer uma amostragem do buffer de backup da SurfaceView, o que era impossível até a API 37.2.

  • Portanto, o setBlurRegions da SurfaceView permite que o app declare regiões de desfoque diretamente.

  • Por exemplo, neste player de vídeo no JetChat, adicionamos o desfoque de superfície SurfaceView quando os controles aparecem e desaparecem.

  • Para usar essa nova API, acesse o setBlurRegions em um método 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
  • Isso usa a lista de áreas de limites, raio de desfoque, alfa e uma lista de raios de canto.

  • Ao transmitir esses limites, eles precisam ser relativos à SurfaceView na tela.

  • Portanto, é preciso acompanhar as coordenadas da SurfaceView e a área que você quer desfocar.

RuntimeColorFilter

  • Outra novidade do Android 16 e versões mais recentes é o RuntimeColorFilter.

  • Antes, com as imagens, só era possível definir um filtro de cor de matriz estática e fazer efeitos como saturação.

  • No entanto, com o Android 16 e versões mais recentes, agora é possível transmitir um sombreador que usa uma cor de entrada, o pixel de entrada para esse conteúdo específico, e produz uma nova cor.

  • Por exemplo, se quiséssemos aplicar um filtro cyberpunk a uma foto, poderíamos definir um pequeno sombreador que é usado como entrada no 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);
        }
    """
  • Esse sombreador produz um tema cyberpunk, mas você pode criar qualquer outro efeito que quiser.

  • Para usar o sombreador, aplicamos o RuntimeColorFilter e o convertemos em um filtro de cor do Compose.

    AsyncImage(
        model = imageUri,
        contentDescription = filter.displayName,
        colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
                       .asComposeColorFilter(),
        contentScale = ContentScale.Crop,
        modifier = Modifier.fillMaxSize(),
    )
  • Como você pode ver neste exemplo, agora podemos criar efeitos mais dinâmicos do que antes sem precisar nos aprofundar no OpenGL, nas configurações do SurfaceView ou em qualquer coisa desse tipo.

  • Isso também pode ser aplicado a qualquer elemento combinável, não apenas a imagens.

Otimização de desempenho gráfico

  • Nossa equipe agradece.

  • Criamos um efeito muito legal, mas vamos garantir que o app ainda seja rápido e suave.

  • Quando rolamos a conversa, todos os efeitos são executados juntos.

  • Vamos começar medindo essa rolagem.

    @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()
        }
    //...
  • Usamos uma macro de comparativo de mercado que inicia um app em um processo novo e rola pela conversa.

  • Em seguida, consultamos o rastro para contar os frames que não cumprem os prazos.

    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%'
  • Isso nos dá seis frames atrasados.

  • Vamos abrir um dos frames atrasados.

  • Portanto, logo antes desse frame, o Compose está fazendo a pré-busca da próxima mensagem antes que ela seja mostrada na visualização.

  • A pré-busca deve se encaixar no curto intervalo entre os frames, mas a preparação dessa mensagem também decodifica a imagem diretamente na linha de execução principal, que é executada por muito tempo sem gerar resultados.

  • Como a linha de execução principal está travada decodificando essa imagem, nenhum frame novo pode ser iniciado.

  • Depois, quando essa mensagem é mostrada na tela, a RenderThread precisa alocar memória e fazer upload de todos esses pixels para a GPU. Assim, a mesma imagem atrasa um segundo frame.

  • Agora, sabemos para onde nosso tempo foi, mas será que precisamos de todo esse trabalho para desenhar esse frame?

  • E como podemos comprovar?

  • O trace nos informa.

  • Sempre que um bitmap grande é desenhado em uma área muito menor na tela, o Android registra um marcador diretamente no rastro.

  • Ela mostra dois números em pixels: a dimensão completa do bitmap e a área que ele realmente cobre na tela.

  • Divida esse valor pela densidade da tela para obter o valor em DP.

  • Em seguida, pesquisamos nosso código com esse valor de DP para encontrar a imagem.

  • É isso que encontramos.

  • A imagem é carregada com painterResource, que não sabe o tamanho do contêiner. Por isso, ela é decodificada em resolução máxima.

    Image(
        painter = painterResource(it),
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(160.dp),
        contentDescription = stringResource(id = R.string.attached_image),
    )
  • Por isso, vamos substituí-lo por AsyncImage da Coil.

    AsyncImage(
        model = it,
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(160.dp),
        contentDescription = stringResource(id = R.string.attached_image),
    )
  • Ele mede o contêiner primeiro para decodificar apenas o pixel necessário, e faz isso em uma linha de execução diferente.

  • Ao executar o mesmo comparativo de mercado novamente, a decodificação de imagens na linha de execução principal caiu para 0, e o frame atrasado caiu para 2.

  • Não vamos depurar esses dois frames aqui, mas a conclusão é simples: tente não calcular pixels que nunca vão chegar ao frame.

  • A decodificação de imagens era grande o suficiente para atrasar os frames, mas nem todo custo aparece como um frame perdido, e ainda há custos menores que podemos evitar ao escrever o código.

  • Vamos conferir duas delas.

  • Sempre que usarmos um RuntimeShader, ele precisará ser criado uma vez, porque, embora seja compilado e armazenado em cache, ele ainda analisa a string de origem sempre que é criado.

  • E sempre que um efeito anima cada frame, podemos envolvê-lo em um graphicsLayer para que o único elemento de animação seja regravado, em vez de redesenhar a interface ao redor dele.

  • O botão "Ir para o final" aparece sempre que rolamos a tela.

  • Isso usa dois RuntimeShader para mostrar esse efeito, e sempre que o botão aparece, recriamos os dois shaders da string de origem.

    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 {
  • A análise desses dois sombreadores e a criação do efeito levam centenas de microssegundos na linha de execução principal sempre que o botão aparece.

  • Em vez disso, criamos o sombreador uma vez para todo o processo ou podemos lembrá-lo e atualizar o 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) }
  • Com essa mudança, a criação desse efeito é quase 40 vezes mais rápida, e o custo quando o botão aparece cai 20 vezes.

  • No brilho do gradiente de malha que vimos antes, o brilho anima cada frame dentro do 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)
    }
  • Portanto, ler o estado na fase de desenho mantém a composição e o layout, mas a lista de exibição não pode atualizar um único comando de desenho no local.

  • Então, o Compose sobe na árvore para encontrar o nó mais próximo com a camada de gráficos, que aqui é o contorno vermelho, que é a tela inteira, e regrava toda essa camada a cada frame.

  • Ao colocar um graphicsLayer diretamente na caixa, o percurso para no contorno verde, de modo que apenas a caixa de brilho é regravada, e a fração de desenho da linha de execução principal cai em dois décimos de milissegundo.

    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)
    }
  • Com o Perfetto, pode ser difícil analisar o que dá errado no trace. Por isso, use a habilidade de análise de trace para coletar alguns insights.

  • Se você quiser mais ajuda para escrever um código do Compose de alto desempenho, instale a CLI do Android e use a habilidade de desempenho do Compose para começar.

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

Resumo e recursos

  • Se quiser ver os exemplos que mostramos hoje, confira o app JetChat, disponível neste link.

Código

Snippets de código

Snippet do Kotlin em 02:47 em "Gradientes de malha e renderização de malha personalizada"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}
Snippet do Kotlin às 02:55 em "Gradientes de malha e renderização de malha personalizada"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
   setVertex(0, 0, Offset(0.0f, 0.0f), indigo)

}
}
Snippet do Kotlin aos 3:05 em "Gradientes de malha e renderização de malha personalizada"
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 em Kotlin aos 03:24 em "Gradientes de malha e renderização de malha personalizada"
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 do Kotlin em 04:09 em "Gradientes de malha e renderização de malha personalizada"
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 do Kotlin em 04:33 em "Gradientes de malha e renderização de malha personalizada"
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 do Kotlin em 05:40 em "Gradientes de malha e renderização de malha personalizada"
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,
    )
//...
}
Trecho de texto em 07:03 em "Gradientes de malha e renderização de malha personalizada"
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;
}
Trecho de texto em 08:01 em "Gradientes de malha e renderização de malha personalizada"
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 do Kotlin às 08:37 em "Gradientes de malha e renderização de malha personalizada"
val spec = remember {
    MeshSpecification.make(
        arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
        vertexStride = 8,
        arrayOf(Varying(TYPE_FLOAT3, "stroke")),
        LineShaders.VERTEX, LineShaders.FRAGMENT)
}
Snippet do Kotlin em 09:06 em "Gradientes de malha e renderização de malha personalizada"
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 do Kotlin em 09:18 em "Gradientes de malha e renderização de malha personalizada"
val mesh = Mesh(
    spec,
    mode = Mesh.TRIANGLE_STRIP,
    vertexBuffer = corners,
    vertexCount = (segments + 1) * 2,
    bounds = RectF(0f, 0f, size.width, size.height)
)
Snippet do Kotlin em 09:24 em "Gradientes de malha e renderização de malha personalizada"
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 em 11:22 em "Suporte a ampla gama de cores"
<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>
Snippet do Kotlin às 11:24 em "Suporte a ampla gama de cores"
// Dynamic toggle in code
DisposableEffect(window) {
   window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
   onDispose {
       window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
   }
}
Snippet do Kotlin em 11:49 em "Suporte a ampla gama de cores"
// 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 do Kotlin aos 12:45 em "Formas poligonais e transformação"
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 do Kotlin em 13:15 em "Formas poligonais e transformação"
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 do Kotlin aos 13:30 em "Formas poligonais e transformação"
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 do Kotlin em 13:53 em "Formas de polígono e transformação"
val recordShape = remember {
    MorphPolygonShape(
        start = PolygonShape.circle(),
        end = PolygonShape.star(
            numPoints = 9,
            innerRadiusRatio = 0.8f,
            outerRounding = CornerRounding(percent = 50),
        ),
    ) { shapeProgress.value }
}
Snippet do Kotlin aos 14:37 em "Efeitos de renderização de desfoque e plano de fundo"
Modifier
   .blur(
       radiusX = 10.dp,
       radiusY = 10.dp,
       edgeTreatment = BlurredEdgeTreatment(
           shape = RoundedCornerShape(8.dp),
       ),
   )
Snippet do Kotlin aos 15:15 em "Efeitos de renderização de desfoque e plano de fundo"
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}
Snippet do Kotlin aos 15:42 em "Efeitos de desfoque e renderização de plano de fundo"
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)
Snippet do Kotlin aos 15:53 em "Efeitos de desfoque e renderização de plano de fundo"
BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)
Snippet do Kotlin às 16:07 em "Efeitos de renderização de desfoque e plano de fundo"
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 do Kotlin aos 17:40 em "Efeitos de desfoque e renderização de plano de fundo"
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 do Kotlin aos 18:09 em "Efeitos de desfoque e renderização de plano de fundo"
JetchatAppBar(
    modifier = modifier
        .backdropBlur(
            tint = MaterialTheme.colorScheme
                .surface.copy(alpha = 0.5f),
            shape = RectangleShape,
            spec = BlurRadiusSpec.uniform(5.dp)
        )
)
Snippet do Kotlin aos 18:30 em "Efeitos de renderização de desfoque e plano de fundo"
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 do Kotlin em 19:10 em "Efeitos de desfoque e renderização de plano de fundo"
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 do Kotlin aos 19:23 em "Efeitos de renderização de desfoque e plano de fundo"
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")

val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)
Snippet do Kotlin aos 20:53 em "Efeitos de desfoque e renderização de plano de fundo"
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 do Kotlin aos 21:33 em "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 do Kotlin aos 21:48 em "RuntimeColorFilter"
AsyncImage(
    model = imageUri,
    contentDescription = filter.displayName,
    colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
                   .asComposeColorFilter(),
    contentScale = ContentScale.Crop,
    modifier = Modifier.fillMaxSize(),
)
Snippet do Kotlin às 22:21 em "Otimização de desempenho de gráficos"
@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 de SQL em 22:32 em "Otimização de desempenho de gráficos"
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 do Kotlin aos 23:54 em "Otimização de desempenho de gráficos"
Image(
    painter = painterResource(it),
    contentScale = ContentScale.Fit,
    modifier = Modifier.size(160.dp),
    contentDescription = stringResource(id = R.string.attached_image),
)
Snippet do Kotlin em 24:02 em "Otimização de desempenho de gráficos"
AsyncImage(
    model = it,
    contentScale = ContentScale.Fit,
    modifier = Modifier.size(160.dp),
    contentDescription = stringResource(id = R.string.attached_image),
)
Snippet do Kotlin em 25:14 em "Otimização de desempenho de gráficos"
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 do Kotlin em 25:32 em "Otimização de desempenho de gráficos"
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 do Kotlin aos 25:49 em "Otimização do desempenho de gráficos"
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 do Kotlin em 26:15 em "Otimização de desempenho de gráficos"
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 de shell em 26:37 em "Otimização de desempenho de gráficos"
android skills add perfetto-trace-analysis
android skills add jetpack-compose-performance