Skip to content

Commit ff32b9b

Browse files
authored
VoiceAssistant redesign changes (#23)
- Another refactor of the visualizer, mainly to remove the use of `Spacer()` and make the layout slightly more predictable. - Fixes a bug with animation jumping again between agent states when using `id`
1 parent 8376284 commit ff32b9b

File tree

1 file changed

+36
-22
lines changed

1 file changed

+36
-22
lines changed

Sources/LiveKitComponents/UI/Visualizer/BarAudioVisualizer.swift

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,44 +112,58 @@ public struct BarAudioVisualizer: View {
112112
GeometryReader { geometry in
113113
let highlightingSequence = animationProperties.highlightingSequence(agentState: agentState)
114114
let highlighted = highlightingSequence[animationPhase % highlightingSequence.count]
115-
let duration = animationProperties.duration(agentState: agentState)
116115

117116
bars(geometry: geometry, highlighted: highlighted)
118117
.onAppear {
119-
animationTask?.cancel()
120-
animationTask = Task {
121-
while !Task.isCancelled {
122-
try? await Task.sleep(nanoseconds: UInt64(duration * Double(NSEC_PER_SEC)))
123-
withAnimation(.easeInOut) { animationPhase += 1 }
124-
}
125-
}
118+
startAnimation(duration: animationProperties.duration(agentState: agentState))
126119
}
127120
.onDisappear {
128-
animationTask?.cancel()
121+
stopAnimation()
129122
}
130-
.animation(.easeOut, value: agentState)
131-
.onChange(of: agentState) { _ in
132-
animationPhase = 0
123+
.onChange(of: agentState) { newState in
124+
startAnimation(duration: animationProperties.duration(agentState: newState))
133125
}
126+
.animation(.easeInOut, value: animationPhase)
127+
.animation(.easeInOut(duration: 0.3), value: agentState)
134128
}
135129
}
136130

137131
@ViewBuilder
138132
private func bars(geometry: GeometryProxy, highlighted: PhaseAnimationProperties.HighlightedBars) -> some View {
139-
let barMinHeight = (geometry.size.width - geometry.size.width * barSpacingFactor * CGFloat(barCount + 2)) / CGFloat(barCount)
133+
let totalSpacing = geometry.size.width * barSpacingFactor * CGFloat(barCount + 1)
134+
let availableWidth = geometry.size.width - totalSpacing
135+
let barWidth = availableWidth / CGFloat(barCount)
136+
let barMinHeight = barWidth // Use bar width as minimum height for square proportions
137+
140138
HStack(alignment: .center, spacing: geometry.size.width * barSpacingFactor) {
141139
ForEach(0 ..< audioProcessor.bands.count, id: \.self) { index in
142-
VStack {
143-
Spacer()
144-
RoundedRectangle(cornerRadius: barMinHeight)
145-
.fill(barColor)
146-
.opacity(highlighted.contains(index) ? 1 : barMinOpacity)
147-
.frame(height: (geometry.size.height - barMinHeight) * CGFloat(audioProcessor.bands[index]) + barMinHeight)
148-
Spacer()
149-
}
140+
RoundedRectangle(cornerRadius: barMinHeight)
141+
.fill(barColor)
142+
.opacity(highlighted.contains(index) ? 1 : barMinOpacity)
143+
.frame(
144+
width: barWidth,
145+
height: (geometry.size.height - barMinHeight) * CGFloat(audioProcessor.bands[index]) + barMinHeight,
146+
alignment: .center
147+
)
148+
.frame(maxHeight: .infinity, alignment: .center)
149+
}
150+
}
151+
.frame(width: geometry.size.width)
152+
}
153+
154+
private func startAnimation(duration: TimeInterval) {
155+
animationTask?.cancel()
156+
animationPhase = 0
157+
animationTask = Task {
158+
while !Task.isCancelled {
159+
try? await Task.sleep(nanoseconds: UInt64(duration * Double(NSEC_PER_SEC)))
160+
animationPhase += 1
150161
}
151162
}
152-
.padding(geometry.size.width * barSpacingFactor)
163+
}
164+
165+
private func stopAnimation() {
166+
animationTask?.cancel()
153167
}
154168
}
155169

0 commit comments

Comments
 (0)