From 30d9a39cb6c43f101e0ab3c5dd4d2bbf35a4a24f Mon Sep 17 00:00:00 2001 From: starry-shivam Date: Sat, 27 Apr 2024 19:07:29 +0530 Subject: [PATCH 1/2] Improve animations and UI Signed-off-by: starry-shivam --- .idea/deploymentTargetDropDown.xml | 15 +- .../ui/common/SelectableChipGroup.kt | 25 ++ .../greenstash/ui/navigation/NavAnimation.kt | 106 +++++++ .../greenstash/ui/navigation/NavGraph.kt | 33 -- .../info/composables/EditTransactionSheet.kt | 3 +- .../info/composables/GoalInfoScreen.kt | 182 +++++------ .../info/composables/PriorityIndicator.kt} | 58 ++-- ...TransactionItem.kt => TransactionItems.kt} | 285 ++++++++++-------- .../screens/input/composables/InputScreen.kt | 101 ++++++- 9 files changed, 523 insertions(+), 285 deletions(-) create mode 100644 app/src/main/java/com/starry/greenstash/ui/navigation/NavAnimation.kt rename app/src/main/java/com/starry/greenstash/ui/{common/DotIndicator.kt => screens/info/composables/PriorityIndicator.kt} (61%) rename app/src/main/java/com/starry/greenstash/ui/screens/info/composables/{TransactionItem.kt => TransactionItems.kt} (52%) diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 0c0c3383..e0d618ba 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -3,7 +3,20 @@ - + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/starry/greenstash/ui/common/SelectableChipGroup.kt b/app/src/main/java/com/starry/greenstash/ui/common/SelectableChipGroup.kt index 65010201..4dcca914 100644 --- a/app/src/main/java/com/starry/greenstash/ui/common/SelectableChipGroup.kt +++ b/app/src/main/java/com/starry/greenstash/ui/common/SelectableChipGroup.kt @@ -1,3 +1,28 @@ +/** + * MIT License + * + * Copyright (c) [2022 - Present] Stɑrry Shivɑm + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + package com.starry.greenstash.ui.common import androidx.compose.animation.AnimatedVisibility diff --git a/app/src/main/java/com/starry/greenstash/ui/navigation/NavAnimation.kt b/app/src/main/java/com/starry/greenstash/ui/navigation/NavAnimation.kt new file mode 100644 index 00000000..2ec00f71 --- /dev/null +++ b/app/src/main/java/com/starry/greenstash/ui/navigation/NavAnimation.kt @@ -0,0 +1,106 @@ +/** + * MIT License + * + * Copyright (c) [2022 - Present] Stɑrry Shivɑm + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +package com.starry.greenstash.ui.navigation + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +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 + +// Duration of the navigation animation +private const val NAVIGATION_ANIM_DURATION = 360 + +/** + * Enter transition for the navigation animation + */ +fun enterTransition() = slideInHorizontally( + initialOffsetX = { NAVIGATION_ANIM_DURATION }, + animationSpec = tween( + durationMillis = (NAVIGATION_ANIM_DURATION * 1.5).toInt(), + easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1f) + ) +) + fadeIn( + animationSpec = tween( + durationMillis = NAVIGATION_ANIM_DURATION, + delayMillis = NAVIGATION_ANIM_DURATION / 4, + easing = LinearOutSlowInEasing + ) +) + +/** + * Exit transition for the navigation animation + */ +fun exitTransition() = slideOutHorizontally( + targetOffsetX = { -NAVIGATION_ANIM_DURATION }, + animationSpec = tween( + durationMillis = (NAVIGATION_ANIM_DURATION * 1.5).toInt(), + easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1f) + ) +) + fadeOut( + animationSpec = tween( + durationMillis = NAVIGATION_ANIM_DURATION, + delayMillis = NAVIGATION_ANIM_DURATION / 4, + easing = LinearOutSlowInEasing + ) +) + +/** + * Enter transition for the pop navigation animation + */ +fun popEnterTransition() = slideInHorizontally( + initialOffsetX = { -NAVIGATION_ANIM_DURATION }, + animationSpec = tween( + durationMillis = (NAVIGATION_ANIM_DURATION * 1.2).toInt(), + easing = CubicBezierEasing(0.6f, 0.05f, 0.19f, 0.95f) + ) +) + fadeIn( + animationSpec = tween( + durationMillis = NAVIGATION_ANIM_DURATION / 2, + delayMillis = NAVIGATION_ANIM_DURATION / 4, + easing = LinearEasing + ) +) + +/** + * Exit transition for the pop navigation animation + */ +fun popExitTransition() = slideOutHorizontally( + targetOffsetX = { NAVIGATION_ANIM_DURATION }, + animationSpec = tween( + durationMillis = (NAVIGATION_ANIM_DURATION * 1.2).toInt(), + easing = CubicBezierEasing(0.6f, 0.05f, 0.19f, 0.95f) + ) +) + fadeOut( + animationSpec = tween( + durationMillis = NAVIGATION_ANIM_DURATION / 2, + delayMillis = NAVIGATION_ANIM_DURATION / 4, + easing = LinearEasing + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt b/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt index 504a52c4..60533874 100644 --- a/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt @@ -25,12 +25,6 @@ package com.starry.greenstash.ui.navigation -import androidx.compose.animation.core.FastOutSlowInEasing -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.foundation.background import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -53,33 +47,6 @@ import com.starry.greenstash.ui.screens.settings.composables.SettingsScreen import com.starry.greenstash.ui.screens.welcome.composables.WelcomeScreen -private const val NAVIGATION_ANIM_DURATION = 300 - -private fun enterTransition() = slideInHorizontally( - initialOffsetX = { NAVIGATION_ANIM_DURATION }, animationSpec = tween( - durationMillis = NAVIGATION_ANIM_DURATION, easing = FastOutSlowInEasing - ) -) + fadeIn(animationSpec = tween(NAVIGATION_ANIM_DURATION)) - -private fun exitTransition() = slideOutHorizontally( - targetOffsetX = { -NAVIGATION_ANIM_DURATION }, animationSpec = tween( - durationMillis = NAVIGATION_ANIM_DURATION, easing = FastOutSlowInEasing - ) -) + fadeOut(animationSpec = tween(NAVIGATION_ANIM_DURATION)) - -private fun popEnterTransition() = slideInHorizontally( - initialOffsetX = { -NAVIGATION_ANIM_DURATION }, animationSpec = tween( - durationMillis = NAVIGATION_ANIM_DURATION, easing = FastOutSlowInEasing - ) -) + fadeIn(animationSpec = tween(NAVIGATION_ANIM_DURATION)) - -private fun popExitTransition() = slideOutHorizontally( - targetOffsetX = { NAVIGATION_ANIM_DURATION }, animationSpec = tween( - durationMillis = NAVIGATION_ANIM_DURATION, easing = FastOutSlowInEasing - ) -) + fadeOut(animationSpec = tween(NAVIGATION_ANIM_DURATION)) - - @Composable fun NavGraph( navController: NavHostController, diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/EditTransactionSheet.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/EditTransactionSheet.kt index 92266b25..ce94b4f6 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/EditTransactionSheet.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/EditTransactionSheet.kt @@ -78,7 +78,8 @@ import java.time.Instant import java.time.LocalDateTime import java.util.TimeZone -@ExperimentalMaterial3Api + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun EditTransactionSheet( transaction: Transaction, diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/GoalInfoScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/GoalInfoScreen.kt index 7610d432..fdab54db 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/GoalInfoScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/GoalInfoScreen.kt @@ -25,6 +25,7 @@ package com.starry.greenstash.ui.screens.info.composables +import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -90,7 +91,6 @@ import com.starry.greenstash.database.goal.GoalPriority import com.starry.greenstash.database.goal.GoalPriority.High import com.starry.greenstash.database.goal.GoalPriority.Low import com.starry.greenstash.database.goal.GoalPriority.Normal -import com.starry.greenstash.ui.common.DotIndicator import com.starry.greenstash.ui.common.ExpandableTextCard import com.starry.greenstash.ui.screens.info.InfoViewModel import com.starry.greenstash.ui.theme.greenstashFont @@ -141,101 +141,107 @@ fun GoalInfoScreen(goalId: String, navController: NavController) { ) { val goalData = state.goalData?.collectAsState(initial = null)?.value - if (goalData == null) { - Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else { - val currencySymbol = viewModel.getDefaultCurrencyValue() - val progressPercent = - ((goalData.getCurrentlySavedAmount() / goalData.goal.targetAmount) * 100).toInt() - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - GoalInfoCard( - currencySymbol = currencySymbol, - targetAmount = goalData.goal.targetAmount, - savedAmount = goalData.getCurrentlySavedAmount(), - daysLeftText = GoalTextUtils.getRemainingDaysText( - context = context, - goalItem = goalData, - datePattern = viewModel.getDateStyle().pattern - ), - progress = progressPercent.toFloat() / 100 - ) - GoalPriorityCard( - goalPriority = goalData.goal.priority, - reminders = goalData.goal.reminder - ) - if (goalData.goal.additionalNotes.isNotEmpty() && goalData.goal.additionalNotes.isNotBlank()) { - GoalNotesCard( - notesText = goalData.goal.additionalNotes - ) - Spacer(modifier = Modifier.height(6.dp)) + Crossfade( + targetState = goalData == null, + label = "GoalDataLoading" + ) { isGoalDataLoading -> + if (isGoalDataLoading) { + Box( + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } - if (goalData.transactions.isNotEmpty()) { - TransactionItem( - goalData.getOrderedTransactions(), - currencySymbol, - viewModel + } else { + val currencySymbol = viewModel.getDefaultCurrencyValue() + val progressPercent = + ((goalData!!.getCurrentlySavedAmount() / goalData.goal.targetAmount) * 100).toInt() + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + GoalInfoCard( + currencySymbol = currencySymbol, + targetAmount = goalData.goal.targetAmount, + savedAmount = goalData.getCurrentlySavedAmount(), + daysLeftText = GoalTextUtils.getRemainingDaysText( + context = context, + goalItem = goalData, + datePattern = viewModel.getDateStyle().pattern + ), + progress = progressPercent.toFloat() / 100 ) - // Show tooltip for swipe functionality. - LaunchedEffect(key1 = true) { - if (viewModel.shouldShowTransactionTip()) { - val result = snackBarHostState.showSnackbar( - message = context.getString(R.string.info_transaction_onboarding_tip), - actionLabel = context.getString(R.string.ok), - duration = SnackbarDuration.Indefinite - ) - - when (result) { - SnackbarResult.ActionPerformed -> { - viewModel.transactionTipDismissed() + GoalPriorityCard( + goalPriority = goalData.goal.priority, + reminders = goalData.goal.reminder + ) + if (goalData.goal.additionalNotes.isNotEmpty() && goalData.goal.additionalNotes.isNotBlank()) { + GoalNotesCard( + notesText = goalData.goal.additionalNotes + ) + Spacer(modifier = Modifier.height(6.dp)) + } + if (goalData.transactions.isNotEmpty()) { + TransactionItems( + goalData.getOrderedTransactions(), + currencySymbol, + viewModel + ) + // Show tooltip for swipe functionality. + LaunchedEffect(key1 = true) { + if (viewModel.shouldShowTransactionTip()) { + val result = snackBarHostState.showSnackbar( + message = context.getString(R.string.info_transaction_onboarding_tip), + actionLabel = context.getString(R.string.ok), + duration = SnackbarDuration.Indefinite + ) + + when (result) { + SnackbarResult.ActionPerformed -> { + viewModel.transactionTipDismissed() + } + + SnackbarResult.Dismissed -> {} } - - SnackbarResult.Dismissed -> {} } } - } - } else { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val compositionResult: LottieCompositionResult = - rememberLottieComposition( - spec = LottieCompositionSpec.RawRes(R.raw.no_transaction_found_lottie) + + } else { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val compositionResult: LottieCompositionResult = + rememberLottieComposition( + spec = LottieCompositionSpec.RawRes(R.raw.no_transaction_found_lottie) + ) + val progressAnimation by animateLottieCompositionAsState( + compositionResult.value, + isPlaying = true, + iterations = 1, + speed = 1f ) - val progressAnimation by animateLottieCompositionAsState( - compositionResult.value, - isPlaying = true, - iterations = 1, - speed = 1f - ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - LottieAnimation( - composition = compositionResult.value, - progress = { progressAnimation }, - modifier = Modifier.size(320.dp), - enableMergePaths = true - ) + LottieAnimation( + composition = compositionResult.value, + progress = { progressAnimation }, + modifier = Modifier.size(320.dp), + enableMergePaths = true + ) - Text( - text = stringResource(id = R.string.info_goal_no_transactions), - fontWeight = FontWeight.SemiBold, - fontFamily = greenstashFont, - fontSize = 20.sp, - modifier = Modifier.padding(start = 12.dp, end = 12.dp) - ) + Text( + text = stringResource(id = R.string.info_goal_no_transactions), + fontWeight = FontWeight.SemiBold, + fontFamily = greenstashFont, + fontSize = 20.sp, + modifier = Modifier.padding(start = 12.dp, end = 12.dp) + ) - Spacer(modifier = Modifier.weight(2f)) + Spacer(modifier = Modifier.weight(2f)) + } } } } @@ -373,10 +379,10 @@ fun GoalPriorityCard(goalPriority: GoalPriority, reminders: Boolean) { } Box(modifier = Modifier.padding(start = 8.dp)) { - DotIndicator(modifier = Modifier.size(8.2f.dp), color = indicatorColor) + PriorityIndicator(modifier = Modifier.size(13.dp), color = indicatorColor) } Text( - modifier = Modifier.padding(start = 14.dp), + modifier = Modifier.padding(start = 12.dp), text = stringResource(id = R.string.info_goal_priority).format(goalPriority.name), fontWeight = FontWeight.Medium, fontFamily = greenstashFont diff --git a/app/src/main/java/com/starry/greenstash/ui/common/DotIndicator.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/PriorityIndicator.kt similarity index 61% rename from app/src/main/java/com/starry/greenstash/ui/common/DotIndicator.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/info/composables/PriorityIndicator.kt index b8765462..c7834063 100644 --- a/app/src/main/java/com/starry/greenstash/ui/common/DotIndicator.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/PriorityIndicator.kt @@ -22,54 +22,56 @@ * SOFTWARE. */ -package com.starry.greenstash.ui.common +package com.starry.greenstash.ui.screens.info.composables -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween -import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Composable -fun DotIndicator(modifier: Modifier = Modifier, color: Color) { - val glowColor by animateColorAsState( - targetValue = color.copy(alpha = 0.5f), +fun PriorityIndicator(modifier: Modifier = Modifier, color: Color) { + val value by rememberInfiniteTransition(label = "value").animateFloat( + initialValue = 0.6f, + targetValue = 1f, animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing), + animation = tween( + durationMillis = 600, + easing = LinearEasing + ), repeatMode = RepeatMode.Reverse - ), label = "GlowColor" + ), label = "animateFloat" ) - Canvas(modifier = modifier) { - val radius = size.width / 2 - - drawCircle( - color = color, - radius = radius, - center = Offset(size.width / 2, size.height / 2) - ) - - drawCircle( - color = glowColor, - radius = radius * 1.5f, - style = Stroke(width = 4.dp.toPx()), - center = Offset(size.width / 2, size.height / 2) - ) - } + Box( + modifier = modifier + .graphicsLayer { + scaleX = value + scaleY = value + } + .size(25.dp) + .clip(CircleShape) + .background(color) + ) } + @Preview(showBackground = true) @Composable private fun DotIndicatorPV() { - DotIndicator(modifier = Modifier.size(18.dp), color = Color.Red) + PriorityIndicator(modifier = Modifier.size(18.dp), color = Color.Red) } \ No newline at end of file diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItem.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItems.kt similarity index 52% rename from app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItem.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItems.kt index 96c6b3eb..50ff0c7d 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItem.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItems.kt @@ -25,8 +25,13 @@ package com.starry.greenstash.ui.screens.info.composables +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -54,6 +59,8 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -86,150 +93,192 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun TransactionItem( +fun TransactionItems( transactions: List, currencySymbol: String, viewModel: InfoViewModel ) { - transactions.forEach { transaction -> - val showEditSheet = remember { mutableStateOf(false) } - val showDeleteDialog = remember { mutableStateOf(false) } + Column { + transactions.forEachIndexed { index, transaction -> + val visibleState = remember { mutableStateOf(false) } + LaunchedEffect(key1 = true) { + delay(index * 80L) // Delay each item by 80ms + visibleState.value = true + } + AnimatedVisibility( + visible = visibleState.value, + enter = fadeIn() + slideInVertically( + initialOffsetY = { fullHeight -> (fullHeight / (1f + transactions.size - index)).toInt() } + ), + exit = fadeOut() + slideOutVertically() + ) { + TransactionItem( + viewModel = viewModel, + transaction = transaction, + currencySymbol = currencySymbol + ) + } + } + } +} - val coroutineScope = rememberCoroutineScope() - val swipeState = rememberSwipeToDismissBoxState( - confirmValueChange = { direction -> - when (direction) { - SwipeToDismissBoxValue.EndToStart -> { - coroutineScope.launch { - delay(180) // allow the swipe to settle. - withContext(Dispatchers.Main) { showEditSheet.value = true } - } - } - SwipeToDismissBoxValue.StartToEnd -> { - coroutineScope.launch { - delay(180) // allow the swipe to settle. - withContext(Dispatchers.Main) { showDeleteDialog.value = true } - } - } +@Composable +private fun TransactionItem( + viewModel: InfoViewModel, + transaction: Transaction, + currencySymbol: String +) { + val showEditSheet = remember { mutableStateOf(false) } + val showDeleteDialog = remember { mutableStateOf(false) } - SwipeToDismissBoxValue.Settled -> {} - } - false // Don't allow it to settle on dismissed state. - } - ) + TransactionSwipeContainer( + transaction = transaction, + currencySymbol = currencySymbol, + showEditSheet = showEditSheet, + showDeleteDialog = showDeleteDialog + ) - val dismissDirection = swipeState.dismissDirection + EditTransactionSheet( + transaction = transaction, + showEditTransaction = showEditSheet, + viewModel = viewModel + ) - SwipeToDismissBox( - state = swipeState, - backgroundContent = { - val color by animateColorAsState( - when (dismissDirection) { - SwipeToDismissBoxValue.EndToStart -> MaterialTheme.colorScheme.primary - SwipeToDismissBoxValue.StartToEnd -> Color.Red.copy(alpha = 0.5f) - SwipeToDismissBoxValue.Settled -> Color.Transparent - }, label = "color" + if (showDeleteDialog.value) { + AlertDialog(onDismissRequest = { + showDeleteDialog.value = false + }, title = { + Text( + text = stringResource(id = R.string.goal_delete_confirmation), + color = MaterialTheme.colorScheme.onSurface, + fontFamily = greenstashFont, + ) + }, confirmButton = { + FilledTonalButton( + onClick = { + showDeleteDialog.value = false + viewModel.deleteTransaction(transaction) + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer ) - val alignment by remember(dismissDirection) { - derivedStateOf { - when (dismissDirection) { - SwipeToDismissBoxValue.EndToStart -> Alignment.CenterEnd - SwipeToDismissBoxValue.StartToEnd -> Alignment.CenterStart - SwipeToDismissBoxValue.Settled -> Alignment.Center - } + ) { + Text(stringResource(id = R.string.confirm), fontFamily = greenstashFont) + } + }, dismissButton = { + TextButton(onClick = { + showDeleteDialog.value = false + }) { + Text(stringResource(id = R.string.cancel), fontFamily = greenstashFont) + } + }, + icon = { + Icon(imageVector = Icons.Rounded.Delete, contentDescription = null) + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TransactionSwipeContainer( + transaction: Transaction, + currencySymbol: String, + showEditSheet: MutableState, + showDeleteDialog: MutableState +) { + val coroutineScope = rememberCoroutineScope() + val swipeState = rememberSwipeToDismissBoxState( + confirmValueChange = { direction -> + when (direction) { + SwipeToDismissBoxValue.EndToStart -> { + coroutineScope.launch { + delay(180) // allow the swipe to settle. + withContext(Dispatchers.Main) { showEditSheet.value = true } } } - val icon by remember(dismissDirection) { - derivedStateOf { - when (dismissDirection) { - SwipeToDismissBoxValue.EndToStart -> R.drawable.ic_goal_edit - SwipeToDismissBoxValue.StartToEnd -> R.drawable.ic_goal_delete - // Placeholder icon, not used anywhere. - SwipeToDismissBoxValue.Settled -> R.drawable.ic_goal_info - } + + SwipeToDismissBoxValue.StartToEnd -> { + coroutineScope.launch { + delay(180) // allow the swipe to settle. + withContext(Dispatchers.Main) { showDeleteDialog.value = true } } } - val scale by animateFloatAsState( - if (swipeState.dismissDirection != SwipeToDismissBoxValue.Settled) 1f else 0.75f, - label = "scale" - ) - - Box( - Modifier - .fillMaxSize() - .background(color) - .padding(horizontal = 20.dp), - contentAlignment = alignment - ) { - Icon( - imageVector = ImageVector.vectorResource(id = icon), - contentDescription = null, - modifier = Modifier.scale(scale) - ) - } - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 4.dp) - .clip(shape = RoundedCornerShape(8.dp)), - enableDismissFromStartToEnd = true, - enableDismissFromEndToStart = true, - content = { - TransactionCard(transaction = transaction, currencySymbol = currencySymbol) + SwipeToDismissBoxValue.Settled -> {} } - ) + false // Don't allow it to settle on dismissed state. + } + ) - EditTransactionSheet( - transaction = transaction, - showEditTransaction = showEditSheet, - viewModel = viewModel - ) + val dismissDirection = swipeState.dismissDirection - if (showDeleteDialog.value) { - AlertDialog(onDismissRequest = { - showDeleteDialog.value = false - }, title = { - Text( - text = stringResource(id = R.string.goal_delete_confirmation), - color = MaterialTheme.colorScheme.onSurface, - fontFamily = greenstashFont, - ) - }, confirmButton = { - FilledTonalButton( - onClick = { - showDeleteDialog.value = false - viewModel.deleteTransaction(transaction) - }, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer - ) - ) { - Text(stringResource(id = R.string.confirm), fontFamily = greenstashFont) - } - }, dismissButton = { - TextButton(onClick = { - showDeleteDialog.value = false - }) { - Text(stringResource(id = R.string.cancel), fontFamily = greenstashFont) + SwipeToDismissBox( + state = swipeState, + backgroundContent = { + val color by animateColorAsState( + when (dismissDirection) { + SwipeToDismissBoxValue.EndToStart -> MaterialTheme.colorScheme.primary + SwipeToDismissBoxValue.StartToEnd -> Color.Red.copy(alpha = 0.5f) + SwipeToDismissBoxValue.Settled -> Color.Transparent + }, label = "color" + ) + val alignment by remember(dismissDirection) { + derivedStateOf { + when (dismissDirection) { + SwipeToDismissBoxValue.EndToStart -> Alignment.CenterEnd + SwipeToDismissBoxValue.StartToEnd -> Alignment.CenterStart + SwipeToDismissBoxValue.Settled -> Alignment.Center + } } - }, - icon = { - Icon(imageVector = Icons.Rounded.Delete, contentDescription = null) + } + val icon by remember(dismissDirection) { + derivedStateOf { + when (dismissDirection) { + SwipeToDismissBoxValue.EndToStart -> R.drawable.ic_goal_edit + SwipeToDismissBoxValue.StartToEnd -> R.drawable.ic_goal_delete + // Placeholder icon, not used anywhere. + SwipeToDismissBoxValue.Settled -> R.drawable.ic_goal_info + } } + } + + val scale by animateFloatAsState( + if (swipeState.dismissDirection != SwipeToDismissBoxValue.Settled) 1f else 0.75f, + label = "scale" ) - } - } + Box( + Modifier + .fillMaxSize() + .background(color) + .padding(horizontal = 20.dp), + contentAlignment = alignment + ) { + Icon( + imageVector = ImageVector.vectorResource(id = icon), + contentDescription = null, + modifier = Modifier.scale(scale) + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 4.dp) + .clip(shape = RoundedCornerShape(8.dp)), + enableDismissFromStartToEnd = true, + enableDismissFromEndToStart = true, + content = { + TransactionCard(transaction = transaction, currencySymbol = currencySymbol) + } + ) } - @Composable -fun TransactionCard(transaction: Transaction, currencySymbol: String) { +private fun TransactionCard(transaction: Transaction, currencySymbol: String) { Card( modifier = Modifier .fillMaxWidth(), diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt index ea0672f0..332251fc 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt @@ -36,6 +36,10 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -62,6 +66,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.Lightbulb import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card @@ -267,6 +272,7 @@ fun InputScreen(editGoalId: String?, navController: NavController) { TapTargetCoordinator( showTapTargets = showTapTargets.value, onComplete = { viewModel.onboardingTapTargetsShown() }, + modifier = Modifier.fillMaxSize() ) { Scaffold( modifier = Modifier @@ -303,20 +309,6 @@ fun InputScreen(editGoalId: String?, navController: NavController) { scrollState.scrollTo(scrollState.maxValue) } - // Show onboarding tip for removing deadline. - LaunchedEffect(key1 = viewModel.state.deadline) { - if (editGoalId != null && viewModel.shouldShowRemoveDeadlineTip()) { - val snackResult = snackBarHostState.showSnackbar( - message = context.getString(R.string.input_remove_deadline_tip), - actionLabel = context.getString(R.string.ok) - ) - if (snackResult == SnackbarResult.ActionPerformed) { - viewModel.removeDeadlineTipShown() - } - - } - } - Column( modifier = Modifier .fillMaxSize() @@ -357,7 +349,7 @@ fun InputScreen(editGoalId: String?, navController: NavController) { modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - GoalIconPicker( + IconPickerCard( goalIcon = goalIcon, onClick = { showIconPickerDialog.value = true }, modifier = Modifier.tapTarget( @@ -417,6 +409,26 @@ fun InputScreen(editGoalId: String?, navController: NavController) { ) Spacer(modifier = Modifier.height(14.dp)) + // Show onboarding tip for removing deadline. + val showRemoveDeadlineTip = remember { mutableStateOf(false) } + LaunchedEffect(key1 = viewModel.state.deadline) { + if (editGoalId != null && viewModel.shouldShowRemoveDeadlineTip()) { + delay(600) // Don't show immediately. + showRemoveDeadlineTip.value = true + } + } + + InputTipCard( + icon = Icons.Filled.Lightbulb, + description = stringResource(id = R.string.input_remove_deadline_tip), + showTipCard = showRemoveDeadlineTip.value, + onDismissRequest = { + showRemoveDeadlineTip.value = false + viewModel.removeDeadlineTipShown() + } + ) + + InputTextFields( viewModel = viewModel, calenderState = calenderState, @@ -466,6 +478,63 @@ fun InputScreen(editGoalId: String?, navController: NavController) { } } +@Composable +fun InputTipCard( + icon: ImageVector, + description: String, + showTipCard: Boolean, + onDismissRequest: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .animateContentSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AnimatedVisibility( + visible = showTipCard, + enter = expandVertically(), + exit = shrinkVertically() + ) { + Card( + modifier = Modifier + .fillMaxWidth(0.86f) + .padding(bottom = 10.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = description, + style = MaterialTheme.typography.titleSmall, + fontFamily = greenstashFont, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { onDismissRequest() }, + modifier = Modifier.align(Alignment.End) + ) { + Text(text = "OK") + } + } + } + } + } +} @Composable private fun GoalImagePicker( @@ -553,7 +622,7 @@ private fun InputQuoteText() { } @Composable -private fun GoalIconPicker( +private fun IconPickerCard( goalIcon: ImageVector, onClick: () -> Unit, // To be used for onboarding tap target. From f0f8bf9ce6ef156520134b39a1bc00d8e127cd4b Mon Sep 17 00:00:00 2001 From: starry-shivam Date: Sun, 28 Apr 2024 14:44:59 +0530 Subject: [PATCH 2/2] Improve filter bottom sheet UI Signed-off-by: starry-shivam --- .idea/deploymentTargetDropDown.xml | 12 +-- .../ui/screens/home/composables/HomeScreen.kt | 73 +++++++------------ .../screens/input/composables/InputScreen.kt | 2 +- app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 8 files changed, 35 insertions(+), 57 deletions(-) diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index e0d618ba..608684fc 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -4,18 +4,18 @@ - + - + - - + + - - + + diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeScreen.kt index 3f7a9789..24fe001d 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeScreen.kt @@ -47,27 +47,24 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState -import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -116,41 +113,15 @@ import kotlinx.coroutines.launch import java.util.Locale -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen(navController: NavController) { val viewModel: HomeViewModel = hiltViewModel() + val allGoalState = viewModel.goalsList.observeAsState(emptyList()) - val modalBottomSheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden - ) - - ModalBottomSheetLayout(sheetState = modalBottomSheetState, - sheetShape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), - sheetElevation = 24.dp, - sheetBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), - sheetContent = { - FilterMenuSheet(viewModel = viewModel) - }, - content = { - HomeScreenContent( - viewModel = viewModel, - navController = navController, - bottomSheetState = modalBottomSheetState, - ) - }) - -} - + val showFilterSheet = remember { mutableStateOf(false) } + val filterSheetState = rememberModalBottomSheetState() -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun HomeScreenContent( - viewModel: HomeViewModel, - navController: NavController, - bottomSheetState: ModalBottomSheetState, -) { - val allGoalState = viewModel.goalsList.observeAsState(emptyList()) val drawerState = rememberDrawerState(DrawerValue.Closed) val searchWidgetState by viewModel.searchWidgetState @@ -161,6 +132,21 @@ private fun HomeScreenContent( val snackBarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() + if (showFilterSheet.value) { + ModalBottomSheet( + onDismissRequest = { + coroutineScope.launch { + filterSheetState.hide() + delay(300) + showFilterSheet.value = false + } + }, + sheetState = filterSheetState + ) { + FilterSheetContent(viewModel = viewModel) + } + } + ModalNavigationDrawer( drawerState = drawerState, gesturesEnabled = drawerState.isOpen, @@ -183,7 +169,9 @@ private fun HomeScreenContent( HomeAppBar( onMenuClicked = { coroutineScope.launch { drawerState.open() } }, onFilterClicked = { - coroutineScope.launch { bottomSheetState.show() } + + showFilterSheet.value = true + }, onSearchClicked = { viewModel.updateSearchWidgetState(newValue = SearchWidgetState.OPENED) }, searchWidgetState = searchWidgetState, @@ -431,19 +419,12 @@ private fun HomeExtendedFAB( @Composable -private fun FilterMenuSheet(viewModel: HomeViewModel) { +private fun FilterSheetContent(viewModel: HomeViewModel) { Column( modifier = Modifier .fillMaxWidth() .padding(12.dp) ) { - Text( - text = stringResource(id = R.string.filter_menu_title), - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp, - fontFamily = greenstashFont, - modifier = Modifier.padding(start = 8.dp, bottom = 6.dp) - ) Row(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) { FilterField.entries.forEach { @@ -462,6 +443,8 @@ private fun FilterMenuSheet(viewModel: HomeViewModel) { } } } + + Spacer(modifier = Modifier.height(16.dp)) } } diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt index 332251fc..00ce47b1 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt @@ -479,7 +479,7 @@ fun InputScreen(editGoalId: String?, navController: NavController) { } @Composable -fun InputTipCard( +private fun InputTipCard( icon: ImageVector, description: String, showTipCard: Boolean, diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2bbee7ad..e898ceea 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -39,7 +39,6 @@ Nueva Meta ¡Comencemos creando un nuevo objetivo de ahorro! Haz clic en el botón de acción flotante para crear un nuevo objetivo de ahorro. - Filter By Busca aquí… 404: ¡Meta no encontrada! diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e9526707..b98c22a7 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -39,7 +39,6 @@ Yeni Hedef Yeni bir tasarruf hedefi oluşturarak başlayalım! Yeni bir tasarruf hedefi oluşturmak için hareketli işlem düğmesine tıklayın. - Filtrele Ara… 404: Hedef bulunamadı!> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f5910df0..34329c42 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -39,7 +39,6 @@ 新目标 让我们从创建一个新的储蓄目标开始吧! 点击浮动操作按钮创建一个新的储蓄目标。 - 过滤,按 搜索此处… 404: 目标未发现!> diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 44d8543a..80f4eeea 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -39,7 +39,6 @@ 新目標 讓我們開始建立新的儲蓄目標吧! 點選浮動動作按鈕以建立新的儲蓄目標。 - 篩選條件 在此搜尋… 404:找不到目標! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a8c4527..7e2261fa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,7 +39,6 @@ New Goal Let\'s start by creating a new saving goal! Click on the floating action button to create a new saving goal. - Filter By Search here… 404: Goal not found!>