Tutorial do Jetpack Compose: replicando o aplicativo de áudio Dribbble, parte 1

Jan 27 2023
Implementando uma IU totalmente personalizada com animações complexas: Waveform Animation No Exyte, tentamos contribuir com o código aberto o máximo possível, desde o lançamento de bibliotecas e componentes até a escrita de artigos e tutoriais. Um tipo de tutorial que fazemos é a replicação — pegando um componente de interface do usuário complexo, implementando-o usando novas estruturas e escrevendo um tutorial ao lado.

Implementando uma IU totalmente personalizada com animações complexas: Waveform Animation

Na Exyte , tentamos contribuir com o código aberto o máximo possível, desde o lançamento de bibliotecas e componentes até a escrita de artigos e tutoriais. Um tipo de tutorial que fazemos é a replicação — pegando um componente de interface do usuário complexo, implementando-o usando novas estruturas e escrevendo um tutorial ao lado. Começamos com o SwiftUI há alguns anos, mas hoje finalmente entramos no Android, usando o framework declarativo de interface do usuário do Google: Jetpack Compose.

Conceito de design do talentoso Taras Migulko @dribbble

Estrutura da Tela

Nosso aplicativo será composto por 3 telas, com transições animadas entre elas:

Para facilitar a leitura, a replicação dessas telas será discutida em vários artigos que abrangem os seguintes elementos e animações da interface do usuário:

  • Forma de onda da primeira tela
  • Painel de ação da segunda tela
  • Recolher o cabeçalho da terceira tela
  • Transição de gesto de arrastar
  • Transição de elemento compartilhado

Criando a forma de onda

Aqui está o que queremos implementar:

Todo o elemento consiste em barras de volume verticais animadas, espaçadas a uma distância regular umas das outras. Vejamos as dimensões básicas que usaremos:

Como você bem sabe, o mundo dos dispositivos Android é rico em telas de tamanhos diferentes, e isso levanta a seguinte questão - como apresentar a forma de onda nas telas de diferentes dispositivos para que ocupe todo o espaço disponível? Para fazer este widget caber em toda a largura da tela, você pode aumentar a largura das linhas proporcionalmente ou pode aumentar o número de linhas. Escolhemos a segunda forma para preservar a nitidez da imagem. Neste caso, o primeiro passo é calcular o número necessário de linhas. Para fazer isso, precisaremos dividir a largura disponível da tela pelo tamanho de uma única barra de volume (largura da barra + largura do intervalo). Além disso, precisamos usar toInt()em vez de roundToInt()porque queremos usar apenas elementos que se ajustam totalmente ao tamanho, independentemente do arredondamento.

val count = (canvasWidth / (barWidthFloat + gapWidthFloat)).toInt().coerceAtMost(MaxLinesCount)

val animatedVolumeWidth = count * (barWidthFloat + gapWidthFloat)
var startOffset = (canvasWidth - animatedVolumeWidth) / 2

val barMinHeight = 0f
val barMaxHeight = canvasHeight / 2f / heightDivider

val heightDivider by animateFloatAsState(
    targetValue = if (isAnimating) 1f else 6f,
    animationSpec = tween(1000, easing = LinearEasing)
)

      
                


val infiniteAnimation = rememberInfiniteTransition()
val animations = mutableListOf<State<Float>>()
val random = remember { Random(System.currentTimeMillis()) }

repeat(15) {
    val durationMillis = random.nextInt(500, 2000)
    animations += infiniteAnimation.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis),
            repeatMode = RepeatMode.Reverse,
         )
    )
}

val initialMultipliers = remember {
    mutableListOf<Float>().apply {
        repeat(MaxLinesCount) { this += random.nextFloat() }
    }
}

1) Obtenha valores aleatórios de altura, no nosso caso eles são repetidos a cada 15 vezes.

2) Adicione initialMultiplierspara currentSizereduzir a chance de valores se repetirem:

3) Use interpolação linear para redimensionar suavemente a altura:

// 1
val currentSize = animations[index % animations.size].value
// 2
var barHeightPercent = initialMultipliers[index] + currentSize
if (barHeightPercent > 1.0f) {
    val diff = barHeightPercent - 1.0f
    barHeightPercent = 1.0f - diff
}
// 3
val barHeight = lerpF(barMinHeight, barMaxHeight, barHeightPercent)

// 1 draw the bar
drawLine(
    color = barColor,
    start = Offset(startOffset, canvasCenterY - barHeight / 2),
    end = Offset(startOffset, canvasCenterY + barHeight / 2),
    strokeWidth = barWidthFloat,
    cap = StrokeCap.Round,
)
// 2 calculate the offset for next bar
startOffset += barWidthFloat + gapWidthFloat

repeat(count) { index ->
    val currentSize = animations[index % animations.size].value
    var barHeightPercent = initialMultipliers[index] + currentSize
    if (barHeightPercent > 1.0f) {
        val diff = barHeightPercent - 1.0f
        barHeightPercent = 1.0f - diff
    }
    val barHeight = lerpF(barMinHeight, barMaxHeight, barHeightPercent)
    drawLine(
        color = barColor,
        start = Offset(startOffset, canvasCenterY - barHeight / 2),
        end = Offset(startOffset, canvasCenterY + barHeight / 2),
        strokeWidth = barWidthFloat,
        cap = StrokeCap.Round,
    )
    startOffset += barWidthFloat + gapWidthFloat
}