Animação dentro e fora da caixa com o Jetpack Compose

Tempo de leitura: 14 minutes

Introdução

As animações têm o poder de fazer com que as interfaces de usuário pareçam vivas e envolventes. No Android, com o Jetpack Compose, esse poder está na ponta de seus dedos, oferecendo ferramentas avançadas para criar interfaces de usuário verdadeiramente dinâmicas. Neste artigo, iremos além do básico e exploraremos os aspectos mais profundos das animações no Jetpack Compose.

Abordaremos uma série de técnicas, desde a criação de movimentos fluidos e baseados em física que adicionam um toque de realismo até sequências coreografadas complexas que trazem uma qualidade narrativa às suas interfaces. Quer você esteja aperfeiçoando suas habilidades ou apenas curioso para saber o que é possível fazer, esta jornada fornecerá insights práticos para que seus aplicativos não apenas funcionem sem problemas, mas também encantem os usuários em cada interação.

Vamos mergulhar de cabeça e descobrir como essas animações podem transformar sua abordagem ao design da interface do usuário, tornando-a mais intuitiva, responsiva e agradável para os usuários.

 

Seção 1 – Manipuladores de animação personalizados no Jetpack Compose

Abraçando a interatividade dinâmica com animações personalizadas

Nesta seção, exploramos o uso de manipuladores de animação personalizados avançados no Jetpack Compose para criar elementos de interface do usuário dinâmicos e interativos. Nosso foco está em um exemplo do mundo real que demonstra como a interação do usuário pode influenciar uma animação de forma significativa.

Exemplo – Movimento interativo de personagens de jogos

Ilustraremos esse conceito com um exemplo em que um personagem de jogo (representado por um ícone de rosto) segue um caminho determinado por um ponto de controle arrastável pelo usuário.

@Composable
fun GameCharacterMovement() {
    val startPosition = Offset(100f, 100f)
    val endPosition = Offset(250f, 400f)
    val controlPoint = remember { mutableStateOf(Offset(200f, 300f)) }
    val position = remember { Animatable(startPosition, Offset.VectorConverter) }

    LaunchedEffect(controlPoint.value) {
        position.animateTo(
            targetValue = endPosition,
            animationSpec = keyframes {
                durationMillis = 5000
                controlPoint.value at 2500 // midway point controlled by the draggable control point
            }
        )
    }

    val onControlPointChange: (offset: Offset) -> Unit = {
        controlPoint.value = it
    }

    Box(modifier = Modifier.fillMaxSize()) {

        Icon(
            Icons.Filled.Face, contentDescription = "Localized description", modifier = Modifier
                .size(50.dp)
                .offset(x = position.value.x.dp, y = position.value.y.dp)
        )

        DraggableControlPoint(controlPoint.value, onControlPointChange)
    }
}

Explicação

  • O GameCharacterMovement anima um ícone que representa um personagem do jogo. O caminho da animação é controlado pelo controlPoint, que é definido e atualizado pela interação do usuário.
  • Animatable é usado para fazer uma transição suave da posição do ícone de startPosition para endPosition.
  • O LaunchedEffect escuta as alterações no valor do controlPoint, reativando a animação sempre que o ponto de controle é movido.
  • animationSpec – É uma configuração que define a duração, o atraso e a atenuação de uma animação. Ela determina como os valores animados mudam ao longo do tempo.
  • keyframes – Permite especificar valores em momentos específicos durante a animação, dando-lhe controle sobre os pontos intermediários da animação. É particularmente útil para criar animações complexas e coreografadas.
  • O bloco de keyframes define a animação como uma sequência de keyframes. Em 2500 milissegundos (o ponto intermediário), o personagem atinge o ponto de controle e continua até a posição final.
