Merge pull request #1528 from davotoula/image-gallery-improve-layout

Image gallery improve layout
This commit is contained in:
Vitor Pamplona
2025-10-23 09:13:44 -04:00
committed by GitHub
@@ -22,29 +22,94 @@ package com.vitorpamplona.amethyst.ui.components
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.Row
import androidx.compose.foundation.layout.Spacer
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
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.richtext.ImageGalleryParagraph
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
private const val ASPECT_RATIO = 4f / 3f
private const val PORTRAIT_ASPECT_RATIO = 3f / 4f
private val IMAGE_SPACING: Dp = Size5dp
private data class FirstImageOrientation(
val aspectRatio: Float,
val isLandscape: Boolean,
)
private fun MediaUrlImage.resolvedAspectRatio(): Float? =
dim
?.takeIf { it.hasSize() }
?.aspectRatio()
?: MediaAspectRatioCache.get(url)
private fun MediaUrlImage?.resolveOrientation(
landscapeDefaultAspectRatio: Float,
portraitDefaultAspectRatio: Float,
defaultToLandscape: Boolean = false,
): FirstImageOrientation {
val ratio = this?.resolvedAspectRatio()
val isLandscape = ratio?.let { it >= 1f } ?: defaultToLandscape
val resolvedAspectRatio =
ratio
?.takeIf { it > 0f }
?: if (isLandscape) landscapeDefaultAspectRatio else portraitDefaultAspectRatio
return FirstImageOrientation(
aspectRatio = resolvedAspectRatio,
isLandscape = isLandscape,
)
}
@Composable
private fun rememberFirstImageOrientation(firstImage: MediaUrlImage?): FirstImageOrientation {
val orientationState =
remember(firstImage) {
mutableStateOf(firstImage.resolveOrientation(ASPECT_RATIO, PORTRAIT_ASPECT_RATIO))
}
LaunchedEffect(firstImage) {
if (firstImage == null) return@LaunchedEffect
if (firstImage.dim?.hasSize() == true) return@LaunchedEffect
while (isActive) {
val ratio = firstImage.resolvedAspectRatio()
if (ratio != null && ratio > 0f) {
orientationState.value = FirstImageOrientation(ratio, ratio >= 1f)
break
}
delay(200)
}
}
return orientationState.value
}
@Composable
private fun GalleryImage(
image: MediaUrlImage,
@@ -115,19 +180,39 @@ private fun TwoImageGallery(
accountViewModel: AccountViewModel,
roundedCorner: Boolean,
) {
Row(
modifier = Modifier.aspectRatio(ASPECT_RATIO),
horizontalArrangement = Arrangement.spacedBy(IMAGE_SPACING),
) {
images.take(2).forEach { image ->
GalleryImage(
image = image,
allImages = images,
modifier = Modifier.weight(1f).fillMaxSize(),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
val orientation = rememberFirstImageOrientation(images.firstOrNull())
if (orientation.isLandscape) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(IMAGE_SPACING),
) {
images.take(2).forEach { image ->
GalleryImage(
image = image,
allImages = images,
modifier = Modifier.fillMaxWidth().aspectRatio(orientation.aspectRatio),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
} else {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(IMAGE_SPACING),
) {
images.take(2).forEach { image ->
GalleryImage(
image = image,
allImages = images,
modifier = Modifier.weight(1f, fill = true).aspectRatio(orientation.aspectRatio),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
}
}
@@ -138,32 +223,74 @@ private fun ThreeImageGallery(
accountViewModel: AccountViewModel,
roundedCorner: Boolean,
) {
Row(
modifier = Modifier.aspectRatio(ASPECT_RATIO),
horizontalArrangement = Arrangement.spacedBy(IMAGE_SPACING),
) {
GalleryImage(
image = images[0],
allImages = images,
modifier = Modifier.weight(2f).fillMaxSize(),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
val firstImage = images.first()
val orientation = rememberFirstImageOrientation(firstImage)
val remainingImages = images.drop(1)
if (orientation.isLandscape) {
Column(
modifier = Modifier.weight(1f).fillMaxSize(),
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(IMAGE_SPACING),
) {
images.drop(1).forEach { image ->
GalleryImage(
image = firstImage,
allImages = images,
modifier = Modifier.fillMaxWidth().aspectRatio(orientation.aspectRatio),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(IMAGE_SPACING),
) {
remainingImages.forEach { image ->
GalleryImage(
image = image,
allImages = images,
modifier = Modifier.weight(1f).aspectRatio(1f),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
}
} else {
BoxWithConstraints {
val horizontalSpacing = IMAGE_SPACING
val columnWidth = ((maxWidth - horizontalSpacing).coerceAtLeast(0.dp)) / 2
val rowHeight = (columnWidth * 2) + IMAGE_SPACING
Row(
modifier = Modifier.fillMaxWidth().height(rowHeight),
horizontalArrangement = Arrangement.spacedBy(horizontalSpacing),
) {
GalleryImage(
image = image,
image = firstImage,
allImages = images,
modifier = Modifier.weight(1f).fillMaxSize(),
modifier = Modifier.width(columnWidth).fillMaxHeight(),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
Column(
modifier = Modifier.width(columnWidth).fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(IMAGE_SPACING),
) {
remainingImages.forEach { image ->
GalleryImage(
image = image,
allImages = images,
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
}
}
}