Skip to content

Commit 904cd45

Browse files
committed
Compose waveform seekbar
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
1 parent 2a8b743 commit 904cd45

3 files changed

Lines changed: 121 additions & 47 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: 25 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
package com.nextcloud.talk.ui.chat
99

1010
import android.text.format.DateUtils
11-
import android.widget.SeekBar
1211
import androidx.compose.foundation.layout.Column
1312
import androidx.compose.foundation.layout.Row
1413
import androidx.compose.foundation.layout.fillMaxWidth
@@ -19,26 +18,32 @@ import androidx.compose.material.icons.Icons
1918
import androidx.compose.material.icons.filled.Pause
2019
import androidx.compose.material.icons.filled.PlayArrow
2120
import androidx.compose.material3.CircularProgressIndicator
21+
import androidx.compose.material3.ExperimentalMaterial3Api
2222
import androidx.compose.material3.Icon
2323
import androidx.compose.material3.IconButton
2424
import androidx.compose.material3.MaterialTheme.colorScheme
2525
import androidx.compose.material3.Text
2626
import androidx.compose.material3.TextButton
2727
import androidx.compose.runtime.Composable
28+
import androidx.compose.runtime.getValue
29+
import androidx.compose.runtime.mutableFloatStateOf
2830
import androidx.compose.runtime.remember
31+
import androidx.compose.runtime.setValue
2932
import androidx.compose.ui.Alignment
3033
import androidx.compose.ui.Modifier
3134
import androidx.compose.ui.graphics.toArgb
3235
import androidx.compose.ui.res.stringResource
3336
import androidx.compose.ui.unit.dp
34-
import androidx.compose.ui.viewinterop.AndroidView
3537
import com.nextcloud.talk.R
3638
import com.nextcloud.talk.chat.ui.model.ChatMessageUi
3739
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
38-
import com.nextcloud.talk.ui.WaveformSeekBar
40+
import com.nextcloud.talk.ui.ComposeWaveformSeekBar
41+
import com.nextcloud.talk.ui.WAVEFORM_SIZE
42+
import com.nextcloud.talk.utils.AudioUtils
3943

4044
private const val SEEKBAR_MAX = 100
4145

46+
@OptIn(ExperimentalMaterial3Api::class)
4247
@Suppress("Detekt.LongMethod", "LongParameterList")
4348
@Composable
4449
fun VoiceMessage(
@@ -57,12 +62,14 @@ fun VoiceMessage(
5762
forceTimeBelow = true,
5863
content = {
5964
val inversePrimaryColor = colorScheme.inversePrimary
60-
val inversePrimary = remember(inversePrimaryColor) { inversePrimaryColor.toArgb() }
65+
remember(inversePrimaryColor) { inversePrimaryColor.toArgb() }
6166
val onPrimaryContainerColor = colorScheme.onPrimaryContainer
62-
val onPrimaryContainer = remember(onPrimaryContainerColor) { onPrimaryContainerColor.toArgb() }
67+
remember(onPrimaryContainerColor) { onPrimaryContainerColor.toArgb() }
6368
val remainingSeconds = (typeContent.durationSeconds - typeContent.playedSeconds).coerceAtLeast(0)
64-
val waveformData = remember(typeContent.waveform) { typeContent.waveform.toFloatArray() }
65-
val lastWaveformData = remember { mutableListOf<Float>() }
69+
val waveformData = remember(typeContent.waveform) {
70+
val floatArr = typeContent.waveform.toFloatArray()
71+
AudioUtils.shrinkFloatArray(floatArr, WAVEFORM_SIZE)
72+
}
6673

6774
Column {
6875
Row(
@@ -84,49 +91,21 @@ fun VoiceMessage(
8491
}
8592
}
8693

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-
107-
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
94+
var sliderValue by remember { mutableFloatStateOf(0f) }
95+
sliderValue = typeContent.seekbarProgress * 1f / SEEKBAR_MAX
10896

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()
97+
ComposeWaveformSeekBar(
98+
sliderValue,
99+
{
100+
val progressI = (it * SEEKBAR_MAX).toInt()
101+
onSeek(message.id, progressI)
102+
sliderValue = it
126103
},
127104
modifier = Modifier
128-
.weight(1f)
129105
.height(56.dp)
106+
.fillMaxWidth()
107+
.padding(8.dp), // or weight(1f),
108+
waveformData
130109
)
131110

132111
TextButton(

0 commit comments

Comments
 (0)