Composable
fun DraggableControlPoint(controlPoint: Offset, onControlPointChange: (Offset) -> Unit) {
    var localPosition by remember { mutableStateOf(controlPoint) }
    Box(
        modifier = Modifier
            .offset {
                IntOffset(
                    x = localPosition.x.roundToInt() - 15,
                    y = localPosition.y.roundToInt() - 15
                )
            }
            .size(30.dp)
            .background(Color.Red, shape = CircleShape)
            .pointerInput(Unit) {
                detectDragGestures(onDragEnd = {
                    onControlPointChange(localPosition)
                }) { _, dragAmount ->
                    // adjust based on screen bounds
                    val newX = (localPosition.x + dragAmount.x).coerceIn(0f, 600f)
                    val newY = (localPosition.y + dragAmount.y).coerceIn(0f, 600f)
                    localPosition = Offset(newX, newY)
                }
            }
    )
}

Explicação

  • DraggableControlPoint é um composable que permite que o usuário altere interativamente a posição do ponto de controle.
  • Arrastar o ponto de controle atualiza localPosition, que é então refletido de volta para o GameCharacterMovement após a conclusão do gesto de arrastar (onDragEnd). Essa interação altera o caminho do ícone animado.

Casos de uso no mundo real

  1. Aplicativos educacionais interativos: em um aplicativo educacional, as animações podem ser usadas para tornar o aprendizado mais envolvente. Por exemplo, arrastar um planeta ao longo de sua órbita em um aplicativo de astronomia para ver diferentes constelações.
  2. Narração de histórias e jogos interativos: Em aplicativos de jogos ou histórias digitais, permitir que os usuários influenciem a história ou o ambiente do jogo por meio de elementos arrastáveis pode criar uma experiência mais envolvente.

 

 

Seção 2 – Coreografando animações complexas no Jetpack Compose

Sincronização de vários elementos para obter efeitos harmoniosos

Nesta seção, vamos nos aprofundar na arte de coreografar animações complexas no Jetpack Compose. Concentramo-nos na criação de animações sincronizadas em que vários elementos interagem perfeitamente, aprimorando a experiência geral do usuário.

A) Animações de reação em cadeia – O efeito dominó

É possível criar um efeito dominó na interface do usuário configurando uma série de animações em que a conclusão de uma aciona o início da próxima.

@Composable
fun DominoEffect() {
    val animatedValues = List(6) { remember { Animatable(0f) } }

    LaunchedEffect(Unit) {
        animatedValues.forEachIndexed { index, animate ->
            animate.animateTo(
                targetValue = 1f,
                animationSpec = tween(durationMillis = 1000, delayMillis = index * 100)
            )
        }
    }

    Box (modifier = Modifier.fillMaxSize()){
      animatedValues.forEachIndexed { index, value ->
        Box(
            modifier = Modifier
                .size(50.dp)
                .offset(x = ((index+1) * 50).dp, y = ((index+1) * 30).dp)
                .background(getRandomColor(index).copy(alpha = value.value))
        )
      }
    }
}

fun getRandomColor(seed: Int): Color {
    val random = Random(seed = seed).nextInt(256)
    return Color(random, random, random)
}

Explicação

  • animatedValues é uma lista de valores Animatable, cada um controlando o alfa (opacidade) de uma caixa.
  • O LaunchedEffect aciona uma sequência de animações para esses valores, criando um efeito escalonado em que cada caixa desaparece após a anterior, semelhante à queda de um dominó.
  • A função getRandomColor gera um tom aleatório de cinza para cada caixa, acrescentando um elemento visual exclusivo a cada componente da sequência.
  • As caixas são posicionadas diagonalmente na tela, aprimorando o efeito dominó.

B) Interactive Scrollable Timeline

Nessa linha do tempo, cada elemento aparecerá e se moverá para sua posição à medida que o usuário rolar pela linha do tempo. Usaremos LazyColumn para a lista rolável e Animatable para a animação.

