DEV Community

Vic_wkx
Vic_wkx

Posted on

Draggable Button: Exploring Interactive UI Elements with Jetpack Compose

Creating interactive UI elements is a fantastic way to enhance user engagement in mobile applications. One such element is a draggable button—a simple yet powerful component that can bring a playful and dynamic feel to your app.

In this blog post, we'll explore how to create draggable buttons using Jetpack Compose, a modern toolkit for building native Android UIs. We'll cover two effects: a basic draggable button and an enhanced version that snaps back to its original position when released.

Basic Draggable Button

Effect Preview

Basic Draggable Button

Full Code

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import com.kevintest.myapplication.ui.theme.MyApplicationTheme
import kotlin.math.roundToInt

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    DraggableButtonScreen(
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun DraggableButtonScreen(modifier: Modifier) {
    Box(
        contentAlignment = Alignment.Center,
        modifier = modifier
            .fillMaxSize()
            .statusBarsPadding()
            .navigationBarsPadding()
    ) {
        DraggableButton()
    }
}

@Composable
private fun DraggableButton() {
    var offset by remember { mutableStateOf(IntOffset.Zero) }
    val context = LocalContext.current
    Button(
        onClick = {},
        modifier = Modifier
            .offset { offset }
            .pointerInput(Unit) {
                detectDragGestures(
                    onDrag = { change, dragAmount ->
                        val maxOffsetX = (context.resources.displayMetrics.widthPixels - size.width) / 2
                        val maxOffsetY = (context.resources.displayMetrics.heightPixels - size.height) / 2
                        offset = IntOffset(
                            (offset.x + dragAmount.x.roundToInt()).coerceIn(-maxOffsetX, maxOffsetX),
                            (offset.y + dragAmount.y.roundToInt()).coerceIn(-maxOffsetY, maxOffsetY)
                        )
                        change.consume()
                    }
                )
            }
    ) {
        Text("Drag me")
    }
}
Enter fullscreen mode Exit fullscreen mode

Code Breakdown

The basic draggable button uses Jetpack Compose's detectDragGestures method to handle drag operations. Here's a step-by-step explanation of the code:

  1. State Initialization: We use remember { mutableStateOf(IntOffset.Zero) } to initialize the button's offset. This state will be updated as the button is dragged.
  2. Drag Gesture Detection: The detectDragGestures method is used to track drag events. Inside this method:
    • onDrag: This callback updates the button's offset based on the drag amount. We use coerceIn to ensure the button stays within the screen boundaries.
    • change.consume(): This call prevents the drag event from propagating further, ensuring smooth interaction.
  3. Screen Boundaries: To prevent the button from being dragged outside the screen, we calculate maxOffsetX and maxOffsetY based on the screen size and button dimensions.

Draggable Button with Snap-Back Effect

Effect Preview

Draggable Button with Snap-Back Effect

Full Code

@Composable
private fun DraggableButton() {
    var offset by remember { mutableStateOf(IntOffset.Zero) }
    var isDragging by remember { mutableStateOf(false) }
    val animatedOffset by animateIntOffsetAsState(
        targetValue = if (isDragging) offset else IntOffset.Zero,
        animationSpec = tween(durationMillis = 300),
        label = "offsetAnimation"
    )
    val context = LocalContext.current
    Button(
        onClick = {},
        modifier = Modifier
            .offset { if (isDragging) offset else animatedOffset }
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragStart = { isDragging = true },
                    onDragEnd = {
                        isDragging = false
                        offset = IntOffset.Zero
                    },
                    onDrag = { change, dragAmount ->
                        val maxOffsetX = (context.resources.displayMetrics.widthPixels - size.width) / 2
                        val maxOffsetY = (context.resources.displayMetrics.heightPixels - size.height) / 2
                        offset = IntOffset(
                            (offset.x + dragAmount.x.roundToInt()).coerceIn(-maxOffsetX, maxOffsetX),
                            (offset.y + dragAmount.y.roundToInt()).coerceIn(-maxOffsetY, maxOffsetY)
                        )
                        change.consume()
                    }
                )
            }
    ) {
        Text("Drag me")
    }
}
Enter fullscreen mode Exit fullscreen mode

Code Breakdown

The snap-back effect adds a fun twist to the draggable button. When the button is released, it smoothly animates back to its original position. This is achieved using Jetpack Compose's animateIntOffsetAsState method.

  1. State Initialization: We introduce an additional state variable isDragging to track whether the button is being dragged.
  2. Animation Logic: The animateIntOffsetAsState method animates the button's offset. When isDragging is false, the button animates back to IntOffset.Zero.
  3. Drag Gesture Updates: The onDragStart callback sets isDragging to true, while onDragEnd resets it to false and snaps the button back to its original position.

That's all, thanks. (=, =)

Top comments (0)