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:
parent
0f96a8ed76
commit
a28feb1a47
@ -442,7 +442,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
// screen, so moving between code tabs or pages of one screen does not replay it.
|
// screen, so moving between code tabs or pages of one screen does not replay it.
|
||||||
val stackDepth = routeStack.size
|
val stackDepth = routeStack.size
|
||||||
val shownDepth = remember { intArrayOf(stackDepth) }
|
val shownDepth = remember { intArrayOf(stackDepth) }
|
||||||
SideEffect { shownDepth[0] = stackDepth }
|
val shownReady = remember { booleanArrayOf(sessionReady) }
|
||||||
|
SideEffect {
|
||||||
|
shownDepth[0] = stackDepth
|
||||||
|
shownReady[0] = sessionReady
|
||||||
|
}
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = currentRoute.takeIf { sessionReady } ?: Route.Repositories,
|
targetState = currentRoute.takeIf { sessionReady } ?: Route.Repositories,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -450,7 +454,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
val fromTab = initialState.tab
|
val fromTab = initialState.tab
|
||||||
val toTab = targetState.tab
|
val toTab = targetState.tab
|
||||||
val tabSwitch = stackDepth == shownDepth[0] && fromTab != null && toTab != null
|
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
|
// 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.
|
// glides in, the whole width, like turning to the next page.
|
||||||
val direction = (toTab.ordinal - fromTab.ordinal).coerceIn(-1, 1)
|
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)
|
consumeCallback(intent)
|
||||||
|
|||||||
@ -41,8 +41,10 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -67,6 +69,8 @@ import com.ubisam.ubgitea.ui.theme.UBGiteaTheme
|
|||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import kotlinx.coroutines.delay
|
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
|
// 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.
|
// thing from the home screen inwards.
|
||||||
@ -310,16 +314,23 @@ fun <S> TabGlide(
|
|||||||
|
|
||||||
const val TabGlideMillis = 320
|
const val TabGlideMillis = 320
|
||||||
|
|
||||||
|
const val SplashLongestWaitMillis = 1500L
|
||||||
|
|
||||||
private val SplashGradient = listOf(Color(0xFF4DB0FF), Color(0xFF2F98F9), Color(0xFF0A6FD8))
|
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
|
* 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.
|
* 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
|
@Composable
|
||||||
fun SplashOverlay() {
|
fun SplashOverlay(settled: Boolean) {
|
||||||
var finished by rememberSaveable { mutableStateOf(false) }
|
var finished by rememberSaveable { mutableStateOf(false) }
|
||||||
if (finished) return
|
if (finished) return
|
||||||
|
val settledNow by rememberUpdatedState(settled)
|
||||||
val alpha = remember { Animatable(1f) }
|
val alpha = remember { Animatable(1f) }
|
||||||
val scale = remember { Animatable(0.85f) }
|
val scale = remember { Animatable(0.85f) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@ -327,6 +338,11 @@ fun SplashOverlay() {
|
|||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
delay(1100)
|
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))
|
alpha.animateTo(0f, tween(280))
|
||||||
finished = true
|
finished = true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
- 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
|
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.
|
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.
|
||||||
|
|||||||
@ -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.
|
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
|
전체 저장소/즐겨찾기 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.
|
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
|
## 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
|
## 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
|
- [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.
|
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
|
- [x] Moving between code tabs, or between pages of one screen, does not replay the screen
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user