0.3.20
This commit is contained in:
@@ -38,7 +38,7 @@ val membyGatewayUrl: String = (project.findProperty("memby.gatewayUrl") as Strin
|
|||||||
val membyDiagnosticLogLevel: String = (project.findProperty("memby.diagnosticLogLevel") as String?)
|
val membyDiagnosticLogLevel: String = (project.findProperty("memby.diagnosticLogLevel") as String?)
|
||||||
?.trim()?.uppercase()?.takeIf { it in setOf("INFO", "DEBUG", "TRACE") } ?: "INFO"
|
?.trim()?.uppercase()?.takeIf { it in setOf("INFO", "DEBUG", "TRACE") } ?: "INFO"
|
||||||
|
|
||||||
val defaultVersionName = "0.3.18"
|
val defaultVersionName = "0.3.20"
|
||||||
val membyVersionName: String =
|
val membyVersionName: String =
|
||||||
(project.findProperty("memby.versionName") as String?)
|
(project.findProperty("memby.versionName") as String?)
|
||||||
?.trim()
|
?.trim()
|
||||||
|
|||||||
@@ -1266,6 +1266,19 @@ internal fun HomeScreen(
|
|||||||
val firstPopulatedRowId = remember(rows) {
|
val firstPopulatedRowId = remember(rows) {
|
||||||
rows.firstOrNull { candidate -> candidate.items.isNotEmpty() }?.id
|
rows.firstOrNull { candidate -> candidate.items.isNotEmpty() }?.id
|
||||||
}
|
}
|
||||||
|
// The metadata panel only needs to know whether the focused row is
|
||||||
|
// Continue Watching, and that answer changes when the viewer crosses into
|
||||||
|
// or out of one row. Reading focusedHomeRowId where the panel is composed
|
||||||
|
// read it in the scope that also declares the LazyColumn below, so every
|
||||||
|
// vertical press invalidated the whole launcher and re-declared the row
|
||||||
|
// list. Derived, the scope is invalidated only when the boolean actually
|
||||||
|
// flips.
|
||||||
|
val focusedRowIsContinueWatching by remember(rows) {
|
||||||
|
derivedStateOf {
|
||||||
|
rows.firstOrNull { it.id == focusedHomeRowId }?.kind ==
|
||||||
|
MediaRowKind.CONTINUE
|
||||||
|
}
|
||||||
|
}
|
||||||
// My Shows is a band of its own above the shelves, so on this page it is the
|
// My Shows is a band of its own above the shelves, so on this page it is the
|
||||||
// first stop below the hero and the thing Up out of the topmost shelf has to
|
// first stop below the hero and the thing Up out of the topmost shelf has to
|
||||||
// return to. Both moves used to jump over it: the hero pointed Down straight
|
// return to. Both moves used to jump over it: the hero pointed Down straight
|
||||||
@@ -1404,9 +1417,7 @@ internal fun HomeScreen(
|
|||||||
homeViewModel = homeViewModel,
|
homeViewModel = homeViewModel,
|
||||||
metadataHeroContentOrder = metadataHeroContentOrder,
|
metadataHeroContentOrder = metadataHeroContentOrder,
|
||||||
metadataHeroTimeRemainingColour = metadataHeroTimeRemainingColour,
|
metadataHeroTimeRemainingColour = metadataHeroTimeRemainingColour,
|
||||||
isContinueWatchingItem = rows.firstOrNull {
|
isContinueWatchingItem = focusedRowIsContinueWatching,
|
||||||
it.id == focusedHomeRowId
|
|
||||||
}?.kind == MediaRowKind.CONTINUE,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(metadataHeight)
|
.height(metadataHeight)
|
||||||
// LazyColumn is drawn later as a sibling. Keep the hero
|
// LazyColumn is drawn later as a sibling. Keep the hero
|
||||||
@@ -1540,6 +1551,29 @@ internal fun HomeScreen(
|
|||||||
row.items.take(8).map { it.id },
|
row.items.take(8).map { it.id },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Only the destination row can act on a vertical focus request,
|
||||||
|
// but a plain read of pendingRowFocus here happened in every
|
||||||
|
// composed row's scope — so one press recomposed all of them
|
||||||
|
// twice, once as the request was posted and again as it was
|
||||||
|
// consumed and set back to null. Derived per row, a row whose
|
||||||
|
// id does not match sees null both times and is never
|
||||||
|
// invalidated; the destination row behaves exactly as before.
|
||||||
|
val rowFocusRequest by remember(row.id) {
|
||||||
|
derivedStateOf {
|
||||||
|
pendingRowFocus?.takeIf { it.rowId == row.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// returnItemId and returnRowId are rewritten by *every* card
|
||||||
|
// taking focus, horizontal travel included, and reading them
|
||||||
|
// here read them in every composed row's scope — so moving one
|
||||||
|
// card along a shelf recomposed every other shelf on screen.
|
||||||
|
// Derived, a row that does not hold the return target sees null
|
||||||
|
// and stays untouched.
|
||||||
|
val rowReturnFocusItemId by remember(row.id) {
|
||||||
|
derivedStateOf {
|
||||||
|
returnItemId.takeIf { returnRowId == row.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
MediaRow(
|
MediaRow(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
row = row,
|
row = row,
|
||||||
@@ -1553,9 +1587,9 @@ internal fun HomeScreen(
|
|||||||
heroEntryFocusRequester = heroRowEntryFocusRequester.takeIf {
|
heroEntryFocusRequester = heroRowEntryFocusRequester.takeIf {
|
||||||
hasContextualHero && row.id == firstPopulatedRowId
|
hasContextualHero && row.id == firstPopulatedRowId
|
||||||
},
|
},
|
||||||
returnFocusItemId = returnItemId.takeIf { returnRowId == row.id },
|
returnFocusItemId = rowReturnFocusItemId,
|
||||||
returnFocusRequester = cardReturnFocusRequester,
|
returnFocusRequester = cardReturnFocusRequester,
|
||||||
verticalFocusRequest = pendingRowFocus,
|
verticalFocusRequest = rowFocusRequest,
|
||||||
onVerticalFocusRequestConsumed = { requestId ->
|
onVerticalFocusRequestConsumed = { requestId ->
|
||||||
if (pendingRowFocus?.requestId == requestId) {
|
if (pendingRowFocus?.requestId == requestId) {
|
||||||
pendingRowFocus = null
|
pendingRowFocus = null
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ fun LandscapeCard(
|
|||||||
MediaCard(
|
MediaCard(
|
||||||
model, item, width, 16f / 9f, preferPrimary = false, showProgress = false,
|
model, item, width, 16f / 9f, preferPrimary = false, showProgress = false,
|
||||||
onFocused, onClick, onLongClick, modifier,
|
onFocused, onClick, onLongClick, modifier,
|
||||||
|
// A shelf card sits under the metadata hero, which names the focused title already.
|
||||||
|
showTitle = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,8 @@ fun PosterCard(
|
|||||||
MediaCard(
|
MediaCard(
|
||||||
model, item, width, 2f / 3f, preferPrimary = true, showProgress,
|
model, item, width, 2f / 3f, preferPrimary = true, showProgress,
|
||||||
onFocused, onClick, onLongClick, modifier,
|
onFocused, onClick, onLongClick, modifier,
|
||||||
|
// A shelf card sits under the metadata hero, which names the focused title already.
|
||||||
|
showTitle = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +254,18 @@ internal fun MediaCard(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
|
/**
|
||||||
|
* Whether the card captions itself with the title.
|
||||||
|
*
|
||||||
|
* False on the launcher's shelves, where the metadata hero above them is already
|
||||||
|
* naming whatever holds focus — as the title treatment where there is a logo, as plain
|
||||||
|
* text where there is not — so a caption there was the same title said twice, once in
|
||||||
|
* the one place the eye is already reading it. True everywhere the card stands on its
|
||||||
|
* own with no hero over it, which today is the genre grid: nothing else there says what
|
||||||
|
* a poster is. The name is in [cardDescription] either way, so a screen reader is told
|
||||||
|
* regardless of what is drawn.
|
||||||
|
*/
|
||||||
|
showTitle: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val widthPx = with(density) { width.roundToPx() }.coerceIn(180, 720)
|
val widthPx = with(density) { width.roundToPx() }.coerceIn(180, 720)
|
||||||
@@ -386,15 +400,17 @@ internal fun MediaCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
if (showTitle) {
|
||||||
model.name,
|
Text(
|
||||||
color = MembyOnSurface,
|
model.name,
|
||||||
fontSize = 14.sp,
|
color = MembyOnSurface,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
fontWeight = FontWeight.SemiBold,
|
||||||
overflow = TextOverflow.Ellipsis,
|
maxLines = 1,
|
||||||
modifier = Modifier.padding(top = 9.dp).fillMaxWidth(),
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
modifier = Modifier.padding(top = 9.dp).fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (model.showSecondaryMetadata) {
|
if (model.showSecondaryMetadata) {
|
||||||
// The media type sits *in* the metadata line rather than over the poster:
|
// The media type sits *in* the metadata line rather than over the poster:
|
||||||
// it is informational, like the year and the runtime beside it, and the
|
// it is informational, like the year and the runtime beside it, and the
|
||||||
@@ -403,7 +419,11 @@ internal fun MediaCard(
|
|||||||
// it cannot make the card taller than one without it.
|
// it cannot make the card taller than one without it.
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(top = 4.dp).fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
// With no title above it this line is the first thing under the
|
||||||
|
// artwork, so it takes over the gap the title was holding.
|
||||||
|
.padding(top = if (showTitle) 4.dp else 9.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
model.typeMark?.let { (icon, _) ->
|
model.typeMark?.let { (icon, _) ->
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+23
-3
@@ -39,13 +39,33 @@ class BaselineProfileGenerator {
|
|||||||
browseHome()
|
browseHome()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks the launcher the way a viewer does, and walks it *vertically*.
|
||||||
|
*
|
||||||
|
* Two Down presses were not enough. Moving between shelves is the most expensive thing
|
||||||
|
* the launcher does — a row is composed as it scrolls in, the focus request is posted
|
||||||
|
* and consumed, and the hero catches up behind it — and none of that was in the profile,
|
||||||
|
* so on a television it ran interpreted. Measured on a Chromecast with Google TV, that
|
||||||
|
* was the difference between a 72ms and a 10ms recomposition at the 90th percentile.
|
||||||
|
*
|
||||||
|
* The walk goes down far enough to compose shelves beyond the first viewport and back up
|
||||||
|
* again, because entering a row from below is a different path from entering it from
|
||||||
|
* above, and both are on the D-pad.
|
||||||
|
*/
|
||||||
private fun MacrobenchmarkScope.browseHome() {
|
private fun MacrobenchmarkScope.browseHome() {
|
||||||
repeat(4) { device.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) }
|
repeat(4) { device.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) }
|
||||||
device.waitForIdle()
|
device.waitForIdle()
|
||||||
device.pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN)
|
repeat(6) {
|
||||||
|
device.pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||||
|
device.waitForIdle()
|
||||||
|
}
|
||||||
|
// Travelling a shelf is what pulls in the card, artwork and metadata composables
|
||||||
|
// for a row that was composed by a vertical move rather than by the first frame.
|
||||||
repeat(3) { device.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) }
|
repeat(3) { device.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) }
|
||||||
device.waitForIdle()
|
device.waitForIdle()
|
||||||
device.pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN)
|
repeat(6) {
|
||||||
device.waitForIdle()
|
device.pressKeyCode(KeyEvent.KEYCODE_DPAD_UP)
|
||||||
|
device.waitForIdle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user