Anonymous
CameraX перекомпоновывает композицию и вызывает сбои на экране при обновлении текста
Сообщение
Anonymous » 31 июл 2024, 19:11
У меня есть экран камеры в Jetpack Compose. У него есть еще один ImageView(
), который обновляется с учетом количества изображений, когда я делаю снимок.
Проблема:
Предварительный просмотр камеры не открывается полностью (прежде чем щелкнуть изображение, посмотрите на нижнюю черную часть. Когда я нажимаю на изображение, оно открывается в полноэкранном режиме.
Каждый раз, когда обновляется SubcomposeAsyncImage , предварительный просмотр камеры меняет композицию и
дает сбой на экране.
Вот код:
CameraScreen:
Код: Выделить всё
@Composable
fun CamScreen(
navController: NavController, viewModel: CamViewModel = hiltViewModel()
) {
CameraContent(
onTakePhoto = viewModel::onTakePhoto,
stackUrl = viewModel.state.value.stackUrl,
stackCount = viewModel.state.value.stackCount ?: 0
)
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraContent(
onTakePhoto: (File) -> Unit,
stackUrl: String,
stackCount: Int
) {
// Camera permission state
val cameraPermissionState = rememberPermissionState(
android.Manifest.permission.CAMERA
)
if (cameraPermissionState.status.isGranted) {
CameraScreenPreview(
onPhotoSaved = onTakePhoto,
stackUrl = stackUrl,
stackCount = stackCount
)
} else {
CameraPermission(cameraPermissionState)
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPermission(cameraPermissionState: PermissionState) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(Color.White),
verticalArrangement = Arrangement.Center
) {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
stringResource(id = R.string.camera_permission_rationale)
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
stringResource(id = R.string.camera_permission_required)
}
Text(textToShow, color = Black)
Button(
onClick = { cameraPermissionState.launchPermissionRequest() },
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 16.dp)
) {
Text("Request permission")
}
}
}
@Composable
fun CameraScreenPreview(
onPhotoSaved: (File) -> Unit,
stackUrl: String,
stackCount: Int
) {
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
val controller = remember {
LifecycleCameraController(context).apply {
setEnabledUseCases(
CameraController.IMAGE_CAPTURE
)
}
}
Scaffold { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
CameraPreview(controller)
if (stackUrl.isNotEmpty()) {
Stack(
url = stackUrl,
stackCount = stackCount,
modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp)
.align(Alignment.BottomStart)
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
horizontalArrangement = Arrangement.SpaceAround
) {
IconButton(onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
takePhoto(controller, context, onPhotoSaved) {}
}, modifier = Modifier.size(80.dp)) {
Icon(
painter = painterResource(id = R.drawable.camera),
contentDescription = stringResource(R.string.take_photo),
tint = Color.Unspecified
)
}
}
}
}
}
@Composable
fun CameraPreview(
controller: LifecycleCameraController
) {
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
factory = {
PreviewView(it).apply {
this.controller = controller
controller.bindToLifecycle(lifecycleOwner)
}
}, modifier = Modifier.fillMaxSize()
)
}
private fun takePhoto(
controller: LifecycleCameraController,
context: Context,
onPhotoSaved: (File) -> Unit,
onError: (ImageCaptureException) -> Unit
) {
val outputDirectory = AppUtils.getOutputDirectory(activityContext = context as Activity)
val photoFile = File(
outputDirectory, "s_receipt_${System.currentTimeMillis()}.jpg"
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
controller.takePicture(outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
onPhotoSaved(photoFile)
}
override fun onError(exception: ImageCaptureException) {
onError(exception)
Timber.tag("Camera").e("Photo capture failed: ${exception.message}", exception)
}
})
}
@Composable
fun Stack(url: String, modifier: Modifier = Modifier, stackCount: Int) {
Box(
modifier = modifier.size(50.dp)
) {
if (url.isNotEmpty()) {
StackItem(url)
}
Box(
modifier = Modifier
.background(
shape = RoundedCornerShape(12.dp), color = LightBlue
)
.padding(
start = 8.dp, end = 8.dp, top = 2.dp, bottom = 2.dp
)
.align(Alignment.BottomEnd)
) {
Text(
text = stackCount.toString(),
fontSize = 12.sp,
lineHeight = 16.sp,
fontWeight = FontWeight(600),
color = Color.White,
)
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun StackItem(url: String, degree: Float = 0f) {
val context = LocalContext.current
val uri = Uri.parse(url)
val contentResolver = context.contentResolver
Card(
modifier = Modifier
.rotate(degree)
.size(45.dp),
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp
),
) {
if (contentResolver.getType(uri)?.contains("pdf") == true || url.contains("pdf")) {
val pdfState = rememberVerticalPdfReaderState(
resource = ResourceType.Local(Uri.parse(url)), isZoomEnable = false
)
VerticalPDFReader(state = pdfState,
modifier = Modifier
.size(45.dp)
.background(color = Color.Gray)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_UP -> {
}
else -> {}
}
true
}
)
} else {
SubcomposeAsyncImage(
model = url,
contentDescription = "receipt",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.size(45.dp)
.clip(RoundedCornerShape(5.dp))
.clickable {
},
loading = {
CircularProgressIndicator(
color = Navy, modifier = Modifier.background(Transparent)
)
},
)
}
}
}
ViewModel:
Код: Выделить всё
@HiltViewModel
open class CamViewModel @Inject constructor(
) : ViewModel() {
private val _state = mutableStateOf(CamState())
val state: State = _state
private val _docs = MutableStateFlow(immutableListOf())
val docs = _docs.asStateFlow()
fun onTakePhoto(file: File) {
val newDocs = ArrayList(docs.value)
newDocs.add(file.path)
_docs.value = newDocs
_state.value = state.value.copy(stackUrl = file.path, stackCount = newDocs.size)
}
}
библиотека камеры:
Код: Выделить всё
cameraxVersion = "1.3.4"
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.lifecycle)
Запись экрана проблемы:
https://imgur.com/a/UBjtlhC
Подробнее здесь:
https://stackoverflow.com/questions/788 ... is-updated
1722442281
Anonymous
У меня есть экран камеры в Jetpack Compose. У него есть еще один ImageView([code]SubcomposeAsyncImage[/code]), который обновляется с учетом количества изображений, когда я делаю снимок. Проблема: [list] [*] Предварительный просмотр камеры не открывается полностью (прежде чем щелкнуть изображение, посмотрите на нижнюю черную часть. Когда я нажимаю на изображение, оно открывается в полноэкранном режиме. [*]Каждый раз, когда обновляется SubcomposeAsyncImage , предварительный просмотр камеры меняет композицию и дает сбой на экране. [/list] Вот код: CameraScreen: [code]@Composable fun CamScreen( navController: NavController, viewModel: CamViewModel = hiltViewModel() ) { CameraContent( onTakePhoto = viewModel::onTakePhoto, stackUrl = viewModel.state.value.stackUrl, stackCount = viewModel.state.value.stackCount ?: 0 ) } @OptIn(ExperimentalPermissionsApi::class) @Composable fun CameraContent( onTakePhoto: (File) -> Unit, stackUrl: String, stackCount: Int ) { // Camera permission state val cameraPermissionState = rememberPermissionState( android.Manifest.permission.CAMERA ) if (cameraPermissionState.status.isGranted) { CameraScreenPreview( onPhotoSaved = onTakePhoto, stackUrl = stackUrl, stackCount = stackCount ) } else { CameraPermission(cameraPermissionState) } } @OptIn(ExperimentalPermissionsApi::class) @Composable fun CameraPermission(cameraPermissionState: PermissionState) { Column( modifier = Modifier .fillMaxSize() .padding(16.dp) .background(Color.White), verticalArrangement = Arrangement.Center ) { val textToShow = if (cameraPermissionState.status.shouldShowRationale) { // If the user has denied the permission but the rationale can be shown, // then gently explain why the app requires this permission stringResource(id = R.string.camera_permission_rationale) } else { // If it's the first time the user lands on this feature, or the user // doesn't want to be asked again for this permission, explain that the // permission is required stringResource(id = R.string.camera_permission_required) } Text(textToShow, color = Black) Button( onClick = { cameraPermissionState.launchPermissionRequest() }, modifier = Modifier .align(Alignment.CenterHorizontally) .padding(top = 16.dp) ) { Text("Request permission") } } } @Composable fun CameraScreenPreview( onPhotoSaved: (File) -> Unit, stackUrl: String, stackCount: Int ) { val haptic = LocalHapticFeedback.current val context = LocalContext.current val controller = remember { LifecycleCameraController(context).apply { setEnabledUseCases( CameraController.IMAGE_CAPTURE ) } } Scaffold { padding -> Box( modifier = Modifier .fillMaxSize() .padding(padding) ) { CameraPreview(controller) if (stackUrl.isNotEmpty()) { Stack( url = stackUrl, stackCount = stackCount, modifier = Modifier .padding(start = 16.dp, bottom = 16.dp) .align(Alignment.BottomStart) ) } Row( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter), horizontalArrangement = Arrangement.SpaceAround ) { IconButton(onClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) takePhoto(controller, context, onPhotoSaved) {} }, modifier = Modifier.size(80.dp)) { Icon( painter = painterResource(id = R.drawable.camera), contentDescription = stringResource(R.string.take_photo), tint = Color.Unspecified ) } } } } } @Composable fun CameraPreview( controller: LifecycleCameraController ) { val lifecycleOwner = LocalLifecycleOwner.current AndroidView( factory = { PreviewView(it).apply { this.controller = controller controller.bindToLifecycle(lifecycleOwner) } }, modifier = Modifier.fillMaxSize() ) } private fun takePhoto( controller: LifecycleCameraController, context: Context, onPhotoSaved: (File) -> Unit, onError: (ImageCaptureException) -> Unit ) { val outputDirectory = AppUtils.getOutputDirectory(activityContext = context as Activity) val photoFile = File( outputDirectory, "s_receipt_${System.currentTimeMillis()}.jpg" ) val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() controller.takePicture(outputOptions, ContextCompat.getMainExecutor(context), object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(output: ImageCapture.OutputFileResults) { onPhotoSaved(photoFile) } override fun onError(exception: ImageCaptureException) { onError(exception) Timber.tag("Camera").e("Photo capture failed: ${exception.message}", exception) } }) } @Composable fun Stack(url: String, modifier: Modifier = Modifier, stackCount: Int) { Box( modifier = modifier.size(50.dp) ) { if (url.isNotEmpty()) { StackItem(url) } Box( modifier = Modifier .background( shape = RoundedCornerShape(12.dp), color = LightBlue ) .padding( start = 8.dp, end = 8.dp, top = 2.dp, bottom = 2.dp ) .align(Alignment.BottomEnd) ) { Text( text = stackCount.toString(), fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight(600), color = Color.White, ) } } } @OptIn(ExperimentalComposeUiApi::class) @Composable fun StackItem(url: String, degree: Float = 0f) { val context = LocalContext.current val uri = Uri.parse(url) val contentResolver = context.contentResolver Card( modifier = Modifier .rotate(degree) .size(45.dp), elevation = CardDefaults.cardElevation( defaultElevation = 2.dp ), ) { if (contentResolver.getType(uri)?.contains("pdf") == true || url.contains("pdf")) { val pdfState = rememberVerticalPdfReaderState( resource = ResourceType.Local(Uri.parse(url)), isZoomEnable = false ) VerticalPDFReader(state = pdfState, modifier = Modifier .size(45.dp) .background(color = Color.Gray) .pointerInteropFilter { when (it.action) { MotionEvent.ACTION_UP -> { } else -> {} } true } ) } else { SubcomposeAsyncImage( model = url, contentDescription = "receipt", contentScale = ContentScale.FillWidth, modifier = Modifier .size(45.dp) .clip(RoundedCornerShape(5.dp)) .clickable { }, loading = { CircularProgressIndicator( color = Navy, modifier = Modifier.background(Transparent) ) }, ) } } } [/code] ViewModel: [code]@HiltViewModel open class CamViewModel @Inject constructor( ) : ViewModel() { private val _state = mutableStateOf(CamState()) val state: State = _state private val _docs = MutableStateFlow(immutableListOf()) val docs = _docs.asStateFlow() fun onTakePhoto(file: File) { val newDocs = ArrayList(docs.value) newDocs.add(file.path) _docs.value = newDocs _state.value = state.value.copy(stackUrl = file.path, stackCount = newDocs.size) } } [/code] библиотека камеры: [code]cameraxVersion = "1.3.4" implementation(libs.androidx.camera.core) implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.lifecycle) [/code] Запись экрана проблемы: https://imgur.com/a/UBjtlhC Подробнее здесь: [url]https://stackoverflow.com/questions/78817380/camerax-is-recomposing-and-making-the-screen-glitch-when-text-is-updated[/url]