@Composable
fun InteractiveTimeline(timelineItems: List<String>) {
    val scrollState = rememberLazyListState()

    LazyColumn(state = scrollState) {
        itemsIndexed(timelineItems) { index, item ->
            val animatableAlpha = remember { Animatable(0f) }
            val isVisible = remember {
                derivedStateOf {
                    scrollState.firstVisibleItemIndex <= index
                }
            }

            LaunchedEffect(isVisible.value) {
                if (isVisible.value) {
                    animatableAlpha.animateTo(
                        1f, animationSpec = tween(durationMillis = 1000)
                    )

                }
            }

            TimelineItem(
                text = item,
                alpha = animatableAlpha.value,
            )
        }
    }
}

@Composable
fun TimelineItem(text: String, alpha: Float) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.DarkGray.copy(alpha = alpha))
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = text,
            color = Color.White,
            modifier = Modifier.fillMaxWidth(),
            textAlign = TextAlign.Center,
            fontSize = 18.sp,
            fontWeight = FontWeight.SemiBold
        )
    }
}

Explicação

  • animatableAlpha controla o alfa (opacidade) de cada item da linha do tempo, inicialmente definido como 0 (totalmente transparente).
  • O estado isVisible é derivado da posição de rolagem atual, determinando se o item deve estar visível.
  • À medida que o usuário rola a tela, o LaunchedEffect aciona a animação de fade-in para os itens que entram na janela de visualização.

Caso de uso

Essa linha do tempo interativa é ideal para aplicativos em que você deseja apresentar uma série de eventos ou etapas de uma forma visualmente atraente. A animação aumenta o envolvimento do usuário, chamando a atenção para os itens à medida que eles são exibidos.

Essas animações não são apenas cativantes, mas também podem ser usadas para orientar a atenção do usuário por meio de uma sequência de eventos ou ações em seu aplicativo.

 

Seção 3 – Animações baseadas em física para realismo no Jetpack Compose

Aproveitamento da física para aprimorar a dinâmica da interface do usuário

Nesta seção, exploramos como integrar princípios de física em animações com o Jetpack Compose, adicionando uma camada de realismo e interatividade à interface do usuário. Vamos nos concentrar em um exemplo de interação de arrasto elástico.

Efeito elástico no arrasto

Este exemplo ilustra uma interação de arrasto elástico em um ícone. Quando arrastado verticalmente, o ícone se estica e se recupera com um efeito elástico, simulando o comportamento de uma mola ou de um elástico.

@Composable
fun ElasticDraggableBox() {
    var animatableOffset by remember { mutableStateOf(Animatable(0f)) }

    Box(modifier = Modifier.fillMaxSize().background(Color(0xFFFFA732)), contentAlignment = Alignment.Center) {
        Box(
            modifier = Modifier
                .offset(y = animatableOffset.value.dp)
                .draggable(
                    orientation = Orientation.Vertical,
                    state = rememberDraggableState { delta ->
                        animatableOffset = Animatable(animatableOffset.value + delta)
                    },
                    onDragStopped = {
                        animatableOffset.animateTo(0f, animationSpec = spring())
                    }
                )
                .size(350.dp),
            contentAlignment = Alignment.Center
        ) {
            Icon(
                Icons.Filled.Favorite,
                contentDescription = "heart",
                modifier = Modifier.size(animatableOffset.value.dp + 150.dp),
                tint = Color.Red
            )
        }
    }
}

Explicação

  • Um Box composable, que contém um ícone, é tornado arrastável usando o modificador draggable.
  • O animatableOffset rastreia o deslocamento vertical do ícone devido ao arrastamento.
  • Durante o arrasto, o tamanho do ícone muda com base na quantidade arrastada, criando um efeito de alongamento.
  • Quando o arrasto é interrompido (onDragStopped), o animatableOffset é animado de volta para 0f usando uma animação de mola, resultando no retorno do ícone ao seu tamanho e posição originais.

 

Seção 4 – Animações baseadas em gestos no Jetpack Compose

