Tutorial de Jetpack Compose: replicación de la aplicación Dribbble Audio, parte 1

Jan 27 2023
Implementación de una interfaz de usuario totalmente personalizada con animaciones complejas: Animación de forma de onda En Exyte tratamos de contribuir al código abierto tanto como podamos, desde la publicación de bibliotecas y componentes hasta la redacción de artículos y tutoriales. Un tipo de tutoriales que hacemos es replicar: tomar un componente de interfaz de usuario complejo, implementarlo usando nuevos marcos y escribir un tutorial al mismo tiempo.

Implementación de una interfaz de usuario totalmente personalizada con animaciones complejas: animación de forma de onda

En Exyte tratamos de contribuir al código abierto tanto como podamos, desde el lanzamiento de bibliotecas y componentes hasta la redacción de artículos y tutoriales. Un tipo de tutoriales que hacemos es replicar: tomar un componente de interfaz de usuario complejo, implementarlo usando nuevos marcos y escribir un tutorial al mismo tiempo. Comenzamos con SwiftUI hace algunos años, pero hoy finalmente incursionamos en Android, usando el framework declarativo de UI de Google: Jetpack Compose.

Concepto de diseño del talentoso Taras Migulko @dribbble

Estructura de la pantalla

Nuestra aplicación constará de 3 pantallas, con transiciones animadas entre ellas:

Para facilitar la lectura, la replicación de estas pantallas se analizará en varios artículos que cubren los siguientes elementos y animaciones de la interfaz de usuario:

  • Forma de onda de la primera pantalla
  • Panel de acción de la segunda pantalla
  • Encabezado colapsado desde la tercera pantalla
  • Transición de gestos de arrastre
  • Transición de elementos compartidos

Crear la forma de onda

Esto es lo que queremos implementar:

Todo el elemento consta de barras de volumen verticales animadas, espaciadas a una distancia regular entre sí. Veamos las dimensiones básicas que usaremos:

Como bien sabes, el mundo de los dispositivos Android es rico en pantallas de diferentes tamaños, y esto plantea la siguiente pregunta: ¿cómo presentar la forma de onda en las pantallas de los diferentes dispositivos para que ocupe todo el espacio disponible? Para hacer que este widget se ajuste al ancho completo de la pantalla, puede aumentar el ancho de las líneas proporcionalmente o puede aumentar el número de líneas. Elegimos la segunda forma de preservar la nitidez de la imagen. En este caso el primer paso es calcular el número de líneas necesarias. Para hacer eso, necesitaremos dividir el ancho de lienzo disponible por el tamaño de una sola barra de volumen (ancho de barra + ancho de espacio). Además, necesitamos usar toInt()en lugar de roundToInt()porque solo queremos usar elementos que se ajusten completamente al tamaño, independientemente del redondeo.

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) Obtener valores de altura aleatorios, en nuestro caso se repiten cada 15 veces.

2) Agregue initialMultipliersa currentSizepara reducir la posibilidad de que los valores se repitan entre sí:

3) Use la interpolación lineal para cambiar el tamaño de la altura sin problemas:

// 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
}