Modificadores gráficos

Além do elemento combinável Canvas, o Compose tem vários elementos gráficos Modifiers úteis que ajudam a mostrar conteúdo personalizado. Esses modificadores são úteis porque podem ser aplicados a qualquer elemento combinável.

Modificadores de desenho

Todos os comandos de exibição são feitos com um modificador no Compose. Há três modificadores principais de exibição no Compose:

O modificador base para exibição é drawWithContent, em que é possível decidir a ordem de renderização do elemento combinável e os comandos de exibição emitidos dentro do modificador. drawBehind é um wrapper conveniente ao redor de drawWithContent que tem a ordem de exibição definida por trás do conteúdo do elemento combinável. drawWithCache chama onDrawBehind ou onDrawWithContent dentro dele mesmo e fornece um mecanismo para armazenar em cache os objetos criados nesses dois.

Modifier.drawWithContent: escolher a ordem de desenho

O modificador Modifier.drawWithContent permite executar operações DrawScope antes ou depois do conteúdo do elemento combinável. Chame drawContent para renderizar o conteúdo real do combinável. Com esse modificador, é possível decidir a ordem das operações se quiser que o conteúdo seja mostrado antes ou depois das operações de renderização personalizadas.

Por exemplo, se você quiser renderizar um gradiente radial sobre o conteúdo para criar um efeito de lanterna na interface, faça o seguinte:

var pointerOffset by remember {
    mutableStateOf(Offset(0f, 0f))
}
Column(
    modifier = Modifier
        .fillMaxSize()
        .pointerInput("dragging") {
            detectDragGestures { change, dragAmount ->
                pointerOffset += dragAmount
            }
        }
        .onSizeChanged {
            pointerOffset = Offset(it.width / 2f, it.height / 2f)
        }
        .drawWithContent {
            drawContent()
            // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI.
            drawRect(
                Brush.radialGradient(
                    listOf(Color.Transparent, Color.Black),
                    center = pointerOffset,
                    radius = 100.dp.toPx(),
                )
            )
        }
) {
    // Your composables here
}

Figure 1: Modifier.drawWithContent used on top of a Composable to create a flashlight type UI experience.

Modifier.drawBehind: desenho atrás de um elemento combinável

O Modifier.drawBehind permite executar operações DrawScope por trás do combinável mostrado na tela. Na implementação, note que Canvas é apenas um wrapper conveniente em torno de Modifier.drawBehind.

Para renderizar um retângulo arredondado atrás de Text, use:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawBehind {
            drawRoundRect(
                Color(0xFFBBAAEE),
                cornerRadius = CornerRadius(10.dp.toPx())
            )
        }
        .padding(4.dp)
)

O que produz este resultado:

Texto e um plano de fundo renderizados usando Modifier.drawBehind
Figura 2: texto e um plano de fundo renderizados usando Modifier.drawBehind.

Modifier.drawWithCache: exibição e armazenamento em cache de objetos de desenho

Modifier.drawWithCache armazena em cache os objetos criados nele. Os objetos são armazenados em cache desde que o tamanho da área de exibição seja o mesmo ou que os objetos de estado lidos não tenham mudado. Esse modificador é útil para melhorar o desempenho das chamadas de desenho, porque evita a necessidade de realocar objetos (como Brush, Shader, Path e outros) criados na renderização.

Como alternativa, também é possível armazenar objetos em cache usando remember, fora do modificador. No entanto, isso nem sempre é possível porque você nem sempre tem acesso à composição. Pode ser mais eficiente usar drawWithCache se os objetos forem usados apenas para renderização.

Por exemplo, se você criar um Brush para renderizar um gradiente atrás de um Text, o uso de drawWithCache armazenará o objeto Brush em cache até que o tamanho da área de exibição seja modificado:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawWithCache {
            val brush = Brush.linearGradient(
                listOf(
                    Color(0xFF9E82F0),
                    Color(0xFF42A5F5)
                )
            )
            onDrawBehind {
                drawRoundRect(
                    brush,
                    cornerRadius = CornerRadius(10.dp.toPx())
                )
            }
        }
)

Como armazenar o objeto Brush em cache com drawWithCache
Figura 3: armazenamento em cache do objeto Brush com drawWithCache

Modificadores gráficos

Modifier.graphicsLayer: aplicar transformações a elementos combináveis

