Tutorial de Jetpack Compose: replicación de la aplicación Dribbble Audio, parte 1
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.
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
}

![¿Qué es una lista vinculada, de todos modos? [Parte 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































