CameraScreen: animate captured image

This commit is contained in:
Pierre-Yves Nicolas
2025-06-25 11:33:46 +02:00
parent 5ba634070f
commit d13fc4199d

View File

@@ -19,6 +19,10 @@ import android.graphics.BitmapFactory
import android.util.Log import android.util.Log
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -32,6 +36,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -53,6 +58,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
@@ -73,6 +79,9 @@ data class CameraUiState(
val captureState: CaptureState val captureState: CaptureState
) )
const val CAPTURED_IMAGE_DISPLAY_DURATION = 1500L
const val ANIMATION_DURATION = 200
@Composable @Composable
fun CameraScreen( fun CameraScreen(
viewModel: MainViewModel, viewModel: MainViewModel,
@@ -91,7 +100,7 @@ fun CameraScreen(
val captureState by viewModel.captureState.collectAsStateWithLifecycle() val captureState by viewModel.captureState.collectAsStateWithLifecycle()
if (captureState.isProcessed()) { if (captureState.isProcessed()) {
LaunchedEffect(captureState) { LaunchedEffect(captureState) {
delay(1500) delay(CAPTURED_IMAGE_DISPLAY_DURATION)
viewModel.addProcessedImage() viewModel.addProcessedImage()
} }
} }
@@ -138,6 +147,7 @@ private fun CameraScreenScaffold(
onCapture: () -> Unit, onCapture: () -> Unit,
onFinalizePressed: () -> Unit, onFinalizePressed: () -> Unit,
) { ) {
Box {
Scaffold( Scaffold(
bottomBar = { bottomBar = {
CameraScreenFooter( CameraScreenFooter(
@@ -147,7 +157,11 @@ private fun CameraScreenScaffold(
) )
} }
) { innerPadding -> ) { innerPadding ->
Box(modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding()).fillMaxSize()) { Box(
modifier = Modifier
.padding(bottom = innerPadding.calculateBottomPadding())
.fillMaxSize()
) {
CameraPreviewWithOverlay(cameraPreview, cameraUiState) CameraPreviewWithOverlay(cameraPreview, cameraUiState)
MessageBox(cameraUiState.liveAnalysisState.inferenceTime) MessageBox(cameraUiState.liveAnalysisState.inferenceTime)
CaptureButton( CaptureButton(
@@ -156,20 +170,52 @@ private fun CameraScreenScaffold(
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.padding(16.dp) .padding(16.dp)
) )
cameraUiState.captureState.processedImage?.let { }
}
CapturedImage(cameraUiState)
}
}
@Composable
private fun CapturedImage(cameraUiState: CameraUiState) {
cameraUiState.captureState.processedImage?.let { image ->
Surface( Surface(
color = Color.Black.copy(alpha = 0.3f), color = Color.Black.copy(alpha = 0.3f),
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(),
) ) {}
{}
var isAnimating by remember { mutableStateOf(false) }
LaunchedEffect(image) {
delay(CAPTURED_IMAGE_DISPLAY_DURATION - ANIMATION_DURATION)
isAnimating = true
}
val transition = updateTransition(targetState = isAnimating, label = "captureAnimation")
val targetOffsetX = 0.dp // TODO real value
val targetOffsetY = 200.dp // TODO real value
val offsetX by transition.animateDp(
transitionSpec = { tween(durationMillis = ANIMATION_DURATION) },
label = "offsetX"
) { if (it) targetOffsetX else 0.dp }
val offsetY by transition.animateDp(
transitionSpec = { tween(durationMillis = ANIMATION_DURATION) },
label = "offsetY"
) { if (it) targetOffsetY else 0.dp }
val scale by transition.animateFloat(
transitionSpec = { tween(durationMillis = ANIMATION_DURATION) },
label = "scale"
) { if (it) 0.3f else 1f }
Image( Image(
bitmap = it.asImageBitmap(), bitmap = image.asImageBitmap(),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize().padding(24.dp) modifier = Modifier
.fillMaxSize()
.padding(24.dp)
.offset(x = offsetX, y = offsetY - 100.dp)
.scale(scale)
) )
} }
}
}
} }
@Composable @Composable