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.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.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.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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp 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.ImageGalleryParagraph
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState 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.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
private const val ASPECT_RATIO = 4f / 3f private const val ASPECT_RATIO = 4f / 3f
private const val PORTRAIT_ASPECT_RATIO = 3f / 4f
private val IMAGE_SPACING: Dp = Size5dp 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 @Composable
private fun GalleryImage( private fun GalleryImage(
image: MediaUrlImage, image: MediaUrlImage,
@@ -115,19 +180,39 @@ private fun TwoImageGallery(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
roundedCorner: Boolean, roundedCorner: Boolean,
) { ) {
Row( val orientation = rememberFirstImageOrientation(images.firstOrNull())
modifier = Modifier.aspectRatio(ASPECT_RATIO),
horizontalArrangement = Arrangement.spacedBy(IMAGE_SPACING), if (orientation.isLandscape) {
) { Column(
images.take(2).forEach { image -> modifier = Modifier.fillMaxWidth(),
GalleryImage( verticalArrangement = Arrangement.spacedBy(IMAGE_SPACING),
image = image, ) {
allImages = images, images.take(2).forEach { image ->
modifier = Modifier.weight(1f).fillMaxSize(), GalleryImage(
roundedCorner = roundedCorner, image = image,
contentScale = ContentScale.Crop, allImages = images,
accountViewModel = accountViewModel, 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, accountViewModel: AccountViewModel,
roundedCorner: Boolean, roundedCorner: Boolean,
) { ) {
Row( val firstImage = images.first()
modifier = Modifier.aspectRatio(ASPECT_RATIO), val orientation = rememberFirstImageOrientation(firstImage)
horizontalArrangement = Arrangement.spacedBy(IMAGE_SPACING), val remainingImages = images.drop(1)
) {
GalleryImage(
image = images[0],
allImages = images,
modifier = Modifier.weight(2f).fillMaxSize(),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
if (orientation.isLandscape) {
Column( Column(
modifier = Modifier.weight(1f).fillMaxSize(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(IMAGE_SPACING), 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( GalleryImage(
image = image, image = firstImage,
allImages = images, allImages = images,
modifier = Modifier.weight(1f).fillMaxSize(), modifier = Modifier.width(columnWidth).fillMaxHeight(),
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
accountViewModel = accountViewModel, 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,
)
}
}
} }
} }
} }