diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 1fbc08d25c5..685d6bd7c1e 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -5,7 +5,7 @@ ----- - [**] Improved accessibility support (handling of increased text size) in in-person payment flows [https://github.com/woocommerce/woocommerce-android/pull/13414] - [*] Automatically select the first available order when filtering is active in the two-pane layout.[https://github.com/woocommerce/woocommerce-android/pull/13491] - +- [*] [Internal] Removal animation of the items from the cart [https://github.com/woocommerce/woocommerce-android/pull/13442] 21.7 ----- diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreen.kt index d957ec612dd..374b493ed08 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreen.kt @@ -5,11 +5,9 @@ import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkHorizontally -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -37,9 +35,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.ColorPainter @@ -258,7 +254,7 @@ private fun CartBodyWithItems( key = { item -> item.id.itemNumber } ) { item -> ProductItem( - modifier = Modifier, + modifier = Modifier.animateItem(), item = item, canRemoveItems = areItemsRemovable, onUIEvent = onUIEvent, @@ -402,117 +398,91 @@ private fun ProductItem( canRemoveItems: Boolean, onUIEvent: (WooPosCartUIEvent) -> Unit, ) { - var hasAnimationStarted by remember { mutableStateOf(item.isAppearanceAnimationPlayed) } - LaunchedEffect(Unit) { - hasAnimationStarted = true - } - - val cardElevation = 6.dp - val elevation by animateDpAsState( - targetValue = if (hasAnimationStarted) cardElevation else 0.dp, - animationSpec = tween(durationMillis = 150, delayMillis = 250), - label = "elevation" - ) - val itemContentDescription = stringResource( id = R.string.woopos_cart_item_content_description, item.name, item.price ) - LaunchedEffect(elevation) { - if (elevation == cardElevation) { - onUIEvent(WooPosCartUIEvent.OnCartItemAppearanceAnimationPlayed(item)) - } - } - - AnimatedVisibility( - visible = hasAnimationStarted, - enter = expandVertically( - animationSpec = tween(durationMillis = 200) - ), - exit = shrinkVertically() + WooPosCard( + modifier = modifier + .height(96.dp) + .semantics { contentDescription = itemContentDescription }, + elevation = 6.dp, + shadowType = ShadowType.Soft, + shape = RoundedCornerShape(8.dp), ) { - WooPosCard( - modifier = modifier - .height(96.dp) - .semantics { contentDescription = itemContentDescription }, - elevation = elevation, - shadowType = ShadowType.Soft, - shape = RoundedCornerShape(8.dp), + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(item.imageUrl) - .crossfade(true) - .build(), - fallback = ColorPainter(WooPosTheme.colors.loadingSkeleton), - error = ColorPainter(WooPosTheme.colors.loadingSkeleton), - placeholder = ColorPainter(WooPosTheme.colors.loadingSkeleton), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(96.dp) - ) + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(item.imageUrl) + .crossfade(true) + .build(), + fallback = ColorPainter(WooPosTheme.colors.loadingSkeleton), + error = ColorPainter(WooPosTheme.colors.loadingSkeleton), + placeholder = ColorPainter(WooPosTheme.colors.loadingSkeleton), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(96.dp) + ) - Spacer(modifier = Modifier.width(16.dp.toAdaptivePadding())) + Spacer(modifier = Modifier.width(16.dp.toAdaptivePadding())) - Column( - modifier = Modifier.weight(1f) - ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = item.name, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.clearAndSetSemantics { } + ) + Spacer(modifier = Modifier.height(4.dp.toAdaptivePadding())) + if (item.description.isNotNullOrEmpty()) { Text( - text = item.name, + text = item.description!!, style = MaterialTheme.typography.body1, - fontWeight = FontWeight.Bold, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, - modifier = Modifier.clearAndSetSemantics { } - ) - Spacer(modifier = Modifier.height(4.dp.toAdaptivePadding())) - if (item.description.isNotNullOrEmpty()) { - Text( - text = item.description!!, - style = MaterialTheme.typography.body1, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colors.secondaryVariant, - modifier = Modifier.clearAndSetSemantics { } - ) - Spacer(modifier = Modifier.height(4.dp.toAdaptivePadding())) - } - Text( - text = item.price, - style = MaterialTheme.typography.body1, color = MaterialTheme.colors.secondaryVariant, modifier = Modifier.clearAndSetSemantics { } ) + Spacer(modifier = Modifier.height(4.dp.toAdaptivePadding())) } + Text( + text = item.price, + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.secondaryVariant, + modifier = Modifier.clearAndSetSemantics { } + ) + } - if (canRemoveItems) { - Spacer(modifier = Modifier.width(8.dp.toAdaptivePadding())) + if (canRemoveItems) { + Spacer(modifier = Modifier.width(8.dp.toAdaptivePadding())) - val removeButtonContentDescription = stringResource( - id = R.string.woopos_remove_item_button_from_cart_content_description, - item.name + val removeButtonContentDescription = stringResource( + id = R.string.woopos_remove_item_button_from_cart_content_description, + item.name + ) + IconButton( + onClick = { onUIEvent(WooPosCartUIEvent.ItemRemovedFromCart(item)) }, + modifier = Modifier + .size(32.dp) + .semantics { contentDescription = removeButtonContentDescription } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_pos_remove_cart_item), + tint = MaterialTheme.colors.onBackground, + contentDescription = null, ) - IconButton( - onClick = { onUIEvent(WooPosCartUIEvent.ItemRemovedFromCart(item)) }, - modifier = Modifier - .size(32.dp) - .semantics { contentDescription = removeButtonContentDescription } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_pos_remove_cart_item), - tint = MaterialTheme.colors.onBackground, - contentDescription = null, - ) - } } - Spacer(modifier = Modifier.width(16.dp.toAdaptivePadding())) } + Spacer(modifier = Modifier.width(16.dp.toAdaptivePadding())) } } } @@ -542,7 +512,6 @@ fun WooPosCartScreenProductsPreview(modifier: Modifier = Modifier) { "VW California VW California, VW California,VW California", description = "test description", price = "€50,000", - isAppearanceAnimationPlayed = true, productType = ProductType.Simple, ), WooPosCartState.Body.WithItems.Item( @@ -556,7 +525,6 @@ fun WooPosCartScreenProductsPreview(modifier: Modifier = Modifier) { description = "test description test description test description test description" + " test description test description test description test description test description", price = "$150,000", - isAppearanceAnimationPlayed = true, productType = ProductType.Simple, ), WooPosCartState.Body.WithItems.Item( @@ -569,7 +537,6 @@ fun WooPosCartScreenProductsPreview(modifier: Modifier = Modifier) { name = "VW California", description = "", price = "€250,000", - isAppearanceAnimationPlayed = true, productType = ProductType.Simple, ) ) @@ -605,7 +572,6 @@ fun WooPosCartScreenCheckoutPreview(modifier: Modifier = Modifier) { name = "VW California", description = null, price = "€50,000", - isAppearanceAnimationPlayed = true, productType = ProductType.Simple, ), WooPosCartState.Body.WithItems.Item( @@ -618,7 +584,6 @@ fun WooPosCartScreenCheckoutPreview(modifier: Modifier = Modifier) { name = "VW California", description = null, price = "$150,000", - isAppearanceAnimationPlayed = true, productType = ProductType.Simple, ), WooPosCartState.Body.WithItems.Item( @@ -631,7 +596,6 @@ fun WooPosCartScreenCheckoutPreview(modifier: Modifier = Modifier) { name = "VW California", description = null, price = "€250,000", - isAppearanceAnimationPlayed = true, productType = ProductType.Simple, ) ) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartState.kt index 8bfdd1b1fa8..986c1ede4de 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartState.kt @@ -30,7 +30,6 @@ data class WooPosCartState( val price: String, val description: String?, val imageUrl: String?, - val isAppearanceAnimationPlayed: Boolean, val productType: ProductType, ) : Parcelable { @Parcelize diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartUIEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartUIEvent.kt index f764c0698d4..6ac272a36ea 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartUIEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartUIEvent.kt @@ -5,5 +5,4 @@ sealed class WooPosCartUIEvent { data class ItemRemovedFromCart(val item: WooPosCartState.Body.WithItems.Item) : WooPosCartUIEvent() data object ClearAllClicked : WooPosCartUIEvent() data object BackClicked : WooPosCartUIEvent() - data class OnCartItemAppearanceAnimationPlayed(val item: WooPosCartState.Body.WithItems.Item) : WooPosCartUIEvent() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt index 4b5bc124380..003c30cd68e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt @@ -93,18 +93,6 @@ class WooPosCartViewModel @Inject constructor( body = WooPosCartState.Body.Empty ) } - - is WooPosCartUIEvent.OnCartItemAppearanceAnimationPlayed -> { - val currentState = _state.value - val currentStateBody = currentState.body as? WooPosCartState.Body.WithItems ?: return - _state.value = currentState.copy( - body = currentStateBody.copy( - itemsInCart = currentState.body.itemsInCart.map { - if (it.id == event.item.id) it.copy(isAppearanceAnimationPlayed = true) else it - } - ) - ) - } } } @@ -268,7 +256,6 @@ class WooPosCartViewModel @Inject constructor( description = null, price = formatPrice(price), imageUrl = firstImageUrl, - isAppearanceAnimationPlayed = false, productType = ProductType.Simple, ) @@ -286,7 +273,6 @@ class WooPosCartViewModel @Inject constructor( description = getNameForPOS(product, resourceProvider), price = formatPrice(price), imageUrl = image?.source, - isAppearanceAnimationPlayed = false, productType = ProductType.Variation, ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt index 6813ad66716..3aa724e93d4 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt @@ -169,7 +169,6 @@ class WooPosCartViewModelTest { name = product.name, price = "10.0$", imageUrl = product.firstImageUrl, - isAppearanceAnimationPlayed = false, productType = ProductType.Simple, description = null, ) @@ -321,7 +320,6 @@ class WooPosCartViewModelTest { name = product1.name, price = "10.0$", imageUrl = product1.firstImageUrl, - isAppearanceAnimationPlayed = false, productType = ProductType.Simple, description = null, ) @@ -423,7 +421,6 @@ class WooPosCartViewModelTest { name = product.name, price = "10.0$", imageUrl = product.firstImageUrl, - isAppearanceAnimationPlayed = false, productType = ProductType.Simple, description = null, ) @@ -499,40 +496,6 @@ class WooPosCartViewModelTest { verify(analyticsTracker).track(WooPosAnalyticsEvent.Event.ItemAddedToCart) } - @Test - fun `given non-empty cart, when OnCartItemAppearanceAnimationPlayed is received, then should update UI`() = runTest { - // GIVEN - val product = ProductTestUtils.generateProduct( - productId = 23L, - productName = "title", - amount = "10.0" - ).copy(firstImageUrl = "url") - - val parentToChildrenEventsMutableFlow = MutableSharedFlow() - whenever(parentToChildrenEventReceiver.events).thenReturn(parentToChildrenEventsMutableFlow) - whenever(getProductById(eq(product.remoteId))).thenReturn(product) - val sut = createSut() - val states = sut.state.captureValues() - - parentToChildrenEventsMutableFlow.emit( - ParentToChildrenEvent.ItemClickedInProductSelector( - WooPosItemsViewModel.ItemClickedData.SimpleProduct( - id = product.remoteId - ) - ) - ) - - // WHEN - val firstItem = (states.last().body as WooPosCartState.Body.WithItems).itemsInCart.first() - val updatedItem = firstItem.copy(isAppearanceAnimationPlayed = true) - sut.onUIEvent(WooPosCartUIEvent.OnCartItemAppearanceAnimationPlayed(updatedItem)) - - // THEN - val finalState = states.last() - val finalItem = (finalState.body as WooPosCartState.Body.WithItems).itemsInCart.first() - assertThat(finalItem.isAppearanceAnimationPlayed).isTrue - } - @Test fun `when simple product added to cart, then should track analytics event with product type simple`() = runTest { // GIVEN