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

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
)
}
}
}
}