feat: Add VideoRecorderPreparationSheet

This commit is contained in:
Myzel394 2023-12-03 18:37:04 +01:00
parent e7e7505592
commit 817e9d96d0
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
9 changed files with 367 additions and 24 deletions

View File

@ -1,5 +1,9 @@
package app.myzel394.alibi.ui
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import androidx.camera.core.CameraX
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -11,6 +15,7 @@ import androidx.compose.foundation.background
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -27,6 +32,7 @@ import app.myzel394.alibi.ui.screens.RecorderScreen
import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen
import app.myzel394.alibi.ui.screens.SettingsScreen
import app.myzel394.alibi.ui.screens.WelcomeScreen
import app.myzel394.alibi.ui.utils.CameraInfo
const val SCALE_IN = 1.25f

View File

@ -0,0 +1,103 @@
package app.myzel394.alibi.ui.components.AudioRecorder.atoms
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.unit.dp
import app.myzel394.alibi.R
import app.myzel394.alibi.ui.utils.CameraInfo
@Composable
fun CameraSelectionButton(
cameraID: CameraInfo.Lens,
selected: Boolean,
onSelected: () -> Unit,
label: String,
description: String? = null,
) {
val backgroundColor by animateColorAsState(
targetValue = if (selected) MaterialTheme.colorScheme.secondaryContainer.copy(
alpha = 0.2f
) else Color.Transparent,
// Make animation about 0.5x faster than default
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy,
),
label = "backgroundColor"
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.clickable(onClick = onSelected)
.background(backgroundColor)
.padding(vertical = 8.dp, horizontal = 12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
selected = selected,
onClick = onSelected,
)
if (description == null) {
Text(
label,
style = MaterialTheme.typography.labelLarge,
)
} else {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
label,
style = MaterialTheme.typography.labelLarge,
)
Text(
description,
style = MaterialTheme.typography.bodySmall,
)
}
}
}
Icon(
CAMERA_LENS_ICON_MAP[cameraID]!!,
contentDescription = null,
modifier = Modifier
.size(24.dp),
)
}
}
val CAMERA_LENS_ICON_MAP = mapOf(
CameraInfo.Lens.BACK to Icons.Default.Camera,
CameraInfo.Lens.FRONT to Icons.Default.Person,
CameraInfo.Lens.EXTERNAL to Icons.Default.Videocam,
)

View File

@ -1,11 +0,0 @@
package app.myzel394.alibi.ui.components.AudioRecorder.atoms
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoRecorderPreparationSheet() {
val sheetState = rememberModalBottomSheetState()
}

View File

@ -0,0 +1,73 @@
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.CameraSelectionButton
import app.myzel394.alibi.ui.models.VideoRecorderSettingsModel
import app.myzel394.alibi.ui.utils.CameraInfo
@Composable
fun CamerasSelection(
cameras: Iterable<CameraInfo>,
videoSettings: VideoRecorderSettingsModel
) {
val CAMERA_LENS_TEXT_MAP = mapOf(
CameraInfo.Lens.BACK to stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_back_label),
CameraInfo.Lens.FRONT to stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_front_label),
CameraInfo.Lens.EXTERNAL to stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_external_label),
)
Column {
if (cameras.count() == 2 && cameras.elementAt(0).id == 0 && cameras.elementAt(1).id == 1) {
CameraSelectionButton(
cameraID = CameraInfo.Lens.BACK,
label = stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_back_label),
selected = videoSettings.cameraID == 0,
onSelected = {
videoSettings.cameraID = 0
},
)
CameraSelectionButton(
cameraID = CameraInfo.Lens.FRONT,
label = stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_front_label),
selected = videoSettings.cameraID == 1,
onSelected = {
videoSettings.cameraID = 1
},
)
} else {
cameras.forEach { camera ->
CameraSelectionButton(
cameraID = CameraInfo.CAMERA_INT_TO_LENS_MAP[camera.id]!!,
selected = videoSettings.cameraID == camera.id,
onSelected = {
videoSettings.cameraID = camera.id
},
label = stringResource(
R.string.ui_videoRecorder_action_start_settings_cameraLens_label,
camera.id
),
description = CAMERA_LENS_TEXT_MAP[camera.lens]!!,
)
}
}
}
}

View File

