mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Add short transitions for navigation, answers and progress on every platform.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
27a845c1d6
commit
59d40c886a
32 changed files with 359 additions and 121 deletions
|
|
@ -34,6 +34,9 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
|||
private set
|
||||
var showChangelog by mutableStateOf(shouldShowChangelog())
|
||||
private set
|
||||
/** True when the latest navigation moved deeper; false for back and tab switches. */
|
||||
var navigatingForward by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
private val stack = mutableStateListOf<Route>(Route.Subjects)
|
||||
val route: Route get() = stack.last()
|
||||
|
|
@ -50,11 +53,13 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
|||
searchOpen = false
|
||||
if (stack.last() == r) return
|
||||
if (r.isRoot) {
|
||||
navigatingForward = false
|
||||
val target = if (r == Route.Welcome) Route.Subjects else r
|
||||
stack.clear()
|
||||
stack.add(target)
|
||||
return
|
||||
}
|
||||
navigatingForward = true
|
||||
if (root != Route.Subjects) {
|
||||
stack.clear()
|
||||
stack.addAll(r.stack())
|
||||
|
|
@ -72,17 +77,20 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
|||
fun openPage(page: String) {
|
||||
val r = routeFromPage(page) ?: return
|
||||
searchOpen = false
|
||||
navigatingForward = true
|
||||
stack.clear()
|
||||
stack.addAll(r.stack())
|
||||
}
|
||||
|
||||
/** Replace the current screen (lesson slides -> its exercise). */
|
||||
fun replace(r: Route) {
|
||||
navigatingForward = true
|
||||
if (stack.size > 1) stack.removeAt(stack.lastIndex)
|
||||
stack.add(r)
|
||||
}
|
||||
|
||||
fun back() {
|
||||
navigatingForward = false
|
||||
when {
|
||||
stack.size > 1 -> stack.removeAt(stack.lastIndex)
|
||||
root != Route.Subjects -> go(Route.Subjects)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@ package org.maturita.maturita
|
|||
|
||||
import android.os.Bundle
|
||||
import java.util.Locale
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
|
|
@ -206,7 +214,24 @@ private fun MaturitaAppBody(vm: AppViewModel) {
|
|||
if (vm.searchOpen) vm.searchOpen = false else vm.back()
|
||||
}
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
when (val r = vm.route) {
|
||||
AnimatedContent(
|
||||
targetState = vm.route,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
transitionSpec = {
|
||||
val tabs = targetState.isRoot && initialState.isRoot
|
||||
if (tabs) {
|
||||
fadeIn(tween(180)) togetherWith fadeOut(tween(140))
|
||||
} else if (vm.navigatingForward) {
|
||||
(fadeIn(tween(220)) + slideInHorizontally(tween(220)) { it / 5 })
|
||||
.togetherWith(fadeOut(tween(150)) + slideOutHorizontally(tween(150)) { -it / 8 })
|
||||
} else {
|
||||
(fadeIn(tween(220)) + slideInHorizontally(tween(220)) { -it / 8 })
|
||||
.togetherWith(fadeOut(tween(150)) + slideOutHorizontally(tween(150)) { it / 5 })
|
||||
}
|
||||
},
|
||||
label = "screen",
|
||||
) { route ->
|
||||
when (val r = route) {
|
||||
Route.Welcome, Route.Subjects -> HomeScreen(vm)
|
||||
Route.Stats -> StatsScreen(vm)
|
||||
Route.Settings -> SettingsScreen(vm)
|
||||
|
|
@ -609,6 +634,13 @@ private fun MaturitaAppBody(vm: AppViewModel) {
|
|||
is Route.BookQuiz -> LitQuizScreen(vm, r.id)
|
||||
is Route.BookPlot -> PlotScreen(vm, r.id)
|
||||
}
|
||||
if (vm.searchOpen) SearchScreen(vm)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = vm.searchOpen,
|
||||
enter = fadeIn(tween(160)),
|
||||
exit = fadeOut(tween(120)),
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) { SearchScreen(vm) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
package org.maturita.maturita.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -36,6 +44,7 @@ import androidx.compose.material3.FilterChip
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -53,9 +62,11 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
|
|
@ -234,8 +245,12 @@ fun ActionBar(
|
|||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
if (feedback.isNotEmpty()) {
|
||||
FeedbackLine(feedback, kind)
|
||||
AnimatedVisibility(
|
||||
visible = feedback.isNotEmpty(),
|
||||
enter = fadeIn(tween(180)) + expandVertically(),
|
||||
exit = fadeOut(tween(120)) + shrinkVertically(),
|
||||
) {
|
||||
FeedbackLine(feedback, kind, animate = false)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
|
|
@ -309,8 +324,27 @@ fun PillButton(label: String, modifier: Modifier = Modifier, onClick: () -> Unit
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun FeedbackLine(text: String, kind: String) {
|
||||
if (text.isEmpty()) return
|
||||
fun SmoothProgress(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colorScheme.primary,
|
||||
trackColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
) {
|
||||
val animated by animateFloatAsState(
|
||||
targetValue = progress.coerceIn(0f, 1f),
|
||||
animationSpec = tween(durationMillis = 380),
|
||||
label = "progress",
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
progress = { animated },
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeedbackLine(text: String, kind: String, animate: Boolean = true) {
|
||||
val color = when (kind) {
|
||||
"ok" -> MaterialTheme.colorScheme.primary
|
||||
"warn" -> MaterialTheme.colorScheme.tertiary
|
||||
|
|
@ -321,6 +355,22 @@ fun FeedbackLine(text: String, kind: String) {
|
|||
"warn" -> Icons.Filled.Warning
|
||||
else -> Icons.Filled.Cancel
|
||||
}
|
||||
if (!animate) {
|
||||
if (text.isEmpty()) return
|
||||
FeedbackRow(text, color, icon)
|
||||
return
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = text.isNotEmpty(),
|
||||
enter = fadeIn(tween(180)) + expandVertically(),
|
||||
exit = fadeOut(tween(120)) + shrinkVertically(),
|
||||
) {
|
||||
FeedbackRow(text, color, icon)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedbackRow(text: String, color: Color, icon: ImageVector) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(icon, null, tint = color, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
|
@ -330,13 +380,18 @@ fun FeedbackLine(text: String, kind: String) {
|
|||
|
||||
@Composable
|
||||
fun Meaning(text: String, visible: Boolean) {
|
||||
if (!visible || text.isEmpty()) return
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = visible && text.isNotEmpty(),
|
||||
enter = fadeIn(tween(180)),
|
||||
exit = fadeOut(tween(120)),
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -397,11 +452,12 @@ fun SegChip(label: String, selected: Boolean, onClick: () -> Unit) {
|
|||
/** Multiple-choice answer with radio state and, after checking, a correct/wrong mark. */
|
||||
@Composable
|
||||
fun OptionRow(text: String, selected: Boolean, mark: Boolean?, enabled: Boolean = true, onClick: () -> Unit) {
|
||||
val container = when (mark) {
|
||||
val target = when (mark) {
|
||||
true -> MaterialTheme.colorScheme.primaryContainer
|
||||
false -> MaterialTheme.colorScheme.errorContainer
|
||||
null -> if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
val container by animateColorAsState(target, tween(180), label = "option")
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import androidx.compose.material3.Card
|
|||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
|
|
@ -479,8 +478,8 @@ private fun VocabEx(vm: AppViewModel, title: String, sub: String?, spec: J, onDo
|
|||
bottomBar = if (cur != null) ({ ActionBar(vm.tr("check"), fb.first, fb.second) { check() } }) else null,
|
||||
) { inner ->
|
||||
Column(Modifier.fillMaxSize().padding(inner).verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
LinearProgressIndicator(
|
||||
progress = { if (total == 0) 0f else done / total.toFloat() },
|
||||
SmoothProgress(
|
||||
progress = if (total == 0) 0f else done / total.toFloat(),
|
||||
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
|
@ -1354,7 +1353,7 @@ fun LitQuizScreen(vm: AppViewModel, id: String) {
|
|||
}
|
||||
} else {
|
||||
val q = qs[idx]
|
||||
LinearProgressIndicator(progress = { idx / qs.size.toFloat() }, modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
|
||||
SmoothProgress(progress = idx / qs.size.toFloat(), modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(vm.fmt("lit_question_fmt", idx + 1, qs.size), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import androidx.compose.material3.FilledTonalButton
|
|||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -116,8 +115,8 @@ fun HomeScreen(vm: AppViewModel) {
|
|||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { pct },
|
||||
SmoothProgress(
|
||||
progress = pct,
|
||||
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.18f),
|
||||
|
|
@ -246,7 +245,7 @@ fun StatsScreen(vm: AppViewModel) {
|
|||
Metric(vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits), Modifier.weight(1f))
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
LinearProgressIndicator(progress = { pct }, modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
|
||||
SmoothProgress(progress = pct, modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -262,8 +261,8 @@ fun StatsScreen(vm: AppViewModel) {
|
|||
Column {
|
||||
if (open && part.totalEx > 0) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { part.doneEx / part.totalEx.toFloat() },
|
||||
SmoothProgress(
|
||||
progress = part.doneEx / part.totalEx.toFloat(),
|
||||
modifier = Modifier.fillMaxWidth().height(4.dp).clip(CircleShape),
|
||||
)
|
||||
}
|
||||
|
|
@ -770,7 +769,6 @@ fun HwYearsScreen(vm: AppViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Composable
|
||||
fun DeHomeScreen(vm: AppViewModel) {
|
||||
DetailScaffold(vm, vm.tr("Deutsch"), vm.tr("de_home_sub")) { inner ->
|
||||
|
|
@ -808,6 +806,7 @@ fun EnYearsScreen(vm: AppViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnYearsScreen(vm: AppViewModel) {
|
||||
DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue