Skip to content

Commit fbbcb80

Browse files
authored
Merge pull request #6033 from nextcloud/issue-6019-waveform-compose
Improving Voice Messages after ChatKit removal
2 parents ca8ed38 + 744a491 commit fbbcb80

3 files changed

Lines changed: 133 additions & 48 deletions

File tree

app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
1313
import com.nextcloud.talk.chat.data.model.ChatMessage
1414
import com.nextcloud.talk.data.database.model.SendStatus
1515
import com.nextcloud.talk.data.user.model.User
16+
import com.nextcloud.talk.ui.PlaybackSpeed
1617
import com.nextcloud.talk.utils.ApiUtils
1718
import com.nextcloud.talk.utils.DrawableUtils
18-
import com.nextcloud.talk.ui.PlaybackSpeed
1919
import java.time.LocalDate
2020

2121
// immutable class for chat message UI. only val, no vars!
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.ui
9+
10+
import androidx.compose.foundation.background
11+
import androidx.compose.foundation.layout.Box
12+
import androidx.compose.foundation.layout.fillMaxWidth
13+
import androidx.compose.foundation.layout.height
14+
import androidx.compose.foundation.layout.size
15+
import androidx.compose.foundation.shape.CircleShape
16+
import androidx.compose.material3.ExperimentalMaterial3Api
17+
import androidx.compose.material3.MaterialTheme
18+
import androidx.compose.material3.Slider
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.remember
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.draw.drawWithCache
23+
import androidx.compose.ui.geometry.Offset
24+
import androidx.compose.ui.graphics.StrokeCap
25+
import androidx.compose.ui.graphics.drawscope.Stroke
26+
import androidx.compose.ui.tooling.preview.Preview
27+
import androidx.compose.ui.unit.dp
28+
29+
const val WAVEFORM_THUMB_SIZE = 20
30+
const val WAVEFORM_SIZE = 30
31+
const val OVERLAP = 0.025
32+
33+
@OptIn(ExperimentalMaterial3Api::class)
34+
@Composable
35+
fun ComposeWaveformSeekBar(value: Float, onValueChange: (Float) -> Unit, modifier: Modifier, waveData: FloatArray) {
36+
val barWidth = Stroke.DefaultMiter
37+
val thumbSize = WAVEFORM_THUMB_SIZE.dp
38+
val inversePrimary = MaterialTheme.colorScheme.inversePrimary
39+
val onPrimaryContainer = MaterialTheme.colorScheme.onPrimaryContainer
40+
41+
Slider(
42+
value = value,
43+
onValueChange = onValueChange,
44+
track = {
45+
Box(
46+
modifier = modifier
47+
.drawWithCache {
48+
onDrawBehind {
49+
val height = this.size.height
50+
val width = this.size.width
51+
val midpoint = (this.size.height / 2f)
52+
53+
val barGap = (width - waveData.size * barWidth) / (waveData.size - 1).toFloat() + 1
54+
for (i in waveData.indices) {
55+
val x: Float = i * (barWidth + barGap)
56+
val y: Float = waveData[i] * height
57+
val isXBeforeThumb = (x / this.size.width) <= value + OVERLAP
58+
59+
drawLine(
60+
if (isXBeforeThumb) inversePrimary else onPrimaryContainer,
61+
start = Offset(x, midpoint - y),
62+
end = Offset(x, midpoint + y),
63+
strokeWidth = Stroke.DefaultMiter,
64+
cap = StrokeCap.Round
65+
)
66+
}
67+
}
68+
}
69+
)
70+
},
71+
thumb = {
72+
Box(
73+
modifier = Modifier
74+
.size(thumbSize)
75+
.background(inversePrimary, shape = CircleShape)
76+
)
77+
}
78+
)
79+
}
80+
81+
@OptIn(ExperimentalMaterial3Api::class)
82+
@Composable
83+
@Preview
84+
fun Preview() {
85+
val waveData = remember { FloatArray(WAVEFORM_SIZE) { (Math.random() % 1).toFloat() } }
86+
87+
ComposeWaveformSeekBar(
88+
0f,
89+
{},
90+
modifier = Modifier
91+
.height(MAX_HEIGHT.dp)
92+
.fillMaxWidth(),
93+
waveData
94+
)
95+
}

app/src/main/java/com/nextcloud/talk/ui/chat/VoiceMessage.kt

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
package com.nextcloud.talk.ui.chat
99

