Issue
I have a problem with compose. I try implement simple calendar. I already have displayed month, and enabled change months by arrows.
src="https://i.stack.imgur.com/IRUnr.png" alt="enter image description here" />
Now I want to add change month by swipe.
I tried with .swipeable
This is sample:
val size = remember { mutableStateOf(Size.Zero) }
val swipeableState = rememberSwipeableState(0)
val width = if (size.value.width == 0f) 1f else size.value.width - 60.dp.value * 2
Box(
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { size.value = Size(it.width.toFloat(), it.height.toFloat()) }
.swipeable(
state = swipeableState,
anchors = mapOf(0f to 0, width to 1),
thresholds = { _, _ -> FractionalThreshold(0.3f) },
orientation = Orientation.Horizontal
)
.background(Color.LightGray)
) {
Box(
Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.size(60.dp)
.background(Color.DarkGray)
)
}
But this not work perfectly. I need to hold outside calendar area and I don't have idea how change state of month (that respond for display calendar).
My question is "how to implement swipe if I want invoke action after swipe".
Solution
You can observe any state with Side Effects. In case you need to wait for an action to complete, you can use if
+ DisposableEffect
:
if (swipeableState.isAnimationRunning) {
DisposableEffect(Unit) {
onDispose {
println("animation finished")
}
}
}
When the animation starts, I create a DisposableEffect
and when it ends, onDispose
is called, which means that the animation is finished.
In the case of your code, this will also be triggered at startup, because when you change the anchors after onSizeChanged
, this also starts the animation. But you can check if the value has been changed, so it's not a big problem.
To solve your basic problem, you also need to have three states: left, middle and right.
A little off-topic, about that line:
val width = if (size.value.width == 0f) 1f else size.value.width - 60.dp.value * 2
- You repeat this calculation every time you recompose, you shouldn't do that. You can use
rememeber
withsize.value.width
as the key, becausewidth
only needs to be recalculated when that value changes. - You are trying to get the pixel value from DP by multiplying by 2. This is wrong, you need to use Density.
So the final code can look like this:
enum class SwipeDirection(val raw: Int) {
Left(0),
Initial(1),
Right(2),
}
@Composable
fun TestScreen() {
var size by remember { mutableStateOf(Size.Zero) }
val swipeableState = rememberSwipeableState(SwipeDirection.Initial)
val density = LocalDensity.current
val boxSize = 60.dp
val width = remember(size) {
if (size.width == 0f) {
1f
} else {
size.width - with(density) { boxSize.toPx() }
}
}
val scope = rememberCoroutineScope()
if (swipeableState.isAnimationRunning) {
DisposableEffect(Unit) {
onDispose {
when (swipeableState.currentValue) {
SwipeDirection.Right -> {
println("swipe right")
}
SwipeDirection.Left -> {
println("swipe left")
}
else -> {
return@onDispose
}
}
scope.launch {
// in your real app if you don't have to display offset,
// snap without animation
// swipeableState.snapTo(SwipeDirection.Initial)
swipeableState.animateTo(SwipeDirection.Initial)
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { size = Size(it.width.toFloat(), it.height.toFloat()) }
.clickable { }
.swipeable(
state = swipeableState,
anchors = mapOf(
0f to SwipeDirection.Left,
width / 2 to SwipeDirection.Initial,
width to SwipeDirection.Right,
),
thresholds = { _, _ -> FractionalThreshold(0.3f) },
orientation = Orientation.Horizontal
)
.background(Color.LightGray)
) {
Box(
Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.size(boxSize)
.background(Color.DarkGray)
)
}
}
Result:
p.s. In case you don't need to animate anything during the swipe, I've moved this logic into a separate modifier.
Answered By - Phil Dukhov
Answer Checked By - David Goodson (JavaFixing Volunteer)