Animation: compute the real destination coordinates

This commit is contained in:
Pierre-Yves Nicolas
2025-06-25 15:58:49 +02:00
parent d13fc4199d
commit 483165450b
2 changed files with 54 additions and 17 deletions

View File

@@ -52,6 +52,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -59,10 +60,14 @@ 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.draw.scale
import androidx.compose.ui.geometry.Offset
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.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -91,6 +96,7 @@ fun CameraScreen(
) { ) {
var previewView by remember { mutableStateOf<PreviewView?>(null) } var previewView by remember { mutableStateOf<PreviewView?>(null) }
val pageIds by viewModel.pageIds.collectAsStateWithLifecycle() val pageIds by viewModel.pageIds.collectAsStateWithLifecycle()
val thumbnailCoords = remember { mutableStateOf(Offset.Zero) }
val captureController = remember { CameraCaptureController() } val captureController = remember { CameraCaptureController() }
DisposableEffect(Unit) { DisposableEffect(Unit) {
@@ -124,7 +130,9 @@ fun CameraScreen(
pageIds = pageIds, pageIds = pageIds,
imageLoader = { id -> viewModel.getBitmap(id) }, imageLoader = { id -> viewModel.getBitmap(id) },
onPageClick = { index -> viewModel.navigateTo(Screen.FinalizeDocument(index)) }, onPageClick = { index -> viewModel.navigateTo(Screen.FinalizeDocument(index)) },
listState = listState listState = listState,
onLastItemPosition =
{ coords -> thumbnailCoords.value = coords.localToWindow(Offset.Zero) }
) )
}, },
cameraUiState = CameraUiState(pageIds.size, liveAnalysisState, captureState), cameraUiState = CameraUiState(pageIds.size, liveAnalysisState, captureState),
@@ -136,6 +144,7 @@ fun CameraScreen(
) )
}, },
onFinalizePressed = onFinalizePressed, onFinalizePressed = onFinalizePressed,
thumbnailCoords = thumbnailCoords,
) )
} }
@@ -146,6 +155,7 @@ private fun CameraScreenScaffold(
cameraUiState: CameraUiState, cameraUiState: CameraUiState,
onCapture: () -> Unit, onCapture: () -> Unit,
onFinalizePressed: () -> Unit, onFinalizePressed: () -> Unit,
thumbnailCoords: MutableState<Offset>,
) { ) {
Box { Box {
Scaffold( Scaffold(
@@ -172,12 +182,12 @@ private fun CameraScreenScaffold(
) )
} }
} }
CapturedImage(cameraUiState) CapturedImage(cameraUiState, thumbnailCoords)
} }
} }
@Composable @Composable
private fun CapturedImage(cameraUiState: CameraUiState) { private fun CapturedImage(cameraUiState: CameraUiState, thumbnailCoords: MutableState<Offset>) {
cameraUiState.captureState.processedImage?.let { image -> cameraUiState.captureState.processedImage?.let { image ->
Surface( Surface(
color = Color.Black.copy(alpha = 0.3f), color = Color.Black.copy(alpha = 0.3f),
@@ -190,8 +200,9 @@ private fun CapturedImage(cameraUiState: CameraUiState) {
isAnimating = true isAnimating = true
} }
val transition = updateTransition(targetState = isAnimating, label = "captureAnimation") val transition = updateTransition(targetState = isAnimating, label = "captureAnimation")
val targetOffsetX = 0.dp // TODO real value val density = LocalDensity.current
val targetOffsetY = 200.dp // TODO real value var targetOffsetX by remember { mutableStateOf(0.dp) }
var targetOffsetY by remember { mutableStateOf(0.dp) }
val offsetX by transition.animateDp( val offsetX by transition.animateDp(
transitionSpec = { tween(durationMillis = ANIMATION_DURATION) }, transitionSpec = { tween(durationMillis = ANIMATION_DURATION) },
@@ -206,16 +217,30 @@ private fun CapturedImage(cameraUiState: CameraUiState) {
label = "scale" label = "scale"
) { if (it) 0.3f else 1f } ) { if (it) 0.3f else 1f }
val justABitToTheTop = 100.dp
Box (modifier = Modifier
.onGloballyPositioned { coordinates ->
val bounds = coordinates.boundsInWindow()
val centerX = bounds.left + bounds.width / 2
val centerY = bounds.top + bounds.height / 2
with(density) {
targetOffsetX = thumbnailCoords.value.x.toDp() - centerX.toDp() + PAGE_LIST_ELEMENT_SIZE_DP.dp
targetOffsetY = thumbnailCoords.value.y.toDp() - centerY.toDp() + justABitToTheTop
}
}
) {
Image( Image(
bitmap = image.asImageBitmap(), bitmap = image.asImageBitmap(),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(24.dp) .padding(24.dp)
.offset(x = offsetX, y = offsetY - 100.dp) .offset(x = offsetX, y = offsetY - justABitToTheTop)
.scale(scale) .scale(scale)
) )
} }
}
} }
@Composable @Composable
@@ -333,6 +358,7 @@ fun CameraScreenPreviewWithProcessedImage() {
private fun ScreenPreview(captureState: CaptureState) { private fun ScreenPreview(captureState: CaptureState) {
val context = LocalContext.current val context = LocalContext.current
MyScanTheme { MyScanTheme {
val thumbnailCoords = remember { mutableStateOf(Offset.Zero) }
CameraScreenScaffold( CameraScreenScaffold(
cameraPreview = { cameraPreview = {
Box( Box(
@@ -356,12 +382,13 @@ private fun ScreenPreview(captureState: CaptureState) {
} }
}, },
onPageClick = {}, onPageClick = {},
listState = LazyListState() listState = LazyListState(),
) )
}, },
cameraUiState = CameraUiState(pageCount = 4, LiveAnalysisState(), captureState), cameraUiState = CameraUiState(pageCount = 4, LiveAnalysisState(), captureState),
onCapture = {}, onCapture = {},
onFinalizePressed = {}, onFinalizePressed = {},
thumbnailCoords = thumbnailCoords,
) )
} }
} }

