DocumentScreen: Landscape mode

This commit is contained in:
Pierre-Yves Nicolas
2025-07-16 13:42:05 +02:00
parent d0a77cfd3d
commit 6e30861dbf
5 changed files with 190 additions and 152 deletions

View File

@@ -74,8 +74,8 @@ fun SecondaryActionButton(
} }
@Composable @Composable
fun BackButton(onClick: () -> Unit) { fun BackButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
IconButton(onClick = onClick) { IconButton(onClick = onClick, modifier = modifier) {
Icon( Icon(
Icons.AutoMirrored.Filled.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back) contentDescription = stringResource(R.string.back)

View File

@@ -29,29 +29,21 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Article import androidx.compose.material.icons.automirrored.filled.Article
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -193,37 +185,31 @@ private fun CameraScreenScaffold(
thumbnailCoords: MutableState<Offset>, thumbnailCoords: MutableState<Offset>,
toAboutScreen: () -> Unit, toAboutScreen: () -> Unit,
) { ) {
val documentBar : @Composable () -> Unit = { var tapCount by remember { mutableStateOf(0) }
DocumentBar( var lastTapTime by remember { mutableStateOf(0L) }
pageListState = pageListState, val tapThreshold = 500L
pageCount = cameraUiState.pageCount, val onPageCountClick = {
onFinalizePressed = onFinalizePressed, val currentTime = System.currentTimeMillis()
onDebugModeSwitched = onDebugModeSwitched, if (currentTime - lastTapTime < tapThreshold) {
isLandscape = cameraUiState.isLandscape tapCount++
) if (tapCount >= 3) {
} onDebugModeSwitched()
Box { tapCount = 0
if (!cameraUiState.isLandscape) {
Scaffold(
bottomBar = documentBar
) { padding ->
val modifier = Modifier.padding(bottom = padding.calculateBottomPadding()).fillMaxSize()
CameraPreviewBox(cameraPreview, cameraUiState, onCapture, modifier)
} }
} else { } else {
Scaffold { innerPadding -> tapCount = 1
Row( }
modifier = Modifier.padding(innerPadding).fillMaxSize() lastTapTime = currentTime
) { }
CameraPreviewBox(cameraPreview, cameraUiState, onCapture, Modifier)
documentBar() Box {
} MyScaffold(
} toAboutScreen = toAboutScreen,
pageListState = pageListState,
bottomBar = { Bar(cameraUiState.pageCount, onPageCountClick, onFinalizePressed) }
) {
modifier -> CameraPreviewBox(cameraPreview, cameraUiState, onCapture, modifier)
} }
AboutScreenNavButton(
onClick = toAboutScreen,
modifier = Modifier.align(Alignment.TopEnd).windowInsetsPadding(WindowInsets.safeDrawing)
)
if (cameraUiState.captureState is CaptureState.CapturePreview) { if (cameraUiState.captureState is CaptureState.CapturePreview) {
CapturedImage(cameraUiState.captureState.processed.asImageBitmap(), thumbnailCoords) CapturedImage(cameraUiState.captureState.processed.asImageBitmap(), thumbnailCoords)
} }
@@ -410,61 +396,6 @@ fun MessageBox(inferenceTime: Long) {
) )
} }
@Composable
fun DocumentBar(
pageListState: CommonPageListState,
pageCount: Int,
onFinalizePressed: () -> Unit,
onDebugModeSwitched: () -> Unit,
isLandscape: Boolean,
) {
var tapCount by remember { mutableStateOf(0) }
var lastTapTime by remember { mutableStateOf(0L) }
val tapThreshold = 500L
val onPageCountClick = {
val currentTime = System.currentTimeMillis()
if (currentTime - lastTapTime < tapThreshold) {
tapCount++
if (tapCount >= 3) {
onDebugModeSwitched()
tapCount = 0
}
} else {
tapCount = 1
}
lastTapTime = currentTime
}
Column (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer)
) {
CommonPageList(pageListState, Modifier.weight(1f))
BottomAppBar(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
if (isLandscape) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Bar(pageCount, onPageCountClick, onFinalizePressed)
}
} else {
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 1.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Bar(pageCount, onPageCountClick, onFinalizePressed)
}
}
}
}
}
@Composable @Composable
private fun Bar( private fun Bar(
pageCount: Int, pageCount: Int,
@@ -479,7 +410,7 @@ private fun Bar(
MainActionButton( MainActionButton(
onClick = onFinalizePressed, onClick = onFinalizePressed,
enabled = pageCount > 0, enabled = pageCount > 0,
text = "Document", text = stringResource(R.string.document),
icon = Icons.AutoMirrored.Filled.Article, icon = Icons.AutoMirrored.Filled.Article,
) )
} }

View File

@@ -22,7 +22,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -40,11 +39,8 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.MutableIntState
@@ -95,28 +91,29 @@ fun DocumentScreen(
BackHandler { BackHandler {
navigation.back() navigation.back()
} }
Scaffold (
topBar = { MyScaffold(
TopAppBar( toAboutScreen = navigation.toAboutScreen,
colors = TopAppBarDefaults.topAppBarColors( pageListState = CommonPageListState(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, pageIds,
titleContentColor = MaterialTheme.colorScheme.onSurface, imageLoader,
), onPageClick = { index -> currentPageIndex.intValue = index },
title = { Text(stringResource(R.string.document)) }, currentPageIndex = currentPageIndex.intValue,
navigationIcon = { BackButton(navigation.back) }, listState = rememberLazyListState(),
actions = { ),
AboutScreenNavButton(onClick = navigation.toAboutScreen) onBack = navigation.back,
} bottomBar = {
BottomBar(showPdfDialog, showNewDocDialog)
},
pageListButton = {
SecondaryActionButton(
icon = Icons.Default.Add,
onClick = navigation.toCameraScreen,
contentDescription = stringResource(R.string.add_page),
) )
}, },
bottomBar = { ) { modifier ->
Column { DocumentPreview(pageIds, imageLoader, currentPageIndex, onDeleteImage, modifier)
PageList(pageIds, imageLoader, currentPageIndex, navigation.toCameraScreen)
BottomBar(showPdfDialog, showNewDocDialog)
}
}
) { padding ->
DocumentPreview(pageIds, imageLoader, currentPageIndex, onDeleteImage, padding)
if (showNewDocDialog.value) { if (showNewDocDialog.value) {
NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog) NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog)
} }
@@ -135,14 +132,12 @@ private fun DocumentPreview(
imageLoader: (String) -> Bitmap?, imageLoader: (String) -> Bitmap?,
currentPageIndex: MutableIntState, currentPageIndex: MutableIntState,
onDeleteImage: (String) -> Unit, onDeleteImage: (String) -> Unit,
padding: PaddingValues, modifier: Modifier,
) { ) {
val imageId = pageIds[currentPageIndex.intValue] val imageId = pageIds[currentPageIndex.intValue]
Column ( Column (
modifier = Modifier modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceContainerLow) .background(MaterialTheme.colorScheme.surfaceContainerLow)
.padding(padding)
) { ) {
Box ( Box (
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -175,13 +170,13 @@ private fun DocumentPreview(
contentDescription = stringResource(R.string.delete_page), contentDescription = stringResource(R.string.delete_page),
onClick = { onDeleteImage(imageId) }, onClick = { onDeleteImage(imageId) },
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.BottomEnd)
.padding(8.dp) .padding(8.dp)
) )
Text("${currentPageIndex.intValue + 1} / ${pageIds.size}", Text("${currentPageIndex.intValue + 1} / ${pageIds.size}",
color = MaterialTheme.colorScheme.inverseOnSurface, color = MaterialTheme.colorScheme.inverseOnSurface,
modifier = Modifier modifier = Modifier
.align(Alignment.TopStart) .align(Alignment.BottomStart)
.padding(all = 16.dp) .padding(all = 16.dp)
.background( .background(
color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f), color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f),
@@ -193,34 +188,6 @@ private fun DocumentPreview(
} }
} }
@Composable
private fun PageList(
pageIds: List<String>,
imageLoader: (String) -> Bitmap?,
currentPageIndex: MutableState<Int>,
toCameraScreen: () -> Unit
) {
Box {
CommonPageList(
CommonPageListState(
pageIds,
imageLoader,
onPageClick = { index -> currentPageIndex.value = index },
currentPageIndex = currentPageIndex.value,
listState = rememberLazyListState()
)
)
SecondaryActionButton(
icon = Icons.Default.Add,
onClick = toCameraScreen,
contentDescription = stringResource(R.string.add_page),
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(8.dp)
)
}
}
@Composable @Composable
private fun BottomBar( private fun BottomBar(
showPdfDialog: MutableState<Boolean>, showPdfDialog: MutableState<Boolean>,
@@ -296,3 +263,10 @@ fun DocumentScreenPreview() {
) )
} }
} }
@Preview(showBackground = true, widthDp = 640, heightDp = 320)
@Composable
fun DocumentScreenPreviewInLandscapeMode() {
DocumentScreenPreview()
}

