From df7b4632b7a71fdd9a3491b806f8d51f592556e2 Mon Sep 17 00:00:00 2001 From: Diego Beraldin Date: Tue, 17 Oct 2023 22:31:21 +0200 Subject: [PATCH] chore: update SwipeableCard sensitivity --- .../core/commonui/components/SwipeableCard.kt | 115 ++++++++++++++++-- 1 file changed, 106 insertions(+), 9 deletions(-) diff --git a/core-commonui/components/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/SwipeableCard.kt b/core-commonui/components/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/SwipeableCard.kt index 56e6d1885..720b60f4a 100644 --- a/core-commonui/components/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/SwipeableCard.kt +++ b/core-commonui/components/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/SwipeableCard.kt @@ -2,32 +2,48 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.material.DismissDirection +import androidx.compose.material.DismissState import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FixedThreshold import androidx.compose.material.FractionalThreshold -import androidx.compose.material.SwipeToDismiss +import androidx.compose.material.ResistanceConfig +import androidx.compose.material.SwipeableDefaults +import androidx.compose.material.ThresholdConfig import androidx.compose.material.rememberDismissState +import androidx.compose.material.swipeable import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlin.math.roundToInt @OptIn(ExperimentalMaterialApi::class) @Composable @@ -47,15 +63,18 @@ fun SwipeableCard( ) { if (enabled) { var width by remember { mutableStateOf(0f) } + val dismissToEndCallback by rememberUpdatedState(onDismissToEnd) + val dismissToStartCallback by rememberUpdatedState(onDismissToStart) + val gestureBeginCallback by rememberUpdatedState(onGestureBegin) val dismissState = rememberDismissState( confirmStateChange = { when (it) { DismissValue.DismissedToEnd -> { - onDismissToEnd?.invoke() + dismissToEndCallback?.invoke() } DismissValue.DismissedToStart -> { - onDismissToStart?.invoke() + dismissToStartCallback?.invoke() } else -> Unit @@ -66,19 +85,20 @@ fun SwipeableCard( val threshold = 0.15f LaunchedEffect(dismissState) { - snapshotFlow { dismissState.offset.value }.map { + snapshotFlow { dismissState.offset.value }.map { offset -> when { - it > width * threshold -> DismissDirection.StartToEnd - it < -width * threshold -> DismissDirection.EndToStart + offset > width * threshold -> DismissDirection.StartToEnd + offset < -width * threshold -> DismissDirection.EndToStart else -> null } }.stateIn(this).onEach { willDismissDirection -> if (willDismissDirection != null) { - onGestureBegin?.invoke() + gestureBeginCallback?.invoke() } }.launchIn(this) } - SwipeToDismiss( + + SwipeToDismiss2( modifier = modifier.onGloballyPositioned { width = it.size.toSize().width }, @@ -89,7 +109,7 @@ fun SwipeableCard( }, background = { val direction = - dismissState.dismissDirection ?: return@SwipeToDismiss + dismissState.dismissDirection ?: DismissDirection.StartToEnd val bgColor by animateColorAsState( backgroundColor(dismissState.targetValue), ) @@ -114,3 +134,80 @@ fun SwipeableCard( content() } } + +/* + * Copied from androidx.material.SwipeToDismiss with different ResistanceConfig and velocity threshold. + */ +@Composable +@ExperimentalMaterialApi +private fun SwipeToDismiss2( + state: DismissState, + modifier: Modifier = Modifier, + directions: Set = setOf( + DismissDirection.EndToStart, + DismissDirection.StartToEnd + ), + dismissThresholds: (DismissDirection) -> ThresholdConfig = { + FixedThreshold(DISMISS_THRESHOLD) + }, + background: @Composable RowScope.() -> Unit, + dismissContent: @Composable RowScope.() -> Unit, +) = BoxWithConstraints(modifier) { + val width = constraints.maxWidth.toFloat() + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + + val anchors = mutableMapOf(0f to DismissValue.Default) + if (DismissDirection.StartToEnd in directions) anchors += width to DismissValue.DismissedToEnd + if (DismissDirection.EndToStart in directions) anchors += -width to DismissValue.DismissedToStart + + val thresholds = { from: DismissValue, to: DismissValue -> + dismissThresholds(getDismissDirection(from, to)!!) + } + Box( + Modifier.swipeable( + state = state, + anchors = anchors, + thresholds = thresholds, + orientation = Orientation.Horizontal, + enabled = state.currentValue == DismissValue.Default, + reverseDirection = isRtl, + resistance = ResistanceConfig( + basis = width, + factorAtMin = SwipeableDefaults.StiffResistanceFactor, + factorAtMax = SwipeableDefaults.StiffResistanceFactor + ), + velocityThreshold = Dp.Infinity + ) + ) { + Row( + content = background, + modifier = Modifier.matchParentSize() + ) + Row( + content = dismissContent, + modifier = Modifier.offset { IntOffset(state.offset.value.roundToInt(), 0) } + ) + } +} + +private fun getDismissDirection(from: DismissValue, to: DismissValue): DismissDirection? { + return when { + // settled at the default state + from == to && from == DismissValue.Default -> null + // has been dismissed to the end + from == to && from == DismissValue.DismissedToEnd -> DismissDirection.StartToEnd + // has been dismissed to the start + from == to && from == DismissValue.DismissedToStart -> DismissDirection.EndToStart + // is currently being dismissed to the end + from == DismissValue.Default && to == DismissValue.DismissedToEnd -> DismissDirection.StartToEnd + // is currently being dismissed to the start + from == DismissValue.Default && to == DismissValue.DismissedToStart -> DismissDirection.EndToStart + // has been dismissed to the end but is now animated back to default + from == DismissValue.DismissedToEnd && to == DismissValue.Default -> DismissDirection.StartToEnd + // has been dismissed to the start but is now animated back to default + from == DismissValue.DismissedToStart && to == DismissValue.Default -> DismissDirection.EndToStart + else -> null + } +} + +private val DISMISS_THRESHOLD = 56.dp