TASK-069: the first screen fades in under the cup instead of gliding

Until the session is known the route shown is the repository list; when it
becomes Home the change played the full-width tab glide just as the launch
cover was fading. That hand-over is now a plain fade, and the cover waits for
it (at most 1.5 s longer) before it leaves.

Agent: main (opus)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
최민영 2026-10-12 16:16:17 +09:00
parent 0f96a8ed76
commit a28feb1a47
4 changed files with 48 additions and 5 deletions

View File

@ -442,7 +442,11 @@ class MainActivity : ComponentActivity() {
// screen, so moving between code tabs or pages of one screen does not replay it.
val stackDepth = routeStack.size
val shownDepth = remember { intArrayOf(stackDepth) }
SideEffect { shownDepth[0] = stackDepth }
val shownReady = remember { booleanArrayOf(sessionReady) }
SideEffect {
shownDepth[0] = stackDepth
shownReady[0] = sessionReady
}
AnimatedContent(
targetState = currentRoute.takeIf { sessionReady } ?: Route.Repositories,
modifier = Modifier.fillMaxSize(),
@ -450,7 +454,12 @@ class MainActivity : ComponentActivity() {
val fromTab = initialState.tab
val toTab = targetState.tab
val tabSwitch = stackDepth == shownDepth[0] && fromTab != null && toTab != null
if (tabSwitch) {
if (!shownReady[0]) {
// The session has just been found: the first screen takes the
// waiting one's place where it stands. Nothing was navigated, so
// nothing slides.
fadeIn(tween(200)).togetherWith(fadeOut(tween(90)))
} else if (tabSwitch) {
// Tabs sit side by side: the old one glides out as the new one
// glides in, the whole width, like turning to the next page.
val direction = (toTab.ordinal - fromTab.ordinal).coerceIn(-1, 1)
@ -685,7 +694,7 @@ class MainActivity : ComponentActivity() {
},
)
}
SplashOverlay()
SplashOverlay(settled = sessionReady || state.sessionReadyOrNull() != null)
}
}
consumeCallback(intent)

View File

@ -41,8 +41,10 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -67,6 +69,8 @@ import com.ubisam.ubgitea.ui.theme.UBGiteaTheme
import kotlin.math.PI
import kotlin.math.sin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
// The app's moving parts. All of them draw the launcher icon's tea cup, so the app looks like one
// thing from the home screen inwards.
@ -310,16 +314,23 @@ fun <S> TabGlide(
const val TabGlideMillis = 320
const val SplashLongestWaitMillis = 1500L
private val SplashGradient = listOf(Color(0xFF4DB0FF), Color(0xFF2F98F9), Color(0xFF0A6FD8))
/**
* The launcher icon, enlarged and steaming, over the whole window for the first second after a
* cold start. It covers the app rather than delaying it: everything underneath loads meanwhile.
*
* It leaves once [settled] says whether there is a session, so its fade does not share frames
* with the first screen arriving underneath, and never stays longer than [SplashLongestWaitMillis]
* past its own second.
*/
@Composable
fun SplashOverlay() {
fun SplashOverlay(settled: Boolean) {
var finished by rememberSaveable { mutableStateOf(false) }
if (finished) return
val settledNow by rememberUpdatedState(settled)
val alpha = remember { Animatable(1f) }
val scale = remember { Animatable(0.85f) }
LaunchedEffect(Unit) {
@ -327,6 +338,11 @@ fun SplashOverlay() {
}
LaunchedEffect(Unit) {
delay(1100)
if (!settledNow) {
withTimeoutOrNull(SplashLongestWaitMillis) { snapshotFlow { settledNow }.first { it } }
// The first screen fades in over the waiting one; let that finish under the cover.
delay(250)
}
alpha.animateTo(0f, tween(280))
finished = true
}

View File

@ -1615,3 +1615,17 @@ This is the append-only implementation and handoff record shared by Codex and Cl
- Caution: published without having been seen on a device, at the product owner's decision.
0.2.3 and older still update through the browser download; the in-app update is first used
going from 0.2.4 to whatever comes next, so it stays unproven until then.
## 2026-10-12 (continued) — TASK-069: the start no longer stutters as the cup leaves
- Request: the product owner reported that at launch the cup seemed to move off towards the top
left as it disappeared, and that this part stuttered.
- Cause found in the code: until the session is known the route shown is the repository list;
once it is known the route becomes Home. Both are tabs, so the change played the full-width tab
glide, at about the moment the cover was fading, on top of Home's first composition.
- What changed: `MainActivity` gives that hand-over a plain fade and no slide. `SplashOverlay`
takes `settled` and, after its own 1.1 seconds, waits until the session is known (at most 1.5
seconds more) and for the fade underneath before it leaves.
- Checks and results: the five required checks pass.
- Caution: diagnosed from the code, not seen on a device. If the stutter is still there, the
remaining suspect is the cost of Home's first frames on that phone.

View File

@ -22,6 +22,10 @@ The app moves where it used to jump, and what moves is the launcher icon's tea c
9. Added after that (product owner, 2026-10-12): the tabs inside a screen glide the same way.
전체 저장소/즐겨찾기 and 이슈/풀 리퀘스트 keep their header and tab row still while the list under
them changes sides. The branch screen's tabs already slid (a pager) and are unchanged.
10. Reported by the product owner (2026-10-12): the start stuttered as the cup left. The first
screen was gliding in sideways underneath, as if a tab had been switched, while the cover was
fading. The first screen now fades in where it stands, and the cover waits for it (at most
1.5 seconds longer) before it leaves.
## Scope
@ -46,7 +50,7 @@ The app moves where it used to jump, and what moves is the launcher icon's tea c
## Acceptance criteria
- [x] Each of the nine items above is in the code and the app builds.
- [x] Each of the ten items above is in the code and the app builds.
- [x] The splash covers the app without delaying it: sign-in, loading and the OAuth callback run
underneath, and taps do not pass through the cover.
- [x] Moving between code tabs, or between pages of one screen, does not replay the screen