1010
import android.text.format.DateUtils
11-
import android.widget.SeekBar
11+
import androidx.compose.animation.core.animateIntAsState
1212
import androidx.compose.foundation.layout.Column
1313
import androidx.compose.foundation.layout.Row
1414
import androidx.compose.foundation.layout.fillMaxWidth
@@ -19,26 +19,34 @@ import androidx.compose.material.icons.Icons
1919
import androidx.compose.material.icons.filled.Pause
2020
import androidx.compose.material.icons.filled.PlayArrow
2121
import androidx.compose.material3.CircularProgressIndicator
22+
import androidx.compose.material3.ExperimentalMaterial3Api
2223
import androidx.compose.material3.Icon
2324
import androidx.compose.material3.IconButton
2425
import androidx.compose.material3.MaterialTheme.colorScheme
2526
import androidx.compose.material3.Text
2627
import androidx.compose.material3.TextButton
2728
import androidx.compose.runtime.Composable
29+
import androidx.compose.runtime.getValue
30+
import androidx.compose.runtime.mutableFloatStateOf
2831
import androidx.compose.runtime.remember
32+
import androidx.compose.runtime.setValue
2933
import androidx.compose.ui.Alignment
3034
import androidx.compose.ui.Modifier
3135
import androidx.compose.ui.graphics.toArgb
3236
import androidx.compose.ui.res.stringResource
3337
import androidx.compose.ui.unit.dp
34-
import androidx.compose.ui.viewinterop.AndroidView
3538
import com.nextcloud.talk.R
3639
import com.nextcloud.talk.chat.ui.model.ChatMessageUi
3740
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
38-
import com.nextcloud.talk.ui.WaveformSeekBar
41+
import com.nextcloud.talk.ui.ComposeWaveformSeekBar
42+
import com.nextcloud.talk.ui.WAVEFORM_SIZE
43+
import com.nextcloud.talk.utils.AudioUtils
3944

4045
private const val SEEKBAR_MAX = 100
46+
private const val START_WAVE_FORM_HEIGHT = 10
47+
private const val END_WAVE_FORM_HEIGHT = 56
4148

49+
@OptIn(ExperimentalMaterial3Api::class)
4250
@Suppress("Detekt.LongMethod", "LongParameterList")
4351
@Composable
4452
fun VoiceMessage(
@@ -57,12 +65,23 @@ fun VoiceMessage(
5765
forceTimeBelow = true,
5866
content = {
5967
val inversePrimaryColor = colorScheme.inversePrimary
60-
val inversePrimary = remember(inversePrimaryColor) { inversePrimaryColor.toArgb() }
68+
remember(inversePrimaryColor) { inversePrimaryColor.toArgb() }
6169
val onPrimaryContainerColor = colorScheme.onPrimaryContainer
62-
val onPrimaryContainer = remember(onPrimaryContainerColor) { onPrimaryContainerColor.toArgb() }
70+
remember(onPrimaryContainerColor) { onPrimaryContainerColor.toArgb() }
6371
val remainingSeconds = (typeContent.durationSeconds - typeContent.playedSeconds).coerceAtLeast(0)
64-
val waveformData = remember(typeContent.waveform) { typeContent.waveform.toFloatArray() }
65-
val lastWaveformData = remember { mutableListOf<Float>() }
72+
val waveformData = remember(typeContent.waveform) {
73+
val floatArr = typeContent.waveform.toFloatArray()
74+
if (floatArr.size < WAVEFORM_SIZE) {
75+
FloatArray(WAVEFORM_SIZE)
76+
} else {
77+
AudioUtils.shrinkFloatArray(floatArr, WAVEFORM_SIZE)
78+
}
79+
}
80+
81+
val animValue by animateIntAsState(
82+
if (waveformData.average() > 0) END_WAVE_FORM_HEIGHT else START_WAVE_FORM_HEIGHT,
83+
label = "size"
84+
)
6685

6786
Column {
6887
Row(
@@ -84,49 +103,20 @@ fun VoiceMessage(
84103
}
85104
}
86105

87-
AndroidView(
88-
factory = { ctx ->
89-
WaveformSeekBar(ctx).apply {
90-
max = SEEKBAR_MAX
91-
setWaveData(waveformData)
92-
setColors(
93-
inversePrimary,
94-
onPrimaryContainer
95-
)
96-
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
97-
override fun onProgressChanged(
98-
seekBar: SeekBar?,
99-
progress: Int,
100-
fromUser: Boolean
101-
) {
102-
if (fromUser) {
103-
onSeek(message.id, progress)
104-
}
105-
}
106+
var sliderValue by remember { mutableFloatStateOf(0f) }
107+
sliderValue = typeContent.seekbarProgress * 1f / SEEKBAR_MAX
106108

107-
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
108-
109-
override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit
110-
})
111-
}
112-
},
113-
update = { seekBar ->
114-
seekBar.max = SEEKBAR_MAX
115-
val waveformChanged = typeContent.waveform != lastWaveformData
116-
if (waveformChanged) {
117-
lastWaveformData.clear()
118-
lastWaveformData.addAll(typeContent.waveform)
119-
seekBar.setWaveData(waveformData)
120-
seekBar.requestLayout()
121-
}
122-
seekBar.setColors(inversePrimary, onPrimaryContainer)
123-
seekBar.progress = typeContent.seekbarProgress
124-
seekBar.isEnabled = !typeContent.isDownloading
125-
seekBar.invalidate()
109+
ComposeWaveformSeekBar(
110+
sliderValue,
111+
{
112+
val progressI = (it * SEEKBAR_MAX).toInt()
113+
onSeek(message.id, progressI)
126114
},
127115
modifier = Modifier
128-
.weight(1f)
129-
.height(56.dp)
116+
.height(animValue.dp)
117+
.fillMaxWidth()
118+
.padding(8.dp), // or weight(1f),
119+
waveformData
130120
)
131121

132122
TextButton(

0 commit comments

Comments
 (0)