diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..1b66a66 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 818b1d1..2d93918 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
# Version 1.1.1:
+- **New**: Added entry animations for carts and cart items.
- **New**: Improved cart screen layout to better distinguish between in-cart and pickup items.
- **Minor**: Enhanced snack bar animations for a more engaging user experience.
- **Minor**: Added support for Traditional Chinese.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fc0c67b..82750c4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -15,7 +15,7 @@ android {
applicationId = "com.d4rk.cartcalculator"
minSdk = 23
targetSdk = 35
- versionCode = 67
+ versionCode = 68
versionName = "1.1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += listOf(
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/animations/Animations.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/animations/Animations.kt
index afd00ac..27b4162 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/animations/Animations.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/animations/Animations.kt
@@ -2,8 +2,11 @@ package com.d4rk.cartcalculator.ui.components.animations
import android.content.Context
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DrawerState
import androidx.compose.material3.SwipeToDismissBoxState
import androidx.compose.material3.SwipeToDismissBoxValue
@@ -22,6 +25,8 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
import com.d4rk.cartcalculator.data.datastore.DataStore
import com.d4rk.cartcalculator.data.model.ui.button.ButtonState
@@ -80,18 +85,47 @@ fun Modifier.hapticDrawerSwipe(drawerState : DrawerState) : Modifier = composed
return@composed this
}
-fun Modifier.hapticSwipeToDismissBox(swipeToDismissBoxState: SwipeToDismissBoxState): Modifier = composed {
- val haptic: HapticFeedback = LocalHapticFeedback.current
- var hasVibrated by remember { mutableStateOf(value = false) }
+fun Modifier.hapticSwipeToDismissBox(swipeToDismissBoxState : SwipeToDismissBoxState) : Modifier =
+ composed {
+ val haptic : HapticFeedback = LocalHapticFeedback.current
+ var hasVibrated by remember { mutableStateOf(value = false) }
- LaunchedEffect(swipeToDismissBoxState.currentValue) {
- if (swipeToDismissBoxState.currentValue != SwipeToDismissBoxValue.Settled && !hasVibrated) {
- haptic.performHapticFeedback(HapticFeedbackType.LongPress)
- hasVibrated = true
- } else if (swipeToDismissBoxState.currentValue == SwipeToDismissBoxValue.Settled) {
- hasVibrated = false
+ LaunchedEffect(swipeToDismissBoxState.currentValue) {
+ if (swipeToDismissBoxState.currentValue != SwipeToDismissBoxValue.Settled && ! hasVibrated) {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ hasVibrated = true
+ }
+ else if (swipeToDismissBoxState.currentValue == SwipeToDismissBoxValue.Settled) {
+ hasVibrated = false
+ }
+ }
+
+ return@composed this
}
- }
- return@composed this
+fun Modifier.animateVisibility(
+ visible : Boolean = true ,
+ index : Int = 0 ,
+ offsetY : Int = 50 ,
+ durationMillis : Int = 300 ,
+ delayPerItem : Int = 64
+) = composed {
+ val alpha = animateFloatAsState(
+ targetValue = if (visible) 1f else 0f , animationSpec = tween(
+ durationMillis = durationMillis , delayMillis = index * delayPerItem
+ ) , label = "Alpha"
+ )
+
+ val offsetYState = animateFloatAsState(
+ targetValue = if (visible) 0f else offsetY.toFloat() , animationSpec = tween(
+ durationMillis = durationMillis , delayMillis = index * delayPerItem
+ ) , label = "OffsetY"
+ )
+
+ this
+ .offset { IntOffset(x = 0 , offsetYState.value.toInt()) }
+ .graphicsLayer {
+ this.alpha = alpha.value
+ }
+ .padding(vertical = 4.dp)
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartScreen.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartScreen.kt
index 502acd8..1913efe 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartScreen.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartScreen.kt
@@ -21,7 +21,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -68,6 +68,7 @@ import com.d4rk.cartcalculator.data.database.table.ShoppingCartItemsTable
import com.d4rk.cartcalculator.data.datastore.DataStore
import com.d4rk.cartcalculator.data.model.ui.screens.UiCartModel
import com.d4rk.cartcalculator.ui.components.ads.AdBanner
+import com.d4rk.cartcalculator.ui.components.animations.animateVisibility
import com.d4rk.cartcalculator.ui.components.animations.bounceClick
import com.d4rk.cartcalculator.ui.components.animations.hapticSwipeToDismissBox
import com.d4rk.cartcalculator.ui.components.dialogs.AddNewCartItemAlertDialog
@@ -86,33 +87,34 @@ fun CartScreen(activity : CartActivity , cartId : Int) {
val uiState : UiCartModel by viewModel.uiState.collectAsState()
val isLoading : Boolean by viewModel.isLoading.collectAsState()
+ val visibilityStates by viewModel.visibilityStates.collectAsState()
val dataStore = DataStore.getInstance(context)
Box(modifier = Modifier.fillMaxSize()) {
- Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ,
- topBar = {
- LargeTopAppBar(title = {
- Text(
- text = uiState.cart?.name
- ?: stringResource(id = R.string.shopping_cart)
- )
- } , navigationIcon = {
- IconButton(onClick = {
- activity.finish()
- }) {
- Icon(Icons.AutoMirrored.Filled.ArrowBack , contentDescription = null)
- }
- } , actions = {
- IconButton(onClick = {
- viewModel.toggleOpenDialog()
- }) {
- Icon(
- Icons.Outlined.AddShoppingCart , contentDescription = null ,
- )
- }
- } , scrollBehavior = scrollBehavior)
- }) { paddingValues ->
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ,
+ topBar = {
+ LargeTopAppBar(title = {
+ Text(
+ text = uiState.cart?.name ?: stringResource(id = R.string.shopping_cart)
+ )
+ } , navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack , contentDescription = null)
+ }
+ } , actions = {
+ IconButton(onClick = {
+ viewModel.toggleOpenDialog()
+ }) {
+ Icon(
+ Icons.Outlined.AddShoppingCart , contentDescription = null ,
+ )
+ }
+ } , scrollBehavior = scrollBehavior)
+ }) { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
@@ -132,7 +134,7 @@ fun CartScreen(activity : CartActivity , cartId : Int) {
}
else {
- val (checkedItems, uncheckedItems) = uiState.cartItems.partition { it.isChecked }
+ val (checkedItems , uncheckedItems) = uiState.cartItems.partition { it.isChecked }
Box {
Column(
modifier = Modifier.fillMaxSize()
@@ -143,21 +145,27 @@ fun CartScreen(activity : CartActivity , cartId : Int) {
if (checkedItems.isNotEmpty()) {
item {
Text(
- text = stringResource(id = R.string.in_cart),
+ text = stringResource(id = R.string.in_cart) ,
style = MaterialTheme.typography.titleMedium ,
- modifier = Modifier.padding(start = 16.dp , top = 8.dp).animateItem()
+ modifier = Modifier
+ .padding(start = 16.dp , top = 8.dp)
+ .animateItem()
)
}
- items(
- items = checkedItems ,
- key = { item -> item.itemId }) { cartItem ->
+ itemsIndexed(items = checkedItems ,
+ key = { _ , item -> item.itemId }) { index , cartItem ->
+ val isVisible = visibilityStates.getOrElse(index) { false }
CartItemComposable(
viewModel = viewModel ,
cartItem = cartItem ,
onMinusClick = { viewModel.decreaseQuantity(cartItem) } ,
onPlusClick = { viewModel.increaseQuantity(cartItem) } ,
uiState = uiState ,
- modifier = Modifier.animateItem()
+ modifier = Modifier
+ .animateItem()
+ .animateVisibility(
+ visible = isVisible , index = index
+ )
)
}
}
@@ -165,21 +173,27 @@ fun CartScreen(activity : CartActivity , cartId : Int) {
if (uncheckedItems.isNotEmpty()) {
item {
Text(
- text = stringResource(id = R.string.items_to_pick_up),
+ text = stringResource(id = R.string.items_to_pick_up) ,
style = MaterialTheme.typography.titleMedium ,
- modifier = Modifier.padding(start = 16.dp , top = 8.dp).animateItem()
+ modifier = Modifier
+ .padding(start = 16.dp , top = 8.dp)
+ .animateItem()
)
}
- items(
- items = uncheckedItems ,
- key = { item -> item.itemId }) { cartItem ->
+ itemsIndexed(items = uncheckedItems ,
+ key = { _ , item -> item.itemId }) { index , cartItem ->
+ val isVisible = visibilityStates.getOrElse(index) { false }
CartItemComposable(
viewModel = viewModel ,
cartItem = cartItem ,
onMinusClick = { viewModel.decreaseQuantity(cartItem) } ,
onPlusClick = { viewModel.increaseQuantity(cartItem) } ,
uiState = uiState ,
- modifier = Modifier.animateItem()
+ modifier = Modifier
+ .animateItem()
+ .animateVisibility(
+ visible = isVisible , index = index
+ )
)
}
}
@@ -357,8 +371,7 @@ fun CartItemComposable(
view.playSoundEffect(SoundEffectConstants.CLICK)
checkedState = isChecked
viewModel.onItemCheckedChange(
- cartItem ,
- isChecked
+ cartItem , isChecked
)
})
Column {
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartViewModel.kt
index ed20edb..f71bfb1 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartViewModel.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartViewModel.kt
@@ -7,6 +7,7 @@ import com.d4rk.cartcalculator.data.datastore.DataStore
import com.d4rk.cartcalculator.data.model.ui.screens.UiCartModel
import com.d4rk.cartcalculator.ui.screens.cart.repository.CartRepository
import com.d4rk.cartcalculator.ui.viewmodel.BaseViewModel
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
@@ -33,6 +34,20 @@ class CartViewModel(application : Application) : BaseViewModel(application) {
calculateTotalPrice()
}
hideLoading()
+ initializeVisibilityStates()
+ }
+ }
+
+ private fun initializeVisibilityStates() {
+ viewModelScope.launch(coroutineExceptionHandler) {
+ delay(timeMillis = 50L)
+ _visibilityStates.value = List(_uiState.value.cartItems.size) { false }
+ _uiState.value.cartItems.indices.forEach { index ->
+ delay(timeMillis = index * 8L)
+ _visibilityStates.value = List(_visibilityStates.value.size) { lessonIndex ->
+ lessonIndex == index || _visibilityStates.value[lessonIndex]
+ }
+ }
}
}
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeScreen.kt
index 2649ef6..beacd2a 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeScreen.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeScreen.kt
@@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteForever
@@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
import com.d4rk.cartcalculator.R
import com.d4rk.cartcalculator.data.database.table.ShoppingCartTable
import com.d4rk.cartcalculator.data.model.ui.screens.UiHomeModel
+import com.d4rk.cartcalculator.ui.components.animations.animateVisibility
import com.d4rk.cartcalculator.ui.components.animations.bounceClick
import com.d4rk.cartcalculator.ui.components.animations.hapticSwipeToDismissBox
import com.d4rk.cartcalculator.ui.components.dialogs.AddNewCartAlertDialog
@@ -56,6 +57,7 @@ fun HomeScreen(
) {
val uiState : UiHomeModel by viewModel.uiState.collectAsState()
val isLoading : Boolean by viewModel.isLoading.collectAsState()
+ val visibilityStates by viewModel.visibilityStates.collectAsState()
val okStringResource = stringResource(id = android.R.string.ok)
LaunchedEffect(uiState.showSnackbar) {
@@ -99,15 +101,26 @@ fun HomeScreen(
.weight(1f)
.padding(bottom = uiState.fabAdHeight)
) {
- items(items = uiState.carts , key = { cart -> cart.cartId }) { cart ->
- CartItemComposable(cart ,
- onDelete = { viewModel.openDeleteCartDialog(it) } ,
- onCardClick = {
- view.playSoundEffect(SoundEffectConstants.CLICK)
- viewModel.openCart(cart)
- } ,
- uiState = uiState ,
- modifier = Modifier.animateItem())
+ itemsIndexed(
+ items = uiState.carts,
+ key = { _, cart -> cart.cartId }
+ ) { index, cart ->
+ val isVisible = visibilityStates.getOrElse(index) { false }
+ CartItemComposable(
+ cart = cart,
+ onDelete = { viewModel.openDeleteCartDialog(cart) },
+ onCardClick = {
+ view.playSoundEffect(SoundEffectConstants.CLICK)
+ viewModel.openCart(cart)
+ },
+ uiState = uiState,
+ modifier = Modifier
+ .animateItem()
+ .animateVisibility(
+ visible = isVisible,
+ index = index
+ )
+ )
}
}
}
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeViewModel.kt
index 246b09d..cd293e5 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeViewModel.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeViewModel.kt
@@ -6,6 +6,7 @@ import com.d4rk.cartcalculator.data.database.table.ShoppingCartTable
import com.d4rk.cartcalculator.data.model.ui.screens.UiHomeModel
import com.d4rk.cartcalculator.ui.screens.home.repository.HomeRepository
import com.d4rk.cartcalculator.ui.viewmodel.BaseViewModel
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -30,6 +31,20 @@ class HomeViewModel(application : Application) : BaseViewModel(application) {
}
}
hideLoading()
+ initializeVisibilityStates()
+ }
+ }
+
+ private fun initializeVisibilityStates() {
+ viewModelScope.launch(coroutineExceptionHandler) {
+ delay(timeMillis = 50L)
+ _visibilityStates.value = List(_uiState.value.carts.size) { false }
+ _uiState.value.carts.indices.forEach { index ->
+ delay(timeMillis = index * 8L)
+ _visibilityStates.value = List(_visibilityStates.value.size) { lessonIndex ->
+ lessonIndex == index || _visibilityStates.value[lessonIndex]
+ }
+ }
}
}
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/BaseViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/BaseViewModel.kt
index 95a26fb..36ded39 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/BaseViewModel.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/BaseViewModel.kt
@@ -4,14 +4,17 @@ import android.app.Application
import android.content.ActivityNotFoundException
import android.util.Log
import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
import com.d4rk.cartcalculator.R
import com.d4rk.cartcalculator.constants.error.ErrorType
import com.d4rk.cartcalculator.data.model.ui.error.UiErrorModel
import com.d4rk.cartcalculator.utils.error.ErrorHandler
import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
import java.io.FileNotFoundException
import java.io.IOException
@@ -27,6 +30,9 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio
handleError(exception)
}
+ val _visibilityStates = MutableStateFlow>(emptyList())
+ val visibilityStates : StateFlow> = _visibilityStates.asStateFlow()
+
private fun handleError(exception: Throwable) {
val errorType: ErrorType = when (exception) {
is SecurityException -> ErrorType.SECURITY_EXCEPTION