home: reveal new front items and brick-pack genre chips

Scroll a recently played row back to the front when a new first item
appears while it is already at/near the start; the lazy layout otherwise
anchors the old first item and hides the new one. Balance the three
genre rows by estimated width and tighten their gap.
This commit is contained in:
austinried
2026-10-04 00:30:07 +00:00
parent ea74fce758
commit 31ed5064a5
4 changed files with 195 additions and 6 deletions
@@ -0,0 +1,41 @@
package com.subtracks.ui.home
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class GenreRowsTest {
private fun weight(genre: String) = genre.length + 6
@Test
fun everyGenreIsPlacedExactlyOnce() {
val genres = listOf("Electronic", "Rock", "Pop", "Jazz", "Classical", "Ambient")
val rows = brickRows(genres, 3)
assertEquals(3, rows.size)
assertEquals(genres.sorted(), rows.flatten().sorted())
}
@Test
fun rowsBalanceByWidthNotCount() {
val genres =
listOf(
"VeryLongGenreName1",
"A",
"B",
"VeryLongGenreName2",
"C",
"D",
"VeryLongGenreName3",
"E",
"F",
)
val rows = brickRows(genres, 3)
val widths = rows.map { row -> row.sumOf(::weight) }
val largestChip = genres.maxOf(::weight)
assertTrue(widths.max() - widths.min() <= largestChip)
}
}
@@ -0,0 +1,56 @@
package com.subtracks.ui.home
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.subtracks.ui.theme.SubtracksTheme
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL)
class HomeRevealScrollTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun aNewFrontItemAtTheStartScrollsBack() {
val labels = mutableStateOf(listOf("a", "b", "c", "d", "e"))
lateinit var state: LazyListState
composeRule.setContent {
SubtracksTheme {
state = rememberLazyListState()
RevealNewFrontItem(labels.value, state) { it }
LazyRow(state = state, modifier = Modifier.fillMaxSize()) {
items(labels.value, key = { it }) { label ->
Text(label, modifier = Modifier.width(200.dp).height(80.dp))
}
}
}
}
composeRule.waitForIdle()
assertEquals(0, state.firstVisibleItemIndex)
composeRule.runOnIdle { labels.value = listOf("z") + labels.value }
composeRule.waitForIdle()
assertEquals(0, state.firstVisibleItemIndex)
}
}
@@ -0,0 +1,37 @@
package com.subtracks.ui.home
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class HomeRevealTest {
@Test
fun noPreviousFrontDoesNotReveal() {
assertFalse(shouldRevealNewFront(previousKey = null, firstKey = "a", firstVisibleItemIndex = 0))
}
@Test
fun noFrontItemDoesNotReveal() {
assertFalse(shouldRevealNewFront(previousKey = "a", firstKey = null, firstVisibleItemIndex = 0))
}
@Test
fun anUnchangedFrontDoesNotReveal() {
assertFalse(shouldRevealNewFront(previousKey = "a", firstKey = "a", firstVisibleItemIndex = 0))
}
@Test
fun aChangedFrontRevealsAtTheStart() {
assertTrue(shouldRevealNewFront(previousKey = "a", firstKey = "b", firstVisibleItemIndex = 0))
}
@Test
fun aChangedFrontRevealsOneRowIn() {
assertTrue(shouldRevealNewFront(previousKey = "a", firstKey = "b", firstVisibleItemIndex = 1))
}
@Test
fun aChangedFrontDoesNotRevealWhenScrolledAway() {
assertFalse(shouldRevealNewFront(previousKey = "a", firstKey = "b", firstVisibleItemIndex = 2))
}
}