feat: Add more animations to recording start

This commit is contained in:
Myzel394 2023-12-16 12:48:00 +01:00
parent b6346ef0e3
commit 096cf56436
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
8 changed files with 234 additions and 164 deletions

View File

@ -1,42 +0,0 @@
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.foundation.layout.width
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun RecordingProgress(
recordingTime: Long,
progress: Float,
) {
// Only show animation when the recording has just started
val recordingJustStarted = recordingTime <= 1L
var progressVisible by remember { mutableStateOf(!recordingJustStarted) }
LaunchedEffect(Unit) {
progressVisible = true
}
AnimatedVisibility(
visible = progressVisible,
enter = expandHorizontally(
tween(1000)
)
) {
LinearProgressIndicator(
progress = progress,
modifier = Modifier
.width(300.dp)
)
}
}

View File

@ -1,44 +0,0 @@
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.graphics.Color
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.ui.components.atoms.Pulsating
import app.myzel394.alibi.ui.utils.formatDuration
@Composable
fun RecordingTime(
time: Long,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Pulsating {
Box(
modifier = Modifier
.size(16.dp)
.clip(CircleShape)
.background(Color.Red)
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = formatDuration(time * 1000),
style = MaterialTheme.typography.headlineLarge,
)
}
}

View File

@ -1,30 +1,103 @@
package app.myzel394.alibi.ui.components.RecorderScreen.molecules
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.EaseOutElastic
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.DeleteButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.PauseResumeButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveButton
import app.myzel394.alibi.ui.utils.RandomStack
import app.myzel394.alibi.ui.utils.rememberInitialRecordingAnimation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun RecordingControl(
isPaused: Boolean,
recordingTime: Long,
onDelete: () -> Unit,
onPauseResume: () -> Unit,
onSave: () -> Unit,
) {
val animateIn = rememberInitialRecordingAnimation(recordingTime)
var deleteButtonAlphaIsIn by rememberSaveable {
mutableStateOf(false)
}
val deleteButtonAlpha by animateFloatAsState(
if (deleteButtonAlphaIsIn) 1f else 0f,
label = "deleteButtonAlpha",
animationSpec = tween(durationMillis = 500)
)
var pauseButtonAlphaIsIn by rememberSaveable {
mutableStateOf(false)
}
val pauseButtonAlpha by animateFloatAsState(
if (pauseButtonAlphaIsIn) 1f else 0f,
label = "pauseButtonAlpha",
animationSpec = tween(durationMillis = 500)
)
var saveButtonAlphaIsIn by rememberSaveable {
mutableStateOf(false)
}
val saveButtonAlpha by animateFloatAsState(
if (saveButtonAlphaIsIn) 1f else 0f,
label = "saveButtonAlpha",
animationSpec = tween(durationMillis = 500)
)
LaunchedEffect(animateIn) {
if (animateIn) {
val stack = RandomStack.of(arrayOf(1, 2, 3).asIterable())
while (!stack.isEmpty()) {
val next = stack.popRandom()
when (next) {
1 -> {
deleteButtonAlphaIsIn = true
}
2 -> {
pauseButtonAlphaIsIn = true
}
3 -> {
saveButtonAlphaIsIn = true
}
}
delay(250)
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
.weight(1f)
.alpha(deleteButtonAlpha),
contentAlignment = Alignment.Center,
) {
DeleteButton(onDelete = onDelete)
@ -32,6 +105,8 @@ fun RecordingControl(
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.alpha(pauseButtonAlpha),
) {
PauseResumeButton(
isPaused = isPaused,
@ -42,7 +117,8 @@ fun RecordingControl(
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
.weight(1f)
.alpha(saveButtonAlpha),
contentAlignment = Alignment.Center,
) {
SaveButton(onSave = onSave)

View File

@ -1,21 +1,34 @@
package app.myzel394.alibi.ui.components.RecorderScreen.molecules
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RecordingProgress
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RecordingTime
import app.myzel394.alibi.ui.components.atoms.Pulsating
import app.myzel394.alibi.ui.utils.formatDuration
import app.myzel394.alibi.ui.utils.isSameDay
import app.myzel394.alibi.ui.utils.rememberInitialRecordingAnimation
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@ -28,51 +41,87 @@ fun RecordingStatus(
recordingStart: LocalDateTime,
maxDuration: Long,
) {
val animateIn = rememberInitialRecordingAnimation(recordingTime)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
RecordingTime(recordingTime)
RecordingProgress(
recordingTime = recordingTime,
progress = progress,
)
Text(
text = stringResource(
R.string.ui_recorder_info_saveNowTime,
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT).format(
LocalDateTime.now().minusSeconds(
min(
maxDuration / 1000,
recordingTime
)
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Pulsating {
Box(
modifier = Modifier
.size(16.dp)
.clip(CircleShape)
.background(Color.Red)
)
),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = formatDuration(recordingTime * 1000),
style = MaterialTheme.typography.headlineLarge,
)
}
Text(
text = recordingStart.let {
if (isSameDay(it, LocalDateTime.now())) {
stringResource(
R.string.ui_recorder_info_startTime_short,
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
.format(it)
)
} else {
stringResource(
R.string.ui_recorder_info_startTime_full,
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)
.format(it)
)
}
},
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
AnimatedVisibility(
visible = animateIn,
enter = expandHorizontally(
tween(1000)
)
) {
LinearProgressIndicator(
progress = progress,
modifier = Modifier
.width(300.dp)
)
}
AnimatedVisibility(visible = animateIn, enter = fadeIn()) {
Text(
text = stringResource(
R.string.ui_recorder_info_saveNowTime,
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)
.format(
LocalDateTime.now().minusSeconds(
min(
maxDuration / 1000,
recordingTime
)
)
)
),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
)
}
AnimatedVisibility(visible = animateIn, enter = fadeIn()) {
Text(
text = recordingStart.let {
if (isSameDay(it, LocalDateTime.now())) {
stringResource(
R.string.ui_recorder_info_startTime_short,
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
.format(it)
)
} else {
stringResource(
R.string.ui_recorder_info_startTime_full,
DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.MEDIUM,
FormatStyle.SHORT
)
.format(it)
)
}
},
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}

View File

@ -1,19 +1,10 @@
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Divider
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -24,13 +15,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.DeleteButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.PauseResumeButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RealtimeAudioVisualizer
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RecordingProgress
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RecordingTime
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveButton
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.MicrophoneStatus
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus
@ -80,6 +65,7 @@ fun AudioRecordingStatus(
RecordingControl(
isPaused = audioRecorder.isPaused,
recordingTime = audioRecorder.recordingTime,
onDelete = {
scope.launch {
runCatching {

View File

@ -1,16 +1,10 @@
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -19,29 +13,17 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.util.TypedValueCompat
import app.myzel394.alibi.R
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.DeleteButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.PauseResumeButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RecordingProgress
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RecordingTime
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.TorchStatus
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus
@ -152,6 +134,7 @@ fun VideoRecordingStatus(
RecordingControl(
isPaused = videoRecorder.isPaused,
recordingTime = videoRecorder.recordingTime,
onDelete = {
scope.launch {
runCatching {

View File

@ -0,0 +1,22 @@
package app.myzel394.alibi.ui.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@Composable
fun rememberInitialRecordingAnimation(recordingTime: Long): Boolean {
// Only show animation when the recording has just started
val recordingJustStarted = recordingTime <= 1L
var progressVisible by rememberSaveable { mutableStateOf(!recordingJustStarted) }
LaunchedEffect(Unit) {
progressVisible = true
}
return progressVisible
}

View File

@ -0,0 +1,40 @@
package app.myzel394.alibi.ui.utils
// A stack that allows you to randomly pop items from it
class RandomStack<T> {
private val stack = mutableListOf<T>()
fun push(item: T) {
stack.add(item)
}
fun pop(): T {
val index = stack.size - 1
val item = stack[index]
stack.removeAt(index)
return item
}
fun popRandom(): T {
val index = (0..<stack.size).random()
val item = stack[index]
stack.removeAt(index)
return item
}
fun isEmpty() = stack.isEmpty()
companion object {
fun <T> of(items: Iterable<T>): RandomStack<T> {
val stack = RandomStack<T>()
items.forEach(stack::push)
return stack
}
}
}