0.2.87
This commit is contained in:
@@ -1106,6 +1106,41 @@ fun MediaMetadataPanel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The quiet surface between the focused title details and the scrolling shelves.
|
||||
*
|
||||
* The backdrop remains visible through the tint, while the fade at the lower edge keeps
|
||||
* this integrated with the rows rather than turning it into an opaque panel.
|
||||
*/
|
||||
@Composable
|
||||
internal fun MetadataHero(
|
||||
item: BaseItem?,
|
||||
loading: Boolean,
|
||||
sectionLabel: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clipToBounds()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
0f to MembySurface.copy(alpha = 0.52f),
|
||||
0.72f to MembySurface.copy(alpha = 0.30f),
|
||||
1f to Color.Transparent,
|
||||
),
|
||||
),
|
||||
) {
|
||||
MediaMetadataPanel(
|
||||
item = item,
|
||||
loading = loading,
|
||||
sectionLabel = sectionLabel,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun metadataPanelContentWidth(availableWidth: Dp, compact: Boolean): Dp =
|
||||
(availableWidth * if (compact) 0.84f else 0.76f)
|
||||
.coerceIn(280.dp, 720.dp)
|
||||
@@ -1804,14 +1839,7 @@ internal fun MediaRow(
|
||||
// can dispose that item during a focus transfer and make Compose
|
||||
// release the same pin twice. LazyListState already preserves the
|
||||
// row position; native TV spatial search safely handles row changes.
|
||||
// A horizontal lazy list clips along its scroll axis, but focused cards
|
||||
// also draw from a scaled layer in the vertical axis. Its measured height
|
||||
// already contains artwork, title and secondary metadata; clip that
|
||||
// complete unit here so the outgoing card cannot paint into another row.
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clipToBounds()
|
||||
.focusGroup(),
|
||||
modifier = Modifier.fillMaxWidth().focusGroup(),
|
||||
) {
|
||||
itemsIndexed(
|
||||
row.items,
|
||||
|
||||
@@ -285,7 +285,7 @@ internal fun FocusedHomeMetadata(
|
||||
// whole state: this panel sits beside the hero and redraws on every focus change as
|
||||
// it is.
|
||||
val homeContent by homeViewModel.content.collectAsStateWithLifecycle()
|
||||
MediaMetadataPanel(
|
||||
MetadataHero(
|
||||
item = focusedItem,
|
||||
loading = homeContent.loading.isNotEmpty(),
|
||||
sectionLabel = sectionLabel,
|
||||
|
||||
@@ -165,7 +165,10 @@ internal fun homeHeaderHeight(viewportHeight: Dp, showHero: Boolean): Dp =
|
||||
if (showHero) {
|
||||
(viewportHeight * 0.46f).coerceIn(232.dp, 300.dp)
|
||||
} else {
|
||||
(viewportHeight * 0.42f).coerceIn(220.dp, 300.dp)
|
||||
// The focused metadata surface is also the clean upper boundary for shelves as
|
||||
// they move vertically. About 20dp more coverage at common TV heights keeps a
|
||||
// departing card's last text line from being exposed without costing another row.
|
||||
(viewportHeight * 0.46f).coerceIn(240.dp, 320.dp)
|
||||
}
|
||||
|
||||
private val LazyListStateMapSaver: Saver<MutableMap<String, LazyListState>, Any> = listSaver(
|
||||
@@ -1377,7 +1380,9 @@ internal fun HomeScreen(
|
||||
sectionLabel = selectedDestination.label,
|
||||
modifier = Modifier
|
||||
.height(metadataHeight)
|
||||
.padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 10.dp),
|
||||
// LazyColumn is drawn later as a sibling. Keep the hero
|
||||
// above any transient row draw overflow during D-pad moves.
|
||||
.zIndex(1f),
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ponzischeme89.memby.ui
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -19,12 +20,14 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
@@ -141,6 +144,80 @@ class HomeMovieHeroScreenshotTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `metadata hero separates focused details from the shelves`() {
|
||||
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
||||
.use(BitmapFactory::decodeStream)
|
||||
.asImageBitmap()
|
||||
val item = movie(
|
||||
"The Last Horizon",
|
||||
2026,
|
||||
"Beyond the mapped worlds, one explorer finds an ocean that remembers every visitor.",
|
||||
8.7,
|
||||
)
|
||||
|
||||
compose.setContent {
|
||||
PreviewSurface(alignment = Alignment.TopStart) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Image(
|
||||
bitmap = artwork,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
MetadataHero(
|
||||
item = item,
|
||||
loading = false,
|
||||
sectionLabel = "Movies",
|
||||
modifier = Modifier
|
||||
.height(homeHeaderHeight(540.dp, showHero = false))
|
||||
.zIndex(1f),
|
||||
)
|
||||
Text(
|
||||
"Matt's Favourites",
|
||||
color = Color(0xFFF1F3F4),
|
||||
fontSize = 18.sp,
|
||||
modifier = Modifier.padding(start = 36.dp, top = 8.dp, bottom = 10.dp),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
repeat(5) { index ->
|
||||
Column(Modifier.weight(1f)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 9f)
|
||||
.clip(RoundedCornerShape(7.dp))
|
||||
.background(Color(0xFF27343D)),
|
||||
)
|
||||
Text(
|
||||
"Favourite ${index + 1}",
|
||||
color = Color.White,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
Text(
|
||||
"2026 • 1h 47m",
|
||||
color = Color(0xFF8F9AA3),
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compose.onRoot().captureRoboImage(
|
||||
"build/screenshots/home-movie-hero/metadata-hero-row-boundary.png",
|
||||
)
|
||||
}
|
||||
|
||||
private fun capture(name: String, movies: List<HomeHeroPick>) {
|
||||
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
||||
.use(BitmapFactory::decodeStream)
|
||||
|
||||
@@ -42,6 +42,15 @@ class HomeMovieHeroTest {
|
||||
assertTrue(540.dp - heroHeight >= 288.dp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `metadata hero adds a modest scalable row boundary`() {
|
||||
val compactTvHeight = homeHeaderHeight(540.dp, showHero = false)
|
||||
|
||||
assertEquals(248.4f, compactTvHeight.value, 0.01f)
|
||||
assertTrue(540.dp - compactTvHeight >= 288.dp)
|
||||
assertEquals(320f, homeHeaderHeight(720.dp, showHero = false).value, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hero alternates new releases and popular movies`() {
|
||||
val rows = listOf(
|
||||
|
||||
Reference in New Issue
Block a user