Aprimorando a experiência do usuário com gestos responsivos
Nesta seção, exploramos como o Jetpack Compose pode ser usado para criar animações que são controladas por gestos do usuário. Vamos nos concentrar em dois exemplos: uma imagem transformável multitoque e uma forma de onda de áudio controlada por gestos.

A) Imagem transformável multitoque
Neste exemplo, criaremos uma visualização de imagem com a qual os usuários podem interagir usando gestos multitoque, como pinçar, aplicar zoom e girar.

@Composable
fun TransformableImage(imageId: Int = R.drawable.android) {
    var scale by remember { mutableStateOf(1f) }
    var rotation by remember { mutableStateOf(0f) }
    var offset by remember { mutableStateOf(Offset.Zero) }

    Box(modifier = Modifier.fillMaxSize().background(Color.DarkGray), contentAlignment = Alignment.Center) {
        Image(
            painter = painterResource(id = imageId),
            contentDescription = "Transformable image",
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .size(300.dp)
                .graphicsLayer(
                    scaleX = scale,
                    scaleY = scale,
                    rotationZ = rotation,
                    translationX = offset.x,
                    translationY = offset.y
                )
                .pointerInput(Unit) {
                    detectTransformGestures { _, pan, zoom, rotate ->
                        scale *= zoom
                        rotation += rotate
                        offset += pan
                    }
                }
        )
    }
}

 

Explicação

  • O Image composable é modificado com graphicsLayer para aplicar transformações como escala, rotação e translação.
  • O pointerInput com detectTransformGestures é usado para lidar com gestos multitoque, atualizando a escala, a rotação e o deslocamento de acordo.

B) Forma de onda controlada por gestos

Esta é uma visualização de forma de onda que muda sua aparência com base nos gestos do usuário, como deslizar e apertar, para controlar aspectos como amplitude e frequência.

@Composable
fun GestureControlledWaveform() {
    var amplitude by remember { mutableStateOf(100f) }
    var frequency by remember { mutableStateOf(1f) }

    Canvas(modifier = Modifier
        .fillMaxSize()
        .pointerInput(Unit) {
            detectDragGestures { _, dragAmount ->
                amplitude += dragAmount.y
                frequency += dragAmount.x / 500f 
                // Adjusting frequency based on drag
            }
        }
        .background(
            Brush.verticalGradient(
                colors = listOf(Color(0xFF003366), Color.White, Color(0xFF66B2FF))
            )
        )) {
        val width = size.width
        val height = size.height
        val path = Path()

        val halfHeight = height / 2
        val waveLength = width / frequency

        path.moveTo(0f, halfHeight)

        for (x in 0 until width.toInt()) {
            val theta = (2.0 * Math.PI * x / waveLength).toFloat()
            val y = halfHeight + amplitude * sin(theta.toDouble()).toFloat()
            path.lineTo(x.toFloat(), y)
        }

        val gradient = Brush.horizontalGradient(
            colors = listOf(Color.Blue, Color.Cyan, Color.Magenta)
        )

        drawPath(
            path = path,
            brush = gradient
        )
    }
}

Explicação

  • amplitude e frequency são variáveis de estado que controlam a amplitude e a frequência da forma de onda, respectivamente.
  • O Canvas composable é usado para desenhar a forma de onda. A lógica de desenho dentro do Canvas calcula a posição Y para cada posição X com base na função senoidal, criando um efeito de onda.
  • O modificador detectDragGestures é usado para atualizar aamplitude e frequencycom base nos gestos de arrastar do usuário. Os arrastamentos horizontais ajustam a frequência e os arrastamentos verticais ajustam a amplitude.
  • À medida que o usuário arrasta pela tela, o formato da forma de onda muda de acordo, criando uma experiência interativa.

