mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Improved rendering of voice messages (#1782)
This commit is contained in:
+25
-12
@@ -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):
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+22
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user