@ -0,0 +1,109 @@
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import app.myzel394.alibi.R
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
import app.myzel394.alibi.ui.components.atoms.GlobalSwitch
import app.myzel394.alibi.ui.models.VideoRecorderSettingsModel
import app.myzel394.alibi.ui.utils.CameraInfo
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoRecorderPreparationSheet(
onDismiss: () -> Unit,
videoSettings: VideoRecorderSettingsModel = viewModel()
) {
val sheetState = rememberModalBottomSheetState(true)
val context = LocalContext.current
val cameras = CameraInfo.queryAvailableCameras(context)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(30.dp),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
Icons.Default.CameraAlt,
contentDescription = null,
modifier = Modifier
.size(80.dp),
)
Text(
stringResource(R.string.ui_videoRecorder_action_start_settings_label),
style = MaterialTheme.typography.labelLarge,
)
}
GlobalSwitch(
label = stringResource(R.string.ui_videoRecorder_action_start_settings_enableAudio_label),
checked = videoSettings.enableAudio,
onCheckedChange = {
videoSettings.enableAudio = it
}
)
Text(
stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_selection_label),
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
)
CamerasSelection(
cameras = cameras,
videoSettings = videoSettings,
)
val label = stringResource(R.string.ui_videoRecorder_action_start_settings_start_label)
Button(
onClick = {},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(BIG_PRIMARY_BUTTON_SIZE)
.semantics {
contentDescription = label
}
) {
Text(label)
}
}
}
}

View File

@ -15,7 +15,6 @@ import androidx.compose.material3.Icon
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.saveable.rememberSaveable
@ -31,7 +30,6 @@ import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.models.VideoRecorderModel
@Composable
@ -41,24 +39,24 @@ fun VideoRecordingStart(
) {
val context = LocalContext.current
// We can't get the current `notificationDetails` inside the
// `onPermissionAvailable` function. We'll instead use this hack
// with `LaunchedEffect` to get the current value.
var startRecording by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(startRecording) {
if (startRecording) {
startRecording = false
var showSheet by rememberSaveable {
mutableStateOf(false)
}
videoRecorder.startRecording(context, appSettings)
}
if (showSheet) {
VideoRecorderPreparationSheet(
onDismiss = {
showSheet = false
},
)
}
PermissionRequester(
permission = Manifest.permission.RECORD_AUDIO,
icon = Icons.Default.Mic,
onPermissionAvailable = {
startRecording = true
}
showSheet = true
},
) { trigger ->
val label = stringResource(R.string.ui_videoRecorder_action_start_label)

View File

@ -0,0 +1,15 @@
package app.myzel394.alibi.ui.models
import android.graphics.Camera
import android.hardware.camera2.CameraManager
import androidx.camera.core.CameraSelector
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class VideoRecorderSettingsModel : ViewModel() {
var enableAudio by mutableStateOf(true)
var cameraID by mutableIntStateOf(0)
}

View File

@ -1,2 +1,44 @@
package app.myzel394.alibi.ui.utils
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
data class CameraInfo(
val lens: Lens,
val id: Int,
) {
enum class Lens(val androidValue: Int) {
BACK(CameraMetadata.LENS_FACING_BACK),
FRONT(CameraMetadata.LENS_FACING_FRONT),
EXTERNAL(CameraMetadata.LENS_FACING_EXTERNAL),
}
companion object {
val CAMERA_INT_TO_LENS_MAP = mapOf(
0 to Lens.BACK,
1 to Lens.FRONT,
2 to Lens.EXTERNAL,
)
fun queryAvailableCameras(context: Context): List<CameraInfo> {
val camera = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
return camera.cameraIdList.map { id ->
val lensFacing =
camera.getCameraCharacteristics(id).get(CameraCharacteristics.LENS_FACING)
?: return@map null
fromCameraId(id, lensFacing)
}.filterNotNull()
}
fun fromCameraId(cameraId: String, lensFacing: Int): CameraInfo {
return CameraInfo(
lens = CAMERA_INT_TO_LENS_MAP[lensFacing]!!,
id = cameraId.toInt(),
)
}
}
}

View File

@ -139,4 +139,12 @@
<string name="ui_settings_value_videoQuality_values_sd">Standard</string>
<string name="ui_settings_value_videoQuality_values_lowest">Lowest</string>
<string name="ui_videoRecorder_action_start_label">Start Video Recording</string>
<string name="ui_videoRecorder_action_start_settings_enableAudio_label">Record with Audio</string>
<string name="ui_videoRecorder_action_start_settings_cameraLens_back_label">Back facing</string>
<string name="ui_videoRecorder_action_start_settings_cameraLens_front_label">Front facing</string>
<string name="ui_videoRecorder_action_start_settings_cameraLens_external_label">External Camera</string>
<string name="ui_videoRecorder_action_start_settings_cameraLens_label">Camera %s</string>
<string name="ui_videoRecorder_action_start_settings_start_label">Start Recording</string>
<string name="ui_videoRecorder_action_start_settings_label">Prepare your recording</string>
<string name="ui_videoRecorder_action_start_settings_cameraLens_selection_label">Select camera</string>
</resources>