mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
feat: Add smooth scrolling to RealtimeAudioVisualizer
This commit is contained in:
parent
f0f20a6594
commit
d7500b52db
@ -11,6 +11,9 @@ import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -23,6 +26,7 @@ import com.arthenica.ffmpegkit.ReturnCode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
@ -44,6 +48,7 @@ class RecorderService: Service() {
|
||||
private var mediaRecorder: MediaRecorder? = null
|
||||
private var onError: MediaRecorder.OnErrorListener? = null
|
||||
private var onStateChange: (RecorderState) -> Unit = {}
|
||||
private var onAmplitudeUpdate: () -> Unit = {}
|
||||
|
||||
private var counter = 0
|
||||
|
||||
@ -79,19 +84,20 @@ class RecorderService: Service() {
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
val progress: Float
|
||||
get() {
|
||||
val start = recordingStart.value ?: return 0f
|
||||
val now = LocalDateTime.now()
|
||||
val duration = now.toEpochSecond(ZoneId.systemDefault().rules.getOffset(now)) - start.toEpochSecond(ZoneId.systemDefault().rules.getOffset(start))
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
return duration / (settings.maxDuration / 1000f)
|
||||
}
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
fun setOnErrorListener(onError: MediaRecorder.OnErrorListener) {
|
||||
this.onError = onError
|
||||
}
|
||||
|
||||
fun setOnAmplitudeUpdateListener(onAmplitudeUpdate: () -> Unit) {
|
||||
this.onAmplitudeUpdate = onAmplitudeUpdate
|
||||
}
|
||||
|
||||
fun setOnStateChangeListener(onStateChange: (RecorderState) -> Unit) {
|
||||
this.onStateChange = onStateChange
|
||||
}
|
||||
@ -133,6 +139,7 @@ class RecorderService: Service() {
|
||||
val amplitude = mediaRecorder!!.maxAmplitude
|
||||
amplitudes.add(amplitude)
|
||||
|
||||
onAmplitudeUpdate()
|
||||
handler.postDelayed(::updateAmplitude, AMPLITUDE_UPDATE_INTERVAL)
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,6 @@ private const val MAX_AMPLITUDE = 10000
|
||||
@Composable
|
||||
fun AudioVisualizer(
|
||||
amplitudes: List<Int>,
|
||||
showAll: Boolean = false
|
||||
) {
|
||||
val primary = MaterialTheme.colorScheme.primary
|
||||
val primaryMuted = primary.copy(alpha = 0.3f)
|
||||
@ -36,17 +35,15 @@ fun AudioVisualizer(
|
||||
translate(width, height) {
|
||||
amplitudes.forEachIndexed { index, amplitude ->
|
||||
val amplitudePercentage = (amplitude.toFloat() / MAX_AMPLITUDE).coerceAtMost(1f)
|
||||
val boxWidth = if (showAll) width / amplitudes.size else 15f
|
||||
val boxHeight = height * amplitudePercentage
|
||||
|
||||
drawRoundRect(
|
||||
color = if (amplitudePercentage > 0.05f) primary else primaryMuted,
|
||||
topLeft = Offset(
|
||||
if (showAll) -width / amplitudes.size * index
|
||||
else 30f * (index - amplitudes.size),
|
||||
-width / amplitudes.size * index,
|
||||
-boxHeight / 2f
|
||||
),
|
||||
size = Size(boxWidth, boxHeight),
|
||||
size = Size(width, boxHeight),
|
||||
cornerRadius = CornerRadius(3f, 3f)
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.locationtest.services.RecorderService
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val MAX_AMPLITUDE = 10000
|
||||
private const val BOX_WIDTH = 15f
|
||||
private const val BOX_GAP = 15f
|
||||
|
||||
@Composable
|
||||
fun RealtimeAudioVisualizer(
|
||||
service: RecorderService,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val amplitudes = service.amplitudes
|
||||
val primary = MaterialTheme.colorScheme.primary
|
||||
val primaryMuted = primary.copy(alpha = 0.3f)
|
||||
|
||||
// Moves the visualizer to the left
|
||||
// A new amplitude is added every 100L milliseconds, so the visualizer moves one
|
||||
// box width + gap in 100L
|
||||
val animationProgress = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
service.setOnAmplitudeUpdateListener {
|
||||
scope.launch {
|
||||
animationProgress.snapTo(0f)
|
||||
animationProgress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 100,
|
||||
easing = LinearEasing
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
) {
|
||||
val height = this.size.height / 2f
|
||||
val width = this.size.width
|
||||
|
||||
translate(width, height) {
|
||||
translate(-animationProgress.value * (BOX_WIDTH + BOX_GAP), 0f) {
|
||||
amplitudes.forEachIndexed { index, amplitude ->
|
||||
val amplitudePercentage = (amplitude.toFloat() / MAX_AMPLITUDE).coerceAtMost(1f)
|
||||
val boxHeight = height * amplitudePercentage
|
||||
|
||||
drawRoundRect(
|
||||
color = if (amplitudePercentage > 0.05f) primary else primaryMuted,
|
||||
topLeft = Offset(
|
||||
(BOX_WIDTH + BOX_GAP) * (index - amplitudes.size),
|
||||
-boxHeight / 2f
|
||||
),
|
||||
size = Size(BOX_WIDTH, boxHeight),
|
||||
cornerRadius = CornerRadius(3f, 3f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -68,7 +68,7 @@ fun RecordingStatus(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Box {}
|
||||
AudioVisualizer(amplitudes = service.amplitudes)
|
||||
RealtimeAudioVisualizer(service = service)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms
|
||||
|
||||
import android.content.ServiceConnection
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -23,6 +24,8 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
@ -50,6 +53,8 @@ fun StartRecording(
|
||||
if (service != null && service.amplitudes.isNotEmpty()) {
|
||||
Box {}
|
||||
}
|
||||
|
||||
val primary = MaterialTheme.colorScheme.primary
|
||||
Button(
|
||||
onClick = {
|
||||
RecorderService.startService(context, connection)
|
||||
@ -79,7 +84,7 @@ fun StartRecording(
|
||||
}
|
||||
}
|
||||
if (service != null && service.amplitudes.isNotEmpty()) {
|
||||
AudioVisualizer(amplitudes = service.amplitudes, showAll = true)
|
||||
AudioVisualizer(amplitudes = service.amplitudes)
|
||||
}
|
||||
if (service?.originalRecordingStart != null)
|
||||
Button(
|
||||
|
Loading…
x
Reference in New Issue
Block a user