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 d2541926cc
commit bd621ff7cf
10 changed files with 60 additions and 34 deletions

View File

@@ -24,7 +24,6 @@ struct SwipeRightAction<Label: View>: ViewModifier {
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
@State private var canStartAction = false
@State private var animate = false
@GestureState private var dragGestureActive = false
@State private var hasReachedActionThreshold = false
@@ -40,7 +39,7 @@ struct SwipeRightAction<Label: View>: ViewModifier {
func body(content: Content) -> some View {
content
.offset(x: xOffset, y: 0.0)
.animation(.interactiveSpring().speed(0.5), value: animate)
.animation(.interactiveSpring().speed(0.5), value: xOffset)
.gesture(DragGesture()
.updating($dragGestureActive) { _, state, _ in
// Available actions should be computed on the fly so we use a gesture state change
@@ -69,7 +68,6 @@ struct SwipeRightAction<Label: View>: ViewModifier {
} else {
hasReachedActionThreshold = false
}
animate = true
}
.onEnded { _ in
if xOffset > actionThreshold {
@@ -77,7 +75,6 @@ struct SwipeRightAction<Label: View>: ViewModifier {
}
xOffset = 0.0
animate = false
}
)
.onChange(of: dragGestureActive, perform: { value in

View File

@@ -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):

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -461,7 +461,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
return AudioRoomTimelineItemContent(body: messageContent.body,
duration: messageContent.info?.duration ?? 0,
duration: (messageContent.audio?.duration ?? 0) / 1000.0,
waveform: waveform,
source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype),
contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body))

View File

@@ -283,7 +283,7 @@ class RoomScreenViewModelTests: XCTestCase {
.store(in: &cancellables)
// Test
let deferred = deferFulfillment(viewModel.context.$viewState.collect(2).first(),
let deferred = deferFulfillment(viewModel.context.$viewState.collect(3).first(),
message: "The existing view state plus one new one should be published.")
viewModel.context.send(viewAction: .tappedOnUser(userID: "bob"))
try await deferred.fulfill()

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:09dc1ab2bf9b5f9576a7f222ee84bd821ae1e608974a5b341cf2c41ce5bfab5e
size 63967
oid sha256:0e12529919d3829493278947168ddf881d9b7ae7929de5912a7a9c759c9be1ce
size 62187

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de7c4ad957c37ad1f46c965985ed4f8884f86a934595c1d4a2ece836b5d5b2c7
size 72812
oid sha256:5da10a2b12265a9e3e40ee408030ebc773c938d70a6cf14c2bf4249296bdf93e
size 72786

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9de9d8875b3a9182562fe07f11777d3cb6132d2c97368f15b7f14ca14004a7af
size 69146
oid sha256:ccfa620baa535643770bd4db7a3c329493ac6bf0351847c03eaadc4831bc7f68
size 69227

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9632b9c96b960ef1ee36ca7980253aaad8e21ded28d741f5d62b33cc4e8a0eba
size 63913
oid sha256:82130c80be62ed9d3ba0d54df5fec6803c3bed6ef0dc00d044696938d5c41598
size 64238