Modifier.graphicsLayer é um modificador que transforma o conteúdo do combinável em uma camada de desenho. Camadas oferecem algumas funções diferentes, como, por exemplo:

  • Isolamento para as instruções de exibição (semelhante ao RenderNode). As instruções de exibição capturadas como parte de uma camada podem ser reemitidas com eficiência pelo pipeline de renderização sem executar novamente o código do aplicativo.
  • Transformações que se aplicam a todas as instruções de exibição contidas em uma camada.
  • Varredura para recursos de composição. Quando uma camada passa por varredura, as instruções de exibição dela são executadas e a saída é capturada em um buffer fora da tela. A composição desse buffer para frames subsequentes é mais rápida que a execução de instruções individuais, mas ela se comportará como um bitmap quando transformações como escalonamento ou rotação forem aplicadas.

Transformações

Modifier.graphicsLayer isola as instruções de exibição. Por exemplo, várias transformações podem ser aplicadas usando Modifier.graphicsLayer. Elas podem ser animadas ou modificadas sem precisar executar novamente a lambda de exibição.

O Modifier.graphicsLayer não muda o tamanho ou a posição medida do elemento combinável, já que isso afeta apenas a fase de renderização. Isso significa que o elemento combinável pode se sobrepor a outros se for renderizado fora dos limites do layout.

As transformações abaixo podem ser aplicadas com esse modificador:

Escala – aumentar o tamanho

scaleX e scaleY servem para aumentar ou diminuir o conteúdo na direção horizontal ou vertical, respectivamente. Um valor de 1.0f indica que não há mudança na escala. O valor 0.5f representa metade da dimensão.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.scaleX = 1.2f
            this.scaleY = 0.8f
        }
)

Figura 4: scaleX e scaleY aplicados a um elemento combinável de imagem
Translação

translationX e translationY podem ser modificados com graphicsLayer, e translationX move o elemento combinável para a esquerda ou direita. translationY move o combinável para cima ou para baixo.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.translationX = 100.dp.toPx()
            this.translationY = 10.dp.toPx()
        }
)

Figura 5: translationX e translationY aplicados à imagem com Modifier.graphicsLayer
Rotação

Defina rotationX para girar horizontalmente, rotationY para girar verticalmente e rotationZ para girar no eixo Z (rotação padrão). Esse valor é especificado em graus (0 a 360).

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

Figura 6: rotationX, rotationY e rotationZ definidos na imagem pelo Modifier.graphicsLayer
Origem

Uma transformOrigin pode ser especificada. Em seguida, ela é usada como o ponto em que as transformações ocorrem. Até o momento, todos os exemplos usaram TransformOrigin.Center, que é (0.5f, 0.5f). Se você especificar a origem em (0f, 0f), as transformações vão começar do canto esquerdo de cima do elemento combinável.

Se você mudar a origem com uma transformação rotationZ, vai notar que o item gira em torno do canto esquerdo de cima do combinável:

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.transformOrigin = TransformOrigin(0f, 0f)
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

Figura 7: rotação aplicada com TransformOrigin definido como 0f, 0f

Recorte e forma

O formato especifica o contorno do conteúdo que será recortado quando clip = true. Neste exemplo, definimos duas caixas para ter dois recortes diferentes, um usando a variável de recorte graphicsLayer e o outro usando o wrapper conveniente Modifier.clip.

Column(modifier = Modifier.padding(16.dp)) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .graphicsLayer {
                clip = true
                shape = CircleShape
            }
            .background(Color(0xFFF06292))
    ) {
        Text(
            "Hello Compose",
            style = TextStyle(color = Color.Black, fontSize = 46.sp),
            modifier = Modifier.align(Alignment.Center)
        )
    }
    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(CircleShape)
            .background(Color(0xFF4DB6AC))
    )
}

O conteúdo da primeira caixa (texto "Hello Compose") é recortado em formato circular:

Recorte aplicado ao combinável do Box
Figura 8: recorte aplicado ao elemento combinável do Box.

Se você aplicar translationY ao círculo rosa de cima, vai notar que os limites do combinável ainda serão os mesmos, mas ele será mostrado atrás do círculo de baixo (e fora dos limites dele).

Recorte aplicado com translationY e borda vermelha para contorno
Figura 9: recorte aplicado com translationY e borda vermelha para contorno

Para recortar o combinável na região em que ele foi renderizado, é possível adicionar outro Modifier.clip(RectangleShape) no início da cadeia de modificadores. O conteúdo permanece dentro dos limites originais.