Observação

  • Esta é uma implementação básica. Para obter uma forma de onda de áudio mais realista, você precisaria integrar dados de áudio reais.
  • A capacidade de resposta da forma de onda aos gestos pode ser ajustada com precisão, ajustando como aamplitude e frequencysão modificadas durante o arrasto.

Este exemplo demonstra como criar uma forma de onda interativa básica no Compose e pode ser estendido ou modificado para casos de uso mais complexos ou para lidar com gestos mais complexos.

 

Seção 5 – Padrões de animação orientados por estado no Jetpack Compose

Animação da IU com base em dados e alterações de estado

Esta seção se concentra na criação de animações que são acionadas por alterações nos dados ou no estado da interface do usuário, aprimorando a interatividade e a capacidade de resposta do aplicativo. Exploraremos dois exemplos específicos: a animação de um gráfico de dados e a implementação de transições de estado em uma IU de vários estados.

A) Animação de gráfico orientada por dados

Este exemplo demonstra um gráfico de linhas animado em que o caminho do gráfico é animado em resposta a alterações no conjunto de dados.

@Composable
fun AnimatedGraphExample() {
    var dataPoints by remember { mutableStateOf(generateRandomDataPoints(5)) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.DarkGray)
    ) {
        AnimatedLineGraph(dataPoints = dataPoints)

        Spacer(modifier = Modifier.height(16.dp))

        Button(
            onClick = {
                dataPoints = generateRandomDataPoints(5)
            },
            modifier = Modifier.align(Alignment.CenterHorizontally),
            colors = ButtonDefaults.buttonColors(containerColor = Color.Green)
        ) {
            Text(
                "Update Data",
                fontWeight = FontWeight.Bold,
                color = Color.DarkGray,
                fontSize = 18.sp
            )
        }
    }
}

@Composable
fun AnimatedLineGraph(dataPoints: List<Float>) {
    val animatableDataPoints = remember { dataPoints.map { Animatable(it) } }
    val path = remember { Path() }

    LaunchedEffect(dataPoints) {
        animatableDataPoints.forEachIndexed { index, animatable ->
            animatable.animateTo(dataPoints[index], animationSpec = TweenSpec(durationMillis = 500))
        }
    }

    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .height(400.dp)
    ) {
        path.reset()
        animatableDataPoints.forEachIndexed { index, animatable ->
            val x = (size.width / (dataPoints.size - 1)) * index
            val y = size.height - (animatable.value * size.height)
            if (index == 0) path.moveTo(x, y) else path.lineTo(x, y)
        }
        drawPath(path, Color.Green, style = Stroke(5f))
    }
}

fun generateRandomDataPoints(size: Int): List<Float> {
    return List(size) { Random.nextFloat() }
}

Explicação

  • O composable AnimatedGraphExample cria um ambiente em que os pontos de dados do gráfico de linhas podem ser atualizados.
  • O gráfico é desenhado em uma Canvas, onde o método drawPath usa valores animados de animatableDataPoints.
  • Para cada ponto de dados no gráfico, precisamos calcular as posições x (horizontal) e y (vertical) correspondentes na tela.
  • Cálculo de x – A posição x é calculada com base no índice do ponto de dados e na largura total da tela. Distribuímos uniformemente os pontos de dados ao longo da largura da tela.
val x = (size.width / (dataPoints.size - 1)) * index
  • Cálculo y – A posição y é calculada com base no valor do ponto de dados (animatable.value) e na altura da tela.
val y = size.height - (animatable.value * size.height)
  • O caminho começa no primeiro ponto de dados e, em seguida, lineTo é usado para desenhar uma linha para cada ponto subsequente, criando a linha do gráfico.
  • O caminho é desenhado com base nos valores animados dos pontos de dados, o que cria o efeito de animação quando os dados são alterados.

B) Transição de estado em uma interface de usuário com vários estados

A implementação de transições de estado em uma IU com vários estados pode ser feita usando o Animatable para animar entre diferentes estados da IU.

