diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecordingProgress.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecordingProgress.kt deleted file mode 100644 index 75dd23e..0000000 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecordingProgress.kt +++ /dev/null @@ -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) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecordingTime.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecordingTime.kt deleted file mode 100644 index 236f6d1..0000000 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecordingTime.kt +++ /dev/null @@ -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, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt index e6d3ac2..b01d97a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt @@ -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) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingStatus.kt index 5f1723a..b170d61 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingStatus.kt @@ -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, + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt index 1fbebc0..874dd61 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt @@ -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 { diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt index 95dfa74..cea2cdd 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt @@ -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 { diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/animations.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/animations.kt new file mode 100644 index 0000000..08b8759 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/animations.kt @@ -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 +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/stack.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/stack.kt new file mode 100644 index 0000000..1b30fd2 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/stack.kt @@ -0,0 +1,40 @@ +package app.myzel394.alibi.ui.utils + +// A stack that allows you to randomly pop items from it +class RandomStack { + private val stack = mutableListOf() + + 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.. of(items: Iterable): RandomStack { + val stack = RandomStack() + + items.forEach(stack::push) + + return stack + } + } +} \ No newline at end of file