Column(modifier = Modifier.padding(16.dp)) {
    Box(
        modifier = Modifier
            .clip(RectangleShape)
            .size(200.dp)
            .border(2.dp, Color.Black)
            .graphicsLayer {
                clip = true
                shape = CircleShape
                translationY = 50.dp.toPx()
            }
            .background(Color(0xFFF06292))
    ) {
        Text(
            "Hello Compose",
            style = TextStyle(color = Color.Black, fontSize = 46.sp),
            modifier = Modifier.align(Alignment.Center)
        )
    }

    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(RoundedCornerShape(500.dp))
            .background(Color(0xFF4DB6AC))
    )
}

Recorte aplicado sobre a transformação graphicsLayer
Figura 10: recorte aplicado à transformação de graphicsLayer

Alfa

Modifier.graphicsLayer pode ser usado para definir um valor alpha (opacidade) para toda a camada. 1.0f é totalmente opaco e 0.0f é invisível.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "clock",
    modifier = Modifier
        .graphicsLayer {
            this.alpha = 0.5f
        }
)

Imagem com o Alfa aplicado
Figura 11: imagem com o Alfa aplicado

Estratégia de composição

Trabalhar com Alfa e transparência pode não ser tão simples quanto mudar um único valor Alfa. Além de mudar um Alfa, também há a opção de definir uma CompositingStrategy em uma graphicsLayer. Uma CompositingStrategy determina como o conteúdo do combinável é colocado na composição com o outro conteúdo já renderizado na tela.

As diferentes estratégias são estas:

Automática (padrão)

A estratégia de composição é determinada pelo restante dos parâmetros graphicsLayer. Ela vai renderizar a camada em um buffer fora da tela se o valor Alfa for menor que 1,0f ou se RenderEffect estiver definido. Sempre que o Alfa for menor que 1f, uma camada de composição vai ser criada automaticamente para renderizar o conteúdo e mostrar esse buffer fora da tela no destino com o Alfa correspondente. Definir um RenderEffect ou uma rolagem esticada sempre renderiza o conteúdo em um buffer fora da tela, independentemente da CompositingStrategy definida.

Fora da tela

O conteúdo do combinável sempre tem varredura para uma textura ou bitmap fora da tela antes da renderização para o destino. Isso é útil para aplicar operações BlendMode para mascarar conteúdo e melhorar o desempenho ao renderizar conjuntos complexos de instruções de exibição.

Um exemplo de uso da CompositingStrategy.Offscreen é com BlendModes. Analisando o exemplo abaixo, vamos supor que você quer remover partes de um combinável Image emitindo um comando de exibição usando BlendMode.Clear. Se a compositingStrategy não for definida como CompositingStrategy.Offscreen, o BlendMode vai interagir com todo o conteúdo abaixo do combinável.

Image(painter = painterResource(id = R.drawable.dog),
   contentDescription = "Dog",
   contentScale = ContentScale.Crop,
   modifier = Modifier
       .size(120.dp)
       .aspectRatio(1f)
       .background(
           Brush.linearGradient(
               listOf(
                   Color(0xFFC5E1A5),
                   Color(0xFF80DEEA)
               )
           )
       )
       .padding(8.dp)
       .graphicsLayer {
           compositingStrategy = CompositingStrategy.Offscreen
       }
       .drawWithCache {
           val path = Path()
           path.addOval(
               Rect(
                   topLeft = Offset.Zero,
                   bottomRight = Offset(size.width, size.height)
               )
           )
           onDrawWithContent {
               clipPath(path) {
                   // this draws the actual image - if you don't call drawContent, it wont
                   // render anything
                   this@onDrawWithContent.drawContent()
               }
               val dotSize = size.width / 8f
               // Clip a white border for the content
               drawCircle(
                   Color.Black,
                   radius = dotSize,
                   center = Offset(
                       x = size.width - dotSize,
                       y = size.height - dotSize
                   ),
                   blendMode = BlendMode.Clear
               )
               // draw the red circle indication
               drawCircle(
                   Color(0xFFEF5350), radius = dotSize * 0.8f,
                   center = Offset(
                       x = size.width - dotSize,
                       y = size.height - dotSize
                   )
               )
           }

       }
)