enum class UIState { StateA, StateB, StateC }

@Composable
fun StateTransitionUI() {
    var currentState by remember { mutableStateOf(UIState.StateA) }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(getBackgroundColorForState(currentState)),
        contentAlignment = Alignment.Center
    ) {
        AnimatedContent(currentState = currentState)

        Button(
            onClick = { currentState = getNextState(currentState) },
            modifier = Modifier.align(Alignment.BottomCenter)
        ) {
            Text("Next State")
        }
    }
}

@Composable
fun AnimatedContent(currentState: UIState) {
    AnimatedVisibility(
        visible = currentState == UIState.StateA,
        enter = fadeIn(animationSpec = tween(durationMillis = 2000)) + expandVertically(),
        exit = fadeOut(animationSpec = tween(durationMillis = 2000)) + shrinkVertically()
    ) {
        Text("This is ${currentState.name}", fontSize = 32.sp)
    }

    // Similar blocks for B and C
}

fun getBackgroundColorForState(state: UIState): Color {
    return when (state) {
        UIState.StateA -> Color.Red
        UIState.StateB -> Color.Green
        UIState.StateC -> Color.Blue
    }
}

fun getNextState(currentState: UIState): UIState {
    return when (currentState) {
        UIState.StateA -> UIState.StateB
        UIState.StateB -> UIState.StateC
        UIState.StateC -> UIState.StateA
    }
}

Explicação

  • Neste exemplo, o AnimatedVisibility é usado para animar o aparecimento e o desaparecimento do conteúdo de cada estado. Isso adiciona um efeito de transição suave quando o estado muda.
  • Para cada estado (StateA, StateB, StateC), há um bloco AnimatedVisibility que controla a visibilidade de seu conteúdo com animações de esmaecimento e expansão/redução.
  • Os parâmetros de enter e exit do AnimatedVisibility definem as animações para quando o conteúdo se torna visível ou oculto, respectivamente.

 

 

Seção 6 – Morphing Shapes no Compose

A animação da transformação entre formas envolve a interpolação das propriedades dessas formas.

fun ShapeMorphingAnimation() {
    val animationProgress = remember { Animatable(0f) }

    LaunchedEffect(Unit) {
        animationProgress.animateTo(
            targetValue = 1f,
            animationSpec = infiniteRepeatable(
                animation = tween(2000, easing = LinearOutSlowInEasing),
                repeatMode = RepeatMode.Reverse
            )
        )
    }

    Canvas(modifier = Modifier.padding(40.dp).fillMaxSize()) {
        val sizeValue = size.width.coerceAtMost(size.height) / 2
        val squareRect = Rect(center = center, sizeValue)

        val morphedPath = interpolateShapes(progress = animationProgress.value, squareRect = squareRect)
        drawPath(morphedPath, color = Color.Blue, style = Fill)
    }
}

fun interpolateShapes(progress: Float, squareRect: Rect): Path {
    val path = Path()

    val cornerRadius = CornerRadius(
        x = lerp(start = squareRect.width / 2, stop = 0f, fraction = progress),
        y = lerp(start = squareRect.height / 2, stop = 0f, fraction = progress)
    )

    path.addRoundRect(
        roundRect = RoundRect(rect = squareRect, cornerRadius = cornerRadius)
    )

    return path
}

fun lerp(start: Float, stop: Float, fraction: Float): Float {
    return (1 - fraction) * start + fraction * stop
}

Explicação

  • ShapeMorphingAnimation configura uma animação infinita que alterna o valor animationProgress entre 0 e 1.
  • O composable Canvas é usado para desenhar a forma. Aqui, definimos as dimensões de um quadrado (squareRect) com base no tamanho da tela.
  • interpolateShapes usa o progresso atual da animação e o retângulo do quadrado para interpolar entre um círculo e um quadrado. Ele usa lerp (interpolação linear) para ajustar gradualmente o cornerRadius de um retângulo arredondado, que representa nossa forma de transformação.
  • Quando o progress é 0, o cornerRadius é a metade do tamanho do retângulo, tornando a forma um círculo. Quando o progress é 1, o cornerRadius é 0, transformando a forma em um quadrado.

 

