You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
343 lines
13 KiB
343 lines
13 KiB
package com.example.jetsnack.ui.home.cart |
|
|
|
import androidx.compose.animation.ExperimentalAnimationApi |
|
import androidx.compose.animation.core.animateDpAsState |
|
import androidx.compose.animation.core.animateFloatAsState |
|
import androidx.compose.foundation.background |
|
import androidx.compose.foundation.layout.* |
|
import androidx.compose.foundation.lazy.LazyColumn |
|
import androidx.compose.foundation.lazy.items |
|
import androidx.compose.foundation.shape.CircleShape |
|
import androidx.compose.material.Icon |
|
import androidx.compose.material.MaterialTheme |
|
import androidx.compose.material.Surface |
|
import androidx.compose.material.Text |
|
import androidx.compose.material.icons.Icons |
|
import androidx.compose.runtime.Composable |
|
import androidx.compose.runtime.collectAsState |
|
import androidx.compose.runtime.getValue |
|
import androidx.compose.runtime.remember |
|
import androidx.compose.ui.Alignment |
|
import androidx.compose.ui.Modifier |
|
import androidx.compose.ui.graphics.RectangleShape |
|
import androidx.compose.ui.graphics.graphicsLayer |
|
import androidx.compose.ui.layout.LastBaseline |
|
import androidx.compose.ui.text.style.TextAlign |
|
import androidx.compose.ui.text.style.TextOverflow |
|
import androidx.compose.ui.unit.Dp |
|
import androidx.compose.ui.unit.dp |
|
import com.example.jetsnack.* |
|
import com.example.jetsnack.model.OrderLine |
|
import com.example.jetsnack.model.SnackCollection |
|
import com.example.jetsnack.model.SnackRepo |
|
import com.example.jetsnack.ui.components.JetsnackButton |
|
import com.example.jetsnack.ui.components.JetsnackDivider |
|
import com.example.jetsnack.ui.components.JetsnackSurface |
|
import com.example.jetsnack.ui.components.SnackCollection |
|
import com.example.jetsnack.ui.home.DestinationBar |
|
import com.example.jetsnack.ui.theme.AlphaNearOpaque |
|
import com.example.jetsnack.ui.theme.JetsnackTheme |
|
import com.example.jetsnack.ui.utils.formatPrice |
|
|
|
|
|
@Composable |
|
fun Cart( |
|
onSnackClick: (Long) -> Unit, |
|
modifier: Modifier = Modifier, |
|
viewModel: CartViewModel = provideCartViewModel() |
|
) { |
|
val orderLines by viewModel.collectOrderLinesAsState(viewModel.orderLines) |
|
val inspiredByCart = remember { SnackRepo.getInspiredByCart() } |
|
Cart( |
|
orderLines = orderLines, |
|
removeSnack = viewModel::removeSnack, |
|
increaseItemCount = viewModel::increaseSnackCount, |
|
decreaseItemCount = viewModel::decreaseSnackCount, |
|
inspiredByCart = inspiredByCart, |
|
onSnackClick = onSnackClick, |
|
modifier = modifier |
|
) |
|
} |
|
|
|
@Composable |
|
expect fun provideCartViewModel(): CartViewModel |
|
|
|
/** |
|
* Android uses ConstraintLayout which is android-only at the moment. |
|
* So we provide an alternative implementation of `ActualCartItem` for other platforms. |
|
*/ |
|
@Composable |
|
expect fun ActualCartItem( |
|
orderLine: OrderLine, |
|
removeSnack: (Long) -> Unit, |
|
increaseItemCount: (Long) -> Unit, |
|
decreaseItemCount: (Long) -> Unit, |
|
onSnackClick: (Long) -> Unit, |
|
modifier: Modifier = Modifier |
|
) |
|
|
|
@Composable |
|
fun Cart( |
|
orderLines: List<OrderLine>, |
|
removeSnack: (Long) -> Unit, |
|
increaseItemCount: (Long) -> Unit, |
|
decreaseItemCount: (Long) -> Unit, |
|
inspiredByCart: SnackCollection, |
|
onSnackClick: (Long) -> Unit, |
|
modifier: Modifier = Modifier |
|
) { |
|
JetsnackSurface(modifier = modifier.fillMaxSize()) { |
|
Box { |
|
CartContent( |
|
orderLines = orderLines, |
|
removeSnack = removeSnack, |
|
increaseItemCount = increaseItemCount, |
|
decreaseItemCount = decreaseItemCount, |
|
inspiredByCart = inspiredByCart, |
|
onSnackClick = onSnackClick, |
|
modifier = Modifier.align(Alignment.TopCenter) |
|
) |
|
DestinationBar(modifier = Modifier.align(Alignment.TopCenter)) |
|
CheckoutBar(modifier = Modifier.align(Alignment.BottomCenter)) |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
expect fun rememberQuantityString(res: Int, qty: Int, vararg args: Any): String |
|
|
|
@Composable |
|
expect fun getCartContentInsets(): WindowInsets |
|
|
|
@OptIn(ExperimentalAnimationApi::class) |
|
@Composable |
|
private fun CartContent( |
|
orderLines: List<OrderLine>, |
|
removeSnack: (Long) -> Unit, |
|
increaseItemCount: (Long) -> Unit, |
|
decreaseItemCount: (Long) -> Unit, |
|
inspiredByCart: SnackCollection, |
|
onSnackClick: (Long) -> Unit, |
|
modifier: Modifier = Modifier |
|
) { |
|
val snackCountFormattedString = rememberQuantityString( |
|
MppR.plurals.cart_order_count, orderLines.size, orderLines.size |
|
) |
|
LazyColumn(modifier) { |
|
item { |
|
Spacer(Modifier.windowInsetsTopHeight(getCartContentInsets())) |
|
Text( |
|
text = stringResource(MppR.string.cart_order_header, snackCountFormattedString), |
|
style = MaterialTheme.typography.h6, |
|
color = JetsnackTheme.colors.brand, |
|
maxLines = 1, |
|
overflow = TextOverflow.Ellipsis, |
|
modifier = Modifier |
|
.heightIn(min = 56.dp) |
|
.padding(horizontal = 24.dp, vertical = 4.dp) |
|
.wrapContentHeight() |
|
) |
|
} |
|
items(orderLines) { orderLine -> |
|
SwipeDismissItem( |
|
background = { offsetX -> |
|
/*Background color changes from light gray to red when the |
|
swipe to delete with exceeds 160.dp*/ |
|
val backgroundColor = if (offsetX < -160.dp) { |
|
JetsnackTheme.colors.error |
|
} else { |
|
JetsnackTheme.colors.uiFloated |
|
} |
|
Column( |
|
modifier = Modifier |
|
.fillMaxWidth() |
|
.fillMaxHeight() |
|
.background(backgroundColor), |
|
horizontalAlignment = Alignment.End, |
|
verticalArrangement = Arrangement.Center |
|
) { |
|
// Set 4.dp padding only if offset is bigger than 160.dp |
|
val padding: Dp by animateDpAsState( |
|
if (offsetX > -160.dp) 4.dp else 0.dp |
|
) |
|
Box( |
|
Modifier |
|
.width(offsetX * -1) |
|
.padding(padding) |
|
) { |
|
// Height equals to width removing padding |
|
val height = (offsetX + 8.dp) * -1 |
|
Surface( |
|
modifier = Modifier |
|
.fillMaxWidth() |
|
.height(height) |
|
.align(Alignment.Center), |
|
shape = CircleShape, |
|
color = JetsnackTheme.colors.error |
|
) { |
|
Box( |
|
modifier = Modifier.fillMaxSize(), |
|
contentAlignment = Alignment.Center |
|
) { |
|
// Icon must be visible while in this width range |
|
if (offsetX < -40.dp && offsetX > -152.dp) { |
|
// Icon alpha decreases as it is about to disappear |
|
val iconAlpha: Float by animateFloatAsState( |
|
if (offsetX < -120.dp) 0.5f else 1f |
|
) |
|
|
|
Icon( |
|
imageVector = Icons.Filled.DeleteForever, |
|
modifier = Modifier |
|
.size(16.dp) |
|
.graphicsLayer(alpha = iconAlpha), |
|
tint = JetsnackTheme.colors.uiBackground, |
|
contentDescription = null, |
|
) |
|
} |
|
/*Text opacity increases as the text is supposed to appear in |
|
the screen*/ |
|
val textAlpha by animateFloatAsState( |
|
if (offsetX > -144.dp) 0.5f else 1f |
|
) |
|
if (offsetX < -120.dp) { |
|
Text( |
|
text = stringResource(id = MppR.string.remove_item), |
|
style = MaterialTheme.typography.subtitle1, |
|
color = JetsnackTheme.colors.uiBackground, |
|
textAlign = TextAlign.Center, |
|
modifier = Modifier |
|
.graphicsLayer( |
|
alpha = textAlpha |
|
) |
|
) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
}, |
|
) { |
|
ActualCartItem( |
|
orderLine = orderLine, |
|
removeSnack = removeSnack, |
|
increaseItemCount = increaseItemCount, |
|
decreaseItemCount = decreaseItemCount, |
|
onSnackClick = onSnackClick |
|
) |
|
} |
|
} |
|
item { |
|
SummaryItem( |
|
subtotal = orderLines.map { it.snack.price * it.count }.sum(), |
|
shippingCosts = 369 |
|
) |
|
} |
|
item { |
|
SnackCollection( |
|
snackCollection = inspiredByCart, |
|
onSnackClick = onSnackClick, |
|
highlight = false |
|
) |
|
Spacer(Modifier.height(56.dp)) |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
fun SummaryItem( |
|
subtotal: Long, |
|
shippingCosts: Long, |
|
modifier: Modifier = Modifier |
|
) { |
|
Column(modifier) { |
|
Text( |
|
text = stringResource(MppR.string.cart_summary_header), |
|
style = MaterialTheme.typography.h6, |
|
color = JetsnackTheme.colors.brand, |
|
maxLines = 1, |
|
overflow = TextOverflow.Ellipsis, |
|
modifier = Modifier |
|
.padding(horizontal = 24.dp) |
|
.heightIn(min = 56.dp) |
|
.wrapContentHeight() |
|
) |
|
Row(modifier = Modifier.padding(horizontal = 24.dp)) { |
|
Text( |
|
text = stringResource(MppR.string.cart_subtotal_label), |
|
style = MaterialTheme.typography.body1, |
|
modifier = Modifier |
|
.weight(1f) |
|
.wrapContentWidth(Alignment.Start) |
|
.alignBy(LastBaseline) |
|
) |
|
Text( |
|
text = formatPrice(subtotal), |
|
style = MaterialTheme.typography.body1, |
|
modifier = Modifier.alignBy(LastBaseline) |
|
) |
|
} |
|
Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { |
|
Text( |
|
text = stringResource(MppR.string.cart_shipping_label), |
|
style = MaterialTheme.typography.body1, |
|
modifier = Modifier |
|
.weight(1f) |
|
.wrapContentWidth(Alignment.Start) |
|
.alignBy(LastBaseline) |
|
) |
|
Text( |
|
text = formatPrice(shippingCosts), |
|
style = MaterialTheme.typography.body1, |
|
modifier = Modifier.alignBy(LastBaseline) |
|
) |
|
} |
|
Spacer(modifier = Modifier.height(8.dp)) |
|
JetsnackDivider() |
|
Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { |
|
Text( |
|
text = stringResource(MppR.string.cart_total_label), |
|
style = MaterialTheme.typography.body1, |
|
modifier = Modifier |
|
.weight(1f) |
|
.padding(end = 16.dp) |
|
.wrapContentWidth(Alignment.End) |
|
.alignBy(LastBaseline) |
|
) |
|
Text( |
|
text = formatPrice(subtotal + shippingCosts), |
|
style = MaterialTheme.typography.subtitle1, |
|
modifier = Modifier.alignBy(LastBaseline) |
|
) |
|
} |
|
JetsnackDivider() |
|
} |
|
} |
|
|
|
@Composable |
|
private fun CheckoutBar(modifier: Modifier = Modifier) { |
|
Column( |
|
modifier.background( |
|
JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque) |
|
) |
|
) { |
|
JetsnackDivider() |
|
Row { |
|
Spacer(Modifier.weight(1f)) |
|
JetsnackButton( |
|
onClick = { /* todo */ }, |
|
shape = RectangleShape, |
|
modifier = Modifier |
|
.padding(horizontal = 12.dp, vertical = 8.dp) |
|
.weight(1f) |
|
) { |
|
Text( |
|
text = stringResource(id = MppR.string.cart_checkout), |
|
modifier = Modifier.fillMaxWidth(), |
|
textAlign = TextAlign.Left, |
|
maxLines = 1 |
|
) |
|
} |
|
} |
|
} |
|
} |