improved image views
This commit is contained in:
parent
1c6a80c43b
commit
44a83a17dd
@ -6,8 +6,11 @@ import androidx.compose.foundation.gestures.detectDragGestures
|
|||||||
import androidx.compose.foundation.gestures.rememberTransformableState
|
import androidx.compose.foundation.gestures.rememberTransformableState
|
||||||
import androidx.compose.foundation.gestures.transformable
|
import androidx.compose.foundation.gestures.transformable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
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.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
|
||||||
@ -35,7 +38,6 @@ 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.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@ -66,6 +68,8 @@ internal fun ImageViewer(
|
|||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val imageBackgroundColor = readerImageBackgroundColor()
|
||||||
|
val imageAspectRatio = image.bitmap.width.toFloat() / image.bitmap.height.toFloat()
|
||||||
|
|
||||||
fun setScale(next: Float) {
|
fun setScale(next: Float) {
|
||||||
scale = next.coerceIn(MinImageScale, MaxImageScale)
|
scale = next.coerceIn(MinImageScale, MaxImageScale)
|
||||||
@ -133,11 +137,11 @@ internal fun ImageViewer(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.background(Color.Black)
|
.background(readerBackground())
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.onPreviewKeyEvent { event ->
|
.onPreviewKeyEvent { event ->
|
||||||
if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
||||||
@ -171,19 +175,28 @@ internal fun ImageViewer(
|
|||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Image(
|
val imageModifier = if (maxWidth / maxHeight > imageAspectRatio) {
|
||||||
bitmap = image.bitmap,
|
Modifier.fillMaxHeight().aspectRatio(imageAspectRatio)
|
||||||
contentDescription = image.title,
|
} else {
|
||||||
modifier = Modifier
|
Modifier.fillMaxWidth().aspectRatio(imageAspectRatio)
|
||||||
.fillMaxSize()
|
}
|
||||||
|
Box(
|
||||||
|
modifier = imageModifier
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = scale
|
scaleX = scale
|
||||||
scaleY = scale
|
scaleY = scale
|
||||||
translationX = offset.x
|
translationX = offset.x
|
||||||
translationY = offset.y
|
translationY = offset.y
|
||||||
},
|
}
|
||||||
|
.background(imageBackgroundColor),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = image.bitmap,
|
||||||
|
contentDescription = image.title,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown
|
|||||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||||
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.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
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.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
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
|
||||||
@ -47,6 +49,7 @@ import androidx.compose.ui.input.pointer.PointerEventPass
|
|||||||
import androidx.compose.ui.input.pointer.PointerType
|
import androidx.compose.ui.input.pointer.PointerType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
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(),
|
image = book.coverImages.firstOrNull() ?: book.bodyImages.firstOrNull(),
|
||||||
modifier = Modifier.width(112.dp).aspectRatio(0.68f),
|
modifier = Modifier.width(112.dp).aspectRatio(0.68f),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
|
fitBackgroundToBitmapBounds = false,
|
||||||
onOpen = onImageOpen,
|
onOpen = onImageOpen,
|
||||||
)
|
)
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.weight(1f)) {
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.weight(1f)) {
|
||||||
@ -564,6 +568,7 @@ private fun BookImage(
|
|||||||
image: Fb2ImageRef?,
|
image: Fb2ImageRef?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentScale: ContentScale = ContentScale.Fit,
|
contentScale: ContentScale = ContentScale.Fit,
|
||||||
|
fitBackgroundToBitmapBounds: Boolean = true,
|
||||||
onOpen: (ViewedBookImage) -> Unit = {},
|
onOpen: (ViewedBookImage) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val binary = remember(book, image) {
|
val binary = remember(book, image) {
|
||||||
@ -573,10 +578,12 @@ private fun BookImage(
|
|||||||
binary?.let { decodeBookImage(it) }
|
binary?.let { decodeBookImage(it) }
|
||||||
}
|
}
|
||||||
val imageTitle = image?.alt?.ifBlank { null } ?: book.title
|
val imageTitle = image?.alt?.ifBlank { null } ?: book.title
|
||||||
Box(
|
val imageBackgroundColor = readerImageBackgroundColor()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.then(
|
.then(
|
||||||
if (bitmap != null && binary != null) {
|
if (bitmap != null && binary != null) {
|
||||||
Modifier.clickable {
|
Modifier.clickable {
|
||||||
@ -596,12 +603,23 @@ private fun BookImage(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
|
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(
|
Image(
|
||||||
bitmap = bitmap,
|
bitmap = bitmap,
|
||||||
contentDescription = imageTitle,
|
contentDescription = imageTitle,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(strings.noImage, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline)
|
Text(strings.noImage, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.lerp
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@ -107,6 +109,16 @@ internal fun themedTopAppBarColors(): TopAppBarColors =
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun readerBackground(): Brush = SolidColor(MaterialTheme.colorScheme.background)
|
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 =
|
internal fun Int.formatCompact(): String =
|
||||||
if (this >= 10_000) "${(this / 1000.0).roundToInt()}k" else toString()
|
if (this >= 10_000) "${(this / 1000.0).roundToInt()}k" else toString()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user