Casos de uso no mundo real

  1. Indicadores de carregamento e progresso – as formas de transformação podem ser usadas para criar indicadores de carregamento ou progresso mais atraentes, proporcionando uma maneira visualmente interessante de indicar o progresso ou os estados de carregamento.
  2. Transições de ícones na interface do usuário – os ícones de transformação podem ser usados para fornecer feedback visual em resposta às ações do usuário. Por exemplo, um botão de reprodução que se transforma em um botão de pausa quando clicado, ou um ícone de menu de hambúrguer que se transforma em uma seta para trás.
  3. Visualização de dados – Em visualizações de dados complexas, a transformação pode ajudar na transição entre diferentes exibições ou estados de dados, facilitando o acompanhamento e a compreensão das alterações ao longo do tempo ou entre categorias.

 

Alguém quer uma queda de neve?

Demonstraremos um sistema de partículas simples para criar um efeito de queda de neve.

data class Snowflake(
    var x: Float,
    var y: Float,
    var radius: Float,
    var speed: Float
)

@Composable
fun SnowfallEffect() {
    val snowflakes = remember { List(100) { generateRandomSnowflake() } }
    val infiniteTransition = rememberInfiniteTransition(label = "")

    val offsetY by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1000f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 5000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ), label = ""
    )

    Canvas(modifier = Modifier.fillMaxSize().background(Color.Black)) {
        snowflakes.forEach { snowflake ->
            drawSnowflake(snowflake, offsetY % size.height)
        }
    }
}

fun generateRandomSnowflake(): Snowflake {
    return Snowflake(
        x = Random.nextFloat(),
        y = Random.nextFloat() * 1000f,
        radius = Random.nextFloat() * 2f + 2f, // Snowflake size
        speed = Random.nextFloat() * 1.2f + 1f  // Falling speed
    )
}

fun DrawScope.drawSnowflake(snowflake: Snowflake, offsetY: Float) {
    val newY = (snowflake.y + offsetY * snowflake.speed) % size.height
    drawCircle(Color.White, radius = snowflake.radius, center = Offset(snowflake.x * size.width, newY))
}

 

Explicação

  • O SnowfallEffect configura um sistema de partículas com vários flocos de neve (objetos Snowflake).
  • Cada Snowflake tem propriedades como posição (x, y), radius (tamanho) e speed.
  • rememberInfiniteTransition e animateFloat são usados para criar um efeito de movimento vertical contínuo, simulando a queda de neve.
  • O Canvas composable é usado para desenhar cada floco de neve. A função drawSnowflake calcula a nova posição de cada floco de neve com base em sua velocidade e no offsetY animado.
  • Os flocos de neve reaparecem na parte superior depois de cair da parte inferior, criando um efeito de queda de neve em loop.

 

 

Conclusão

Ao concluirmos essa exploração das animações no Jetpack Compose, fica claro que as animações são mais do que apenas enfeites visuais. Elas são ferramentas cruciais para criar experiências de usuário envolventes, intuitivas e agradáveis.

Abraçando a interatividade

Desde o movimento dinâmico do personagem do jogo até a linha do tempo interativa, vimos como as animações podem tornar as interações com o usuário mais envolventes e informativas.

Criando experiências realistas

O efeito de queda de neve e as formas que se transformam mostram a capacidade desse kit de ferramentas de trazer realismo e fluidez para o mundo digital. Essas animações ajudam a criar experiências imersivas que repercutem nos usuários.

Simplificando a complexidade

Seja na coreografia de vários elementos ou na animação de transições de estado, a simplicidade com que isso pode ser feito se destaca.