From c1674b47640ec06dd08d56444d781e7caac2d19b Mon Sep 17 00:00:00 2001 From: chill pill 244 Date: Tue, 2 Jun 2026 22:32:17 -0700 Subject: [PATCH] ui: mobile layout polish + search screen spacing + glass morphism card outline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchScreen: reduce heading→cards gap (bottom 8→4dp), reduce title row bottom padding (8→2→4dp) and LazyRow top content padding (22→8dp) - ArvioFocus: add restBorderAlpha glass morphism edge (0.5dp @ ~0.2α at rest, fades to full focus outline on selection); new showRestBorder param on ArvioFocusableSurface so opt-in per component - MediaCard: enable showRestBorder=true; always-on 2.5dp outline width; mobile cardTitle font size 14sp (TV stays 15sp) - HomeScreen (mobile): card spacing 10→14dp; catalog-to-catalog gap 8→20dp (via LazyColumn spacedBy); heading→cards gap 8→4dp Co-Authored-By: Claude Sonnet 4.6 --- .../com/arflix/tv/ui/components/MediaCard.kt | 9 ++++--- .../arflix/tv/ui/screens/home/HomeScreen.kt | 26 +++++++++---------- .../tv/ui/screens/search/SearchScreen.kt | 12 ++++----- .../com/arflix/tv/ui/skin/ArvioFocus.kt | 23 ++++++++++++---- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/MediaCard.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/MediaCard.kt index a83f115d..dc719072 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/MediaCard.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/MediaCard.kt @@ -43,6 +43,7 @@ import com.arflix.tv.data.model.MediaType import com.arflix.tv.ui.skin.ArvioFocusableSurface import com.arflix.tv.ui.skin.ArvioSkin import com.arflix.tv.ui.skin.rememberArvioCardShape +import com.arflix.tv.util.LocalDeviceType import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.draw.clip import androidx.compose.ui.zIndex @@ -102,6 +103,7 @@ fun MediaCard( var isFocused by remember { mutableStateOf(false) } val visualFocused = isFocusedOverride || isFocused + val isMobile = LocalDeviceType.current.isTouchDevice() val aspectRatio = if (isLandscape) 16f / 9f else 2f / 3f // Landscape cards should prefer wide artwork/backdrops. @@ -136,8 +138,7 @@ fun MediaCard( } val shape = rememberArvioCardShape(ArvioSkin.radius.md) - val showFocusOutline = visualFocused - val jumpBorderWidth = if (showFocusOutline) 2.5.dp else 0.dp + val jumpBorderWidth = 2.5.dp val context = LocalContext.current val density = LocalDensity.current @@ -192,6 +193,7 @@ fun MediaCard( backgroundColor = ArvioSkin.colors.surface, outlineColor = ArvioSkin.colors.focusOutline, outlineWidth = jumpBorderWidth, + showRestBorder = true, focusedScale = focusedScale, pressedScale = 0.97f, focusedTransformOriginX = 0.5f, @@ -446,7 +448,8 @@ fun MediaCard( Text( text = item.title, - style = ArvioSkin.typography.cardTitle, + style = if (isMobile) ArvioSkin.typography.cardTitle.copy(fontSize = 14.sp) + else ArvioSkin.typography.cardTitle, color = if (visualFocused) { ArvioSkin.colors.textPrimary } else { diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt index 16934307..d96cf3c3 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt @@ -2617,12 +2617,12 @@ private fun MobileHomeRowsLayer( onItemLongClick: ((MediaItem, Boolean) -> Unit)? = null, onCategoryVisiblePosition: (String, Int) -> Unit = { _, _ -> } ) { - val mobileItemSpacing = 10.dp + val mobileItemSpacing = 14.dp LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(bottom = 80.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(20.dp) ) { // Hero carousel — profile/search row + banner card pager item(key = "mobile_hero", contentType = "mobile_hero") { @@ -2646,7 +2646,7 @@ private fun MobileHomeRowsLayer( val isCollectionRow = category.id.startsWith("collection_row_") val rowKey = remember(category.id) { "home:${category.id}" } val rowUsePosterCards = rememberCatalogueRowLayoutMode(rowKey) == CardLayoutMode.POSTER - val rowMobileItemWidth = if (rowUsePosterCards) 124.dp else 200.dp + val rowMobileItemWidth = if (rowUsePosterCards) 120.dp else 200.dp val rowState = rememberLazyListState() LaunchedEffect(rowState, category.id) { @@ -2661,12 +2661,12 @@ private fun MobileHomeRowsLayer( } } - Column(modifier = Modifier.padding(bottom = 8.dp)) { + Column(modifier = Modifier.padding(bottom = 0.dp)) { // Section title Row( modifier = Modifier.padding( start = contentStartPadding, - bottom = 8.dp + bottom = 4.dp ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -2854,9 +2854,9 @@ private fun TvHomeRowsLayer( rememberCatalogueRowLayoutMode("home:${category.id}") == CardLayoutMode.POSTER } val categoryHeightsPx = remember(renderedCategories, rowLayoutModes, density) { - renderedCategories.mapIndexed { idx, category -> + renderedCategories.mapIndexed { idx, _ -> val usePoster = rowLayoutModes.getOrNull(idx) ?: false - val heightDp = if (usePoster) 252.dp else 202.dp + val heightDp = if (usePoster) 245.dp else 202.dp with(density) { heightDp.toPx() } } } @@ -2972,7 +2972,7 @@ private fun TvHomeRowsLayer( val rowIsFocused = !focusState.isSidebarFocused && actualRowIndex == focusState.currentRowIndex val rowKey = remember(category.id) { "home:${category.id}" } val rowUsePosterCards = rememberCatalogueRowLayoutMode(rowKey) == CardLayoutMode.POSTER - val rowHeight = if (rowUsePosterCards) 252.dp else 202.dp + val rowHeight = if (rowUsePosterCards) 245.dp else 202.dp Box( modifier = Modifier .fillMaxWidth() @@ -3218,7 +3218,7 @@ private fun ContentRow( usePosterCards } val cardAspectRatio = if (effectivePosterMode) 2f / 3f else 16f / 9f - val itemWidth = if (effectivePosterMode) 119.dp else 210.dp + val itemWidth = if (effectivePosterMode) 105.dp else 210.dp val itemSpacing = 14.dp val totalItems = category.items.size val maxFirstIndex = remember(totalItems) { @@ -3323,7 +3323,7 @@ private fun ContentRow( ) { // Section title - clean white text, aligned with cards Row( - modifier = Modifier.padding(start = startPadding, bottom = 12.dp), + modifier = Modifier.padding(start = startPadding, bottom = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -3363,8 +3363,8 @@ private fun ContentRow( contentPadding = PaddingValues( start = startPadding, end = railEndPadding, - top = 14.dp, - bottom = 14.dp + top = 8.dp, + bottom = 8.dp ), horizontalArrangement = Arrangement.spacedBy(itemSpacing), userScrollEnabled = false @@ -3464,7 +3464,7 @@ private fun ContentRow( if (railFocusOverlayActive) { ArvioFocusableSurface( modifier = Modifier - .padding(start = startPadding, top = 14.dp) + .padding(start = startPadding, top = 8.dp) .width(itemWidth) .aspectRatio(cardAspectRatio) .zIndex(4f), diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt index 8b76322b..be68257a 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt @@ -913,16 +913,16 @@ private fun RowsLayer( val itemWidth = if (isTouchDevice) { if (rowUsePosterCards) 110.dp else 170.dp } else { - if (rowUsePosterCards) 134.dp else 260.dp + if (rowUsePosterCards) 105.dp else 210.dp } val baseRowHeight = if (isTouchDevice) { if (rowUsePosterCards) 260.dp else 190.dp } else if (rowUsePosterCards) { // Poster cards (2:3) need extra vertical room for title + date below the image - if (screenHeight <= 640) 314.dp else 352.dp + if (screenHeight <= 640) 271.dp else 309.dp } else { // Landscape cards still render title + subtitle below artwork. - if (screenHeight <= 640) 238.dp else 302.dp + if (screenHeight <= 640) 210.dp else 274.dp } val rowHeight = baseRowHeight + focusBleedPadding // Fade non-current rows @@ -934,7 +934,7 @@ private fun RowsLayer( Box(modifier = Modifier.fillMaxWidth().height(rowHeight).graphicsLayer { alpha = rowAlpha }) { Column { Row( - modifier = Modifier.padding(start = focusBleedPadding, bottom = 8.dp, top = 4.dp), + modifier = Modifier.padding(start = focusBleedPadding, bottom = 4.dp, top = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -998,7 +998,7 @@ private fun RowsLayer( contentPadding = PaddingValues( start = focusBleedPadding, end = itemWidth + 56.dp, - top = focusBleedPadding, + top = 8.dp, bottom = focusBleedPadding + 12.dp ), horizontalArrangement = Arrangement.spacedBy(18.dp) @@ -1039,7 +1039,7 @@ private fun RowsLayer( @Composable private fun ContentGrid(items: List, usePosterCards: Boolean, isLoading: Boolean, isTouchDevice: Boolean, onItemClick: (MediaItem) -> Unit, onLoadMore: () -> Unit) { val screenHeight = LocalConfiguration.current.screenHeightDp - val itemWidth = if (usePosterCards) 134.dp else 260.dp + val itemWidth = if (usePosterCards) 105.dp else 210.dp val gridState = rememberLazyGridState() LaunchedEffect(gridState.firstVisibleItemIndex, items.size) { val lv = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0; if (items.isNotEmpty() && lv >= items.size - 8) onLoadMore() } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt b/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt index 04dd8a6d..6c4f4564 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.drawscope.Stroke @@ -49,6 +50,7 @@ fun Modifier.arvioFocusable( useGradientBorder: Boolean = false, // Arctic Fuse 2: SOLID border, not gradient gradientStartColor: Color = Color(0xFFFF00FF), // Magenta (unused when solid) gradientEndColor: Color = Color(0xFF00D4FF), // Cyan (unused when solid) + showRestBorder: Boolean = false, animateFocus: Boolean = true, onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, @@ -92,6 +94,13 @@ fun Modifier.arvioFocusable( } val highlightAlpha = if (visualFocused) 1f else animatedHighlightAlpha + // Subtle luminous edge always visible on cards that opt in (glass morphism). + val restBorderAlpha by animateFloatAsState( + targetValue = if (showRestBorder && !visualFocused) 0.4f else 0f, + animationSpec = tween(durationMillis = 150, easing = tokens.easing), + label = "arvio_rest_border", + ) + val originX = if (visualFocused) focusedTransformOriginX.coerceIn(0f, 1f) else 0.5f val focusTransformOrigin = TransformOrigin(originX, 0.5f) @@ -146,13 +155,14 @@ fun Modifier.arvioFocusable( Modifier } - val borderModifier = if (highlightAlpha > 0.01f) { + val borderModifier = if (highlightAlpha > 0.01f || restBorderAlpha > 0.01f) { Modifier.drawWithCache { val outline = shape.createOutline(size, layoutDirection, this) - val borderWidth = outlineWidth.toPx() - val ringColor = resolvedOutlineColor.copy(alpha = highlightAlpha) + val borderWidth = if (highlightAlpha > 0f) outlineWidth.toPx() else 0.5.dp.toPx() + val ringAlpha = if (highlightAlpha > 0f) highlightAlpha else restBorderAlpha * 0.5f + val ringColor = resolvedOutlineColor.copy(alpha = ringAlpha) val glowStrokeWidth = glowWidth.toPx() - val drawGlow = glowStrokeWidth > 0.01f && glowAlpha > 0.01f + val drawGlow = highlightAlpha > 0.3f && glowStrokeWidth > 0.01f && glowAlpha > 0.01f val glowColor = resolvedOutlineColor.copy(alpha = highlightAlpha * glowAlpha) onDrawWithContent { @@ -181,7 +191,8 @@ fun Modifier.arvioFocusable( } is Outline.Generic -> { if (drawGlow) { - drawPath(path = outline.path, color = glowColor, style = Stroke(width = borderWidth + glowStrokeWidth)) + val path = Path().apply { addPath(outline.path) } + drawPath(path = path, color = glowColor, style = Stroke(width = borderWidth + glowStrokeWidth)) } drawPath(path = outline.path, color = ringColor, style = Stroke(width = borderWidth)) } @@ -215,6 +226,7 @@ fun ArvioFocusableSurface( useGradientBorder: Boolean = false, // Arctic Fuse 2: SOLID border, not gradient gradientStartColor: Color = ArvioSkin.colors.focusGradientStart, gradientEndColor: Color = ArvioSkin.colors.focusGradientEnd, + showRestBorder: Boolean = false, animateFocus: Boolean = true, enabled: Boolean = true, enableSystemFocus: Boolean = true, @@ -246,6 +258,7 @@ fun ArvioFocusableSurface( useGradientBorder = useGradientBorder, gradientStartColor = gradientStartColor, gradientEndColor = gradientEndColor, + showRestBorder = showRestBorder, animateFocus = animateFocus, onClick = onClick, onLongClick = onLongClick,