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
import android.annotation.SuppressLint
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -33,6 +35,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.AppTopAppBar
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.DeleteItemsDialog
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(
modifier = modifier,
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
@@ -173,7 +193,7 @@ fun CategoryDetailScreen(
modifier = Modifier.width(220.dp)
) {
DropdownMenuItem(
text = {
text = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
@@ -202,6 +222,22 @@ fun CategoryDetailScreen(
},
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(
@@ -209,22 +245,28 @@ fun CategoryDetailScreen(
)
)
// Category Header Card with Progress and Action Buttons
CategoryHeaderCard(
subtitle = subtitle,
categoryProgress = categoryProgress,
onStartExerciseClick = {
val categories = listOf(category)
val categoryIds = categories.joinToString(",") { it?.id.toString() }
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
},
onEditClick = {
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
},
onDeleteClick = {
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
}
)
// Category Header Card with Progress and Action Buttons (animated)
androidx.compose.animation.AnimatedVisibility(
visible = isHeaderVisible,
enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(),
exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically()
) {
CategoryHeaderCard(
subtitle = subtitle,
categoryProgress = categoryProgress,
onStartExerciseClick = {
val categories = listOf(category)
val categoryIds = categories.joinToString(",") { it?.id.toString() }
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
},
onEditClick = {
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
},
onDeleteClick = {
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
}
)
}
}
}
) { paddingValues ->
@@ -236,7 +278,8 @@ fun CategoryDetailScreen(
navController = navController,
isRemoveFromCategoryEnabled = category is TagCategory,
showTopBar = false,
enableNavigationButtons = true
enableNavigationButtons = true,
listState = listState
)
// Dialogs
@@ -275,12 +318,13 @@ fun CategoryHeaderCard(
Card(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = 16.dp, vertical = 16.dp),
shape = RoundedCornerShape(20.dp),
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(
modifier = Modifier
@@ -299,22 +343,22 @@ fun CategoryHeaderCard(
)
}
// Progress Circle
// Progress Circle - smaller size
if (categoryProgress != null) {
CategoryProgressCircle(
totalItems = categoryProgress.totalItems,
itemsCompleted = categoryProgress.itemsCompleted,
itemsInStages = categoryProgress.itemsInStages,
newItems = categoryProgress.newItems,
circleSize = 120.dp,
circleSize = 100.dp,
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(20.dp))
}
// Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Start Exercise Button (Primary)
PrimaryButton(
@@ -324,30 +368,6 @@ fun CategoryHeaderCard(
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(
topBar = {
AppTopAppBar(
title = "TODO",
title = stringResource(R.string.label_all_categories),
navigationIcon = {
if (isSelectionMode) {
IconButton(onClick = {

View File

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

View File

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