From 921bafe5b9cab21fe12232984bcfeb831d23cd5b Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 13 May 2024 16:48:14 -0400 Subject: [PATCH] feat: allow overring back/close buttons in modals add an override for back to navigate to bucket debugger when enabled to allow changing balance currencies Signed-off-by: Brandon McAnsh --- .../getcode/navigation/screens/ChatScreens.kt | 51 +++++-- .../getcode/navigation/screens/MainScreens.kt | 8 +- .../navigation/screens/ModalScreens.kt | 48 +++---- .../com/getcode/navigation/screens/Modals.kt | 43 +++--- .../navigation/screens/WithdrawalScreens.kt | 6 +- .../com/getcode/ui/components/SheetTitle.kt | 48 +++++-- .../getcode/ui/components/SwipeableView.kt | 134 ------------------ 7 files changed, 119 insertions(+), 219 deletions(-) delete mode 100644 app/src/main/java/com/getcode/ui/components/SwipeableView.kt diff --git a/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt b/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt index 4f61215b1..bfa2f255a 100644 --- a/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt @@ -6,7 +6,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.BubbleChart import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -16,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -35,9 +39,10 @@ import com.getcode.model.ID import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.theme.BrandLight import com.getcode.theme.CodeTheme +import com.getcode.ui.components.SheetTitleDefaults import com.getcode.ui.components.SheetTitleText -import com.getcode.ui.utils.getActivityScopedViewModel import com.getcode.ui.components.chat.localized +import com.getcode.ui.utils.getActivityScopedViewModel import com.getcode.util.formatDateRelatively import com.getcode.view.main.balance.BalanceScreeen import com.getcode.view.main.balance.BalanceSheetViewModel @@ -45,7 +50,6 @@ import com.getcode.view.main.chat.ChatScreen import com.getcode.view.main.chat.ChatViewModel import com.getcode.view.main.chat.conversation.ChatConversationScreen import com.getcode.view.main.chat.conversation.ConversationViewModel -import com.getcode.view.main.giveKin.GiveKinScreen import com.getcode.view.main.home.HomeViewModel import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn @@ -73,18 +77,43 @@ data object BalanceModal : ChatGraph, ModalRoot { derivedStateOf { state.isBucketDebuggerVisible } } + val backButton = @Composable { + when { + isViewingBuckets -> SheetTitleDefaults.BackButton() + !isViewingBuckets && state.isBucketDebuggerEnabled -> { + Icon( + imageVector = Icons.Rounded.BubbleChart, + contentDescription = "", + tint = Color.White, + ) + } + else -> Unit + } + } + ModalContainer( navigator = navigator, onLogoClicked = {}, - backButton = { isViewingBuckets }, - onBackClicked = isViewingBuckets.takeIf { it }?.let { - { - viewModel.dispatchEvent( - BalanceSheetViewModel.Event.OnDebugBucketsVisible(false) - ) + backButton = backButton, + backButtonEnabled = { isViewingBuckets || state.isBucketDebuggerEnabled }, + onBackClicked = when { + isViewingBuckets -> { + { + viewModel.dispatchEvent( + BalanceSheetViewModel.Event.OnDebugBucketsVisible(false) + ) + } + } + state.isBucketDebuggerEnabled -> { + { + viewModel.dispatchEvent( + BalanceSheetViewModel.Event.OnDebugBucketsVisible(true) + ) + } } + else -> null }, - closeButton = close@{ + closeButtonEnabled = close@{ if (viewModel.stateFlow.value.isBucketDebuggerVisible) return@close false if (navigator.isVisible) { it is BalanceModal @@ -127,7 +156,7 @@ data class ChatScreen(val chatId: ID) : ChatGraph, ModalContent { ModalContainer( titleString = { state.title.localized }, - backButton = { it is ChatScreen }, + backButtonEnabled = { it is ChatScreen }, ) { val messages = vm.chatMessages.collectAsLazyPagingItems() ChatScreen(state = state, messages = messages, dispatch = vm::dispatchEvent) @@ -197,7 +226,7 @@ data class ChatMessageConversationScreen(val messageId: ID) : AppScreen(), ChatG } } }, - backButton = { it is ChatMessageConversationScreen }, + backButtonEnabled = { it is ChatMessageConversationScreen }, ) { val messages = vm.messages.collectAsLazyPagingItems() ChatConversationScreen(state, messages, vm::dispatchEvent) diff --git a/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt b/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt index 671a8aa5c..b514811b5 100644 --- a/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt @@ -86,7 +86,7 @@ data object GiveKinModal : AppScreen(), MainGraph, ModalRoot { override fun Content() { val navigator = LocalCodeNavigator.current ModalContainer( - closeButton = { + closeButtonEnabled = { if (navigator.isVisible) { it is GiveKinModal } else { @@ -125,7 +125,7 @@ data class RequestKinModal( if (showClose) { ModalContainer( - closeButton = { + closeButtonEnabled = { if (navigator.isVisible) { it is RequestKinModal } else { @@ -137,7 +137,7 @@ data class RequestKinModal( } } else { ModalContainer( - backButton = { + backButtonEnabled = { if (navigator.isVisible) { it is RequestKinModal } else { @@ -168,7 +168,7 @@ data object AccountModal : MainGraph, ModalRoot { ModalContainer( displayLogo = true, onLogoClicked = { viewModel.dispatchEvent(AccountSheetViewModel.Event.LogoClicked) }, - closeButton = { + closeButtonEnabled = { if (navigator.isVisible) { it is AccountModal } else { diff --git a/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt b/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt index eb73d6abb..761b88423 100644 --- a/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt @@ -1,7 +1,6 @@ package com.getcode.navigation.screens import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.core.screen.ScreenKey @@ -32,15 +31,8 @@ import com.getcode.view.main.getKin.GetKinSheetViewModel import com.getcode.view.main.getKin.ReferFriend import com.getcode.view.main.tip.EnterTipScreen import com.getcode.view.main.tip.RequestTipScreen -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.delayFlow -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import timber.log.Timber @Parcelize @@ -53,7 +45,7 @@ data object DepositKinScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is DepositKinScreen }) { + ModalContainer(backButtonEnabled = { it is DepositKinScreen }) { AccountDeposit() } @@ -74,7 +66,7 @@ data object FaqScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is FaqScreen }) { + ModalContainer(backButtonEnabled = { it is FaqScreen }) { AccountFaq(getViewModel()) } @@ -95,7 +87,7 @@ data object AccountDebugOptionsScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is AccountDebugOptionsScreen }) { + ModalContainer(backButtonEnabled = { it is AccountDebugOptionsScreen }) { BetaFlagsScreen(getViewModel()) } @@ -116,7 +108,7 @@ data object AccountDetailsScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is AccountDetailsScreen }) { + ModalContainer(backButtonEnabled = { it is AccountDetailsScreen }) { AccountDetails(getActivityScopedViewModel()) } } @@ -132,7 +124,7 @@ data object BackupScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is BackupScreen }) { + ModalContainer(backButtonEnabled = { it is BackupScreen }) { BackupKey(getViewModel()) } @@ -154,7 +146,7 @@ data object PhoneNumberScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is PhoneNumberScreen }) { + ModalContainer(backButtonEnabled = { it is PhoneNumberScreen }) { AccountPhone(getViewModel()) } } @@ -181,7 +173,7 @@ data class PhoneVerificationScreen( override fun Content() { val navigator = LocalCodeNavigator.current val viewModel = getStackScopedViewModel(key) - ModalContainer(backButton = { it is PhoneVerificationScreen }) { + ModalContainer(backButtonEnabled = { it is PhoneVerificationScreen }) { PhoneVerify(viewModel, arguments) { navigator.show(PhoneAreaSelectionModal(key)) } @@ -202,7 +194,7 @@ data class PhoneAreaSelectionModal(val providedKey: String) : MainGraph, ModalCo val navigator = LocalCodeNavigator.current val vm = getStackScopedViewModel(providedKey) - ModalContainer(closeButton = { it is PhoneAreaSelectionModal }) { + ModalContainer(closeButtonEnabled = { it is PhoneAreaSelectionModal }) { PhoneCountrySelection(viewModel = vm) { navigator.hide() } @@ -229,7 +221,7 @@ data class PhoneConfirmationScreen( @Composable override fun Content() { - ModalContainer(backButton = { it is PhoneConfirmationScreen }) { + ModalContainer(backButtonEnabled = { it is PhoneConfirmationScreen }) { PhoneConfirm( getViewModel(), arguments = arguments, @@ -249,7 +241,7 @@ data object DeleteCodeScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is DeleteCodeScreen }) { + ModalContainer(backButtonEnabled = { it is DeleteCodeScreen }) { DeleteCodeAccount() } } @@ -265,7 +257,7 @@ data object DeleteConfirmationScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is DeleteConfirmationScreen }) { + ModalContainer(backButtonEnabled = { it is DeleteConfirmationScreen }) { ConfirmDeleteAccount(getViewModel()) } } @@ -278,7 +270,7 @@ data object ReferFriendScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is DeleteConfirmationScreen }) { + ModalContainer(backButtonEnabled = { it is DeleteConfirmationScreen }) { ReferFriend() } } @@ -297,7 +289,7 @@ data object CurrencySelectionModal : MainGraph, ModalContent { override fun Content() { val navigator = LocalCodeNavigator.current ModalContainer( - backButton = { + backButtonEnabled = { if (navigator.isVisible) { it is CurrencySelectionModal } else { @@ -339,7 +331,7 @@ data class BuyMoreKinModal( if (showClose) { ModalContainer( - closeButton = { + closeButtonEnabled = { if (navigator.isVisible) { it is BuyMoreKinModal } else { @@ -351,7 +343,7 @@ data class BuyMoreKinModal( } } else { ModalContainer( - backButton = { + backButtonEnabled = { if (navigator.isVisible) { it is BuyMoreKinModal } else { @@ -387,7 +379,7 @@ data class EnterTipModal(val isInChat: Boolean = false) : MainGraph, ModalRoot { val navigator = LocalCodeNavigator.current if (isInChat) { ModalContainer( - backButton = { + backButtonEnabled = { if (navigator.isVisible) { it is EnterTipModal } else { @@ -401,7 +393,7 @@ data class EnterTipModal(val isInChat: Boolean = false) : MainGraph, ModalRoot { } } else { ModalContainer( - closeButton = { + closeButtonEnabled = { if (navigator.isVisible) { it is EnterTipModal } else { @@ -427,7 +419,7 @@ data object RequestTip : MainGraph, ModalContent { override fun Content() { val navigator = LocalCodeNavigator.current ModalContainer( - backButton = { + backButtonEnabled = { if (navigator.isVisible) { it is RequestTip } else { @@ -451,7 +443,7 @@ data object GetKinModal : MainGraph, ModalRoot { val viewModel = getViewModel() ModalContainer( - closeButton = { + closeButtonEnabled = { if (navigator.isVisible) { it is GetKinModal } else { @@ -476,7 +468,7 @@ data object BuySellScreen : MainGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is BuySellScreen }) { + ModalContainer(backButtonEnabled = { it is BuySellScreen }) { BuyAndSellKin(getViewModel()) } diff --git a/app/src/main/java/com/getcode/navigation/screens/Modals.kt b/app/src/main/java/com/getcode/navigation/screens/Modals.kt index d1d2c12ca..ea3876c89 100644 --- a/app/src/main/java/com/getcode/navigation/screens/Modals.kt +++ b/app/src/main/java/com/getcode/navigation/screens/Modals.kt @@ -1,46 +1,35 @@ package com.getcode.navigation.screens -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.LocalOverscrollConfiguration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey import com.getcode.LocalBetaFlags import com.getcode.MainRoot -import com.getcode.R import com.getcode.TopLevelViewModel import com.getcode.navigation.core.CodeNavigator import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.theme.CodeTheme -import com.getcode.ui.components.CodeCircularProgressIndicator import com.getcode.ui.components.SheetTitle +import com.getcode.ui.components.SheetTitleDefaults import com.getcode.ui.components.SheetTitleText import com.getcode.ui.components.keyboardAsState import com.getcode.ui.utils.getActivityScopedViewModel @@ -56,9 +45,9 @@ internal fun NamedScreen.ModalContainer( ModalContainer( navigator = LocalCodeNavigator.current, displayLogo = false, - backButton = { false }, + backButtonEnabled = { false }, onLogoClicked = {}, - closeButton = closeButton, + closeButtonEnabled = closeButton, screenContent = screenContent, ) } @@ -73,9 +62,9 @@ internal fun NamedScreen.ModalContainer( ModalContainer( navigator = LocalCodeNavigator.current, displayLogo = displayLogo, - backButton = { false }, + backButtonEnabled = { false }, onLogoClicked = onLogoClicked, - closeButton = closeButton, + closeButtonEnabled = closeButton, screenContent = screenContent, ) } @@ -87,9 +76,11 @@ internal fun NamedScreen.ModalContainer( displayLogo: Boolean = false, titleString: @Composable (NamedScreen?) -> String? = { name }, title: @Composable BoxScope.() -> Unit = { }, - backButton: (Screen?) -> Boolean = { false }, + backButton: @Composable () -> Unit = { SheetTitleDefaults.BackButton() }, + backButtonEnabled: (Screen?) -> Boolean = { false }, onBackClicked: (() -> Unit)? = null, - closeButton: (Screen?) -> Boolean = { false }, + closeButton: @Composable () -> Unit = { SheetTitleDefaults.CloseButton() }, + closeButtonEnabled: (Screen?) -> Boolean = { false }, onCloseClicked: (() -> Unit)? = null, onLogoClicked: () -> Unit = { }, screenContent: @Composable () -> Unit @@ -103,12 +94,12 @@ internal fun NamedScreen.ModalContainer( derivedStateOf { navigator.lastModalItem } } - val isBackEnabled by remember(backButton, lastItem) { - derivedStateOf { backButton(lastItem) } + val isBackEnabled by remember(backButtonEnabled, lastItem) { + derivedStateOf { backButtonEnabled(lastItem) } } - val isCloseEnabled by remember(closeButton, lastItem) { - derivedStateOf { closeButton(lastItem) } + val isCloseEnabled by remember(closeButtonEnabled, lastItem) { + derivedStateOf { closeButtonEnabled(lastItem) } } val keyboardVisible by keyboardAsState() @@ -134,8 +125,10 @@ internal fun NamedScreen.ModalContainer( displayLogo = displayLogo, onLogoClicked = onLogoClicked, // hide while transitioning to/from other destinations - backButton = isBackEnabled, - closeButton = isCloseEnabled, + backButton = backButton, + closeButton = closeButton, + backButtonEnabled = isBackEnabled, + closeButtonEnabled = isCloseEnabled, onBackIconClicked = onBackClicked?.let { { it() } } ?: { hideSheet { navigator.pop() } }, onCloseIconClicked = onCloseClicked?.let { { it() } } diff --git a/app/src/main/java/com/getcode/navigation/screens/WithdrawalScreens.kt b/app/src/main/java/com/getcode/navigation/screens/WithdrawalScreens.kt index 3d9a07b5c..a642c4a10 100644 --- a/app/src/main/java/com/getcode/navigation/screens/WithdrawalScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/WithdrawalScreens.kt @@ -29,7 +29,7 @@ internal data object WithdrawalAmountScreen : WithdrawalGraph, ModalContent { @Composable override fun Content() { - ModalContainer(backButton = { it is WithdrawalAmountScreen }) { + ModalContainer(backButtonEnabled = { it is WithdrawalAmountScreen }) { AccountWithdrawAmount(viewModel = getViewModel()) } @@ -73,7 +73,7 @@ data class WithdrawalAddressScreen(override val arguments: WithdrawalArgs = With @Composable override fun Content() { - ModalContainer(backButton = { it is WithdrawalAddressScreen }) { + ModalContainer(backButtonEnabled = { it is WithdrawalAddressScreen }) { AccountWithdrawAddress(getViewModel(), arguments) } } @@ -111,7 +111,7 @@ data class WithdrawalSummaryScreen(override val arguments: WithdrawalArgs = With @Composable override fun Content() { - ModalContainer(backButton = { it is WithdrawalSummaryScreen }) { + ModalContainer(backButtonEnabled = { it is WithdrawalSummaryScreen }) { AccountWithdrawSummary(getViewModel(), arguments) } } diff --git a/app/src/main/java/com/getcode/ui/components/SheetTitle.kt b/app/src/main/java/com/getcode/ui/components/SheetTitle.kt index 642745dc2..c6561559f 100644 --- a/app/src/main/java/com/getcode/ui/components/SheetTitle.kt +++ b/app/src/main/java/com/getcode/ui/components/SheetTitle.kt @@ -37,15 +37,37 @@ fun BoxScope.SheetTitleText(modifier: Modifier = Modifier, text: String) { ) } +object SheetTitleDefaults { + @Composable + fun BackButton() { + Icon( + imageVector = Icons.Outlined.ArrowBack, + contentDescription = "", + tint = Color.White, + ) + } + + @Composable + fun CloseButton() { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = "", + tint = Color.White, + ) + } +} + @Composable fun SheetTitle( modifier: Modifier = Modifier, title: @Composable BoxScope.() -> Unit = { }, displayLogo: Boolean = false, onLogoClicked: () -> Unit = { }, - backButton: Boolean = false, + backButton: @Composable () -> Unit = { SheetTitleDefaults.BackButton() }, + backButtonEnabled: Boolean = false, onBackIconClicked: () -> Unit = {}, - closeButton: Boolean = !backButton, + closeButton: @Composable () -> Unit = { SheetTitleDefaults.CloseButton() }, + closeButtonEnabled: Boolean = !backButtonEnabled, onCloseIconClicked: () -> Unit = {}, ) { Surface( @@ -60,32 +82,30 @@ fun SheetTitle( .fillMaxWidth() .height(topBarHeight), ) { - if (closeButton) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = "", - tint = Color.White, + if (closeButtonEnabled) { + Box( modifier = Modifier .align(Alignment.CenterEnd) .padding(end = CodeTheme.dimens.inset) .wrapContentWidth() .size(CodeTheme.dimens.staticGrid.x6) .unboundedClickable { onCloseIconClicked() } - ) + ) { + closeButton() + } } - if (backButton) { - Icon( - imageVector = Icons.Outlined.ArrowBack, - contentDescription = "", - tint = Color.White, + if (backButtonEnabled) { + Box( modifier = Modifier .align(Alignment.CenterStart) .padding(start = CodeTheme.dimens.inset) .wrapContentWidth() .size(CodeTheme.dimens.staticGrid.x6) .unboundedClickable { onBackIconClicked() } - ) + ) { + backButton() + } } if (displayLogo) { diff --git a/app/src/main/java/com/getcode/ui/components/SwipeableView.kt b/app/src/main/java/com/getcode/ui/components/SwipeableView.kt deleted file mode 100644 index 4710455b8..000000000 --- a/app/src/main/java/com/getcode/ui/components/SwipeableView.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.getcode.ui.components - -import android.annotation.SuppressLint -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import kotlinx.coroutines.launch -import kotlin.math.roundToInt - -enum class SwipeCardState { - DEFAULT, - LEFT, - RIGHT -} - -@SuppressLint("CoroutineCreationDuringComposition") -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun SwipeableView( - modifier: Modifier = Modifier, - leftSwipeCard: (@Composable () -> Unit)? = null, - rightSwipeCard: (@Composable () -> Unit)? = null, - leftSwiped: (() -> Unit)? = null, - rightSwiped: (() -> Unit)? = null, - isSwipeEnabled: Boolean = true, - animationSpec: AnimationSpec = tween(250), - thresholds: (from: SwipeCardState, to: SwipeCardState) -> ThresholdConfig = { _, _ -> - FractionalThreshold( - 0.25f - ) - }, - velocityThreshold: Dp = 125.dp, - mainCard: @Composable () -> Unit, -) { - ConstraintLayout(modifier = modifier) { - val (mainCardRef, actionCardRef) = createRefs() - val swipeableState = rememberSwipeableState( - initialValue = SwipeCardState.DEFAULT, - animationSpec = animationSpec - ) - val coroutineScope = rememberCoroutineScope() - - /* Tracks if left or right action card to be shown */ - val swipeLeftCardVisible = remember { mutableStateOf(true) } - - /* Disable swipe when card is animating back to default position */ - val swipeEnabled = remember { mutableStateOf(isSwipeEnabled) } - - val maxWidthInPx = with(LocalDensity.current) { - LocalConfiguration.current.screenWidthDp.dp.toPx() - } - - val anchors = hashMapOf(0f to SwipeCardState.DEFAULT) - if (leftSwipeCard != null) - anchors[-maxWidthInPx] = SwipeCardState.LEFT - - if (rightSwipeCard != null) - anchors[maxWidthInPx] = SwipeCardState.RIGHT - - /* This surface is for action card which is below the main card */ - Surface( - color = Color.Transparent, - content = if (swipeLeftCardVisible.value) { - leftSwipeCard - } else { - rightSwipeCard - } ?: {}, - modifier = Modifier - .fillMaxWidth() - .constrainAs(actionCardRef) { - top.linkTo(mainCardRef.top) - bottom.linkTo(mainCardRef.bottom) - height = Dimension.fillToConstraints - } - ) - - Surface( - color = Color.Transparent, - modifier = Modifier - .fillMaxWidth() - .offset { - var offset = swipeableState.offset.value.roundToInt() - if (offset < 0 && leftSwipeCard == null) offset = 0 - if (offset > 0 && rightSwipeCard == null) offset = 0 - IntOffset(offset, 0) - } - .swipeable( - state = swipeableState, - anchors = anchors, - orientation = Orientation.Horizontal, - enabled = swipeEnabled.value && isSwipeEnabled, - thresholds = thresholds, - velocityThreshold = velocityThreshold - ) - .constrainAs(mainCardRef) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - }) { - - if (swipeableState.currentValue == SwipeCardState.LEFT && !swipeableState.isAnimationRunning) { - leftSwiped?.invoke() - coroutineScope.launch { - swipeEnabled.value = false - swipeableState.animateTo(SwipeCardState.DEFAULT) - swipeEnabled.value = true - } - } else if (swipeableState.currentValue == SwipeCardState.RIGHT && !swipeableState.isAnimationRunning) { - rightSwiped?.invoke() - coroutineScope.launch { - swipeEnabled.value = false - swipeableState.animateTo(SwipeCardState.DEFAULT) - swipeEnabled.value = true - } - } - - swipeLeftCardVisible.value = swipeableState.offset.value <= 0 - - mainCard() - } - } -} \ No newline at end of file