kotlinandroid-jetpack-composedraggable

How to make a composable draggable using AnchoredDraggableState?


I'm trying to create an anchored draggable and it just doesn't work. Here is my code:

import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.core.tween
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
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.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.example.anchoreddraggablestateexample.ui.theme.AnchoredDraggableStateExampleTheme
import kotlin.math.roundToInt

class MainActivity : ComponentActivity() {
    @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AnchoredDraggableStateExampleTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Box(
                        Modifier
                            .fillMaxSize()
                            .background(Color.White)
                            .padding(innerPadding)
                    ) {
                        Row(Modifier.align(Alignment.Center)) {
                            SlideToAction()
                        }
                    }
                }
            }
        }
    }
}

enum class SlideToActionAnchors {
    Start,
    End
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SlideToAction() {
    val decayAnimationSpec = rememberSplineBasedDecay<Float>()
    val dragState = remember {
        AnchoredDraggableState(
            initialValue = SlideToActionAnchors.End,
            anchors = DraggableAnchors {
                SlideToActionAnchors.Start at 0f
                SlideToActionAnchors.End at 500f
            },
            positionalThreshold = { d -> d * 0.9f},
            velocityThreshold = { Float.POSITIVE_INFINITY },
            snapAnimationSpec = tween(),
            decayAnimationSpec = decayAnimationSpec
        )
    }

    Box(
        Modifier.fillMaxWidth()
            .background(Color.Red)
    ) {
        Box(
            Modifier
                .size(80.dp)
                .background(Color.Green)
                .anchoredDraggable(
                    dragState,
                    Orientation.Horizontal,
                    enabled = true
                )
                .offset {
                    IntOffset(
                        x = dragState
                            .requireOffset()
                            .roundToInt(),
                        y = 0
                    )
                }
        )
    }
}

And here's my dependencies:

[versions]
agp = "8.1.4"
kotlin = "1.9.24"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.5"
activityCompose = "1.9.2"
composeBom = "2024.09.01"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

I did New Project > Empty Activity in Android Studio and dumped this code in. The green box is not draggable.


Solution

  • You need to use the anchoredDraggable modifier on the container, not on the composable that will be moving. It works similar to the swipeable modifier in that regard:

    It's important to note that this modifier does not move the element, it only detects the gesture. You need to hold the state and represent it on screen by, for example, moving the element via the offset modifier.

    Also, the offset modifier should go before size.

    Box(
        Modifier.fillMaxWidth()
            .background(Color.Red)
            .anchoredDraggable(
                state = dragState,
                orientation = Orientation.Horizontal,
                enabled = true
            )
    ) {
        Box(
            Modifier
                .offset { IntOffset(dragState.requireOffset().roundToInt(), 0) }
                .size(80.dp)
                .background(Color.Green)
        )
    }