Skip to content

Commit

Permalink
Move all state management to SwipeableActionsState
Browse files Browse the repository at this point in the history
  • Loading branch information
saket committed Sep 21, 2023
1 parent e9f8a22 commit 12a022d
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 61 deletions.
4 changes: 2 additions & 2 deletions library/src/commonMain/kotlin/me/saket/swipe/ActionFinder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ internal data class SwipeActionMeta(
)

internal data class ActionFinder(
private val left: List<SwipeAction>,
private val right: List<SwipeAction>
val left: List<SwipeAction>,
val right: List<SwipeAction>
) {

fun actionAt(offset: Float, totalWidth: Int): SwipeActionMeta? {
Expand Down
63 changes: 19 additions & 44 deletions library/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsBox.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@file:Suppress("NAME_SHADOWING")

package me.saket.swipe

import androidx.compose.animation.animateColorAsState
Expand All @@ -14,24 +12,20 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.runtime.Composable
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.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
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 kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt

/**
Expand All @@ -54,69 +48,50 @@ fun SwipeableActionsBox(
backgroundUntilSwipeThreshold: Color = Color.DarkGray,
content: @Composable BoxScope.() -> Unit
) = BoxWithConstraints(modifier) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val leftActions = if (isRtl) endActions else startActions
val rightActions = if (isRtl) startActions else endActions

state.canSwipeTowardsRight = leftActions.isNotEmpty()
state.canSwipeTowardsLeft = rightActions.isNotEmpty()

val ripple = remember {
SwipeRippleState()
}
val actions = remember(leftActions, rightActions) {
ActionFinder(left = leftActions, right = rightActions)
state.also {
it.layoutWidth = constraints.maxWidth
it.swipeThreshold = swipeThreshold
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
it.actions = remember(endActions, startActions, isRtl) {
ActionFinder(
left = if (isRtl) endActions else startActions,
right = if (isRtl) startActions else endActions,
)
}
}

val offset = state.offset.value
val thresholdCrossed = abs(offset) > LocalDensity.current.run { swipeThreshold.toPx() }

var swipedAction: SwipeActionMeta? by remember {
mutableStateOf(null)
}
val visibleAction: SwipeActionMeta? = remember(offset, actions) {
actions.actionAt(offset, totalWidth = constraints.maxWidth)
}
val backgroundColor: Color by animateColorAsState(
when {
swipedAction != null -> swipedAction!!.value.background
!thresholdCrossed -> backgroundUntilSwipeThreshold
visibleAction == null -> Color.Transparent
else -> visibleAction.value.background
state.swipedAction != null -> state.swipedAction!!.value.background
!state.hasCrossedSwipeThreshold() -> backgroundUntilSwipeThreshold
state.visibleAction != null -> state.visibleAction!!.value.background
else -> Color.Transparent
}
)

val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.absoluteOffset { IntOffset(x = offset.roundToInt(), y = 0) }
.drawOverContent { ripple.draw(scope = this) }
.absoluteOffset { IntOffset(x = state.offset.value.roundToInt(), y = 0) }
.drawOverContent { state.ripple.draw(scope = this) }
.draggable(
orientation = Horizontal,
enabled = !state.isResettingOnRelease,
onDragStopped = {
scope.launch {
if (thresholdCrossed && visibleAction != null) {
swipedAction = visibleAction
swipedAction!!.value.onSwipe()
ripple.animate(action = swipedAction!!)
}
}
scope.launch {
state.resetOffset()
swipedAction = null
state.handleOnDragStopped()
}
},
state = state.draggableState,
),
content = content
)

(swipedAction ?: visibleAction)?.let { action ->
(state.swipedAction ?: state.visibleAction)?.let { action ->
ActionIconBox(
modifier = Modifier.matchParentSize(),
action = action,
offset = offset,
offset = state.offset.value,
backgroundColor = backgroundColor,
content = { action.value.icon() }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlin.math.abs

@Composable
fun rememberSwipeableActionsState(): SwipeableActionsState {
return remember { SwipeableActionsState() }
return remember { SwipeableActionsState() }.apply {
density = LocalDensity.current
}
}

/**
Expand All @@ -31,32 +42,58 @@ class SwipeableActionsState internal constructor() {
/**
* Whether [SwipeableActionsBox] is currently animating to reset its offset after it was swiped.
*/
var isResettingOnRelease: Boolean by mutableStateOf(false)
private set
val isResettingOnRelease: Boolean by derivedStateOf {
swipedAction != null
}

internal var canSwipeTowardsRight = false
internal var canSwipeTowardsLeft = false
internal var layoutWidth: Int by mutableIntStateOf(0)
internal var swipeThreshold: Dp by mutableStateOf(0.dp)
internal val ripple = SwipeRippleState()
internal lateinit var density: Density

internal var actions: ActionFinder by mutableStateOf(
ActionFinder(left = emptyList(), right = emptyList())
)
internal val visibleAction: SwipeActionMeta? by derivedStateOf {
actions.actionAt(offsetState.value, totalWidth = layoutWidth)
}
internal var swipedAction: SwipeActionMeta? by mutableStateOf(null)

internal val draggableState = DraggableState { delta ->
val targetOffset = offsetState.value + delta
val isAllowed = isResettingOnRelease
|| targetOffset > 0f && canSwipeTowardsRight
|| targetOffset < 0f && canSwipeTowardsLeft

val canSwipeTowardsRight = actions.left.isNotEmpty()
val canSwipeTowardsLeft = actions.right.isNotEmpty()

// Add some resistance if needed.
val isAllowed = (targetOffset > 0f && canSwipeTowardsRight) || (targetOffset < 0f && canSwipeTowardsLeft)
offsetState.value += if (isAllowed) delta else delta / 10
}

internal suspend fun resetOffset() {
draggableState.drag(MutatePriority.PreventUserInput) {
isResettingOnRelease = true
try {
Animatable(offsetState.value).animateTo(targetValue = 0f, tween(durationMillis = animationDurationMs)) {
internal fun hasCrossedSwipeThreshold(): Boolean {
return abs(offsetState.value) > density.run { swipeThreshold.toPx() }
}

internal suspend fun handleOnDragStopped() = coroutineScope {
launch {
if (hasCrossedSwipeThreshold()) {
visibleAction?.let { action ->
swipedAction = action
action.value.onSwipe()
ripple.animate(action = action)
}
}
}
launch {
draggableState.drag(MutatePriority.PreventUserInput) {
Animatable(offsetState.value).animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = animationDurationMs),
) {
dragBy(value - offsetState.value)
}
} finally {
isResettingOnRelease = false
}
swipedAction = null
}
}
}

0 comments on commit 12a022d

Please sign in to comment.