Tutorial Jetpack Compose: replica dell'app audio Dribbble, parte 1
Implementazione di un'interfaccia utente completamente personalizzata con animazioni complesse: Waveform Animation
In Exyte cerchiamo di contribuire all'open-source il più possibile, dal rilascio di librerie e componenti alla scrittura di articoli e tutorial. Un tipo di tutorial che facciamo è la replica: prendere un componente dell'interfaccia utente complesso, implementarlo utilizzando nuovi framework e scrivere un tutorial a fianco. Abbiamo iniziato con SwiftUI alcuni anni fa, ma oggi finalmente entriamo in Android, utilizzando il framework UI dichiarativo di Google: Jetpack Compose.
Struttura dello schermo
La nostra app sarà composta da 3 schermate, con transizioni animate tra di loro:
Per facilità di lettura, la replica di queste schermate verrà discussa in più articoli che trattano i seguenti elementi dell'interfaccia utente e animazioni:
- Forma d'onda dalla prima schermata
- Pannello delle azioni dalla seconda schermata
- Collapsing Header dalla terza schermata
- Trascina la transizione del gesto
- Transizione elemento condiviso
Creazione della forma d'onda
Ecco cosa vogliamo implementare:
L'intero elemento è costituito da barre del volume verticali animate, distanziate a una distanza regolare l'una dall'altra. Diamo un'occhiata alle dimensioni di base che useremo:
Come ben sai, il mondo dei dispositivi Android è ricco di schermi di diverse dimensioni, e questo pone la seguente domanda: come presentare la forma d'onda sugli schermi dei diversi dispositivi in modo che occupi tutto lo spazio disponibile? Per fare in modo che questo widget si adatti all'intera larghezza dello schermo, puoi aumentare la larghezza delle linee proporzionalmente o puoi aumentare il numero di linee. Abbiamo scelto il secondo modo per preservare la nitidezza dell'immagine. In questo caso il primo passo è calcolare il numero necessario di righe. Per fare ciò, dovremo dividere la larghezza della tela disponibile per la dimensione di una singola barra del volume (larghezza della barra + larghezza dello spazio). Inoltre, dobbiamo utilizzare toInt()invece di roundToInt()perché vogliamo utilizzare solo elementi che si adattino perfettamente alla dimensione, indipendentemente dall'arrotondamento.
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) Ottieni valori di altezza casuali, nel nostro caso vengono ripetuti ogni 15 volte.
2) Aggiungi initialMultipliersa currentSizeper ridurre la possibilità che i valori si ripetano:
3) Usa l'interpolazione lineare per ridimensionare uniformemente l'altezza:
// 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
}

![Che cos'è un elenco collegato, comunque? [Parte 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































