commit bd2fa00c5373a1e8bce6f230ac88024680648829
parent 3157db2a5b406bd617543b88f1edd5457cab81f9
Author: Devota Aabel <daabel@mozilla.com>
Date: Thu, 9 Oct 2025 16:38:11 +0000
Bug 1988271- Update number of columns on the Stories Discover More page to display based on different window size classes r=android-reviewers,007
Differential Revision: https://phabricator.services.mozilla.com/D265168
Diffstat:
3 files changed, 93 insertions(+), 18 deletions(-)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/ListItemTabSurface.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/ListItemTabSurface.kt
@@ -4,9 +4,16 @@
package org.mozilla.fenix.compose
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
@@ -22,9 +29,11 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.PreviewLightDark
@@ -86,6 +95,9 @@ fun ListItemTabSurface(
private = false,
targetSize = imageWidth,
contentScale = imageContentScale,
+ placeholder = {
+ Box(modifier = Modifier.size(IMAGE_SIZE.dp).skeletonLoader())
+ },
)
Spacer(Modifier.width(FirefoxTheme.layout.space.static100))
@@ -131,3 +143,38 @@ private fun ListItemTabSurfaceWithCustomBackgroundPreview() {
}
}
}
+
+/**
+ * Applies a shimmering skeleton loading effect to the current [Modifier].
+ *
+ * This can be used as a placeholder for UI elements while their content is loading.
+ *
+ * @param durationMillis The duration in milliseconds of the shimmer animation cycle.
+ * Defaults to `1000`.
+ * @param color1 The starting color of the gradient animation. Defaults to [Color.LightGray].
+ * @param color2 The ending color of the gradient animation. Defaults to [Color.White].
+ *
+ * @return A [Modifier] that displays a skeleton loader effect.
+ */
+@Composable
+fun Modifier.skeletonLoader(
+ durationMillis: Int = 1000,
+ color1: Color = Color.LightGray,
+ color2: Color = Color.White,
+): Modifier {
+ val transition = rememberInfiniteTransition(label = "")
+
+ val color by transition.animateColor(
+ initialValue = color1,
+ targetValue = color2,
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ label = "",
+ )
+
+ return drawBehind {
+ drawRect(color = color)
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt
@@ -30,6 +30,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview
+import mozilla.components.compose.base.theme.layout.AcornWindowSize
import mozilla.components.compose.base.utils.BackInvokedHandler
import org.mozilla.fenix.R
import org.mozilla.fenix.components.appstate.recommendations.ContentRecommendationsState
@@ -116,14 +117,26 @@ private fun Stories(
state: ContentRecommendationsState,
interactor: PocketStoriesInteractor,
) {
+ val windowSizeClass = FirefoxTheme.windowSize
+ val columnCount = when (windowSizeClass) {
+ AcornWindowSize.Small -> 1
+ AcornWindowSize.Medium -> 2
+ AcornWindowSize.Large -> 3
+ }
+
+ val verticalPadding = if (windowSizeClass != AcornWindowSize.Small) {
+ 16.dp
+ } else {
+ 12.dp
+ }
+
LazyVerticalGrid(
- columns = GridCells.Fixed(1),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp),
+ columns = GridCells.Fixed(columnCount),
+ verticalArrangement = Arrangement.spacedBy(verticalPadding),
+ horizontalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.CenterHorizontally),
) {
itemsIndexed(state.pocketStories) { index, story ->
StoryCard(
- modifier = Modifier.padding(horizontal = 16.dp),
story = story,
onClick = interactor::onStoryClicked,
)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoryCard.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoryCard.kt
@@ -5,11 +5,12 @@
package org.mozilla.fenix.home.pocket.ui
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.Spacer
+import androidx.compose.foundation.layout.aspectRatio
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.foundation.shape.RoundedCornerShape
@@ -33,13 +34,14 @@ import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.Image
+import org.mozilla.fenix.compose.skeletonLoader
import org.mozilla.fenix.home.fake.FakeHomepagePreview
import org.mozilla.fenix.theme.FirefoxTheme
import kotlin.math.roundToInt
private val cardShape = RoundedCornerShape(8.dp)
private val defaultCardContentPadding = 8.dp
-private val imageSize = 345.dp
+private val imageWidth = 345.dp
private val imageHeight = 180.dp
@OptIn(ExperimentalMaterial3Api::class)
@@ -49,6 +51,15 @@ internal fun StoryCard(
onClick: (story: PocketStory, position: Triple<Int, Int, Int>) -> Unit,
modifier: Modifier = Modifier,
) {
+ val imageUrl = story.imageUrl.replace(
+ "{wh}",
+ with(LocalDensity.current) {
+ "${imageWidth.toPx().roundToInt()}x${
+ imageWidth.toPx().roundToInt()
+ }"
+ },
+ )
+
Card(
onClick = {
onClick(story, Triple(0, 0, 0))
@@ -61,24 +72,18 @@ internal fun StoryCard(
Column(
modifier = Modifier.padding(all = defaultCardContentPadding),
) {
- val imageUrl = story.imageUrl.replace(
- "{wh}",
- with(LocalDensity.current) {
- "${imageSize.toPx().roundToInt()}x${
- imageSize.toPx().roundToInt()
- }"
- },
- )
-
Image(
url = imageUrl,
modifier = Modifier
.fillMaxWidth()
- .height(imageHeight)
+ .aspectRatio(imageWidth / imageHeight)
.clip(cardShape),
private = false,
- targetSize = imageSize,
+ targetSize = imageWidth,
contentScale = ContentScale.Crop,
+ placeholder = {
+ Placeholder()
+ },
)
Column(
@@ -89,6 +94,7 @@ internal fun StoryCard(
text = story.title,
color = FirefoxTheme.colors.textPrimary,
overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
style = FirefoxTheme.typography.headline7,
)
@@ -104,7 +110,7 @@ internal fun StoryCard(
is PocketRecommendedStory,
is PocketSponsoredStory,
- -> {
+ -> {
// no-op, don't handle these [PocketStory] types as they are no longer
// supported after the Merino recommendation migration.
}
@@ -129,6 +135,15 @@ internal fun StoryCard(
}
}
+@Composable
+private fun Placeholder() {
+ Box(
+ modifier = Modifier
+ .aspectRatio(imageWidth / imageHeight)
+ .skeletonLoader(),
+ )
+}
+
@PreviewLightDark
@Composable
private fun StoryCardPreview() {