View File

@@ -67,6 +67,7 @@ fun CommonPageList(
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
if (isLandscape) { if (isLandscape) {
LazyColumn ( LazyColumn (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier modifier = modifier
) { ) {
itemsIndexed(state.pageIds) { index, id -> itemsIndexed(state.pageIds) { index, id ->

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.mydomain.myscan.view
import android.content.res.Configuration
import androidx.compose.foundation.background
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
@Composable
fun MyScaffold(
toAboutScreen: () -> Unit,
pageListState: CommonPageListState,
pageListButton: (@Composable () -> Unit)? = null,
bottomBar: @Composable () -> Unit,
onBack: (() -> Unit)? = null,
content: @Composable (Modifier) -> Unit,
) {
Box {
if (!isLandscape(LocalConfiguration.current)) {
Scaffold(
bottomBar = { DocumentBar(pageListState, bottomBar, Modifier, pageListButton) }
) { innerPadding ->
content(Modifier.padding(innerPadding).fillMaxSize())
}
} else {
Scaffold { innerPadding ->
Row(
modifier = Modifier.padding(innerPadding).fillMaxSize()
) {
content(Modifier.weight(2f))
DocumentBar(pageListState, bottomBar, Modifier.weight(1f), pageListButton)
}
}
}
if (onBack != null) {
BackButton(
onBack,
modifier = Modifier.align(Alignment.TopStart).windowInsetsPadding(WindowInsets.safeDrawing)
)
}
AboutScreenNavButton(
onClick = toAboutScreen,
modifier = Modifier.align(Alignment.TopEnd).windowInsetsPadding(WindowInsets.safeDrawing)
)
}
}
@Composable
fun DocumentBar(
pageListState: CommonPageListState,
buttonBar: @Composable () -> Unit,
modifier: Modifier = Modifier,
pageListButton: (@Composable () -> Unit)? = null,
) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.background(MaterialTheme.colorScheme.surfaceContainer)
) {
var modifier: Modifier = Modifier
if (isLandscape(LocalConfiguration.current)) {
modifier = modifier.weight(1f).fillMaxWidth()
}
Box (modifier) {
CommonPageList(pageListState, modifier = Modifier.fillMaxWidth())
if (pageListButton != null) {
Box (Modifier
.align(Alignment.BottomEnd)
.padding(horizontal = 8.dp, vertical = 16.dp)
) {
pageListButton()
}
}
}
BottomAppBar(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
if (isLandscape(LocalConfiguration.current)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
buttonBar()
}
} else {
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 1.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
buttonBar()
}
}
}
}
}
fun isLandscape(configuration: Configuration): Boolean {
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}