From 1f91dd0b9283c3cd8f37c66cc308aa1cacb1d65c Mon Sep 17 00:00:00 2001 From: Iftakhar Husan <AC5636@student.jamk.fi> Date: Sat, 6 Jan 2024 20:33:40 +0200 Subject: [PATCH] Add siwpe gesture to edit and delete shopping item --- E13RoomShoppingList/.idea/gradle.xml | 1 + E13RoomShoppingList/.idea/misc.xml | 1 - .../e13roomshoppinglist/ShoppingListScreen.kt | 185 ++++++++++++++---- 3 files changed, 149 insertions(+), 38 deletions(-) diff --git a/E13RoomShoppingList/.idea/gradle.xml b/E13RoomShoppingList/.idea/gradle.xml index 32522c1..0897082 100644 --- a/E13RoomShoppingList/.idea/gradle.xml +++ b/E13RoomShoppingList/.idea/gradle.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> + <component name="GradleMigrationSettings" migrationVersion="1" /> <component name="GradleSettings"> <option name="linkedExternalProjectsSettings"> <GradleProjectSettings> diff --git a/E13RoomShoppingList/.idea/misc.xml b/E13RoomShoppingList/.idea/misc.xml index 0ad17cb..8978d23 100644 --- a/E13RoomShoppingList/.idea/misc.xml +++ b/E13RoomShoppingList/.idea/misc.xml @@ -1,4 +1,3 @@ -<?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> diff --git a/E13RoomShoppingList/app/src/main/java/com/example/e13roomshoppinglist/ShoppingListScreen.kt b/E13RoomShoppingList/app/src/main/java/com/example/e13roomshoppinglist/ShoppingListScreen.kt index da2ae25..43b113a 100644 --- a/E13RoomShoppingList/app/src/main/java/com/example/e13roomshoppinglist/ShoppingListScreen.kt +++ b/E13RoomShoppingList/app/src/main/java/com/example/e13roomshoppinglist/ShoppingListScreen.kt @@ -1,9 +1,14 @@ package com.example.e13roomshoppinglist +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -14,31 +19,46 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TextField +import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ShoppingListScreen( state: ShoppingListState, @@ -50,7 +70,10 @@ fun ShoppingListScreen( FloatingActionButton(onClick = { onEvent(ShoppingListEvent.OpenAddingNewShoppingListItemDialog) }) { - Icon(imageVector = Icons.Default.Add, contentDescription = "Add new shopping list item") + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add new shopping list item" + ) } }, modifier = Modifier.padding(16.dp) @@ -84,45 +107,86 @@ fun ShoppingListScreen( } } } - Divider(thickness = 2.dp, color = Color.LightGray, modifier = Modifier.padding(bottom = 8.dp)) + Divider( + thickness = 2.dp, + color = Color.LightGray, + modifier = Modifier.padding(bottom = 8.dp) + ) LazyColumn( contentPadding = padding, modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - items(state.shoppingListItems) { shoppingItem -> - Row(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = "${shoppingItem.name} - 🧮 ${shoppingItem.count}", - fontSize = 18.sp - ) - Text(text = "💶 ${shoppingItem.price}", fontSize = 16.sp) - } + items(state.shoppingListItems, key = {shoppingListItem -> if (shoppingListItem.id!==null) shoppingListItem.id else 0}) { shoppingItem -> + val dismissState = rememberDismissState( + confirmValueChange = { + when (it) { + DismissValue.DismissedToEnd -> { + onEvent(ShoppingListEvent.DeleteShoppingListItem(shoppingItem)) + onShowToast("Deleted ${shoppingItem.name} shopping list item via swipe gesture") + true + } + DismissValue.DismissedToStart -> { + onEvent( + ShoppingListEvent.OpenEditingShoppingListItem( + shoppingItem + ) + ) + onShowToast("Opened ${shoppingItem.name} shopping list item edit dialog via swipe gesture") + false + } + else -> { + onShowToast("Swipe gesture aborted") + false + } + } + }) + SwipeToDismiss( + state = dismissState, + background = { SwipeBackground(dismissState = dismissState) }, + dismissContent = { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .background(Color.LightGray) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "${shoppingItem.name} - 🧮 ${shoppingItem.count}", + fontSize = 18.sp + ) + Text(text = "💶 ${shoppingItem.price}", fontSize = 16.sp) + } - IconButton(onClick = { - onEvent(ShoppingListEvent.DeleteShoppingListItem(shoppingItem)) - .also { - onShowToast("Deleted shopping list item ${shoppingItem.name}") + IconButton(onClick = { + onEvent(ShoppingListEvent.DeleteShoppingListItem(shoppingItem)) + .also { + onShowToast("Deleted shopping list item ${shoppingItem.name}") + } + }) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete Shopping List Item" + ) } - }) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete Shopping List Item" - ) - } - IconButton(onClick = { - onEvent(ShoppingListEvent.OpenEditingShoppingListItem(shoppingItem)) - }) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = "Edit Shopping List Item" - ) - } - } - Divider(thickness = 1.dp, color = Color.LightGray) + IconButton(onClick = { + onEvent( + ShoppingListEvent.OpenEditingShoppingListItem( + shoppingItem + ) + ) + }) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit Shopping List Item" + ) + } + } + }) } item { Row( @@ -131,7 +195,11 @@ fun ShoppingListScreen( .height(70.dp), verticalAlignment = Alignment.CenterVertically ) { - Text(text = "Empty item to fill up room so that the last item's edit and delete button does not go under FAB.", fontSize = 12.sp, color = Color.Gray) + Text( + text = "Empty item to fill up room so that the last item's edit and delete button does not go under FAB.", + fontSize = 12.sp, + color = Color.Gray + ) } } } @@ -200,15 +268,15 @@ fun AddShoppingListItemDialog( }, confirmButton = { Button(onClick = { - if (state.newShoppingListItem.name.length<3){ + if (state.newShoppingListItem.name.length < 3) { onShowToast("Item Name is too short!") return@Button } - if (state.newShoppingListItem.count.toIntOrNull()===null){ + if (state.newShoppingListItem.count.toIntOrNull() === null) { onShowToast("Invalid value for Count! Failed to convert value to Int.") return@Button } - if (state.newShoppingListItem.price.toFloatOrNull()===null){ + if (state.newShoppingListItem.price.toFloatOrNull() === null) { onShowToast("Invalid value for Price! Failed to convert value to Float.") return@Button } @@ -216,7 +284,11 @@ fun AddShoppingListItemDialog( if (state.isAddingNewShoppingListItem) { onEvent(ShoppingListEvent.InsertNewShoppingListItemToDb).also { onShowToast("Added new item to shopping list!") } } else if (state.isEditingShoppingListItem) { - onEvent(ShoppingListEvent.UpdateCurrentlyEditingShoppingListItem).also { onShowToast("Updated shopping list item ${state.currentlyEditingShoppingListItem?.name}!") } + onEvent(ShoppingListEvent.UpdateCurrentlyEditingShoppingListItem).also { + onShowToast( + "Updated shopping list item ${state.currentlyEditingShoppingListItem?.name}!" + ) + } } }) { Text(text = if (state.isEditingShoppingListItem) "Update" else "Insert") @@ -234,4 +306,43 @@ fun AddShoppingListItemDialog( } } ) -} \ No newline at end of file +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun SwipeBackground(dismissState: DismissState) { + val direction = dismissState.dismissDirection ?: return + + val color by animateColorAsState( + when (dismissState.targetValue) { + DismissValue.Default -> Color.Transparent + DismissValue.DismissedToEnd -> Color.Red + DismissValue.DismissedToStart -> Color.Green + }, label = "" + ) + val alignment = when (direction) { + DismissDirection.StartToEnd -> Alignment.CenterStart + DismissDirection.EndToStart -> Alignment.CenterEnd + } + val icon = when (direction) { + DismissDirection.StartToEnd -> Icons.Filled.Delete + DismissDirection.EndToStart -> Icons.Filled.Edit + } + val scale by animateFloatAsState( + if (dismissState.targetValue == DismissValue.Default) 0.8f else 1.25f, label = "" + ) + + Box( + Modifier + .fillMaxSize() + .background(color) + .padding(horizontal = 20.dp), + contentAlignment = alignment + ) { + Icon( + icon, + contentDescription = "Delete", + modifier = Modifier.scale(scale) + ) + } +} -- GitLab