mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Add smooth animation to voice messages play progress (#7698)
* Add linear animation to voice message playback progress Instead of animating the small `progress` value changes, calculate an animation for the range `maxProgress - progress`, with a linear interpolator so it's completely smooth. Previously, the progress was updated in 'steps' that were quite noticeable.
This commit is contained in:
+1
@@ -39,5 +39,6 @@ fun aVoiceMessagePreviewState() = VoiceMessageState.Preview(
|
||||
showCursor = false,
|
||||
playbackProgress = 0f,
|
||||
time = 10.seconds,
|
||||
duration = 10.seconds,
|
||||
waveform = WaveFormSamples.realisticWaveForm,
|
||||
)
|
||||
|
||||
+3
@@ -142,6 +142,9 @@ fun TimelineItemVoiceView(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(34.dp),
|
||||
isPlaying = state.isPlaying,
|
||||
durationMs = state.durationMs,
|
||||
playbackSpeed = state.playbackSpeed,
|
||||
seekEnabled = !isTalkbackActive(),
|
||||
onSeek = { state.eventSink(VoiceMessageEvent.Seek(it)) },
|
||||
)
|
||||
|
||||
+4
@@ -245,12 +245,16 @@ class DefaultVoiceMessageComposerPresenter(
|
||||
val playerTime by remember(playerState, recorderState) { derivedStateOf { displayTime(playerState, recorderState) } }
|
||||
val waveform by remember(recorderState) { derivedStateOf { recorderState.finishedWaveform() } }
|
||||
|
||||
val recorderDuration = (recorderState as VoiceRecorderState.Finished).duration
|
||||
val duration = playerState.durationMs?.milliseconds ?: recorderDuration
|
||||
|
||||
return VoiceMessageState.Preview(
|
||||
isSending = isSending,
|
||||
isPlaying = playerState.isPlaying,
|
||||
showCursor = showCursor,
|
||||
playbackProgress = playerState.progress,
|
||||
time = playerTime,
|
||||
duration = duration,
|
||||
waveform = waveform,
|
||||
)
|
||||
}
|
||||
|
||||
+4
@@ -63,6 +63,7 @@ class VoiceMessageComposerPlayer(
|
||||
playState = it.playState,
|
||||
currentPosition = it.currentPosition,
|
||||
progress = calcProgress(it),
|
||||
durationMs = if (it.playState != PlayState.Stopped) it.duration else null,
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
@@ -183,17 +184,20 @@ class VoiceMessageComposerPlayer(
|
||||
* @property playState Whether this player is currently playing. See [PlayState].
|
||||
* @property currentPosition The elapsed time of this player in milliseconds.
|
||||
* @property progress The progress of this player between 0 and 1.
|
||||
* @property durationMs The player-reported duration in milliseconds, when known and the player is not stopped.
|
||||
*/
|
||||
data class State(
|
||||
val playState: PlayState,
|
||||
val currentPosition: Long,
|
||||
val progress: Float,
|
||||
val durationMs: Long?,
|
||||
) {
|
||||
companion object {
|
||||
val Initial = State(
|
||||
playState = PlayState.Stopped,
|
||||
currentPosition = 0L,
|
||||
progress = 0f,
|
||||
durationMs = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+23
-4
@@ -103,6 +103,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
||||
|
||||
companion object {
|
||||
private val RECORDING_DURATION = 1.seconds
|
||||
private val PLAYER_DURATION = 10.seconds
|
||||
private val FIRST_LEVEL_DURATION = 500.milliseconds
|
||||
private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toImmutableList())
|
||||
private val FIRST_RECORDING_STATE = VoiceMessageState.Recording(FIRST_LEVEL_DURATION, listOf(0.1f).toImmutableList())
|
||||
@@ -266,6 +267,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
||||
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
|
||||
assertThat((finalState.voiceMessageState as VoiceMessageState.Preview).duration).isEqualTo(RECORDING_DURATION)
|
||||
startRecordResult.assertions().isCalledOnce()
|
||||
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||
deleteRecordingResult.assertions().isNeverCalled()
|
||||
@@ -302,6 +304,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
val finalState = awaitItem().also {
|
||||
assertThat(it.voiceMessageState).isEqualTo(aPlayingState())
|
||||
assertThat((it.voiceMessageState as VoiceMessageState.Preview).duration).isEqualTo(PLAYER_DURATION)
|
||||
}
|
||||
startRecordResult.assertions().isCalledOnce()
|
||||
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||
@@ -338,17 +341,30 @@ class DefaultVoiceMessageComposerPresenterTest {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f)))
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 0.seconds, showCursor = true))
|
||||
assertThat(voiceMessageState).isEqualTo(
|
||||
aPreviewState(playbackProgress = 0.5f, time = 0.seconds, showCursor = true)
|
||||
)
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 5.seconds, showCursor = true))
|
||||
assertThat(voiceMessageState).isEqualTo(
|
||||
aPreviewState(playbackProgress = 0.5f, time = 0.seconds, showCursor = true, duration = PLAYER_DURATION)
|
||||
)
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(
|
||||
aPreviewState(playbackProgress = 0.5f, time = 5.seconds, showCursor = true, duration = PLAYER_DURATION)
|
||||
)
|
||||
eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.2f)))
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.2f, time = 5.seconds, showCursor = true))
|
||||
assertThat(voiceMessageState).isEqualTo(
|
||||
aPreviewState(playbackProgress = 0.2f, time = 5.seconds, showCursor = true, duration = PLAYER_DURATION)
|
||||
)
|
||||
}
|
||||
val finalState = awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.2f, time = 2.seconds, showCursor = true))
|
||||
assertThat(voiceMessageState).isEqualTo(
|
||||
aPreviewState(playbackProgress = 0.2f, time = 2.seconds, showCursor = true, duration = PLAYER_DURATION)
|
||||
)
|
||||
}
|
||||
|
||||
testPauseAndDestroy(finalState)
|
||||
@@ -827,6 +843,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
||||
playbackProgress: Float = 0f,
|
||||
isSending: Boolean = false,
|
||||
time: Duration = RECORDING_DURATION,
|
||||
duration: Duration = RECORDING_DURATION,
|
||||
showCursor: Boolean = false,
|
||||
waveform: List<Float> = voiceRecorder.waveform,
|
||||
) = VoiceMessageState.Preview(
|
||||
@@ -834,6 +851,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
||||
playbackProgress = playbackProgress,
|
||||
isSending = isSending,
|
||||
time = time,
|
||||
duration = duration,
|
||||
showCursor = showCursor,
|
||||
waveform = waveform.toImmutableList(),
|
||||
)
|
||||
@@ -844,6 +862,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
||||
playbackProgress = 0.1f,
|
||||
showCursor = true,
|
||||
time = RECORDING_DURATION,
|
||||
duration = PLAYER_DURATION,
|
||||
)
|
||||
|
||||
private fun aPausedState() =
|
||||
|
||||
Reference in New Issue
Block a user