Improved rendering of voice messages (#1782)

This commit is contained in:
Nicolas Mauri
2023-09-22 16:53:17 +02:00
committed by GitHub
parent bc1910e9bb
commit e9ce24fd9c
10 changed files with 60 additions and 34 deletions
@@ -33,11 +33,11 @@ struct VoiceRoomPlaybackView: View {
var onPlayPause: () -> Void = { }
var onSeek: (Double) -> Void = { _ in }
var onWaveformDragStateChanged: (Bool) -> Void = { _ in }
var onScrubbing: (Bool) -> Void = { _ in }
private enum DragState: Equatable {
case inactive
case pressing
case pressing(progress: Double)
case dragging(progress: Double)
var progress: Double {
@@ -69,6 +69,7 @@ struct VoiceRoomPlaybackView: View {
}
@GestureState private var dragState = DragState.inactive
@State private var tapProgress: Double = .zero
var timeLabelContent: String {
// Display the duration if progress is 0.0
@@ -76,6 +77,10 @@ struct VoiceRoomPlaybackView: View {
return Self.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: playbackViewState.duration * percent))
}
var showWaveformCursor: Bool {
playbackViewState.playing || dragState.isDragging
}
var body: some View {
HStack {
HStack {
@@ -87,18 +92,25 @@ struct VoiceRoomPlaybackView: View {
}
.padding(.vertical, 6)
GeometryReader { geometry in
WaveformView(waveform: playbackViewState.waveform, progress: playbackViewState.progress)
WaveformView(waveform: playbackViewState.waveform, progress: playbackViewState.progress, showCursor: showWaveformCursor)
// Add a gesture to drag the waveform
.gesture(LongPressGesture()
.gesture(SpatialTapGesture()
.simultaneously(with: LongPressGesture())
.sequenced(before: DragGesture(coordinateSpace: .local))
.updating($dragState) { value, state, _ in
switch value {
// Long press begins.
case .first(true):
state = .pressing
// (SpatialTap, LongPress) begins.
case .first(let spatialLongPress) where spatialLongPress.second == true:
// Compute the progress with the spatialTap location
let progress = (spatialLongPress.first?.location ?? .zero).x / geometry.size.width
state = .pressing(progress: progress)
// Long press confirmed, dragging may begin.
case .second(true, let drag):
let progress: Double = (drag?.location.x ?? .zero) / geometry.size.width
case .second(let spatialLongPress, let drag) where spatialLongPress.second == true:
var progress: Double = tapProgress
// Compute the progress with drag location
if let loc = drag?.location {
progress = loc.x / geometry.size.width
}
state = .dragging(progress: progress)
// Dragging ended or the long press cancelled.
default:
@@ -111,9 +123,10 @@ struct VoiceRoomPlaybackView: View {
.onChange(of: dragState) { newDragState in
switch newDragState {
case .inactive:
onWaveformDragStateChanged(false)
case .pressing:
onWaveformDragStateChanged(true)
onScrubbing(false)
case .pressing(let progress):
tapProgress = progress
onScrubbing(true)
feedbackGenerator.prepare()
sendFeedback = true
case .dragging(let progress):
@@ -35,7 +35,7 @@ struct VoiceRoomTimelineView: View {
VoiceRoomPlaybackView(playbackViewState: playbackViewState,
onPlayPause: onPlaybackPlayPause,
onSeek: onPlaybackSeek(_:),
onWaveformDragStateChanged: onPlaybackDragStateChanged(_:))
onScrubbing: onPlaybackScrubbing(_:))
.fixedSize(horizontal: false, vertical: true)
}
}
@@ -48,7 +48,7 @@ struct VoiceRoomTimelineView: View {
context.send(viewAction: .seekAudio(itemID: timelineItem.id, progress: progress))
}
private func onPlaybackDragStateChanged(_ dragging: Bool) {
private func onPlaybackScrubbing(_ dragging: Bool) {
if dragging {
context.send(viewAction: .disableLongPress(itemID: timelineItem.id))
} else {
@@ -40,11 +40,12 @@ extension Waveform {
}
struct WaveformView: View {
let lineWidth: CGFloat = 2
let linePadding: CGFloat = 2
private let lineWidth: CGFloat = 2
private let linePadding: CGFloat = 2
var waveform: Waveform
let minimumGraphAmplitude: CGFloat = 1
private let minimumGraphAmplitude: CGFloat = 1
var progress: CGFloat = 0.0
var showCursor = false
var body: some View {
GeometryReader { geometry in
@@ -64,9 +65,9 @@ struct WaveformView: View {
var xOffset: CGFloat = lineWidth / 2
var index = 0
while xOffset < width - lineWidth {
while xOffset <= width {
let sample = CGFloat(index >= normalisedData.count ? 0 : normalisedData[index])
let drawingAmplitude = max(minimumGraphAmplitude, sample * height)
let drawingAmplitude = max(minimumGraphAmplitude, sample * (height - 2))
path.move(to: CGPoint(x: xOffset, y: centerY - drawingAmplitude / 2))
path.addLine(to: CGPoint(x: xOffset, y: centerY + drawingAmplitude / 2))
@@ -76,13 +77,28 @@ struct WaveformView: View {
}
.stroke(Color.compound.iconSecondary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
}
// Display a cursor
.overlay(alignment: .leading) {
RoundedRectangle(cornerRadius: 1).fill(Color.compound.iconAccentTertiary)
.offset(CGSize(width: cursorPosition(progress: progress, width: geometry.size.width), height: 0.0))
.frame(width: lineWidth, height: geometry.size.height)
.opacity(showCursor ? 1 : 0)
}
}
}
private func cursorPosition(progress: Double, width: Double) -> Double {
guard progress > 0 else {
return 0
}
let width = (width * progress)
return width - width.truncatingRemainder(dividingBy: lineWidth + linePadding)
}
}
struct WaveformView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
WaveformView(waveform: Waveform.mockWaveform)
WaveformView(waveform: Waveform.mockWaveform, progress: 0.5)
.frame(width: 140, height: 50)
}
}