Como usar efeitos de renderização no Jetpack Compose para obter visuais impressionantes
Conteudo
Plano de fundo
O Jetpack Compose oferece uma ampla variedade de ferramentas e componentes para criar interfaces de usuário envolventes, e uma das joias menos conhecidas do Compose é o RenderEffect.
Nesta postagem do blog, exploraremos o RenderEffect criando alguns exemplos interessantes com um efeito de renderização.
O que é RenderEffect?
O RenderEffect permite que você aplique efeitos visuais aos componentes da interface do usuário. Esses efeitos podem incluir borrões, sombreadores personalizados ou quaisquer outras transformações visuais que você possa imaginar. No entanto, ele está disponível para a API 31 e superior.
Em nosso exemplo, usaremos o RenderEffect para criar um efeito de desfoque e sombreamento para o nosso botão flutuante expansível e alguns componentes de bônus.
O que implementaremos neste blog?
Comece a usar…
O BlurContainer
Na primeira etapa, vamos começar apresentando o “BlurContainer”. Esse componente exclusivo acrescenta uma camada extra de elegância visual e cativação à nossa interface de usuário, criando um efeito visual impressionante.
Ele abriga um modificador de desfoque personalizado que leva nosso efeito de renderização ao próximo nível.
@Composable fun BlurContainer( modifier: Modifier = Modifier, blur: Float = 60f, component: @Composable BoxScope.() -> Unit, content: @Composable BoxScope.() -> Unit = {}, ) { Box(modifier, contentAlignment = Alignment.Center) { Box( modifier = Modifier .customBlur(blur), content = component, ) Box( contentAlignment = Alignment.Center ) { content() } } } fun Modifier.customBlur(blur: Float) = this.then( graphicsLayer { if (blur > 0f) renderEffect = RenderEffect .createBlurEffect( blur, blur, Shader.TileMode.DECAL, ) .asComposeRenderEffect() } )
- A extensão do modificador ‘customBlur‘ usa um parâmetro de desfoque, que especifica a intensidade do efeito de desfoque.
- Ela é usada para aplicar um
graphicsLayer
ao Composable, que, por sua vez, aplica um efeito de desfoque usandoRenderEffect.createBlurEffect
. OgraphicsLayer
é usado para aplicar efeitos de renderização ao Composable.
Veja como fica o efeito de desfoque:
Com esse modificador, podemos adicionar facilmente efeitos de desfoque a qualquer Composable, encadeando-o aos modificadores existentes.
Aplicação do efeito de renderização ao contêiner principal
Para isso, usaremos um shader personalizado, o RuntimeShader
, e o graphicsLayer
do Jetpack Compose para obter o efeito visual desejado no contêiner pai.
Antes de nos aprofundarmos em como o efeito de renderização é aplicado, vamos entender como o RuntimeShader
é inicializado.
@Language("AGSL") const val ShaderSource = """ uniform shader composable; uniform float visibility; half4 main(float2 cord) { half4 color = composable.eval(cord); color.a = step(visibility, color.a); return color; } """ val runtimeShader = remember { RuntimeShader(ShaderSource) }
Neste trecho de código, criamos uma instância do RuntimeShader
. A função remember garante que o shader seja inicializado apenas uma vez, evitando sobrecarga desnecessária. Passamos nosso código-fonte de shader personalizado (ShaderSource
) para o construtor do RuntimeShader
.
Nosso ShaderSource
é uma parte crucial do efeito de renderização. Ele foi escrito em uma linguagem de sombreamento chamada AGSL (Android Graphics Shading Language). Vamos dar uma olhada mais de perto:
shader uniforme composable
: Essa linha declara uma variável de shader uniforme chamada “composable”. Essa variável variável é usada para amostrar as cores dos elementos Composable se quisermos quisermos aplicar o efeito de renderização.uniform float visibility
(visibilidade de flutuação uniforme): Declaramos uma variável de flutuação uniforme chamada “visibility” (visibilidade). Essa variável controla a intensidade do efeito do sombreador especificando um limite.half4 main(float2 cord)
: A função principal é o ponto de entrada do sombreador. Ela recebe uma coordenada 2D (cord
) e retorna uma cor na forma dehalf4
, que representa uma cor com componentes vermelho, verde, azul e alfa.half4 color = composable.eval(cord)
: Aqui, obtemos uma amostra da cor da variável uniforme do shader “composable” na coordenada fornecida.color.a = step(visibility, color.a)
: Aplicamos o efeito do sombreador definindo o componente alfa (color.a
) como 0 ou 1 com base no limite de “visibilidade”.return color
: Por fim, retornamos a cor modificada.
Confira o AGSL Shader no aplicativo JetLagged da compose-samples.
Aplicação do efeito de renderização
Com nosso RuntimeShader
e ShaderSource
prontos, agora podemos aplicar o efeito de renderização usando o graphicsLayer
:
Box( modifier .graphicsLayer { runtimeShader.setFloatUniform("visibility", 0.2f) renderEffect = RenderEffect .createRuntimeShaderEffect( runtimeShader, "composable" ) .asComposeRenderEffect() }, content = content, )
Veja a seguir um detalhamento de como isso funciona:
runtimeShader.setFloatUniform("visibility", 0.2f)
: Definimos a variável uniforme “visibility” em nosso shader para controlar a intensidade do efeito. Nesse caso, nós a definimos como 0,2f, mas você pode ajustar esse valor para obter o efeito desejado.renderEffect = RenderEffect.createRuntimeShaderEffect(...)
: Criamos umRenderEffect
usando o métodocreateRuntimeShaderEffect
. Esse método usa nossoruntimeShader
e o nome “composable”, que corresponde à variável do shader em nossoShaderSource
..asComposeRenderEffect()
: Convertemos oRenderEffect
em um formato compatível com o Compose usandoasComposeRenderEffect()
.
Ao aplicar esse efeito de renderização no graphicsLayer
, obtemos o efeito de sombreamento nos componentes da interface do usuário contidos na caixa.
Para reunir todos esses elementos e aplicar nosso efeito de renderização sem problemas, criaremos um ShaderContainer
componível como este:
@Language("AGSL") const val Source = """ uniform shader composable; uniform float visibility; half4 main(float2 cord) { half4 color = composable.eval(cord); color.a = step(visibility, color.a); return color; } """ @Composable fun ShaderContainer( modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { val runtimeShader = remember { RuntimeShader(Source) } Box( modifier .graphicsLayer { runtimeShader.setFloatUniform("visibility", 0.2f) renderEffect = RenderEffect .createRuntimeShaderEffect( runtimeShader, "composable" ) .asComposeRenderEffect() }, content = content ) }
Aqui está o efeito visual do BlurContainer envolvido pelo ShaderContainer:
Agora que construímos com sucesso a base para o nosso efeito de renderização com o ShaderContainer
e o BlurContainer
, é hora de reunir tudo isso criando o ExtendedFabRenderEffect
. Esse Composable será a peça central do nosso botão flutuante expansível com efeitos de renderização dinâmica.
ExtendedFabRenderEffect
O composable ExtendedFabRenderEffect
é responsável por orquestrar toda a interface do usuário, animar a expansão do botão e lidar com o efeito de renderização. Vamos ver como ele funciona e como cria uma experiência de usuário visualmente atraente.
Animação suave
Criar uma animação suave e fluida é essencial para uma experiência de usuário refinada. Para isso, aplicamos a animação alfa:
A animação alfa
gerencia a transparência dos botões. Quando expandido é verdadeiro, os botões se tornam totalmente opacos; caso contrário, eles desaparecem. Assim como a animação de deslocamento, usamos a função animateFloatAsState
com parâmetros apropriados para garantir transições suaves.
var expanded: Boolean by remember { mutableStateOf(false) } val alpha by animateFloatAsState( targetValue = if (expanded) 1f else 0f, animationSpec = tween(durationMillis = 1000, easing = LinearEasing), label = "" )
Combinação de efeitos
Agora, combinamos o efeito de renderização, o ShaderContainer, com nossos botões para criar uma interface de usuário coerente. Dentro do ShaderContainer
, colocamos vários ButtonComponent
Composables, cada um representando um botão com um ícone e uma interação específicos.
ShaderContainer( modifier = Modifier.fillMaxSize() ) { ButtonComponent( Modifier.padding( paddingValues = PaddingValues( bottom = 80.dp ) * FastOutSlowInEasing .transform((alpha)) ), onClick = { expanded = !expanded } ) { Icon( imageVector = Icons.Default.Edit, contentDescription = null, tint = Color.White, modifier = Modifier.alpha(alpha) ) } ButtonComponent( Modifier.padding( paddingValues = PaddingValues( bottom = 160.dp ) * FastOutSlowInEasing.transform(alpha) ), onClick = { expanded = !expanded } ) { Icon( imageVector = Icons.Default.LocationOn, contentDescription = null, tint = Color.White, modifier = Modifier.alpha(alpha) ) } ButtonComponent( Modifier.padding( paddingValues = PaddingValues( bottom = 240.dp ) * FastOutSlowInEasing.transform(alpha) ), onClick = { expanded = !expanded } ) { Icon( imageVector = Icons.Default.Delete, contentDescription = null, tint = Color.White, modifier = Modifier.alpha(alpha) ) } ButtonComponent( Modifier.align(Alignment.BottomEnd), onClick = { expanded = !expanded }, ) { val rotation by animateFloatAsState( targetValue = if (expanded) 45f else 0f, label = "", animationSpec = tween(1000, easing = FastOutSlowInEasing) ) Icon( imageVector = Icons.Default.Add, contentDescription = null, modifier = Modifier.rotate(rotation), tint = Color.White ) } }
Com essa configuração, o ShaderContainer
atua como um pano de fundo para os nossos botões e o efeito de renderização é perfeitamente aplicado aos botões por meio do ButtonComponent
Composables. O modificador alpha
garante que os botões se tornem visíveis ou invisíveis com base no estado de expansão, criando uma interface de usuário polida e dinâmica.
Anatomia do ButtonComponent
O ButtonComponent
foi projetado para encapsular cada botão dentro do menu expansível. Ele oferece a flexibilidade de personalizar a aparência e o comportamento do botão.
Veja a seguir como o ButtonComponent
é estruturado:
@Composable fun BoxScope.ButtonComponent( modifier: Modifier = Modifier, background: Color = Color.Black, onClick: () -> Unit, content: @Composable BoxScope.() -> Unit ) { // Applying the Blur Effect with the BlurContainer BlurContainer( modifier = modifier .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onClick, ) .align(Alignment.BottomEnd), component = { Box( Modifier .size(40.dp) .background(color = background, CircleShape) ) } ) { // Content (Icon or other elements) inside the button Box( Modifier.size(80.dp), content = content, contentAlignment = Alignment.Center, ) } }
E é isso, conseguimos o efeito desejado com o código acima!
TextRenderEffect
O coração do TextRenderEffect
é a exibição dinâmica do texto. Usaremos uma lista de frases e citações motivadoras que serão apresentadas ao usuário. Essas frases incluirão sentimentos como “Alcance seus objetivos”, “Realize seus sonhos” e outros.
val animateTextList = listOf( "\"Reach your goals\"", "\"Achieve your dreams\"", "\"Be happy\"", "\"Be healthy\"", "\"Get rid of depression\"", "\"Overcome loneliness\"" )
Criaremos a variável de estado textToDisplay
para manter e exibir essas frases, criando uma sequência animada.
Animação do texto
Para tornar a exibição do texto atraente, utilizaremos algumas animações importantes:
- Blur Effect: Aplicaremos um efeito
blur
ao texto. O valor do desfoque é animado de 0 a 30 e de volta a 0, usando uma animação de atenuação linear. Isso cria um efeito visual sutil e hipnotizante que aprimora a aparência do texto. - Text Transition: Usaremos o
LaunchedEffect
para percorrer a lista de frases, exibindo cada uma delas por um determinado período. Quando otextToDisplay
muda, ocorre uma animaçãoscaleIn
, apresentando o novo texto com um efeito scale-in e, quando ele sai da transição, é aplicado um efeitoscaleOut
. Isso proporciona uma maneira visualmente agradável de introduzir e encerrar o texto.
Integração completa com o ShaderContainer
@Composable fun TextRenderEffect() { val animateTextList = listOf( "\"Reach your goals\"", "\"Achieve your dreams\"", "\"Be happy\"", "\"Be healthy\"", "\"Get rid of depression\"", "\"Overcome loneliness\"" ) var index by remember { mutableIntStateOf(0) } var textToDisplay by remember { mutableStateOf("") } val blur = remember { Animatable(0f) } LaunchedEffect(textToDisplay) { blur.animateTo(30f, tween(easing = LinearEasing)) blur.animateTo(0f, tween(easing = LinearEasing)) } LaunchedEffect(key1 = animateTextList) { while (index <= animateTextList.size) { textToDisplay = animateTextList[index] delay(3000) index = (index + 1) % animateTextList.size } } ShaderContainer( modifier = Modifier.fillMaxSize() ) { BlurContainer( modifier = Modifier.fillMaxSize(), blur = blur.value, component = { AnimatedContent( targetState = textToDisplay, modifier = Modifier .fillMaxWidth(), transitionSpec = { (scaleIn()).togetherWith( scaleOut() ) }, label = "" ) { text -> Text( modifier = Modifier .fillMaxWidth(), text = text, style = MaterialTheme.typography.headlineLarge, color = MaterialTheme.colorScheme.onPrimaryContainer, textAlign = TextAlign.Center ) } } ) {} } }
ImageRenderEffect
Nossa exploração do RenderEffect no Jetpack Compose continua com o intrigante ImageRenderEffect
. Esse Composable leva a renderização de imagens a um novo patamar, introduzindo transições de imagens dinâmicas e efeitos de renderização cativantes. Vamos nos aprofundar em como ele é construído e como aprimora a experiência visual.
Transições dinâmicas de imagens
O núcleo do ImageRenderEffect
está em sua capacidade de fazer a transição entre imagens de forma visualmente atraente. Para demonstrar isso, configuraremos um cenário básico em que duas imagens, ic_first
e ic_second
, serão alternadas em um evento de clique.
var image by remember { mutableIntStateOf(R.drawable.ic_first) }
A variável de estado image
contém a imagem exibida no momento e, com um simples clique no botão, os usuários podem alternar entre as duas.
Criação de efeitos envolventes
- Blur Effect: Assim como em nossos exemplos anteriores, aplicamos um efeito de desfoque às imagens. O valor do desfoque é animado de 0 a 100 e volta a 0, criando um efeito visual hipnotizante que aprimora a transição da imagem.
val blur = remember { Animatable(0f) } LaunchedEffect(image) { blur.animateTo(100f, tween(easing = LinearEasing)) blur.animateTo(0f, tween(easing = LinearEasing)) }
- Image Transition: O coração da transição de imagens é o
AnimatedContent
Composable. Ele lida com a transição suave entre as imagens, combinando um efeito defadeIn
escaleIn
para a imagem que entra na cena e um efeito defadeOut
escaleOut
para a imagem que sai da cena.
AnimatedContent( targetState = image, modifier = Modifier.fillMaxWidth(), transitionSpec = { (fadeIn(tween(easing = LinearEasing)) + scaleIn( tween(1_000, easing = LinearEasing) )).togetherWith( fadeOut( tween(1_000, easing = LinearEasing) ) + scaleOut( tween(1_000, easing = LinearEasing) ) ) }, label = "" ) { image -> Image( painter = painterResource(id = image), modifier = Modifier.size(200.dp), contentDescription = "" ) }
Integração perfeita com o ShaderContainer
Assim como em nossos exemplos anteriores, o ImageRenderEffect
está integrado em um ShaderContainer
. Isso nos permite combinar as transições de imagem e os efeitos de renderização, criando uma experiência visual cativante e envolvente.
@Composable fun ImageRenderEffect() { var image by remember { mutableIntStateOf(R.drawable.ic_first) } val blur = remember { Animatable(0f) } LaunchedEffect(image) { blur.animateTo(100f, tween(easing = LinearEasing)) blur.animateTo(0f, tween(easing = LinearEasing)) } Column( modifier = Modifier .wrapContentSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { ShaderContainer( modifier = Modifier .animateContentSize() .clipToBounds() .fillMaxWidth() ) { BlurContainer( modifier = Modifier.fillMaxWidth(), blur = blur.value, component = { AnimatedContent( targetState = image, modifier = Modifier .fillMaxWidth(), transitionSpec = { (fadeIn(tween(easing = LinearEasing)) + scaleIn( tween( 1_000, easing = LinearEasing ) )).togetherWith( fadeOut( tween( 1_000, easing = LinearEasing ) ) + scaleOut( tween( 1_000, easing = LinearEasing ) ) ) }, label = "" ) { image -> Image( painter = painterResource(id = image), modifier = Modifier .size(200.dp), contentDescription = "" ) } }) {} } Spacer(modifier = Modifier.height(20.dp)) Button( onClick = { image = if (image == R.drawable.ic_first) R.drawable.ic_second else R.drawable.ic_first }, colors = ButtonDefaults.buttonColors( containerColor = Color.Black ) ) { Text("Change Image") } } }
Conclusão
Ao compreender o ShaderContainer
, o BlurContainer
, o ShaderSource
e o modificador customBlur
, você tem as ferramentas para criar efeitos de renderização impressionantes em seus aplicativos Jetpack Compose. Esses elementos fornecem uma base para explorar e experimentar vários efeitos visuais e shaders personalizados, abrindo um mundo de possibilidades criativas para seus designs de UI.
Boa codificação!
Segue o código fonte completo (Github)