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
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")
}
}
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:
- State Initialization: We use
remember { mutableStateOf(IntOffset.Zero) }
to initialize the button's offset. This state will be updated as the button is dragged. - 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.
- onDrag: This callback updates the button's offset based on the drag amount. We use
- 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
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")
}
}
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.
- State Initialization: We introduce an additional state variable
isDragging
to track whether the button is being dragged. - Animation Logic: The
animateIntOffsetAsState
method animates the button's offset. WhenisDragging
isfalse
, the button animates back toIntOffset.Zero
. - Drag Gesture Updates: The
onDragStart
callback setsisDragging
totrue
, whileonDragEnd
resets it tofalse
and snaps the button back to its original position.
That's all, thanks. (=, =)
Top comments (0)