improved image views

This commit is contained in:
Sergey Chernov 2026-05-24 08:53:23 +03:00
parent 1c6a80c43b
commit 44a83a17dd
3 changed files with 62 additions and 19 deletions

View File

@ -6,8 +6,11 @@ import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -35,7 +38,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -66,6 +68,8 @@ internal fun ImageViewer(
val focusRequester = remember { FocusRequester() }
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val imageBackgroundColor = readerImageBackgroundColor()
val imageAspectRatio = image.bitmap.width.toFloat() / image.bitmap.height.toFloat()
fun setScale(next: Float) {
scale = next.coerceIn(MinImageScale, MaxImageScale)
@ -133,11 +137,11 @@ internal fun ImageViewer(
}
},
) { padding ->
Box(
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.background(Color.Black)
.background(readerBackground())
.focusRequester(focusRequester)
.onPreviewKeyEvent { event ->
if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
@ -171,19 +175,28 @@ internal fun ImageViewer(
},
contentAlignment = Alignment.Center,
) {
Image(
bitmap = image.bitmap,
contentDescription = image.title,
modifier = Modifier
.fillMaxSize()
val imageModifier = if (maxWidth / maxHeight > imageAspectRatio) {
Modifier.fillMaxHeight().aspectRatio(imageAspectRatio)
} else {
Modifier.fillMaxWidth().aspectRatio(imageAspectRatio)
}
Box(
modifier = imageModifier
.graphicsLayer {
scaleX = scale
scaleY = scale
translationX = offset.x
translationY = offset.y
},
contentScale = ContentScale.Fit,
)
}
.background(imageBackgroundColor),
) {
Image(
bitmap = image.bitmap,
contentDescription = image.title,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit,
)
}
}
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -47,6 +49,7 @@ import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -368,6 +371,7 @@ internal fun CoverAndTitle(book: Fb2Book, onImageOpen: (ViewedBookImage) -> Unit
image = book.coverImages.firstOrNull() ?: book.bodyImages.firstOrNull(),
modifier = Modifier.width(112.dp).aspectRatio(0.68f),
contentScale = ContentScale.Crop,
fitBackgroundToBitmapBounds = false,
onOpen = onImageOpen,
)
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.weight(1f)) {
@ -564,6 +568,7 @@ private fun BookImage(
image: Fb2ImageRef?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
fitBackgroundToBitmapBounds: Boolean = true,
onOpen: (ViewedBookImage) -> Unit = {},
) {
val binary = remember(book, image) {
@ -573,10 +578,12 @@ private fun BookImage(
binary?.let { decodeBookImage(it) }
}
val imageTitle = image?.alt?.ifBlank { null } ?: book.title
Box(
val imageBackgroundColor = readerImageBackgroundColor()
val density = LocalDensity.current
BoxWithConstraints(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.background(MaterialTheme.colorScheme.surface)
.then(
if (bitmap != null && binary != null) {
Modifier.clickable {
@ -596,12 +603,23 @@ private fun BookImage(
contentAlignment = Alignment.Center,
) {
if (bitmap != null) {
Image(
bitmap = bitmap,
contentDescription = imageTitle,
modifier = Modifier.fillMaxWidth(),
contentScale = contentScale,
)
val imageAspectRatio = bitmap.width.toFloat() / bitmap.height.coerceAtLeast(1).toFloat()
val imageModifier = if (fitBackgroundToBitmapBounds) {
val bitmapWidth = with(density) { bitmap.width.toDp() }
Modifier
.width(if (bitmapWidth < maxWidth) bitmapWidth else maxWidth)
.aspectRatio(imageAspectRatio)
} else {
Modifier.fillMaxSize()
}
Box(imageModifier.background(imageBackgroundColor)) {
Image(
bitmap = bitmap,
contentDescription = imageTitle,
modifier = Modifier.fillMaxSize(),
contentScale = contentScale,
)
}
} else {
Text(strings.noImage, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline)
}

View File

@ -22,7 +22,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
@ -107,6 +109,16 @@ internal fun themedTopAppBarColors(): TopAppBarColors =
@Composable
internal fun readerBackground(): Brush = SolidColor(MaterialTheme.colorScheme.background)
@Composable
internal fun readerImageBackgroundColor(): Color {
val surface = MaterialTheme.colorScheme.surface
return if (surface.isVisuallyDark()) lerp(surface, Color.White, 0.5f) else surface
}
private fun Color.isVisuallyDark(): Boolean {
val luminance = 0.2126f * red + 0.7152f * green + 0.0722f * blue
return luminance < 0.5f
}
internal fun Int.formatCompact(): String =
if (this >= 10_000) "${(this / 1000.0).roundToInt()}k" else toString()