Skip to content

Commit

Permalink
feat: Added entry animations for carts and cart items
Browse files Browse the repository at this point in the history
Enhanced user experience with smooth animations:

- Implemented entry animations for carts and cart items to create a more engaging user interface.
  • Loading branch information
Mihai-Cristian Condrea committed Nov 22, 2024
1 parent edaad1e commit 75a2e07
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 61 deletions.
8 changes: 8 additions & 0 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -143,43 +145,55 @@ 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
)
)
}
}

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
)
)
}
}
Expand Down Expand Up @@ -357,8 +371,7 @@ fun CartItemComposable(
view.playSoundEffect(SoundEffectConstants.CLICK)
checkedState = isChecked
viewModel.onItemCheckedChange(
cartItem ,
isChecked
cartItem , isChecked
)
})
Column {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
}
}
}
}

Expand Down
Loading

0 comments on commit 75a2e07

Please sign in to comment.