Ao definir a CompositingStrategy como Offscreen, uma textura vai ser criada fora da tela para executar os comandos (aplicando o BlendMode apenas ao conteúdo desse combinável). Em seguida, ele é renderizado por cima do conteúdo que já está renderizado na tela, sem afetar o resto que está sendo mostrado.

Modifier.drawWithContent em uma imagem mostrando uma indicação de círculo com o BlendMode.Clear dentro do app
Figura 12: Modifier.drawWithContent em uma imagem mostrando uma indicação de círculo com o BlendMode.Clear e CompositingStrategy.Offscreen no app

Se você não usou a CompositingStrategy.Offscreen, os resultados da aplicação de BlendMode.Clear limpam todos os pixels no destino, independente do que estava definido, deixando o buffer de renderização da janela (preto) visível. Muitos dos BlendModes que envolvem Alfa não vão funcionar como esperado sem um buffer fora da tela. Observe o anel preto em volta do indicador de círculo vermelho:

Modifier.drawWithContent em uma imagem mostrando uma indicação de círculo, com o BlendMode.Clear e nenhuma CompositingStrategy definida
Figura 13: Modifier.drawWithContent em uma imagem que mostra uma indicação de círculo com BlendMode.Clear e nenhuma CompositingStrategy definida

Para entender melhor: se o app tivesse um segundo plano de janela translúcido e você não usasse a CompositingStrategy.Offscreen, o BlendMode interagiria com o app inteiro. Ele limparia todos os pixels para mostrar o app ou o plano de fundo por baixo, como neste exemplo:

Nenhuma CompositingStrategy definida e usando BlendMode.Clear com um app que tem um segundo plano de janela translúcido. O plano de fundo rosa é mostrado na área ao redor do círculo de status vermelho.
Figura 14: nenhuma CompositingStrategy definida e usando BlendMode.Clear com um app que tem um segundo plano de janela translúcido. Observe como o plano de fundo rosa aparece na área ao redor do círculo de status vermelho.

É importante notar que, ao usar CompositingStrategy.Offscreen, uma textura fora da tela que tem o tamanho da área de exibição é criada e renderizada novamente na tela. Por padrão, todos os comandos de exibição concluídos com essa estratégia são recortados para essa região. O snippet de código abaixo ilustra as diferenças ao mudar para texturas fora da tela:

@Composable
fun CompositingStrategyExamples() {
   Column(
       modifier = Modifier
           .fillMaxSize()
           .wrapContentSize(Alignment.Center)
   ) {
       /** Does not clip content even with a graphics layer usage here. By default, graphicsLayer
       does not allocate + rasterize content into a separate layer but instead is used
       for isolation. That is draw invalidations made outside of this graphicsLayer will not
       re-record the drawing instructions in this composable as they have not changed **/
       Canvas(
           modifier = Modifier
               .graphicsLayer()
               .size(100.dp) // Note size of 100 dp here
               .border(2.dp, color = Color.Blue)
       ) {
           // ... and drawing a size of 200 dp here outside the bounds
           drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx()))
       }

       Spacer(modifier = Modifier.size(300.dp))

       /** Clips content as alpha usage here creates an offscreen buffer to rasterize content
       into first then draws to the original destination **/
       Canvas(
           modifier = Modifier
               // force to an offscreen buffer
               .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
               .size(100.dp) // Note size of 100 dp here
               .border(2.dp, color = Color.Blue)
       ) {
           /** ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the
           content gets clipped **/
           drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx()))
       }
   }
}

CompositingStrategy.Auto x CompositingStrategy.Offscreen – a estratégia offscreen faz recortes na região, enquanto a auto não faz isso
Figura 15: CompositingStrategy.Auto x CompositingStrategy.Offscreen – a estratégia offscreen faz recortes na região, enquanto a auto não faz isso
ModulateAlpha

Essa estratégia de composição modifica o Alfa para cada uma das instruções de exibição gravadas em graphicsLayer. Ela não vai criar um buffer fora da tela para valores Alfa abaixo de 1,0f, a menos que um RenderEffect seja definido. Portanto, ela pode ser mais eficiente para renderização Alfa. Por outro lado, ela pode fornecer resultados diferentes para conteúdo sobreposto. Para casos de uso em que sabemos com antecedência que o conteúdo não está sobreposto, isso pode fornecer um desempenho melhor do que CompositingStrategy.Auto com valores Alfa menores que 1.

Confira abaixo outro exemplo de diferentes estratégias de composição, neste caso, como aplicar diferentes Alfas em diferentes partes dos elementos combináveis e aplicar uma estratégia Modulate:

