Give the single card a turn of its own

48d3a04 · Gabriel Morell · 2026-09-08 20:46

11 files +42 -0
Message
{commit_body(@commit)}

Files changed

modified clients/android/app/src/main/java/io/neiam/apolloscrib/ui/BoardScreen.kt
+42 −0
@@ -1,5 +1,11 @@
1 1 package io.neiam.apolloscrib.ui
2 2
3 +import androidx.compose.animation.AnimatedContent
4 +import androidx.compose.animation.fadeIn
5 +import androidx.compose.animation.fadeOut
6 +import androidx.compose.animation.slideInVertically
7 +import androidx.compose.animation.slideOutVertically
8 +import androidx.compose.animation.togetherWith
3 9 import androidx.compose.animation.core.Spring
4 10 import androidx.compose.animation.core.spring
5 11 import androidx.compose.animation.core.tween
@@ -283,6 +289,41 @@ fun BoardScreen(
283 289 }
284 290 }
285 291
292 + // One card is a different animation from three.
293 + //
294 + // With a stack, a turn moves two cards and replaces one, and the
295 + // movement is the story -- `animateItem` shows what stayed. With a
296 + // single card nothing stays, so there is nothing to move and a
297 + // fade is all a list animation can offer. That reads as a stutter
298 + // rather than a turn.
299 + //
300 + // So it gets its own: the old card leaves upward and the new one
301 + // arrives from below, which is the direction a flick pushes it.
302 + // The gesture and the animation then agree about which way the
303 + // board runs, and the timer's turn looks like the one you make.
304 + if (richCards == 1 && turn.window.isNotEmpty()) {
305 + item(key = "single") {
306 + val card = cards[turn.window.first()]
307 +
308 + AnimatedContent(
309 + targetState = card,
310 + transitionSpec = {
311 + (slideInVertically(tween(320)) { height -> height } +
312 + fadeIn(tween(220)))
313 + .togetherWith(
314 + slideOutVertically(tween(320)) { height -> -height } +
315 + fadeOut(tween(180))
316 + )
317 + },
318 + label = "card"
319 + ) { shown ->
320 + when (shown) {
321 + is RichItem.Transit -> RichCard(shown.entry, shown.route)
322 + is RichItem.Facts -> RichFactsCard(shown.facts)
323 + }
324 + }
325 + }
326 + } else {
286 327 items(turn.window.map { cards[it] }, key = { it.key }) { card ->
287 328 // Keyed, so the list knows the two cards that stayed are the
288 329 // same two cards. Without that every turn is three removals
@@ -299,6 +340,7 @@ fun BoardScreen(
299 340 is RichItem.Facts -> RichFactsCard(card.facts, slide)
300 341 }
301 342 }
343 + }
302 344 } else {
303 345 entries.forEach { entry ->
304 346 // No preview means the renderer had nothing worth a card -- a

Parents: b4bebfd