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.
Como renderizar modificadores
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 dos desenhos
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 }
Modifier.drawBehind
: renderização por trá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:
Modifier.drawWithCache
: renderizar e armazenar objetos de exibição em cache
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()) ) } } )
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 } )
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() } )
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 } )
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 } )
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:
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).
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)) ) }
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 } )
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.
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:
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:
É 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())) } } }
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 CompositingStrategy_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)
Gravar o conteúdo de um elemento combinável em um bitmap
Um caso de uso comum é criar um Bitmap
a partir de um elemento combinável. Para copiar o
conteúdo do elemento combinável para uma Bitmap
, crie uma GraphicsLayer
usando
rememberGraphicsLayer()
.
Redirecione os comandos de desenho para a nova camada usando drawWithContent()
e
graphicsLayer.record{}
. Em seguida, desenhe a camada na tela visível usando
drawLayer
:
val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() Box( modifier = Modifier .drawWithContent { // call record to capture the content in the graphics layer graphicsLayer.record { // draw the contents of the composable into the graphics layer this@drawWithContent.drawContent() } // draw the graphics layer on the visible canvas drawLayer(graphicsLayer) } .clickable { coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() // do something with the newly acquired bitmap } } .background(Color.White) ) { Text("Hello Android", fontSize = 26.sp) }
Você pode salvar o bitmap no disco e compartilhá-lo. Para mais detalhes, consulte o exemplo completo de snippet. Verifique as permissões do dispositivo antes de tentar salvar no disco.
Modificador de exibição 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()
. Em seguida, você pode extrair operações de exibição
comuns para modificadores de exibição 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() )
Outros recursos
Para mais exemplos usando graphicsLayer
e uma renderização personalizada, confira os
recursos abaixo:
- Como fazer Jellyfish se mover no Compose (link em inglês)
- Android Dev Summit 2022: Layouts e gráficos no Compose (vídeo em inglês)
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Elementos gráficos no Compose
- Personalizar uma imagem {:#customize-image}
- Kotlin para Jetpack Compose