refine CategoryDetailScreen UI and add scroll-to-hide header animation

This commit is contained in:
jonasgaudian
2026-02-17 23:13:39 +01:00
parent f2a6a58c05
commit ebfd097bf8
4 changed files with 75 additions and 53 deletions

View File

@@ -3,6 +3,7 @@
package eu.gaudian.translator.view.vocabulary package eu.gaudian.translator.view.vocabulary
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@@ -33,6 +35,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -51,7 +54,6 @@ import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.PrimaryButton import eu.gaudian.translator.view.composable.PrimaryButton
import eu.gaudian.translator.view.composable.SecondaryButton
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
import eu.gaudian.translator.view.dialogs.EditCategoryDialog import eu.gaudian.translator.view.dialogs.EditCategoryDialog
@@ -150,6 +152,24 @@ fun CategoryDetailScreen(
} }
} }
// Scroll state for animation
val listState = rememberLazyListState()
var isHeaderVisible by remember { mutableStateOf(true) }
var previousIndex by remember { mutableStateOf(0) }
var previousScrollOffset by remember { mutableStateOf(0) }
// Detect scroll direction to show/hide header (same as LibraryScreen)
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
.collect { (index, offset) ->
val isScrollingDown = index > previousIndex || (index == previousIndex && offset > previousScrollOffset)
val isAtTop = index == 0 && offset <= 4
isHeaderVisible = if (isAtTop) true else !isScrollingDown
previousIndex = index
previousScrollOffset = offset
}
}
AppScaffold( AppScaffold(
modifier = modifier, modifier = modifier,
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
@@ -173,7 +193,7 @@ fun CategoryDetailScreen(
modifier = Modifier.width(220.dp) modifier = Modifier.width(220.dp)
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -202,6 +222,22 @@ fun CategoryDetailScreen(
}, },
leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) } leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
) )
DropdownMenuItem(
text = { Text(stringResource(R.string.label_edit)) },
onClick = {
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
showMenu = false
},
leadingIcon = { Icon(AppIcons.Edit, contentDescription = null) }
)
DropdownMenuItem(
text = { Text(stringResource(R.string.label_delete)) },
onClick = {
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
showMenu = false
},
leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
)
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
@@ -209,22 +245,28 @@ fun CategoryDetailScreen(
) )
) )
// Category Header Card with Progress and Action Buttons // Category Header Card with Progress and Action Buttons (animated)
CategoryHeaderCard( androidx.compose.animation.AnimatedVisibility(
subtitle = subtitle, visible = isHeaderVisible,
categoryProgress = categoryProgress, enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(),
onStartExerciseClick = { exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically()
val categories = listOf(category) ) {
val categoryIds = categories.joinToString(",") { it?.id.toString() } CategoryHeaderCard(
navController.navigate("vocabulary_exercise/false?categories=$categoryIds") subtitle = subtitle,
}, categoryProgress = categoryProgress,
onEditClick = { onStartExerciseClick = {
categoryViewModel.setShowEditCategoryDialog(true, categoryId) val categories = listOf(category)
}, val categoryIds = categories.joinToString(",") { it?.id.toString() }
onDeleteClick = { navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId) },
} onEditClick = {
) categoryViewModel.setShowEditCategoryDialog(true, categoryId)
},
onDeleteClick = {
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
}
)
}
} }
} }
) { paddingValues -> ) { paddingValues ->
@@ -236,7 +278,8 @@ fun CategoryDetailScreen(
navController = navController, navController = navController,
isRemoveFromCategoryEnabled = category is TagCategory, isRemoveFromCategoryEnabled = category is TagCategory,
showTopBar = false, showTopBar = false,
enableNavigationButtons = true enableNavigationButtons = true,
listState = listState
) )
// Dialogs // Dialogs
@@ -275,12 +318,13 @@ fun CategoryHeaderCard(
Card( Card(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 16.dp),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) border = BorderStroke(1.dp, MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -299,22 +343,22 @@ fun CategoryHeaderCard(
) )
} }
// Progress Circle // Progress Circle - smaller size
if (categoryProgress != null) { if (categoryProgress != null) {
CategoryProgressCircle( CategoryProgressCircle(
totalItems = categoryProgress.totalItems, totalItems = categoryProgress.totalItems,
itemsCompleted = categoryProgress.itemsCompleted, itemsCompleted = categoryProgress.itemsCompleted,
itemsInStages = categoryProgress.itemsInStages, itemsInStages = categoryProgress.itemsInStages,
newItems = categoryProgress.newItems, newItems = categoryProgress.newItems,
circleSize = 120.dp, circleSize = 100.dp,
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(20.dp))
} }
// Action Buttons // Action Buttons
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Start Exercise Button (Primary) // Start Exercise Button (Primary)
PrimaryButton( PrimaryButton(
@@ -324,30 +368,6 @@ fun CategoryHeaderCard(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
Spacer(modifier = Modifier.height(12.dp))
// Secondary Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Edit Button
SecondaryButton(
text = stringResource(R.string.label_edit),
icon = AppIcons.Edit,
onClick = onEditClick,
modifier = Modifier.weight(1f)
)
// Delete Button
SecondaryButton(
text = stringResource(R.string.label_delete),
icon = AppIcons.Delete,
onClick = onDeleteClick,
modifier = Modifier.weight(1f)
)
}
} }
} }
} }

View File

@@ -100,7 +100,7 @@ fun CategoryListScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = "TODO", title = stringResource(R.string.label_all_categories),
navigationIcon = { navigationIcon = {
if (isSelectionMode) { if (isSelectionMode) {
IconButton(onClick = { IconButton(onClick = {

View File

@@ -105,11 +105,12 @@ fun AllCardsListScreen(
itemsToShow: List<VocabularyItem> = emptyList(), itemsToShow: List<VocabularyItem> = emptyList(),
isRemoveFromCategoryEnabled: Boolean = false, isRemoveFromCategoryEnabled: Boolean = false,
showTopBar: Boolean = true, showTopBar: Boolean = true,
enableNavigationButtons: Boolean = false enableNavigationButtons: Boolean = false,
listState: androidx.compose.foundation.lazy.LazyListState = rememberLazyListState()
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val lazyListState = rememberLazyListState() val lazyListState = listState
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val exportImportViewModel: ExportImportViewModel = hiltViewModel(viewModelStoreOwner = activity) val exportImportViewModel: ExportImportViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)

View File

@@ -1113,4 +1113,5 @@
<string name="cd_searchh">Search</string> <string name="cd_searchh">Search</string>
<string name="label_search_cards">Search cards</string> <string name="label_search_cards">Search cards</string>
<string name="label_learnedd">learned</string> <string name="label_learnedd">learned</string>
<string name="label_all_categoriess">All Categories</string>
</resources> </resources>