From 483165450b84adf32fd9806f4bc30fedabc4de86 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:58:49 +0200 Subject: [PATCH] Animation: compute the real destination coordinates --- .../org/mydomain/myscan/view/CameraScreen.kt | 57 ++++++++++++++----- .../java/org/mydomain/myscan/view/PageList.kt | 14 ++++- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt index b714540..41e9b5d 100644 --- a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt @@ -52,6 +52,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -59,10 +60,14 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color 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.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -91,6 +96,7 @@ fun CameraScreen( ) { var previewView by remember { mutableStateOf(null) } val pageIds by viewModel.pageIds.collectAsStateWithLifecycle() + val thumbnailCoords = remember { mutableStateOf(Offset.Zero) } val captureController = remember { CameraCaptureController() } DisposableEffect(Unit) { @@ -124,7 +130,9 @@ fun CameraScreen( pageIds = pageIds, imageLoader = { id -> viewModel.getBitmap(id) }, 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), @@ -136,6 +144,7 @@ fun CameraScreen( ) }, onFinalizePressed = onFinalizePressed, + thumbnailCoords = thumbnailCoords, ) } @@ -146,6 +155,7 @@ private fun CameraScreenScaffold( cameraUiState: CameraUiState, onCapture: () -> Unit, onFinalizePressed: () -> Unit, + thumbnailCoords: MutableState, ) { Box { Scaffold( @@ -172,12 +182,12 @@ private fun CameraScreenScaffold( ) } } - CapturedImage(cameraUiState) + CapturedImage(cameraUiState, thumbnailCoords) } } @Composable -private fun CapturedImage(cameraUiState: CameraUiState) { +private fun CapturedImage(cameraUiState: CameraUiState, thumbnailCoords: MutableState) { cameraUiState.captureState.processedImage?.let { image -> Surface( color = Color.Black.copy(alpha = 0.3f), @@ -190,8 +200,9 @@ private fun CapturedImage(cameraUiState: CameraUiState) { isAnimating = true } val transition = updateTransition(targetState = isAnimating, label = "captureAnimation") - val targetOffsetX = 0.dp // TODO real value - val targetOffsetY = 200.dp // TODO real value + val density = LocalDensity.current + var targetOffsetX by remember { mutableStateOf(0.dp) } + var targetOffsetY by remember { mutableStateOf(0.dp) } val offsetX by transition.animateDp( transitionSpec = { tween(durationMillis = ANIMATION_DURATION) }, @@ -206,15 +217,29 @@ private fun CapturedImage(cameraUiState: CameraUiState) { label = "scale" ) { if (it) 0.3f else 1f } - Image( - bitmap = image.asImageBitmap(), - contentDescription = null, - modifier = Modifier - .fillMaxSize() - .padding(24.dp) - .offset(x = offsetX, y = offsetY - 100.dp) - .scale(scale) - ) + + 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( + bitmap = image.asImageBitmap(), + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + .offset(x = offsetX, y = offsetY - justABitToTheTop) + .scale(scale) + ) + } } } @@ -333,6 +358,7 @@ fun CameraScreenPreviewWithProcessedImage() { private fun ScreenPreview(captureState: CaptureState) { val context = LocalContext.current MyScanTheme { + val thumbnailCoords = remember { mutableStateOf(Offset.Zero) } CameraScreenScaffold( cameraPreview = { Box( @@ -356,12 +382,13 @@ private fun ScreenPreview(captureState: CaptureState) { } }, onPageClick = {}, - listState = LazyListState() + listState = LazyListState(), ) }, cameraUiState = CameraUiState(pageCount = 4, LiveAnalysisState(), captureState), onCapture = {}, onFinalizePressed = {}, + thumbnailCoords = thumbnailCoords, ) } } diff --git a/app/src/main/java/org/mydomain/myscan/view/PageList.kt b/app/src/main/java/org/mydomain/myscan/view/PageList.kt index 3481e0f..a0d6eab 100644 --- a/app/src/main/java/org/mydomain/myscan/view/PageList.kt +++ b/app/src/main/java/org/mydomain/myscan/view/PageList.kt @@ -36,8 +36,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.dp +const val PAGE_LIST_ELEMENT_SIZE_DP = 120 + @Composable fun CommonPageList( pageIds: List, @@ -45,6 +49,7 @@ fun CommonPageList( onPageClick: (Int) -> Unit, listState: LazyListState = rememberLazyListState(), currentPageIndex: Int? = null, + onLastItemPosition: ((LayoutCoordinates) -> Unit)? = null, ) { LazyRow ( state = listState, @@ -60,15 +65,20 @@ fun CommonPageList( val image = imageLoader(id) if (image != null) { val bitmap = image.asImageBitmap() + val isSelected = index == currentPageIndex val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent - val maxImageSize = 120.dp - val modifier = + val maxImageSize = PAGE_LIST_ELEMENT_SIZE_DP.dp + var modifier = if (bitmap.height > bitmap.width) Modifier.height(maxImageSize) else Modifier.width(maxImageSize) + val isLastItem = index == pageIds.lastIndex + if (isLastItem && onLastItemPosition != null) { + modifier = modifier.onGloballyPositioned(onLastItemPosition) + } Image( bitmap = bitmap, contentDescription = null,