From 2ccfafc051b44902e773501adf08620c32487ac2 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 22 Nov 2024 17:56:38 +0100 Subject: [PATCH 01/15] feat: Use `SwipeToDismiss` library to be able to delete Transfers --- app/build.gradle.kts | 2 + .../components/transfer/TransferItemList.kt | 89 +++++++++++++++++-- .../TransfersListWithExpiredBottomSheet.kt | 3 + .../ui/screen/main/received/ReceivedScreen.kt | 4 + .../ui/screen/main/sent/SentScreen.kt | 4 + .../main/transfers/TransfersViewModel.kt | 10 +++ gradle/libs.versions.toml | 4 +- 7 files changed, 109 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f663a9326..ea0bbffd7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -102,6 +102,8 @@ dependencies { implementation(libs.navigation.compose) implementation(libs.androidx.constraintlayout.compose) + implementation(libs.androidx.material) + implementation(libs.androidx.adaptive) implementation(libs.androidx.adaptive.layout) implementation(libs.androidx.adaptive.navigation) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index d16879a50..3d1c6e243 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -17,29 +17,42 @@ */ package com.infomaniak.swisstransfer.ui.components.transfer -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Surface import androidx.compose.material3.Text 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.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.TransferUi import com.infomaniak.multiplatform_swisstransfer.common.models.TransferDirection import com.infomaniak.swisstransfer.R import com.infomaniak.swisstransfer.ui.previewparameter.TransferUiListPreviewParameter +import com.infomaniak.swisstransfer.ui.theme.CustomShapes import com.infomaniak.swisstransfer.ui.theme.Margin import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark +@OptIn(ExperimentalMaterialApi::class) @Composable fun TransferItemList( modifier: Modifier = Modifier, direction: TransferDirection, getSelectedTransferUuid: () -> String?, getTransfers: () -> List, + onSwiped: (String) -> Unit, onClick: (TransferUi) -> Unit, ) { @@ -62,11 +75,74 @@ fun TransferItemList( key = { getTransfers()[it].uuid }, contentType = { getTransfers()[it] }, itemContent = { + val transfer = getTransfers()[it] - TransferItem( - transfer = transfer, - isSelected = { selectedTransferUuid == transfer.uuid }, - onClick = { onClick(transfer) }, + + val dismissState = rememberDismissState( + confirmStateChange = { dismissValue -> + val shouldDismiss = + dismissValue == DismissValue.DismissedToEnd || dismissValue == DismissValue.DismissedToStart + if (shouldDismiss) onSwiped(transfer.uuid) // TODO: We should probably add an animation here + shouldDismiss + }, + ) + + // TODO: the "Using a material import while also using the material3 library" + SwipeToDismiss( + state = dismissState, + directions = setOf(DismissDirection.EndToStart), + background = { + val dismissDirection = dismissState.dismissDirection ?: return@SwipeToDismiss + val color by animateColorAsState( + targetValue = when (dismissState.targetValue) { + DismissValue.Default -> Color.LightGray + else -> SwissTransferTheme.materialColors.error + }, + label = "Color animation", + ) + val alignment = when (dismissDirection) { + DismissDirection.StartToEnd -> Alignment.CenterStart + DismissDirection.EndToStart -> Alignment.CenterEnd + } + val scale by animateFloatAsState( + targetValue = if (dismissState.targetValue == DismissValue.Default) 0.75f else 1.0f, + label = "Scale animation", + ) + + Card( + modifier = Modifier.fillMaxSize(), + shape = CustomShapes.SMALL, + backgroundColor = color, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + contentAlignment = alignment, + ) { + Icon( + imageVector = Icons.Default.Delete, + modifier = Modifier.scale(scale), + contentDescription = null, + ) + } + } + }, + dismissContent = { + Card( + elevation = animateDpAsState( + targetValue = if (dismissState.dismissDirection != null) 4.dp else 0.dp, + label = "Elevation animation", + ).value, + shape = CustomShapes.SMALL, + ) { + TransferItem( + transfer = transfer, + isSelected = { selectedTransferUuid == transfer.uuid }, + onClick = { onClick(transfer) }, + ) + } + }, ) }, ) @@ -82,6 +158,7 @@ private fun Preview(@PreviewParameter(TransferUiListPreviewParameter::class) tra direction = TransferDirection.SENT, getSelectedTransferUuid = { null }, getTransfers = { transfers }, + onSwiped = {}, onClick = {}, ) } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransfersListWithExpiredBottomSheet.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransfersListWithExpiredBottomSheet.kt index 65355976a..63a181ece 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransfersListWithExpiredBottomSheet.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransfersListWithExpiredBottomSheet.kt @@ -42,6 +42,7 @@ fun TransfersListWithExpiredBottomSheet( navigateToDetails: (transferUuid: String) -> Unit, getSelectedTransferUuid: () -> String?, getTransfers: () -> List, + onSwiped: (String) -> Unit, ) { var isExpirySheetVisible: Boolean by rememberSaveable { mutableStateOf(false) } @@ -53,6 +54,7 @@ fun TransfersListWithExpiredBottomSheet( direction = direction, getSelectedTransferUuid = getSelectedTransferUuid, getTransfers = getTransfers, + onSwiped = onSwiped, onClick = { transfer -> when { transfer.expiresInDays < 0 -> { @@ -93,6 +95,7 @@ private fun Preview(@PreviewParameter(TransferUiListPreviewParameter::class) tra navigateToDetails = {}, getSelectedTransferUuid = { null }, getTransfers = { transfers }, + onSwiped = {}, ) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/received/ReceivedScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/received/ReceivedScreen.kt index ea3835177..2b0a985c5 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/received/ReceivedScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/received/ReceivedScreen.kt @@ -52,6 +52,7 @@ fun ReceivedScreen( navigateToDetails = navigateToDetails, getSelectedTransferUuid = getSelectedTransferUuid, getTransfers = { transfers!! }, + onSwiped = transfersViewModel::deleteSwipedTransfer, ) } } @@ -61,6 +62,7 @@ private fun ReceivedScreen( navigateToDetails: (transferUuid: String) -> Unit, getSelectedTransferUuid: () -> String?, getTransfers: () -> List, + onSwiped: (String) -> Unit, ) { val areTransfersEmpty by remember { derivedStateOf { getTransfers().isEmpty() } } @@ -80,6 +82,7 @@ private fun ReceivedScreen( navigateToDetails = navigateToDetails, getSelectedTransferUuid = getSelectedTransferUuid, getTransfers = getTransfers, + onSwiped = onSwiped, ) } } @@ -94,6 +97,7 @@ private fun Preview() { navigateToDetails = {}, getSelectedTransferUuid = { null }, getTransfers = { emptyList() }, + onSwiped = {}, ) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/sent/SentScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/sent/SentScreen.kt index 096d1eb6a..2d8b3d08e 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/sent/SentScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/sent/SentScreen.kt @@ -59,6 +59,7 @@ fun SentScreen( navigateToDetails = navigateToDetails, getSelectedTransferUuid = getSelectedTransferUuid, getTransfers = { transfers!! }, + onSwiped = transfersViewModel::deleteSwipedTransfer, ) } } @@ -68,6 +69,7 @@ private fun SentScreen( navigateToDetails: (transferUuid: String) -> Unit, getSelectedTransferUuid: () -> String?, getTransfers: () -> List, + onSwiped: (String) -> Unit, ) { val areTransfersEmpty by remember { derivedStateOf { getTransfers().isEmpty() } } @@ -88,6 +90,7 @@ private fun SentScreen( navigateToDetails = navigateToDetails, getSelectedTransferUuid = getSelectedTransferUuid, getTransfers = getTransfers, + onSwiped = onSwiped, ) } } @@ -102,6 +105,7 @@ private fun Preview(@PreviewParameter(TransferUiListPreviewParameter::class) tra navigateToDetails = {}, getSelectedTransferUuid = { null }, getTransfers = { transfers }, + onSwiped = {}, ) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersViewModel.kt index 5686ebe21..10deeaea2 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersViewModel.kt @@ -59,6 +59,16 @@ class TransfersViewModel @Inject constructor( } } + fun deleteSwipedTransfer(transferUuid: String) { + viewModelScope.launch(ioDispatcher) { + runCatching { + transferManager.deleteTransfer(transferUuid) + }.onFailure { + SentryLog.e(TAG, "Failure for deleteTransfer", it) + } + } + } + companion object { private val TAG = TransfersViewModel::class.java.simpleName } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 015a032a1..d57bc4b2b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ junit = "4.13.2" junitVersion = "1.2.1" kotlin = "2.0.20" lifecycleRuntimeKtx = "2.8.7" +material = "1.8.0-alpha06" navigation = "2.8.4" qrose = "1.0.1" recaptcha = "18.6.1" @@ -32,9 +33,10 @@ androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constr androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "composeAlpha" } #TODO: To be removed when compose 1.8.0 is stable +compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "composeAlpha" } # TODO: To be removed when compose 1.8.0 is stable compose-material3 = { group = "androidx.compose.material3", name = "material3" } compose-material3-adaptative-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" } compose-ui = { group = "androidx.compose.ui", name = "ui" } From 80226b19fe26eb549f803c4eb39b4c8db4b783e7 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 12:11:43 +0100 Subject: [PATCH 02/15] refactor: Use `AnchoredDraggableState` instead since `SwipeToDismiss` is deprecated --- app/build.gradle.kts | 2 - .../ui/components/transfer/TransferItem.kt | 130 +++++++++++++----- .../components/transfer/TransferItemList.kt | 87 +----------- gradle/libs.versions.toml | 4 +- 4 files changed, 99 insertions(+), 124 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ea0bbffd7..f663a9326 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -102,8 +102,6 @@ dependencies { implementation(libs.navigation.compose) implementation(libs.androidx.constraintlayout.compose) - implementation(libs.androidx.material) - implementation(libs.androidx.adaptive) implementation(libs.androidx.adaptive.layout) implementation(libs.androidx.adaptive.navigation) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt index 308e5c981..7a17193b1 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt @@ -17,16 +17,26 @@ */ package com.infomaniak.swisstransfer.ui.components.transfer +import androidx.compose.animation.core.exponentialDecay +import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import com.infomaniak.core2.FORMAT_DATE_TITLE import com.infomaniak.core2.format import com.infomaniak.multiplatform_swisstransfer.common.ext.toDateFromSeconds @@ -43,6 +53,12 @@ import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.HumanReadableSizeUtils import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark import com.infomaniak.swisstransfer.ui.utils.isExpired +import kotlin.math.roundToInt + +enum class DragAnchors { + Start, + End, +} @OptIn(ExperimentalLayoutApi::class) @Composable @@ -66,52 +82,90 @@ fun TransferItem( null } - Card( - onClick = onClick, - colors = CardDefaults.cardColors(containerColor = SwissTransferTheme.materialColors.surfaceContainerHighest), - shape = CustomShapes.SMALL, - border = border, + val density = LocalDensity.current + val state = remember { + // AnchoredDraggableState(initialValue = DragAnchors.Start) + AnchoredDraggableState( + initialValue = DragAnchors.Start, + positionalThreshold = { it * 0.5f }, + velocityThreshold = { with(density) { 100.dp.toPx() } }, + snapAnimationSpec = tween(), + decayAnimationSpec = exponentialDecay(), + // confirmValueChange =, + // anchors = DraggableAnchors { + // DragAnchors.Start to 0.0f + // DragAnchors.End to 400.0f + // }, + ).apply { + updateAnchors( + newAnchors = DraggableAnchors { + DragAnchors.Start to 0.0f + DragAnchors.End to 400.0f + }, + ) + } + } + // val anchors = remember(density) { + // + // // val replyOffset = with(density) { 48.dp.toPx() } + // // DraggableAnchors { + // // DragAnchors.Start to 0.0f + // // DragAnchors.End to replyOffset + // // } + // } + // SideEffect { state.updateAnchors(anchors) } + + Box( + modifier = Modifier.anchoredDraggable(state, Orientation.Horizontal), ) { - Row( - modifier = Modifier.padding(Margin.Medium), - verticalAlignment = Alignment.CenterVertically, + Card( + modifier = Modifier.offset { IntOffset(x = state.requireOffset().roundToInt(), y = 0) }, + onClick = onClick, + colors = CardDefaults.cardColors(containerColor = SwissTransferTheme.materialColors.surfaceContainerHighest), + shape = CustomShapes.SMALL, + border = border, ) { + Row( + modifier = Modifier.padding(Margin.Medium), + verticalAlignment = Alignment.CenterVertically, + ) { - Column(modifier = Modifier.weight(1.0f)) { + Column(modifier = Modifier.weight(1.0f)) { - Text( - text = createdDate, - style = SwissTransferTheme.typography.bodyMedium, - color = SwissTransferTheme.colors.primaryTextColor, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - ) + Text( + text = createdDate, + style = SwissTransferTheme.typography.bodyMedium, + color = SwissTransferTheme.colors.primaryTextColor, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) - Spacer(Modifier.height(Margin.Mini)) - TextDotText( - firstText = { uploadedSize }, - secondText = { expiryText }, - optionalSecondTextColor = expiryColor, - ) + Spacer(Modifier.height(Margin.Mini)) + TextDotText( + firstText = { uploadedSize }, + secondText = { expiryText }, + optionalSecondTextColor = expiryColor, + ) - Spacer(Modifier.height(Margin.Mini)) - ContextualFlowRow( - itemCount = files.count(), - maxLines = 1, - horizontalArrangement = Arrangement.spacedBy(Margin.Mini), - overflow = ContextualFlowRowOverflow.expandIndicator { TransferFilePreview(remainingFilesCount = totalItemCount - shownItemCount) }, - ) { index -> - TransferFilePreview(file = files[index]) + Spacer(Modifier.height(Margin.Mini)) + ContextualFlowRow( + itemCount = files.count(), + maxLines = 1, + horizontalArrangement = Arrangement.spacedBy(Margin.Mini), + overflow = ContextualFlowRowOverflow.expandIndicator { TransferFilePreview(remainingFilesCount = totalItemCount - shownItemCount) }, + ) { index -> + TransferFilePreview(file = files[index]) + } } - } - Spacer(Modifier.width(Margin.Medium)) - Icon( - imageVector = AppIcons.ChevronRightThick, - contentDescription = null, - modifier = Modifier.size(Dimens.SmallIconSize), - tint = SwissTransferTheme.colors.iconColor, - ) + Spacer(Modifier.width(Margin.Medium)) + Icon( + imageVector = AppIcons.ChevronRightThick, + contentDescription = null, + modifier = Modifier.size(Dimens.SmallIconSize), + tint = SwissTransferTheme.colors.iconColor, + ) + } } } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index 3d1c6e243..00589f892 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -17,35 +17,23 @@ */ package com.infomaniak.swisstransfer.ui.components.transfer -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Surface import androidx.compose.material3.Text 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.scale -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.TransferUi import com.infomaniak.multiplatform_swisstransfer.common.models.TransferDirection import com.infomaniak.swisstransfer.R import com.infomaniak.swisstransfer.ui.previewparameter.TransferUiListPreviewParameter -import com.infomaniak.swisstransfer.ui.theme.CustomShapes import com.infomaniak.swisstransfer.ui.theme.Margin import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark -@OptIn(ExperimentalMaterialApi::class) @Composable fun TransferItemList( modifier: Modifier = Modifier, @@ -75,74 +63,11 @@ fun TransferItemList( key = { getTransfers()[it].uuid }, contentType = { getTransfers()[it] }, itemContent = { - val transfer = getTransfers()[it] - - val dismissState = rememberDismissState( - confirmStateChange = { dismissValue -> - val shouldDismiss = - dismissValue == DismissValue.DismissedToEnd || dismissValue == DismissValue.DismissedToStart - if (shouldDismiss) onSwiped(transfer.uuid) // TODO: We should probably add an animation here - shouldDismiss - }, - ) - - // TODO: the "Using a material import while also using the material3 library" - SwipeToDismiss( - state = dismissState, - directions = setOf(DismissDirection.EndToStart), - background = { - val dismissDirection = dismissState.dismissDirection ?: return@SwipeToDismiss - val color by animateColorAsState( - targetValue = when (dismissState.targetValue) { - DismissValue.Default -> Color.LightGray - else -> SwissTransferTheme.materialColors.error - }, - label = "Color animation", - ) - val alignment = when (dismissDirection) { - DismissDirection.StartToEnd -> Alignment.CenterStart - DismissDirection.EndToStart -> Alignment.CenterEnd - } - val scale by animateFloatAsState( - targetValue = if (dismissState.targetValue == DismissValue.Default) 0.75f else 1.0f, - label = "Scale animation", - ) - - Card( - modifier = Modifier.fillMaxSize(), - shape = CustomShapes.SMALL, - backgroundColor = color, - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - contentAlignment = alignment, - ) { - Icon( - imageVector = Icons.Default.Delete, - modifier = Modifier.scale(scale), - contentDescription = null, - ) - } - } - }, - dismissContent = { - Card( - elevation = animateDpAsState( - targetValue = if (dismissState.dismissDirection != null) 4.dp else 0.dp, - label = "Elevation animation", - ).value, - shape = CustomShapes.SMALL, - ) { - TransferItem( - transfer = transfer, - isSelected = { selectedTransferUuid == transfer.uuid }, - onClick = { onClick(transfer) }, - ) - } - }, + TransferItem( + transfer = transfer, + isSelected = { selectedTransferUuid == transfer.uuid }, + onClick = { onClick(transfer) }, ) }, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d57bc4b2b..015a032a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,6 @@ junit = "4.13.2" junitVersion = "1.2.1" kotlin = "2.0.20" lifecycleRuntimeKtx = "2.8.7" -material = "1.8.0-alpha06" navigation = "2.8.4" qrose = "1.0.1" recaptcha = "18.6.1" @@ -33,10 +32,9 @@ androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constr androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "composeAlpha" } # TODO: To be removed when compose 1.8.0 is stable +compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "composeAlpha" } #TODO: To be removed when compose 1.8.0 is stable compose-material3 = { group = "androidx.compose.material3", name = "material3" } compose-material3-adaptative-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" } compose-ui = { group = "androidx.compose.ui", name = "ui" } From 7b81517b6b0ad981c4e9b1084669aa7463c98009 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 11:56:05 +0100 Subject: [PATCH 03/15] refactor: Move `AnchoredDraggableState` to its own file + Add samples to test things --- .../ui/components/transfer/TransferItem.kt | 130 ++--- .../transfer/TransferItemDraggable.kt | 465 ++++++++++++++++++ .../components/transfer/TransferItemList.kt | 22 +- 3 files changed, 519 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt index 7a17193b1..308e5c981 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt @@ -17,26 +17,16 @@ */ package com.infomaniak.swisstransfer.ui.components.transfer -import androidx.compose.animation.core.exponentialDecay -import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp import com.infomaniak.core2.FORMAT_DATE_TITLE import com.infomaniak.core2.format import com.infomaniak.multiplatform_swisstransfer.common.ext.toDateFromSeconds @@ -53,12 +43,6 @@ import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.HumanReadableSizeUtils import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark import com.infomaniak.swisstransfer.ui.utils.isExpired -import kotlin.math.roundToInt - -enum class DragAnchors { - Start, - End, -} @OptIn(ExperimentalLayoutApi::class) @Composable @@ -82,90 +66,52 @@ fun TransferItem( null } - val density = LocalDensity.current - val state = remember { - // AnchoredDraggableState(initialValue = DragAnchors.Start) - AnchoredDraggableState( - initialValue = DragAnchors.Start, - positionalThreshold = { it * 0.5f }, - velocityThreshold = { with(density) { 100.dp.toPx() } }, - snapAnimationSpec = tween(), - decayAnimationSpec = exponentialDecay(), - // confirmValueChange =, - // anchors = DraggableAnchors { - // DragAnchors.Start to 0.0f - // DragAnchors.End to 400.0f - // }, - ).apply { - updateAnchors( - newAnchors = DraggableAnchors { - DragAnchors.Start to 0.0f - DragAnchors.End to 400.0f - }, - ) - } - } - // val anchors = remember(density) { - // - // // val replyOffset = with(density) { 48.dp.toPx() } - // // DraggableAnchors { - // // DragAnchors.Start to 0.0f - // // DragAnchors.End to replyOffset - // // } - // } - // SideEffect { state.updateAnchors(anchors) } - - Box( - modifier = Modifier.anchoredDraggable(state, Orientation.Horizontal), + Card( + onClick = onClick, + colors = CardDefaults.cardColors(containerColor = SwissTransferTheme.materialColors.surfaceContainerHighest), + shape = CustomShapes.SMALL, + border = border, ) { - Card( - modifier = Modifier.offset { IntOffset(x = state.requireOffset().roundToInt(), y = 0) }, - onClick = onClick, - colors = CardDefaults.cardColors(containerColor = SwissTransferTheme.materialColors.surfaceContainerHighest), - shape = CustomShapes.SMALL, - border = border, + Row( + modifier = Modifier.padding(Margin.Medium), + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = Modifier.padding(Margin.Medium), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1.0f)) { + Column(modifier = Modifier.weight(1.0f)) { - Text( - text = createdDate, - style = SwissTransferTheme.typography.bodyMedium, - color = SwissTransferTheme.colors.primaryTextColor, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - ) + Text( + text = createdDate, + style = SwissTransferTheme.typography.bodyMedium, + color = SwissTransferTheme.colors.primaryTextColor, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) - Spacer(Modifier.height(Margin.Mini)) - TextDotText( - firstText = { uploadedSize }, - secondText = { expiryText }, - optionalSecondTextColor = expiryColor, - ) + Spacer(Modifier.height(Margin.Mini)) + TextDotText( + firstText = { uploadedSize }, + secondText = { expiryText }, + optionalSecondTextColor = expiryColor, + ) - Spacer(Modifier.height(Margin.Mini)) - ContextualFlowRow( - itemCount = files.count(), - maxLines = 1, - horizontalArrangement = Arrangement.spacedBy(Margin.Mini), - overflow = ContextualFlowRowOverflow.expandIndicator { TransferFilePreview(remainingFilesCount = totalItemCount - shownItemCount) }, - ) { index -> - TransferFilePreview(file = files[index]) - } + Spacer(Modifier.height(Margin.Mini)) + ContextualFlowRow( + itemCount = files.count(), + maxLines = 1, + horizontalArrangement = Arrangement.spacedBy(Margin.Mini), + overflow = ContextualFlowRowOverflow.expandIndicator { TransferFilePreview(remainingFilesCount = totalItemCount - shownItemCount) }, + ) { index -> + TransferFilePreview(file = files[index]) } - - Spacer(Modifier.width(Margin.Medium)) - Icon( - imageVector = AppIcons.ChevronRightThick, - contentDescription = null, - modifier = Modifier.size(Dimens.SmallIconSize), - tint = SwissTransferTheme.colors.iconColor, - ) } + + Spacer(Modifier.width(Margin.Medium)) + Icon( + imageVector = AppIcons.ChevronRightThick, + contentDescription = null, + modifier = Modifier.size(Dimens.SmallIconSize), + tint = SwissTransferTheme.colors.iconColor, + ) } } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt new file mode 100644 index 000000000..e5e332358 --- /dev/null +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt @@ -0,0 +1,465 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.infomaniak.swisstransfer.ui.components.transfer + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.animate +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.overscroll +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.infomaniak.swisstransfer.ui.theme.CustomShapes +import com.infomaniak.swisstransfer.ui.theme.Dimens +import kotlin.math.max +import kotlin.math.roundToInt + +private enum class SwipeToDismissAnchors { + Start, + HalfStart, + Center, + HalfEnd, + End, +} + +@Composable +fun SwipeToDismissComponent( + content: @Composable () -> Unit, +) { + + BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + // .clip(shape = CustomShapes.SMALL) + ) { + + val density = LocalDensity.current + val draggableWidth = maxWidth + val containerWidthPx = with(density) { draggableWidth.toPx() } + // val dismissThreshold = with(density) { 56.dp.toPx() } + val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { + AnchoredDraggableState(initialValue = SwipeToDismissAnchors.End) + } + + SideEffect { + state.updateAnchors( + newAnchors = DraggableAnchors { + SwipeToDismissAnchors.Start at -containerWidthPx + // SwipeToDismissAnchors.Center at -(containerWidthPx * 0.9f) + SwipeToDismissAnchors.End at 0.0f + }, + ) + } + + // Box(Modifier.width(draggableWidth)) { + Box( + modifier = Modifier + // .width(draggableWidth) + .border(width = Dimens.BorderWidth, color = Color.Black, shape = CustomShapes.SMALL) + .clip(shape = CustomShapes.SMALL) + // .background(Color.Red) + .offset { + IntOffset( + x = state + .requireOffset() + .roundToInt(), + y = 0, + ) + } + .anchoredDraggable( + state = state, + orientation = Orientation.Horizontal, + flingBehavior = AnchoredDraggableDefaults.flingBehavior( + state = state, + // positionalThreshold = { + // it * 0.2f + // // private val DISMISS_THRESHOLD = 56.dp + // // containerWidthPx - dismissThreshold + // }, + ), + ), + // .background(Color.Red), + // shape = CustomShapes.SMALL, + // colors = CardDefaults.cardColors().copy(containerColor = Color.Red), + content = { content() }, + ) + // } + } +} + +@Preview +@Composable +fun DraggableAnchorsSample() { + var anchors by remember { mutableStateOf(DraggableAnchors {}) } + var offset by rememberSaveable { mutableFloatStateOf(0f) } + val thumbSize = 16.dp + val thumbSizePx = with(LocalDensity.current) { thumbSize.toPx() } + Box( + Modifier + .width(100.dp) + // Our anchors depend on this box's size, so we obtain the size from onSizeChanged and + // use updateAnchors to let the state know about the new anchors + .onSizeChanged { layoutSize -> + anchors = DraggableAnchors { + SwipeToDismissAnchors.Start at 0f + SwipeToDismissAnchors.End at layoutSize.width - thumbSizePx + } + } + .border(2.dp, Color.Black) + ) { + Box( + Modifier + .size(thumbSize) + .offset { IntOffset(x = offset.roundToInt(), y = 0) } + .draggable( + state = + rememberDraggableState { delta -> + offset = + (offset + delta).coerceIn( + anchors.minPosition(), + anchors.maxPosition() + ) + }, + orientation = Orientation.Horizontal, + onDragStopped = { velocity -> + val closestAnchor = anchors.positionOf(anchors.closestAnchor(offset)!!) + animate(offset, closestAnchor, velocity) { value, _ -> offset = value } + } + ) + .background(Color.Red) + ) + } +} + +@Composable +fun AnchoredDraggableCustomAnchoredSample() { + @Suppress("unused") + // Using AnchoredDraggableState's anchoredDrag APIs, we can build a custom animation + suspend fun AnchoredDraggableState.customAnimation( + target: T, + snapAnimationSpec: AnimationSpec, + velocity: Float = lastVelocity, + ) { + anchoredDrag(target) { latestAnchors, latestTarget -> + // If the anchors change while this block is suspending, it will get cancelled and + // restarted with the latest anchors and latest target + val targetOffset = latestAnchors.positionOf(latestTarget) + if (!targetOffset.isNaN()) { + animate( + initialValue = offset, + initialVelocity = velocity, + targetValue = targetOffset, + animationSpec = snapAnimationSpec + ) { value, velocity -> + dragTo(value, velocity) + } + } + } + } +} + +@Preview +@Composable +fun AnchoredDraggableLayoutDependentAnchorsSample() { + val state = + rememberSaveable(saver = AnchoredDraggableState.Saver()) { + AnchoredDraggableState(initialValue = SwipeToDismissAnchors.Center) + } + val draggableSize = 60.dp + val draggableSizePx = with(LocalDensity.current) { draggableSize.toPx() } + Box( + Modifier + .fillMaxWidth() + // Our anchors depend on this box's size, so we obtain the size from onSizeChanged and + // use updateAnchors to let the state know about the new anchors + .onSizeChanged { layoutSize -> + val dragEndPoint = layoutSize.width - draggableSizePx + state.updateAnchors( + DraggableAnchors { + SwipeToDismissAnchors.Start at 0f + SwipeToDismissAnchors.HalfStart at dragEndPoint * .25f + SwipeToDismissAnchors.Center at dragEndPoint * .5f + SwipeToDismissAnchors.HalfEnd at dragEndPoint * .75f + SwipeToDismissAnchors.End at dragEndPoint + } + ) + } + .visualizeDraggableAnchors(state, Orientation.Horizontal) + ) { + Box( + Modifier + .size(draggableSize) + .offset { + IntOffset( + x = state + .requireOffset() + .roundToInt(), y = 0 + ) + } + .anchoredDraggable(state = state, orientation = Orientation.Horizontal) + .background(Color.Red) + ) + } +} + +@Preview +@Composable +fun AnchoredDraggableWithOverscrollSample() { + val state = + rememberSaveable(saver = AnchoredDraggableState.Saver()) { + AnchoredDraggableState(initialValue = SwipeToDismissAnchors.Center) + } + val draggableSize = 80.dp + val draggableSizePx = with(LocalDensity.current) { draggableSize.toPx() } + val overscrollEffect = rememberOverscrollEffect() + + Box( + Modifier + .fillMaxWidth() + .onSizeChanged { layoutSize -> + val dragEndPoint = layoutSize.width - draggableSizePx + state.updateAnchors( + DraggableAnchors { + SwipeToDismissAnchors.Start at 0f + SwipeToDismissAnchors.Center at dragEndPoint / 2f + SwipeToDismissAnchors.End at dragEndPoint + } + ) + } + ) { + Box( + Modifier + .size(draggableSize) + .offset { + IntOffset( + x = state + .requireOffset() + .roundToInt(), y = 0 + ) + } + // pass the overscrollEffect to AnchoredDraggable + .anchoredDraggable( + state, + Orientation.Horizontal, + overscrollEffect = overscrollEffect + ) + .overscroll(overscrollEffect) + .background(Color.Red) + ) + } +} + +@Preview +@Composable +fun AnchoredDraggableProgressSample() { + val state = + rememberSaveable(saver = AnchoredDraggableState.Saver()) { + AnchoredDraggableState(initialValue = SwipeToDismissAnchors.Center) + } + val draggableSize = 60.dp + val draggableSizePx = with(LocalDensity.current) { draggableSize.toPx() } + Column( + Modifier + .fillMaxWidth() + // Our anchors depend on this box's size, so we obtain the size from onSizeChanged and + // use updateAnchors to let the state know about the new anchors + .onSizeChanged { layoutSize -> + val dragEndPoint = layoutSize.width - draggableSizePx + state.updateAnchors( + DraggableAnchors { + SwipeToDismissAnchors.Start at 0f + SwipeToDismissAnchors.Center at dragEndPoint * .5f + SwipeToDismissAnchors.End at dragEndPoint + } + ) + } + ) { + // Read progress in a snapshot-backed context to receive updates. This could be e.g. a + // derived state, snapshotFlow or other snapshot-aware context like the graphicsLayer + // block. + val centerToStartProgress by remember { + derivedStateOf { + state.progress( + from = SwipeToDismissAnchors.Center, + to = SwipeToDismissAnchors.Start, + ) + } + } + val centerToEndProgress by remember { + derivedStateOf { + state.progress( + from = SwipeToDismissAnchors.Center, + to = SwipeToDismissAnchors.End, + ) + } + } + Box { + Box( + Modifier + .fillMaxWidth() + .height(draggableSize) + .graphicsLayer { alpha = max(centerToStartProgress, centerToEndProgress) } + .background(Color.Black) + ) + Box( + Modifier + .size(draggableSize) + .offset { + IntOffset( + x = state + .requireOffset() + .roundToInt(), y = 0 + ) + } + .anchoredDraggable(state, Orientation.Horizontal) + .background(Color.Red) + ) + } + } +} + +@Preview +@Composable +fun AnchoredDraggableDynamicAnchorsSample() { + val open = "Open" + val closed = "Closed" + + @Composable + fun DrawerLayout( + state: AnchoredDraggableState, + activePositions: List = listOf(open, closed), + modifier: Modifier = Modifier, + drawerContent: @Composable () -> Unit, + content: @Composable () -> Unit + ) { + Box(modifier) { + Box(Modifier.anchoredDraggable(state, Orientation.Horizontal)) { content() } + Box( + Modifier + .onSizeChanged { measuredSize -> + state.updateAnchors( + DraggableAnchors { + if (closed in activePositions) { + closed at -measuredSize.width.toFloat() + } + if (open in activePositions) { + open at 0f + } + } + ) + } + .offset { + IntOffset( + x = state + .requireOffset() + .roundToInt(), y = 0 + ) + } + ) { + drawerContent() + } + } + } + + val state = + rememberSaveable(saver = AnchoredDraggableState.Saver()) { + AnchoredDraggableState(initialValue = closed) + } + val activePositions = remember { mutableStateListOf(open, closed) } + DrawerLayout( + state, + activePositions, + drawerContent = { + Button( + onClick = { + if (closed in activePositions) { + activePositions.remove(closed) + } else { + activePositions.add(closed) + } + } + ) { + val text = + if (closed in activePositions) { + "Click to disallow closing drawer" + } else { + "Click to allow closing" + } + Text(text) + } + }, + ) { + Text("Swipe to expand Drawer") + } +} + +/** + * A [Modifier] that visualizes the anchors attached to an [AnchoredDraggableState] as lines along + * the cross axis of the layout (start to end for [Orientation.Vertical], top to end for + * [Orientation.Horizontal]). This is useful to debug components with a complex set of anchors, or + * for AnchoredDraggable development. + * + * @param state The state whose anchors to visualize + * @param orientation The orientation of the [anchoredDraggable] + * @param lineColor The color of the visualization lines + * @param lineStrokeWidth The stroke width of the visualization lines + * @param linePathEffect The path effect used to draw the visualization lines + */ +private fun Modifier.visualizeDraggableAnchors( + state: AnchoredDraggableState<*>, + orientation: Orientation, + lineColor: Color = Color.Black, + lineStrokeWidth: Float = 10f, + linePathEffect: PathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 30f)) +) = drawWithContent { + drawContent() + state.anchors.forEach { _, position -> + val startOffset = + Offset( + x = if (orientation == Orientation.Horizontal) position else 0f, + y = if (orientation == Orientation.Vertical) position else 0f + ) + val endOffset = + Offset( + x = if (orientation == Orientation.Horizontal) startOffset.x else size.height, + y = if (orientation == Orientation.Vertical) startOffset.y else size.width + ) + drawLine( + color = lineColor, + start = startOffset, + end = endOffset, + strokeWidth = lineStrokeWidth, + pathEffect = linePathEffect + ) + } +} diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index 00589f892..175af7607 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -58,17 +58,27 @@ fun TransferItemList( item { Text(text = stringResource(titleRes), style = SwissTransferTheme.typography.h1) } + // item { AnchoredDraggableAnchorsFromCompositionSample() } + // item { DraggableAnchorsSample() } + // item { AnchoredDraggableCustomAnchoredSample() } // TODO: C'est juste une animation + // item { AnchoredDraggableLayoutDependentAnchorsSample() } // TODO: Ça plante + // item { AnchoredDraggableWithOverscrollSample() } // TODO: Ça plante + // item { AnchoredDraggableProgressSample() } // TODO: Ça plante + // item { AnchoredDraggableDynamicAnchorsSample() } // TODO: Ça plante + items( count = getTransfers().count(), key = { getTransfers()[it].uuid }, contentType = { getTransfers()[it] }, itemContent = { - val transfer = getTransfers()[it] - TransferItem( - transfer = transfer, - isSelected = { selectedTransferUuid == transfer.uuid }, - onClick = { onClick(transfer) }, - ) + SwipeToDismissComponent { + val transfer = getTransfers()[it] + TransferItem( + transfer = transfer, + isSelected = { selectedTransferUuid == transfer.uuid }, + onClick = { onClick(transfer) }, + ) + } }, ) } From ba48cf5693f074e9741cd9531f8070723f4ce57e Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 08:15:14 +0100 Subject: [PATCH 04/15] refactor: Use `SwipeToDismissBox` instead which is more tailored to our use case --- .../transfer/TransferItemDraggable.kt | 188 ++++++++++++------ 1 file changed, 130 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt index e5e332358..530d5a955 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt @@ -15,6 +15,8 @@ */ package com.infomaniak.swisstransfer.ui.components.transfer +import android.util.Log +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animate import androidx.compose.foundation.background @@ -23,24 +25,21 @@ import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.overscroll import androidx.compose.foundation.rememberOverscrollEffect -import androidx.compose.material3.Button -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import com.infomaniak.swisstransfer.ui.theme.CustomShapes -import com.infomaniak.swisstransfer.ui.theme.Dimens import kotlin.math.max import kotlin.math.roundToInt @@ -54,67 +53,140 @@ private enum class SwipeToDismissAnchors { @Composable fun SwipeToDismissComponent( + callback: () -> Unit = {}, content: @Composable () -> Unit, ) { - BoxWithConstraints( - modifier = Modifier - .fillMaxWidth() - // .clip(shape = CustomShapes.SMALL) - ) { + val dismissState = rememberSwipeToDismissBoxState( + confirmValueChange = { dismissValue -> + Log.e("TOTO", "SwipeToDismissComponent - : ${dismissValue.name}") + val shouldDismiss = dismissValue == SwipeToDismissBoxValue.EndToStart + // TODO: We should probably add an animation here + if (shouldDismiss) callback() + shouldDismiss + }, + ) - val density = LocalDensity.current - val draggableWidth = maxWidth - val containerWidthPx = with(density) { draggableWidth.toPx() } - // val dismissThreshold = with(density) { 56.dp.toPx() } - val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { - AnchoredDraggableState(initialValue = SwipeToDismissAnchors.End) + SwipeToDismissBox( + state = dismissState, + enableDismissFromStartToEnd = false, + backgroundContent = { + val color by animateColorAsState( + if (dismissState.targetValue == SwipeToDismissBoxValue.Settled) Color.LightGray else Color.Red + // when (dismissState.targetValue) { + // SwipeToDismissBoxValue.Settled -> Color.LightGray + // SwipeToDismissBoxValue.StartToEnd -> Color.Green + // SwipeToDismissBoxValue.EndToStart -> Color.Red + // } + ) + Box( + Modifier + .fillMaxSize() + .background(color) + ) } - - SideEffect { - state.updateAnchors( - newAnchors = DraggableAnchors { - SwipeToDismissAnchors.Start at -containerWidthPx - // SwipeToDismissAnchors.Center at -(containerWidthPx * 0.9f) - SwipeToDismissAnchors.End at 0.0f - }, + ) { + OutlinedCard(shape = RectangleShape) { + ListItem( + headlineContent = { Text("Cupcake") }, + supportingContent = { Text("Swipe me left or right!") } ) } - - // Box(Modifier.width(draggableWidth)) { - Box( - modifier = Modifier - // .width(draggableWidth) - .border(width = Dimens.BorderWidth, color = Color.Black, shape = CustomShapes.SMALL) - .clip(shape = CustomShapes.SMALL) - // .background(Color.Red) - .offset { - IntOffset( - x = state - .requireOffset() - .roundToInt(), - y = 0, - ) - } - .anchoredDraggable( - state = state, - orientation = Orientation.Horizontal, - flingBehavior = AnchoredDraggableDefaults.flingBehavior( - state = state, - // positionalThreshold = { - // it * 0.2f - // // private val DISMISS_THRESHOLD = 56.dp - // // containerWidthPx - dismissThreshold - // }, - ), - ), - // .background(Color.Red), - // shape = CustomShapes.SMALL, - // colors = CardDefaults.cardColors().copy(containerColor = Color.Red), - content = { content() }, - ) - // } } + + // BoxWithConstraints { + // + // val dismissThreshold = 0.5f + // val minIconScale = 1.0f + // val maxIconScale = 1.5f + // + // val containerWidthPx = with(LocalDensity.current) { maxWidth.toPx() } + // val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { + // AnchoredDraggableState( + // initialValue = SwipeToDismissAnchors.End, + // ) + // } + // + // LaunchedEffect(state.currentValue) { + // Log.i("TOTO", "SwipeToDismissComponent - state.currentValue: ${state.currentValue}") + // if (state.currentValue == SwipeToDismissAnchors.Start) { + // Log.e("TOTO", "SwipeToDismissComponent - START reached !") + // } + // } + // + // val swipeProgress by remember { + // derivedStateOf { + // val progress = state.progress(from = SwipeToDismissAnchors.End, to = SwipeToDismissAnchors.Start) + // // if (toto > 0.5f) callback() + // Log.d("TOTO", "SwipeToDismissComponent | state: ${state.currentValue} | progress: ${progress}") + // progress + // } + // } + // + // // TODO: Red & Gray dark theme background colors need to be handled + // val backgroundColor by animateColorAsState( + // targetValue = if (swipeProgress > dismissThreshold) SwissTransferTheme.materialColors.error else Color.LightGray, + // label = "Background color animation", + // ) + // val iconScale by animateFloatAsState( + // targetValue = if (swipeProgress > dismissThreshold) { + // maxIconScale + // } else { + // minIconScale + (swipeProgress * 2.0f * (maxIconScale - minIconScale)) + // }, + // label = "Icon scaling animation", + // ) + // + // SideEffect { + // state.updateAnchors( + // newAnchors = DraggableAnchors { + // SwipeToDismissAnchors.Start at -containerWidthPx + // // SwipeToDismissAnchors.Center at -(containerWidthPx * dismissThreshold) + // SwipeToDismissAnchors.End at 0.0f + // }, + // ) + // } + // + // Box( + // modifier = Modifier + // .clip(shape = CustomShapes.SMALL) + // .background(backgroundColor), + // ) { + // Box( + // modifier = Modifier + // .align(Alignment.CenterEnd) + // .padding(end = Margin.Large), + // ) { + // // TODO: The dark theme icon color needs to be handled + // Icon( + // imageVector = Icons.Default.Delete, + // modifier = Modifier.scale(iconScale), + // contentDescription = null, + // ) + // } + // + // Box( + // modifier = Modifier + // .offset { + // IntOffset( + // x = state + // .requireOffset() + // .roundToInt(), + // y = 0, + // ) + // } + // .anchoredDraggable( + // state = state, + // orientation = Orientation.Horizontal, + // // flingBehavior = AnchoredDraggableDefaults.flingBehavior( + // // state = state, + // // positionalThreshold = { it * dismissThreshold }, + // // ), + // ), + // content = { content() }, + // ) + // } + // } } @Preview From b970b266d413c6f45dc09f3db8b04125acb22ccf Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 11:22:07 +0100 Subject: [PATCH 05/15] style: Display correct UI for Transfer item --- .../ui/components/transfer/TransferItem.kt | 4 +- .../transfer/TransferItemDraggable.kt | 88 ++++++++++++++----- .../components/transfer/TransferItemList.kt | 5 +- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt index 308e5c981..6454fa89a 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItem.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -48,6 +49,7 @@ import com.infomaniak.swisstransfer.ui.utils.isExpired @Composable fun TransferItem( transfer: TransferUi, + shape: Shape = CustomShapes.SMALL, isSelected: () -> Boolean, onClick: () -> Unit, ) { @@ -69,7 +71,7 @@ fun TransferItem( Card( onClick = onClick, colors = CardDefaults.cardColors(containerColor = SwissTransferTheme.materialColors.surfaceContainerHighest), - shape = CustomShapes.SMALL, + shape = shape, border = border, ) { Row( diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt index 530d5a955..138aebe52 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt @@ -19,27 +19,36 @@ import android.util.Log import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animate +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.overscroll import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect -import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import com.infomaniak.swisstransfer.ui.theme.Margin +import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import kotlin.math.max import kotlin.math.roundToInt @@ -53,45 +62,76 @@ private enum class SwipeToDismissAnchors { @Composable fun SwipeToDismissComponent( + shape: Shape, callback: () -> Unit = {}, content: @Composable () -> Unit, ) { - val dismissState = rememberSwipeToDismissBoxState( - confirmValueChange = { dismissValue -> - Log.e("TOTO", "SwipeToDismissComponent - : ${dismissValue.name}") - val shouldDismiss = dismissValue == SwipeToDismissBoxValue.EndToStart + val defaultColor = Color.LightGray + val swipedColor = SwissTransferTheme.materialColors.error + val minIconScale = 1.0f + val maxIconScale = 1.5f + val swipedElevation = 4.dp + + val state = rememberSwipeToDismissBoxState( + confirmValueChange = { value -> + Log.d("TOTO", "SwipeToDismissComponent - : ${value.name}") + val shouldDismiss = value == SwipeToDismissBoxValue.EndToStart // TODO: We should probably add an animation here - if (shouldDismiss) callback() + if (shouldDismiss) { + // TODO: This check triggers twice, why ?? :( + Log.e("TOTO", "SwipeToDismissComponent - DISMISS GOGOGO") + callback() + } shouldDismiss }, ) + // TODO: Red & Gray dark theme background colors need to be handled + val backgroundColor by animateColorAsState( + targetValue = if (state.targetValue == SwipeToDismissBoxValue.Settled) defaultColor else swipedColor, + label = "Background color animation", + ) + val iconScale by animateFloatAsState( + targetValue = if (state.targetValue == SwipeToDismissBoxValue.Settled) minIconScale else maxIconScale, + label = "Icon scale animation", + ) + val contentElevation by animateDpAsState( + targetValue = if (state.dismissDirection == SwipeToDismissBoxValue.EndToStart) swipedElevation else 0.dp, + label = "Content elevation animation", + ) + SwipeToDismissBox( - state = dismissState, + state = state, enableDismissFromStartToEnd = false, backgroundContent = { - val color by animateColorAsState( - if (dismissState.targetValue == SwipeToDismissBoxValue.Settled) Color.LightGray else Color.Red - // when (dismissState.targetValue) { - // SwipeToDismissBoxValue.Settled -> Color.LightGray - // SwipeToDismissBoxValue.StartToEnd -> Color.Green - // SwipeToDismissBoxValue.EndToStart -> Color.Red - // } - ) Box( - Modifier + modifier = Modifier .fillMaxSize() - .background(color) - ) + .clip(shape) + .background(backgroundColor), + ) { + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = Margin.Large), + ) { + // TODO: The dark theme icon color needs to be handled + Icon( + imageVector = Icons.Default.Delete, + modifier = Modifier.scale(iconScale), + contentDescription = null, + ) + } + } } ) { - OutlinedCard(shape = RectangleShape) { - ListItem( - headlineContent = { Text("Cupcake") }, - supportingContent = { Text("Swipe me left or right!") } - ) - } + + Surface( + shape = shape, + shadowElevation = contentElevation, + content = { content() }, + ) } // BoxWithConstraints { diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index 175af7607..f4939b377 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -30,6 +30,7 @@ import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.TransferU import com.infomaniak.multiplatform_swisstransfer.common.models.TransferDirection import com.infomaniak.swisstransfer.R import com.infomaniak.swisstransfer.ui.previewparameter.TransferUiListPreviewParameter +import com.infomaniak.swisstransfer.ui.theme.CustomShapes import com.infomaniak.swisstransfer.ui.theme.Margin import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark @@ -49,6 +50,7 @@ fun TransferItemList( TransferDirection.SENT -> R.string.sentFilesTitle TransferDirection.RECEIVED -> R.string.receivedFilesTitle } + val itemShape = CustomShapes.SMALL LazyColumn( modifier = modifier, @@ -71,10 +73,11 @@ fun TransferItemList( key = { getTransfers()[it].uuid }, contentType = { getTransfers()[it] }, itemContent = { - SwipeToDismissComponent { + SwipeToDismissComponent(shape = itemShape) { val transfer = getTransfers()[it] TransferItem( transfer = transfer, + shape = itemShape, isSelected = { selectedTransferUuid == transfer.uuid }, onClick = { onClick(transfer) }, ) From 03fbccef37006a59089f8462e6749135451ab64b Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 11:50:31 +0100 Subject: [PATCH 06/15] chore: Remove now unused previous versions of SwipeToDismissComponent code --- .../transfer/TransferItemDraggable.kt | 449 ------------------ .../components/transfer/TransferItemList.kt | 8 - 2 files changed, 457 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt index 138aebe52..2f4f0ce7b 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt @@ -52,14 +52,6 @@ import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import kotlin.math.max import kotlin.math.roundToInt -private enum class SwipeToDismissAnchors { - Start, - HalfStart, - Center, - HalfEnd, - End, -} - @Composable fun SwipeToDismissComponent( shape: Shape, @@ -133,445 +125,4 @@ fun SwipeToDismissComponent( content = { content() }, ) } - - // BoxWithConstraints { - // - // val dismissThreshold = 0.5f - // val minIconScale = 1.0f - // val maxIconScale = 1.5f - // - // val containerWidthPx = with(LocalDensity.current) { maxWidth.toPx() } - // val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { - // AnchoredDraggableState( - // initialValue = SwipeToDismissAnchors.End, - // ) - // } - // - // LaunchedEffect(state.currentValue) { - // Log.i("TOTO", "SwipeToDismissComponent - state.currentValue: ${state.currentValue}") - // if (state.currentValue == SwipeToDismissAnchors.Start) { - // Log.e("TOTO", "SwipeToDismissComponent - START reached !") - // } - // } - // - // val swipeProgress by remember { - // derivedStateOf { - // val progress = state.progress(from = SwipeToDismissAnchors.End, to = SwipeToDismissAnchors.Start) - // // if (toto > 0.5f) callback() - // Log.d("TOTO", "SwipeToDismissComponent | state: ${state.currentValue} | progress: ${progress}") - // progress - // } - // } - // - // // TODO: Red & Gray dark theme background colors need to be handled - // val backgroundColor by animateColorAsState( - // targetValue = if (swipeProgress > dismissThreshold) SwissTransferTheme.materialColors.error else Color.LightGray, - // label = "Background color animation", - // ) - // val iconScale by animateFloatAsState( - // targetValue = if (swipeProgress > dismissThreshold) { - // maxIconScale - // } else { - // minIconScale + (swipeProgress * 2.0f * (maxIconScale - minIconScale)) - // }, - // label = "Icon scaling animation", - // ) - // - // SideEffect { - // state.updateAnchors( - // newAnchors = DraggableAnchors { - // SwipeToDismissAnchors.Start at -containerWidthPx - // // SwipeToDismissAnchors.Center at -(containerWidthPx * dismissThreshold) - // SwipeToDismissAnchors.End at 0.0f - // }, - // ) - // } - // - // Box( - // modifier = Modifier - // .clip(shape = CustomShapes.SMALL) - // .background(backgroundColor), - // ) { - // Box( - // modifier = Modifier - // .align(Alignment.CenterEnd) - // .padding(end = Margin.Large), - // ) { - // // TODO: The dark theme icon color needs to be handled - // Icon( - // imageVector = Icons.Default.Delete, - // modifier = Modifier.scale(iconScale), - // contentDescription = null, - // ) - // } - // - // Box( - // modifier = Modifier - // .offset { - // IntOffset( - // x = state - // .requireOffset() - // .roundToInt(), - // y = 0, - // ) - // } - // .anchoredDraggable( - // state = state, - // orientation = Orientation.Horizontal, - // // flingBehavior = AnchoredDraggableDefaults.flingBehavior( - // // state = state, - // // positionalThreshold = { it * dismissThreshold }, - // // ), - // ), - // content = { content() }, - // ) - // } - // } -} - -@Preview -@Composable -fun DraggableAnchorsSample() { - var anchors by remember { mutableStateOf(DraggableAnchors {}) } - var offset by rememberSaveable { mutableFloatStateOf(0f) } - val thumbSize = 16.dp - val thumbSizePx = with(LocalDensity.current) { thumbSize.toPx() } - Box( - Modifier - .width(100.dp) - // Our anchors depend on this box's size, so we obtain the size from onSizeChanged and - // use updateAnchors to let the state know about the new anchors - .onSizeChanged { layoutSize -> - anchors = DraggableAnchors { - SwipeToDismissAnchors.Start at 0f - SwipeToDismissAnchors.End at layoutSize.width - thumbSizePx - } - } - .border(2.dp, Color.Black) - ) { - Box( - Modifier - .size(thumbSize) - .offset { IntOffset(x = offset.roundToInt(), y = 0) } - .draggable( - state = - rememberDraggableState { delta -> - offset = - (offset + delta).coerceIn( - anchors.minPosition(), - anchors.maxPosition() - ) - }, - orientation = Orientation.Horizontal, - onDragStopped = { velocity -> - val closestAnchor = anchors.positionOf(anchors.closestAnchor(offset)!!) - animate(offset, closestAnchor, velocity) { value, _ -> offset = value } - } - ) - .background(Color.Red) - ) - } -} - -@Composable -fun AnchoredDraggableCustomAnchoredSample() { - @Suppress("unused") - // Using AnchoredDraggableState's anchoredDrag APIs, we can build a custom animation - suspend fun AnchoredDraggableState.customAnimation( - target: T, - snapAnimationSpec: AnimationSpec, - velocity: Float = lastVelocity, - ) { - anchoredDrag(target) { latestAnchors, latestTarget -> - // If the anchors change while this block is suspending, it will get cancelled and - // restarted with the latest anchors and latest target - val targetOffset = latestAnchors.positionOf(latestTarget) - if (!targetOffset.isNaN()) { - animate( - initialValue = offset, - initialVelocity = velocity, - targetValue = targetOffset, - animationSpec = snapAnimationSpec - ) { value, velocity -> - dragTo(value, velocity) - } - } - } - } -} - -@Preview -@Composable -fun AnchoredDraggableLayoutDependentAnchorsSample() { - val state = - rememberSaveable(saver = AnchoredDraggableState.Saver()) { - AnchoredDraggableState(initialValue = SwipeToDismissAnchors.Center) - } - val draggableSize = 60.dp - val draggableSizePx = with(LocalDensity.current) { draggableSize.toPx() } - Box( - Modifier - .fillMaxWidth() - // Our anchors depend on this box's size, so we obtain the size from onSizeChanged and - // use updateAnchors to let the state know about the new anchors - .onSizeChanged { layoutSize -> - val dragEndPoint = layoutSize.width - draggableSizePx - state.updateAnchors( - DraggableAnchors { - SwipeToDismissAnchors.Start at 0f - SwipeToDismissAnchors.HalfStart at dragEndPoint * .25f - SwipeToDismissAnchors.Center at dragEndPoint * .5f - SwipeToDismissAnchors.HalfEnd at dragEndPoint * .75f - SwipeToDismissAnchors.End at dragEndPoint - } - ) - } - .visualizeDraggableAnchors(state, Orientation.Horizontal) - ) { - Box( - Modifier - .size(draggableSize) - .offset { - IntOffset( - x = state - .requireOffset() - .roundToInt(), y = 0 - ) - } - .anchoredDraggable(state = state, orientation = Orientation.Horizontal) - .background(Color.Red) - ) - } -} - -@Preview -@Composable -fun AnchoredDraggableWithOverscrollSample() { - val state = - rememberSaveable(saver = AnchoredDraggableState.Saver()) { - AnchoredDraggableState(initialValue = SwipeToDismissAnchors.Center) - } - val draggableSize = 80.dp - val draggableSizePx = with(LocalDensity.current) { draggableSize.toPx() } - val overscrollEffect = rememberOverscrollEffect() - - Box( - Modifier - .fillMaxWidth() - .onSizeChanged { layoutSize -> - val dragEndPoint = layoutSize.width - draggableSizePx - state.updateAnchors( - DraggableAnchors { - SwipeToDismissAnchors.Start at 0f - SwipeToDismissAnchors.Center at dragEndPoint / 2f - SwipeToDismissAnchors.End at dragEndPoint - } - ) - } - ) { - Box( - Modifier - .size(draggableSize) - .offset { - IntOffset( - x = state - .requireOffset() - .roundToInt(), y = 0 - ) - } - // pass the overscrollEffect to AnchoredDraggable - .anchoredDraggable( - state, - Orientation.Horizontal, - overscrollEffect = overscrollEffect - ) - .overscroll(overscrollEffect) - .background(Color.Red) - ) - } -} - -@Preview -@Composable -fun AnchoredDraggableProgressSample() { - val state = - rememberSaveable(saver = AnchoredDraggableState.Saver()) { - AnchoredDraggableState(initialValue = SwipeToDismissAnchors.Center) - } - val draggableSize = 60.dp - val draggableSizePx = with(LocalDensity.current) { draggableSize.toPx() } - Column( - Modifier - .fillMaxWidth() - // Our anchors depend on this box's size, so we obtain the size from onSizeChanged and - // use updateAnchors to let the state know about the new anchors - .onSizeChanged { layoutSize -> - val dragEndPoint = layoutSize.width - draggableSizePx - state.updateAnchors( - DraggableAnchors { - SwipeToDismissAnchors.Start at 0f - SwipeToDismissAnchors.Center at dragEndPoint * .5f - SwipeToDismissAnchors.End at dragEndPoint - } - ) - } - ) { - // Read progress in a snapshot-backed context to receive updates. This could be e.g. a - // derived state, snapshotFlow or other snapshot-aware context like the graphicsLayer - // block. - val centerToStartProgress by remember { - derivedStateOf { - state.progress( - from = SwipeToDismissAnchors.Center, - to = SwipeToDismissAnchors.Start, - ) - } - } - val centerToEndProgress by remember { - derivedStateOf { - state.progress( - from = SwipeToDismissAnchors.Center, - to = SwipeToDismissAnchors.End, - ) - } - } - Box { - Box( - Modifier - .fillMaxWidth() - .height(draggableSize) - .graphicsLayer { alpha = max(centerToStartProgress, centerToEndProgress) } - .background(Color.Black) - ) - Box( - Modifier - .size(draggableSize) - .offset { - IntOffset( - x = state - .requireOffset() - .roundToInt(), y = 0 - ) - } - .anchoredDraggable(state, Orientation.Horizontal) - .background(Color.Red) - ) - } - } -} - -@Preview -@Composable -fun AnchoredDraggableDynamicAnchorsSample() { - val open = "Open" - val closed = "Closed" - - @Composable - fun DrawerLayout( - state: AnchoredDraggableState, - activePositions: List = listOf(open, closed), - modifier: Modifier = Modifier, - drawerContent: @Composable () -> Unit, - content: @Composable () -> Unit - ) { - Box(modifier) { - Box(Modifier.anchoredDraggable(state, Orientation.Horizontal)) { content() } - Box( - Modifier - .onSizeChanged { measuredSize -> - state.updateAnchors( - DraggableAnchors { - if (closed in activePositions) { - closed at -measuredSize.width.toFloat() - } - if (open in activePositions) { - open at 0f - } - } - ) - } - .offset { - IntOffset( - x = state - .requireOffset() - .roundToInt(), y = 0 - ) - } - ) { - drawerContent() - } - } - } - - val state = - rememberSaveable(saver = AnchoredDraggableState.Saver()) { - AnchoredDraggableState(initialValue = closed) - } - val activePositions = remember { mutableStateListOf(open, closed) } - DrawerLayout( - state, - activePositions, - drawerContent = { - Button( - onClick = { - if (closed in activePositions) { - activePositions.remove(closed) - } else { - activePositions.add(closed) - } - } - ) { - val text = - if (closed in activePositions) { - "Click to disallow closing drawer" - } else { - "Click to allow closing" - } - Text(text) - } - }, - ) { - Text("Swipe to expand Drawer") - } -} - -/** - * A [Modifier] that visualizes the anchors attached to an [AnchoredDraggableState] as lines along - * the cross axis of the layout (start to end for [Orientation.Vertical], top to end for - * [Orientation.Horizontal]). This is useful to debug components with a complex set of anchors, or - * for AnchoredDraggable development. - * - * @param state The state whose anchors to visualize - * @param orientation The orientation of the [anchoredDraggable] - * @param lineColor The color of the visualization lines - * @param lineStrokeWidth The stroke width of the visualization lines - * @param linePathEffect The path effect used to draw the visualization lines - */ -private fun Modifier.visualizeDraggableAnchors( - state: AnchoredDraggableState<*>, - orientation: Orientation, - lineColor: Color = Color.Black, - lineStrokeWidth: Float = 10f, - linePathEffect: PathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 30f)) -) = drawWithContent { - drawContent() - state.anchors.forEach { _, position -> - val startOffset = - Offset( - x = if (orientation == Orientation.Horizontal) position else 0f, - y = if (orientation == Orientation.Vertical) position else 0f - ) - val endOffset = - Offset( - x = if (orientation == Orientation.Horizontal) startOffset.x else size.height, - y = if (orientation == Orientation.Vertical) startOffset.y else size.width - ) - drawLine( - color = lineColor, - start = startOffset, - end = endOffset, - strokeWidth = lineStrokeWidth, - pathEffect = linePathEffect - ) - } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index f4939b377..8763a8550 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -60,14 +60,6 @@ fun TransferItemList( item { Text(text = stringResource(titleRes), style = SwissTransferTheme.typography.h1) } - // item { AnchoredDraggableAnchorsFromCompositionSample() } - // item { DraggableAnchorsSample() } - // item { AnchoredDraggableCustomAnchoredSample() } // TODO: C'est juste une animation - // item { AnchoredDraggableLayoutDependentAnchorsSample() } // TODO: Ça plante - // item { AnchoredDraggableWithOverscrollSample() } // TODO: Ça plante - // item { AnchoredDraggableProgressSample() } // TODO: Ça plante - // item { AnchoredDraggableDynamicAnchorsSample() } // TODO: Ça plante - items( count = getTransfers().count(), key = { getTransfers()[it].uuid }, From e5beede0891d558d1104ebd15be6cb5dd477639e Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 11:51:17 +0100 Subject: [PATCH 07/15] fix: Don't use `confirmValueChange` of `SwipeToDismissBoxState` to detect end of swipe, it looks buggy --- .../transfer/TransferItemDraggable.kt | 90 +++++++++++-------- .../components/transfer/TransferItemList.kt | 5 +- 2 files changed, 59 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt index 2f4f0ce7b..2b34b3a41 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt @@ -17,48 +17,41 @@ package com.infomaniak.swisstransfer.ui.components.transfer import android.util.Log import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.animate import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.overscroll -import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable +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.drawWithContent import androidx.compose.ui.draw.scale -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.TransferUi +import com.infomaniak.swisstransfer.ui.previewparameter.TransferUiListPreviewParameter +import com.infomaniak.swisstransfer.ui.theme.CustomShapes import com.infomaniak.swisstransfer.ui.theme.Margin import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme -import kotlin.math.max -import kotlin.math.roundToInt +import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark @Composable fun SwipeToDismissComponent( - shape: Shape, - callback: () -> Unit = {}, + contentShape: Shape, + onSwiped: () -> Unit = {}, content: @Composable () -> Unit, ) { + // Configuration + val dismissThreshold = 0.5f val defaultColor = Color.LightGray val swipedColor = SwissTransferTheme.materialColors.error val minIconScale = 1.0f @@ -66,19 +59,29 @@ fun SwipeToDismissComponent( val swipedElevation = 4.dp val state = rememberSwipeToDismissBoxState( - confirmValueChange = { value -> - Log.d("TOTO", "SwipeToDismissComponent - : ${value.name}") - val shouldDismiss = value == SwipeToDismissBoxValue.EndToStart - // TODO: We should probably add an animation here - if (shouldDismiss) { - // TODO: This check triggers twice, why ?? :( - Log.e("TOTO", "SwipeToDismissComponent - DISMISS GOGOGO") - callback() - } - shouldDismiss - }, + // TODO: This block of code is commented for now, while we investigate the multiple triggers of dismiss state + // confirmValueChange = { value -> + // val shouldDismiss = value == SwipeToDismissBoxValue.EndToStart + // // TODO: This check triggers twice, why ?? :( + // if (shouldDismiss) { + // Log.i("TOTO", "SwipeToDismiss state : $value") + // // TODO: We should probably add an animation here + // onSwiped() + // } else { + // Log.d("TOTO", "SwipeToDismiss state : $value") + // } + // shouldDismiss + // }, + positionalThreshold = { totalDistance -> totalDistance * dismissThreshold }, ) + // TODO: Sometimes, this check triggers several times in a row, why ? + if (state.currentValue == SwipeToDismissBoxValue.EndToStart) { + Log.i("TOTO", "SwipeToDismiss state : EndToStart") + // TODO: We should probably add an animation here + onSwiped() + } + // TODO: Red & Gray dark theme background colors need to be handled val backgroundColor by animateColorAsState( targetValue = if (state.targetValue == SwipeToDismissBoxValue.Settled) defaultColor else swipedColor, @@ -100,7 +103,7 @@ fun SwipeToDismissComponent( Box( modifier = Modifier .fillMaxSize() - .clip(shape) + .clip(contentShape) .background(backgroundColor), ) { Box( @@ -118,11 +121,28 @@ fun SwipeToDismissComponent( } } ) { - Surface( - shape = shape, + shape = contentShape, shadowElevation = contentElevation, content = { content() }, ) } } + +@PreviewLightAndDark +@Composable +private fun Preview(@PreviewParameter(TransferUiListPreviewParameter::class) transfers: List) { + val contentShape = CustomShapes.SMALL + SwissTransferTheme { + Surface { + SwipeToDismissComponent(contentShape = contentShape) { + TransferItem( + transfer = transfers.first(), + shape = contentShape, + isSelected = { false }, + onClick = {}, + ) + } + } + } +} diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index 8763a8550..a277699b4 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -65,7 +65,10 @@ fun TransferItemList( key = { getTransfers()[it].uuid }, contentType = { getTransfers()[it] }, itemContent = { - SwipeToDismissComponent(shape = itemShape) { + SwipeToDismissComponent( + contentShape = itemShape, + onSwiped = { /* TODO */ }, + ) { val transfer = getTransfers()[it] TransferItem( transfer = transfer, From f37ff875efe2dba976c76b2783a5b1d166c0a93b Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 11:53:58 +0100 Subject: [PATCH 08/15] chore: Rename SwipeToDismissComponent and move it to correct package --- ...raggable.kt => SwipeToDismissComponent.kt} | 25 +++++++++++-------- .../components/transfer/TransferItemList.kt | 1 + 2 files changed, 15 insertions(+), 11 deletions(-) rename app/src/main/java/com/infomaniak/swisstransfer/ui/components/{transfer/TransferItemDraggable.kt => SwipeToDismissComponent.kt} (85%) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt similarity index 85% rename from app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt rename to app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt index 2b34b3a41..1dffe78d4 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemDraggable.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt @@ -1,19 +1,21 @@ /* - * Copyright 2023 The Android Open Source Project + * Infomaniak SwissTransfer - Android + * Copyright (C) 2024 Infomaniak Network SA * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ -package com.infomaniak.swisstransfer.ui.components.transfer +package com.infomaniak.swisstransfer.ui.components import android.util.Log import androidx.compose.animation.animateColorAsState @@ -37,6 +39,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.TransferUi +import com.infomaniak.swisstransfer.ui.components.transfer.TransferItem import com.infomaniak.swisstransfer.ui.previewparameter.TransferUiListPreviewParameter import com.infomaniak.swisstransfer.ui.theme.CustomShapes import com.infomaniak.swisstransfer.ui.theme.Margin diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index a277699b4..8bd8a472f 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.TransferUi import com.infomaniak.multiplatform_swisstransfer.common.models.TransferDirection import com.infomaniak.swisstransfer.R +import com.infomaniak.swisstransfer.ui.components.SwipeToDismissComponent import com.infomaniak.swisstransfer.ui.previewparameter.TransferUiListPreviewParameter import com.infomaniak.swisstransfer.ui.theme.CustomShapes import com.infomaniak.swisstransfer.ui.theme.Margin From de33fd5adcbdb81b4adac860a70854ffd9c24f86 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 12:15:24 +0100 Subject: [PATCH 09/15] feat: Actually use the `onSwiped` callback to delete a Transfer --- .../swisstransfer/ui/components/transfer/TransferItemList.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index 8bd8a472f..ab04d9ee4 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -66,11 +66,11 @@ fun TransferItemList( key = { getTransfers()[it].uuid }, contentType = { getTransfers()[it] }, itemContent = { + val transfer = getTransfers()[it] SwipeToDismissComponent( contentShape = itemShape, - onSwiped = { /* TODO */ }, + onSwiped = { onSwiped(transfer.uuid) }, ) { - val transfer = getTransfers()[it] TransferItem( transfer = transfer, shape = itemShape, From 2e7ae556a328e5e9a1139618a1f81901c91c6bf9 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 12:49:09 +0100 Subject: [PATCH 10/15] refactor: Streamline configuration usage logic --- .../swisstransfer/ui/components/SwipeToDismissComponent.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt index 1dffe78d4..ae63ad7a8 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt @@ -68,7 +68,6 @@ fun SwipeToDismissComponent( // // TODO: This check triggers twice, why ?? :( // if (shouldDismiss) { // Log.i("TOTO", "SwipeToDismiss state : $value") - // // TODO: We should probably add an animation here // onSwiped() // } else { // Log.d("TOTO", "SwipeToDismiss state : $value") @@ -81,17 +80,16 @@ fun SwipeToDismissComponent( // TODO: Sometimes, this check triggers several times in a row, why ? if (state.currentValue == SwipeToDismissBoxValue.EndToStart) { Log.i("TOTO", "SwipeToDismiss state : EndToStart") - // TODO: We should probably add an animation here onSwiped() } // TODO: Red & Gray dark theme background colors need to be handled val backgroundColor by animateColorAsState( - targetValue = if (state.targetValue == SwipeToDismissBoxValue.Settled) defaultColor else swipedColor, + targetValue = if (state.targetValue == SwipeToDismissBoxValue.EndToStart) swipedColor else defaultColor, label = "Background color animation", ) val iconScale by animateFloatAsState( - targetValue = if (state.targetValue == SwipeToDismissBoxValue.Settled) minIconScale else maxIconScale, + targetValue = if (state.targetValue == SwipeToDismissBoxValue.EndToStart) maxIconScale else minIconScale, label = "Icon scale animation", ) val contentElevation by animateDpAsState( From b2683eae7bdaf7ac3ddb5d13c1f688a9574ce9b6 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 27 Nov 2024 12:52:45 +0100 Subject: [PATCH 11/15] refactor: Remove logs & temporary test code --- .../ui/components/SwipeToDismissComponent.kt | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt index ae63ad7a8..54af11259 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt @@ -17,7 +17,6 @@ */ package com.infomaniak.swisstransfer.ui.components -import android.util.Log import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState @@ -62,26 +61,10 @@ fun SwipeToDismissComponent( val swipedElevation = 4.dp val state = rememberSwipeToDismissBoxState( - // TODO: This block of code is commented for now, while we investigate the multiple triggers of dismiss state - // confirmValueChange = { value -> - // val shouldDismiss = value == SwipeToDismissBoxValue.EndToStart - // // TODO: This check triggers twice, why ?? :( - // if (shouldDismiss) { - // Log.i("TOTO", "SwipeToDismiss state : $value") - // onSwiped() - // } else { - // Log.d("TOTO", "SwipeToDismiss state : $value") - // } - // shouldDismiss - // }, positionalThreshold = { totalDistance -> totalDistance * dismissThreshold }, ) - // TODO: Sometimes, this check triggers several times in a row, why ? - if (state.currentValue == SwipeToDismissBoxValue.EndToStart) { - Log.i("TOTO", "SwipeToDismiss state : EndToStart") - onSwiped() - } + if (state.currentValue == SwipeToDismissBoxValue.EndToStart) onSwiped() // TODO: Red & Gray dark theme background colors need to be handled val backgroundColor by animateColorAsState( From 5322190993dc7be5510ab99a67bc5b089b517628 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 28 Nov 2024 13:06:29 +0100 Subject: [PATCH 12/15] style: Use correct Icon & Colors --- .../ui/components/SwipeToDismissComponent.kt | 18 +-- .../swisstransfer/ui/images/icons/Bin.kt | 144 +++++++++--------- .../swisstransfer/ui/theme/ColorDark.kt | 4 + .../swisstransfer/ui/theme/ColorLight.kt | 3 + .../swisstransfer/ui/theme/Theme.kt | 3 + 5 files changed, 88 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt index 54af11259..7195584a8 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt @@ -24,8 +24,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -33,12 +31,13 @@ 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.graphics.Shape import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.TransferUi import com.infomaniak.swisstransfer.ui.components.transfer.TransferItem +import com.infomaniak.swisstransfer.ui.images.AppImages.AppIcons +import com.infomaniak.swisstransfer.ui.images.icons.Bin import com.infomaniak.swisstransfer.ui.previewparameter.TransferUiListPreviewParameter import com.infomaniak.swisstransfer.ui.theme.CustomShapes import com.infomaniak.swisstransfer.ui.theme.Margin @@ -54,8 +53,6 @@ fun SwipeToDismissComponent( // Configuration val dismissThreshold = 0.5f - val defaultColor = Color.LightGray - val swipedColor = SwissTransferTheme.materialColors.error val minIconScale = 1.0f val maxIconScale = 1.5f val swipedElevation = 4.dp @@ -66,9 +63,12 @@ fun SwipeToDismissComponent( if (state.currentValue == SwipeToDismissBoxValue.EndToStart) onSwiped() - // TODO: Red & Gray dark theme background colors need to be handled val backgroundColor by animateColorAsState( - targetValue = if (state.targetValue == SwipeToDismissBoxValue.EndToStart) swipedColor else defaultColor, + targetValue = if (state.targetValue == SwipeToDismissBoxValue.EndToStart) { + SwissTransferTheme.colors.swipeDelete + } else { + SwissTransferTheme.colors.swipeDefault + }, label = "Background color animation", ) val iconScale by animateFloatAsState( @@ -95,9 +95,9 @@ fun SwipeToDismissComponent( .align(Alignment.CenterEnd) .padding(end = Margin.Large), ) { - // TODO: The dark theme icon color needs to be handled Icon( - imageVector = Icons.Default.Delete, + imageVector = AppIcons.Bin, + tint = SwissTransferTheme.colors.swipeIcon, modifier = Modifier.scale(iconScale), contentDescription = null, ) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/images/icons/Bin.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/images/icons/Bin.kt index ac2d5b551..fcb280e10 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/images/icons/Bin.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/images/icons/Bin.kt @@ -25,8 +25,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.StrokeCap.Companion.Butt -import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path @@ -34,6 +32,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.infomaniak.swisstransfer.ui.images.AppImages import com.infomaniak.swisstransfer.ui.images.AppImages.AppIcons +import androidx.compose.ui.graphics.StrokeCap.Companion.Round as strokeCapRound +import androidx.compose.ui.graphics.StrokeJoin.Companion.Round as strokeJoinRound val AppIcons.Bin: ImageVector get() { @@ -42,88 +42,82 @@ val AppIcons.Bin: ImageVector _bin = Builder( name = "Bin", - defaultWidth = 16.0.dp, - defaultHeight = 16.0.dp, - viewportWidth = 16.0f, - viewportHeight = 16.0f, + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, ).apply { path( - fill = SolidColor(Color(0xFFF7FCFA)), stroke = null, strokeLineWidth = 0.0f, - strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, - pathFillType = NonZero + fill = SolidColor(Color(0x00000000)), + stroke = SolidColor(Color(0xFF9F9F9F)), + strokeLineWidth = 1.0f, + strokeLineCap = strokeCapRound, + strokeLineJoin = strokeJoinRound, + strokeLineMiter = 4.0f, + pathFillType = NonZero, ) { - moveTo(15.502f, 2.115f) - horizontalLineTo(11.236f) - verticalLineTo(1.621f) - curveTo(11.236f, 1.198f, 11.093f, 0.775f, 10.738f, 0.493f) - curveTo(10.382f, 0.211f, 10.027f, 0.0f, 9.6f, 0.0f) - horizontalLineTo(6.471f) - curveTo(5.973f, 0.0f, 5.618f, 0.211f, 5.333f, 0.493f) - curveTo(4.978f, 0.775f, 4.836f, 1.198f, 4.836f, 1.621f) - verticalLineTo(2.185f) - horizontalLineTo(0.569f) - curveTo(0.284f, 2.185f, 0.0f, 2.396f, 0.0f, 2.749f) - curveTo(0.0f, 3.101f, 0.213f, 3.313f, 0.569f, 3.313f) - horizontalLineTo(1.707f) - lineTo(2.631f, 14.52f) - curveTo(2.631f, 14.943f, 2.844f, 15.295f, 3.129f, 15.577f) - curveTo(3.413f, 15.859f, 3.84f, 16.0f, 4.196f, 16.0f) - horizontalLineTo(11.804f) - curveTo(12.231f, 16.0f, 12.587f, 15.859f, 12.871f, 15.577f) - curveTo(13.156f, 15.295f, 13.369f, 14.943f, 13.369f, 14.52f) - lineTo(14.293f, 3.313f) - horizontalLineTo(15.431f) - curveTo(15.716f, 3.313f, 16.0f, 3.101f, 16.0f, 2.749f) - curveTo(16.0f, 2.396f, 15.787f, 2.115f, 15.502f, 2.115f) - close() - moveTo(5.902f, 1.621f) - curveTo(5.902f, 1.48f, 5.973f, 1.339f, 6.044f, 1.269f) - curveTo(6.116f, 1.198f, 6.258f, 1.128f, 6.4f, 1.128f) - horizontalLineTo(9.6f) - curveTo(9.742f, 1.128f, 9.884f, 1.198f, 9.956f, 1.269f) - curveTo(10.027f, 1.339f, 10.169f, 1.48f, 10.169f, 1.621f) - verticalLineTo(2.185f) - horizontalLineTo(5.902f) - verticalLineTo(1.621f) - close() - moveTo(12.373f, 14.379f) - curveTo(12.373f, 14.52f, 12.302f, 14.661f, 12.231f, 14.731f) - curveTo(12.16f, 14.802f, 12.018f, 14.872f, 11.876f, 14.872f) - horizontalLineTo(4.196f) - curveTo(4.053f, 14.872f, 3.911f, 14.802f, 3.84f, 14.731f) - curveTo(3.769f, 14.661f, 3.698f, 14.52f, 3.698f, 14.379f) - lineTo(2.773f, 3.172f) - horizontalLineTo(13.369f) - lineTo(12.373f, 14.379f) - close() + moveTo(20.609f, 4.826f) + lineTo(18.875f, 21.287f) + curveTo(18.826f, 21.757f, 18.604f, 22.192f, 18.253f, 22.508f) + curveTo(17.902f, 22.825f, 17.447f, 23.0f, 16.974f, 23.0f) + horizontalLineTo(7.026f) + curveTo(6.553f, 23.0f, 6.097f, 22.825f, 5.746f, 22.509f) + curveTo(5.395f, 22.192f, 5.173f, 21.757f, 5.124f, 21.287f) + lineTo(3.391f, 4.826f) } path( - fill = SolidColor(Color(0xFFF7FCFA)), stroke = null, strokeLineWidth = 0.0f, - strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, - pathFillType = NonZero + fill = SolidColor(Color(0x00000000)), + stroke = SolidColor(Color(0xFF9F9F9F)), + strokeLineWidth = 1.0f, + strokeLineCap = strokeCapRound, + strokeLineJoin = strokeJoinRound, + strokeLineMiter = 4.0f, + pathFillType = NonZero, ) { - moveTo(6.471f, 5.85f) - curveTo(6.116f, 5.85f, 5.902f, 6.062f, 5.902f, 6.414f) - verticalLineTo(11.7f) - curveTo(5.902f, 11.982f, 6.116f, 12.194f, 6.471f, 12.194f) - curveTo(6.827f, 12.194f, 7.04f, 11.982f, 7.04f, 11.63f) - verticalLineTo(6.414f) - curveTo(6.969f, 6.062f, 6.756f, 5.85f, 6.471f, 5.85f) - close() + moveTo(1.0f, 4.826f) + horizontalLineTo(23.0f) } path( - fill = SolidColor(Color(0xFFF7FCFA)), stroke = null, strokeLineWidth = 0.0f, - strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, - pathFillType = NonZero + fill = SolidColor(Color(0x00000000)), + stroke = SolidColor(Color(0xFF9F9F9F)), + strokeLineWidth = 1.0f, + strokeLineCap = strokeCapRound, + strokeLineJoin = strokeJoinRound, + strokeLineMiter = 4.0f, + pathFillType = NonZero, ) { - moveTo(9.6f, 5.85f) - curveTo(9.316f, 5.85f, 9.031f, 6.062f, 9.031f, 6.414f) - verticalLineTo(11.7f) - curveTo(9.031f, 11.982f, 9.244f, 12.264f, 9.6f, 12.264f) - curveTo(9.956f, 12.264f, 10.169f, 12.053f, 10.169f, 11.7f) - verticalLineTo(6.414f) - curveTo(10.169f, 6.062f, 9.956f, 5.85f, 9.6f, 5.85f) - close() + moveTo(7.696f, 4.826f) + verticalLineTo(1.957f) + curveTo(7.696f, 1.703f, 7.796f, 1.46f, 7.976f, 1.28f) + curveTo(8.155f, 1.101f, 8.399f, 1.0f, 8.652f, 1.0f) + horizontalLineTo(15.348f) + curveTo(15.601f, 1.0f, 15.845f, 1.101f, 16.024f, 1.28f) + curveTo(16.204f, 1.46f, 16.304f, 1.703f, 16.304f, 1.957f) + verticalLineTo(4.826f) + } + path( + fill = SolidColor(Color(0x00000000)), + stroke = SolidColor(Color(0xFF9F9F9F)), + strokeLineWidth = 1.0f, + strokeLineCap = strokeCapRound, + strokeLineJoin = strokeJoinRound, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(10.0f, 9.0f) + verticalLineTo(17.8f) + } + path( + fill = SolidColor(Color(0x00000000)), + stroke = SolidColor(Color(0xFF9F9F9F)), + strokeLineWidth = 1.0f, + strokeLineCap = strokeCapRound, + strokeLineJoin = strokeJoinRound, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(14.0f, 9.0f) + verticalLineTo(17.8f) } }.build() diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt index 069a511db..b72d9e6eb 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt @@ -40,6 +40,7 @@ private const val specific5 = 0xFF49DEFD // Extra palette private const val elephant = 0xFF666666 private const val white = 0xFFFFFFFF +private const val black = 0xFF000000 private const val black_translucent = 0x80000000 private const val warning = 0xFFFFAA4C @@ -95,4 +96,7 @@ val CustomDarkColorScheme = CustomColorScheme( transferListStroke = Color(green_main), highlightedColor = Color(green_dark), warning = Color(warning), + swipeDefault = Color(elephant), + swipeDelete = Color(error), + swipeIcon = Color(black), ) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorLight.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorLight.kt index 331201aa3..a86c77f39 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorLight.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorLight.kt @@ -96,4 +96,7 @@ val CustomLightColorScheme = CustomColorScheme( transferListStroke = Color(green_dark), highlightedColor = Color(green_secondary), warning = Color(warning), + swipeDefault = Color(shark), + swipeDelete = Color(error), + swipeIcon = Color(white), ) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/Theme.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/Theme.kt index b89c20655..f54234cc0 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/Theme.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/Theme.kt @@ -90,6 +90,9 @@ data class CustomColorScheme( val transferListStroke: Color = Color.Unspecified, val highlightedColor: Color = Color.Unspecified, val warning: Color = Color.Unspecified, + val swipeDefault: Color = Color.Unspecified, + val swipeDelete: Color = Color.Unspecified, + val swipeIcon: Color = Color.Unspecified, ) private val Shapes = Shapes( From 4b00d4d5058d935cc80255c011d1a497e5a1bc55 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 28 Nov 2024 09:38:06 +0100 Subject: [PATCH 13/15] style: Use a lighter shade of grey for Dark Theme swipe background --- .../java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt index b72d9e6eb..d9f68364d 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/theme/ColorDark.kt @@ -38,6 +38,7 @@ private const val specific4 = 0xFFEAC35D private const val specific5 = 0xFF49DEFD // Extra palette +private const val grey = 0xFFCFCFCF private const val elephant = 0xFF666666 private const val white = 0xFFFFFFFF private const val black = 0xFF000000 @@ -96,7 +97,7 @@ val CustomDarkColorScheme = CustomColorScheme( transferListStroke = Color(green_main), highlightedColor = Color(green_dark), warning = Color(warning), - swipeDefault = Color(elephant), + swipeDefault = Color(grey), swipeDelete = Color(error), swipeIcon = Color(black), ) From 45754a58570318c2bebf8f749c137e1b2f5c145c Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 28 Nov 2024 09:42:17 +0100 Subject: [PATCH 14/15] refactor: Clean code before review --- .../ui/components/SwipeToDismissComponent.kt | 14 +++++++++----- .../ui/components/transfer/TransferItemList.kt | 2 +- .../ui/screen/main/received/ReceivedScreen.kt | 2 +- .../ui/screen/main/sent/SentScreen.kt | 2 +- .../ui/screen/main/transfers/TransfersViewModel.kt | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt index 7195584a8..5e57adef5 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt @@ -24,7 +24,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart +import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -61,10 +65,10 @@ fun SwipeToDismissComponent( positionalThreshold = { totalDistance -> totalDistance * dismissThreshold }, ) - if (state.currentValue == SwipeToDismissBoxValue.EndToStart) onSwiped() + if (state.currentValue == EndToStart) onSwiped() val backgroundColor by animateColorAsState( - targetValue = if (state.targetValue == SwipeToDismissBoxValue.EndToStart) { + targetValue = if (state.targetValue == EndToStart) { SwissTransferTheme.colors.swipeDelete } else { SwissTransferTheme.colors.swipeDefault @@ -72,11 +76,11 @@ fun SwipeToDismissComponent( label = "Background color animation", ) val iconScale by animateFloatAsState( - targetValue = if (state.targetValue == SwipeToDismissBoxValue.EndToStart) maxIconScale else minIconScale, + targetValue = if (state.targetValue == EndToStart) maxIconScale else minIconScale, label = "Icon scale animation", ) val contentElevation by animateDpAsState( - targetValue = if (state.dismissDirection == SwipeToDismissBoxValue.EndToStart) swipedElevation else 0.dp, + targetValue = if (state.dismissDirection == EndToStart) swipedElevation else 0.dp, label = "Content elevation animation", ) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt index ab04d9ee4..6cec38fc2 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/transfer/TransferItemList.kt @@ -47,11 +47,11 @@ fun TransferItemList( ) { val selectedTransferUuid = getSelectedTransferUuid() + val itemShape = CustomShapes.SMALL val titleRes = when (direction) { TransferDirection.SENT -> R.string.sentFilesTitle TransferDirection.RECEIVED -> R.string.receivedFilesTitle } - val itemShape = CustomShapes.SMALL LazyColumn( modifier = modifier, diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/received/ReceivedScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/received/ReceivedScreen.kt index 2b0a985c5..2f7c9eaf4 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/received/ReceivedScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/received/ReceivedScreen.kt @@ -52,7 +52,7 @@ fun ReceivedScreen( navigateToDetails = navigateToDetails, getSelectedTransferUuid = getSelectedTransferUuid, getTransfers = { transfers!! }, - onSwiped = transfersViewModel::deleteSwipedTransfer, + onSwiped = transfersViewModel::deleteTransfer, ) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/sent/SentScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/sent/SentScreen.kt index 2d8b3d08e..a4a541c93 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/sent/SentScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/sent/SentScreen.kt @@ -59,7 +59,7 @@ fun SentScreen( navigateToDetails = navigateToDetails, getSelectedTransferUuid = getSelectedTransferUuid, getTransfers = { transfers!! }, - onSwiped = transfersViewModel::deleteSwipedTransfer, + onSwiped = transfersViewModel::deleteTransfer, ) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersViewModel.kt index 10deeaea2..c48eb7160 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersViewModel.kt @@ -59,7 +59,7 @@ class TransfersViewModel @Inject constructor( } } - fun deleteSwipedTransfer(transferUuid: String) { + fun deleteTransfer(transferUuid: String) { viewModelScope.launch(ioDispatcher) { runCatching { transferManager.deleteTransfer(transferUuid) From 6039aba710bf5528e13648fed7084ea86a2321aa Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 28 Nov 2024 09:49:29 +0100 Subject: [PATCH 15/15] refactor: Move configuration values to constants --- .../ui/components/SwipeToDismissComponent.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt index 5e57adef5..afd6aba1a 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/components/SwipeToDismissComponent.kt @@ -48,6 +48,11 @@ import com.infomaniak.swisstransfer.ui.theme.Margin import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark +private const val DISMISS_THRESHOLD = 0.5f +private const val MIN_ICON_SCALE = 1.0f +private const val MAX_ICON_SCALE = 1.5f +private const val SWIPED_ELEVATION = 4 + @Composable fun SwipeToDismissComponent( contentShape: Shape, @@ -55,14 +60,8 @@ fun SwipeToDismissComponent( content: @Composable () -> Unit, ) { - // Configuration - val dismissThreshold = 0.5f - val minIconScale = 1.0f - val maxIconScale = 1.5f - val swipedElevation = 4.dp - val state = rememberSwipeToDismissBoxState( - positionalThreshold = { totalDistance -> totalDistance * dismissThreshold }, + positionalThreshold = { totalDistance -> totalDistance * DISMISS_THRESHOLD }, ) if (state.currentValue == EndToStart) onSwiped() @@ -76,11 +75,11 @@ fun SwipeToDismissComponent( label = "Background color animation", ) val iconScale by animateFloatAsState( - targetValue = if (state.targetValue == EndToStart) maxIconScale else minIconScale, + targetValue = if (state.targetValue == EndToStart) MAX_ICON_SCALE else MIN_ICON_SCALE, label = "Icon scale animation", ) val contentElevation by animateDpAsState( - targetValue = if (state.dismissDirection == EndToStart) swipedElevation else 0.dp, + targetValue = if (state.dismissDirection == EndToStart) SWIPED_ELEVATION.dp else 0.dp, label = "Content elevation animation", )