View File

@@ -36,8 +36,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
const val PAGE_LIST_ELEMENT_SIZE_DP = 120
@Composable @Composable
fun CommonPageList( fun CommonPageList(
pageIds: List<String>, pageIds: List<String>,
@@ -45,6 +49,7 @@ fun CommonPageList(
onPageClick: (Int) -> Unit, onPageClick: (Int) -> Unit,
listState: LazyListState = rememberLazyListState(), listState: LazyListState = rememberLazyListState(),
currentPageIndex: Int? = null, currentPageIndex: Int? = null,
onLastItemPosition: ((LayoutCoordinates) -> Unit)? = null,
) { ) {
LazyRow ( LazyRow (
state = listState, state = listState,
@@ -60,15 +65,20 @@ fun CommonPageList(
val image = imageLoader(id) val image = imageLoader(id)
if (image != null) { if (image != null) {
val bitmap = image.asImageBitmap() val bitmap = image.asImageBitmap()
val isSelected = index == currentPageIndex val isSelected = index == currentPageIndex
val borderColor = val borderColor =
if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
val maxImageSize = 120.dp val maxImageSize = PAGE_LIST_ELEMENT_SIZE_DP.dp
val modifier = var modifier =
if (bitmap.height > bitmap.width) if (bitmap.height > bitmap.width)
Modifier.height(maxImageSize) Modifier.height(maxImageSize)
else else
Modifier.width(maxImageSize) Modifier.width(maxImageSize)
val isLastItem = index == pageIds.lastIndex
if (isLastItem && onLastItemPosition != null) {
modifier = modifier.onGloballyPositioned(onLastItemPosition)
}
Image( Image(
bitmap = bitmap, bitmap = bitmap,
contentDescription = null, contentDescription = null,