feat: Adding camera preview functionality

This commit is contained in:
Myzel394 2023-12-03 22:16:09 +01:00
parent 817e9d96d0
commit 40eee79aa3
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
6 changed files with 244 additions and 63 deletions

View File

@ -2,6 +2,7 @@ package app.myzel394.alibi
import android.content.Context
import android.os.Bundle
import android.view.MotionEvent
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity

View File

@ -0,0 +1,57 @@
package app.myzel394.alibi.ui.components.AudioRecorder.atoms
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.launch
@Composable
fun CameraPreview(
modifier: Modifier = Modifier,
scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER,
cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
) {
val coroutineScope = rememberCoroutineScope()
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = modifier,
factory = { context ->
val previewView = PreviewView(context).apply {
this.scaleType = scaleType
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
// CameraX Preview UseCase
val previewUseCase = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
coroutineScope.launch {
val cameraProvider = ProcessCameraProvider.getInstance(context).get()
try {
// Must unbind the use-cases before rebinding them.
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, previewUseCase
)
} catch (ex: Exception) {
}
}
previewView
}
)
}

View File

@ -1,12 +1,27 @@
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
import android.graphics.Point
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
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.fillMaxHeight
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
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.CameraAlt
@ -21,88 +36,167 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
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
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInteropFilter
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.compose.ui.window.Popup
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.AudioRecorder.atoms.CameraPreview
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)
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
ExperimentalComposeUiApi::class
)
@Composable
fun VideoRecorderPreparationSheet(
onDismiss: () -> Unit,
videoSettings: VideoRecorderSettingsModel = viewModel()
videoSettings: VideoRecorderSettingsModel = viewModel(),
onPreviewVisible: () -> Unit,
onPreviewHidden: () -> Unit,
showPreview: Boolean,
) {
val sheetState = rememberModalBottomSheetState(true)
val context = LocalContext.current
val cameras = CameraInfo.queryAvailableCameras(context)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(
if (showPreview)
CameraPreview(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(30.dp),
.fillMaxWidth()
.fillMaxHeight()
)
else {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
Box(
) {
Icon(
Icons.Default.CameraAlt,
contentDescription = null,
Column(
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
.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,
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(BIG_PRIMARY_BUTTON_SIZE)
)
}
)
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 = {},
Popup(
alignment = Alignment.BottomCenter,
) {
val label =
stringResource(R.string.ui_videoRecorder_action_start_settings_start_label)
Box(
modifier = Modifier
.pointerInput(Unit) {
awaitEachGesture {
while (true) {
val event = awaitPointerEvent()
// consume all changes
if (!event.changes.elementAt(0).pressed) {
onPreviewHidden()
break
}
}
}
}
.let {
if (showPreview) it.alpha(0.2f) else it
}
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(BIG_PRIMARY_BUTTON_SIZE)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.padding(16.dp)
.semantics {
contentDescription = label
}
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
onPreviewVisible()
}
)
},
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(label)
Text(
label,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
}

View File

@ -36,6 +36,9 @@ import app.myzel394.alibi.ui.models.VideoRecorderModel
fun VideoRecordingStart(
videoRecorder: VideoRecorderModel,
appSettings: AppSettings,
onHideAudioRecording: () -> Unit,
onShowAudioRecording: () -> Unit,
showPreview: Boolean,
) {
val context = LocalContext.current
@ -48,6 +51,9 @@ fun VideoRecordingStart(
onDismiss = {
showSheet = false
},
onPreviewVisible = onHideAudioRecording,
onPreviewHidden = onShowAudioRecording,
showPreview = showPreview,
)
}

View File

@ -46,6 +46,9 @@ fun StartRecording(
// settings will be used, instead of the actual settings.
appSettings: AppSettings,
onSaveLastRecording: () -> Unit,
onHideTopBar: () -> Unit,
onShowTopBar: () -> Unit,
showAudioRecorder: Boolean,
) {
val context = LocalContext.current
@ -56,13 +59,17 @@ fun StartRecording(
) {
Spacer(modifier = Modifier.weight(1f))
AudioRecordingStart(
audioRecorder = audioRecorder,
appSettings = appSettings,
)
if (showAudioRecorder)
AudioRecordingStart(
audioRecorder = audioRecorder,
appSettings = appSettings,
)
VideoRecordingStart(
videoRecorder = videoRecorder,
appSettings = appSettings,
onHideAudioRecording = onHideTopBar,
onShowAudioRecording = onShowTopBar,
showPreview = !showAudioRecorder,
)
Text(

View File

@ -253,6 +253,14 @@ fun RecorderScreen(
}
}
)
// TopAppBar and AudioRecordingStart should be hidden when
// the video preview is visible.
// We need to preview the video inline to
// be able to capture the touch release event.
var topBarVisible by remember { mutableStateOf(true) }
Scaffold(
snackbarHost = {
SnackbarHost(
@ -270,23 +278,24 @@ fun RecorderScreen(
)
},
topBar = {
TopAppBar(
title = {
Text(stringResource(R.string.app_name))
},
actions = {
IconButton(
onClick = {
navController.navigate(Screen.Settings.route)
},
) {
Icon(
Icons.Default.Settings,
contentDescription = null
)
if (topBarVisible)
return@Scaffold TopAppBar(
title = {
Text(stringResource(R.string.app_name))
},
actions = {
IconButton(
onClick = {
navController.navigate(Screen.Settings.route)
},
) {
Icon(
Icons.Default.Settings,
contentDescription = null
)
}
}
}
)
)
},
) { padding ->
Box(
@ -305,6 +314,13 @@ fun RecorderScreen(
videoRecorder = videoRecorder,
appSettings = appSettings,
onSaveLastRecording = ::saveRecording,
showAudioRecorder = topBarVisible,
onHideTopBar = {
topBarVisible = false
},
onShowTopBar = {
topBarVisible = true
},
)
}
}