@Preview
@Composable
fun CompositingStratgey_ModulateAlpha() {
  Column(
      modifier = Modifier
          .fillMaxSize()
          .padding(32.dp)
  ) {
      // Base drawing, no alpha applied
      Canvas(
          modifier = Modifier.size(200.dp)
      ) {
          drawSquares()
      }

      Spacer(modifier = Modifier.size(36.dp))

      // Alpha 0.5f applied to whole composable
      Canvas(modifier = Modifier
          .size(200.dp)
          .graphicsLayer {
              alpha = 0.5f
          }) {
          drawSquares()
      }
      Spacer(modifier = Modifier.size(36.dp))

      // 0.75f alpha applied to each draw call when using ModulateAlpha
      Canvas(modifier = Modifier
          .size(200.dp)
          .graphicsLayer {
              compositingStrategy = CompositingStrategy.ModulateAlpha
              alpha = 0.75f
          }) {
          drawSquares()
      }
  }
}

private fun DrawScope.drawSquares() {

  val size = Size(100.dp.toPx(), 100.dp.toPx())
  drawRect(color = Red, size = size)
  drawRect(
      color = Purple, size = size,
      topLeft = Offset(size.width / 4f, size.height / 4f)
  )
  drawRect(
      color = Yellow, size = size,
      topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f)
  )
}

val Purple = Color(0xFF7E57C2)
val Yellow = Color(0xFFFFCA28)
val Red = Color(0xFFEF5350)

O ModulateAlpha aplica o conjunto Alfa a cada comando de exibição
Figura 16: o ModulateAlpha aplica o conjunto Alfa a cada comando de exibição individual

Gravar o conteúdo de um elemento combinável em um bitmap

Um caso de uso comum é criar um Bitmap usando um elemento combinável. Para copiar o conteúdo do elemento combinável em uma Picture, use o método drawIntoCanvas:

Column(
    modifier = Modifier
        .padding(padding)
        .fillMaxSize()
        .drawWithCache {
            // Example that shows how to redirect rendering to an Android Picture and then
            // draw the picture into the original destination
            val width = this.size.width.toInt()
            val height = this.size.height.toInt()

            onDrawWithContent {
                val pictureCanvas =
                    androidx.compose.ui.graphics.Canvas(
                        picture.beginRecording(
                            width,
                            height
                        )
                    )
                // requires at least 1.6.0-alpha01+
                draw(this, this.layoutDirection, pictureCanvas, this.size) {
                    this@onDrawWithContent.drawContent()
                }
                picture.endRecording()

                drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
            }
        }

) {
    ScreenContentToCapture()
}

O snippet acima desenha o conteúdo do elemento combinável no objeto Picture. Para converter o Picture em um Bitmap:

val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    Bitmap.createBitmap(picture)
} else {
    val bitmap = Bitmap.createBitmap(
        picture.width,
        picture.height,
        Bitmap.Config.ARGB_8888
    )
    val canvas = android.graphics.Canvas(bitmap)
    canvas.drawColor(android.graphics.Color.WHITE)
    canvas.drawPicture(picture)
    bitmap
}

Assim, o bitmap pode ser salvo em disco e compartilhado. Para mais detalhes, consulte o snippet completo.

Modificador de desenho personalizado

Para criar seu próprio modificador personalizado, implemente a interface DrawModifier. Isso dá acesso a um ContentDrawScope, que é o mesmo que é exposto ao usar Modifier.drawWithContent(). Depois, é possível extrair operações de exibição comuns para modificadores personalizados para limpar o código e fornecer wrappers convenientes. Por exemplo, Modifier.background() é um DrawModifier conveniente.

Se você quiser implementar um Modifier que gire o conteúdo verticalmente, crie desta maneira:

class FlippedModifier : DrawModifier {
    override fun ContentDrawScope.draw() {
        scale(1f, -1f) {
            this@draw.drawContent()
        }
    }
}

fun Modifier.flipped() = this.then(FlippedModifier())

Em seguida, use este modificador de inversão aplicado em Text:

Text(
    "Hello Compose!",
    modifier = Modifier
        .flipped()
)

Modificador de inversão personalizado no texto
Figura 17: modificador de inversão personalizado no texto.

Outros recursos

Para mais exemplos usando graphicsLayer e uma renderização personalizada, confira os recursos abaixo: