Table of Contents
The LongPressDraggable
composable function is the foundation of our seamless D&D experience. It allows us to wrap any content (ex. horizontal pager) with dragging behavior, making it draggable upon a long-press gesture.
Code
@Composable
fun LongPressDraggable(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit
) {val state = remember DragTargetInfo()
CompositionLocalProvider(
LocalDragTargetInfo provides state
) {
Box(modifier = modifier.fillMaxSize())
content()
if (state.isDragging == true)
var targetSize by remember
mutableStateOf(IntSize.Zero)
Box(modifier = Modifier
.graphicsLayer
val offset = (state.dragPosition + state.dragOffset)
// will scale the dragged item being dragged by 50%
scaleX = 1.5f
scaleY = 1.5f
// adds a bit of transparency
alpha = if (targetSize == IntSize.Zero) 0f else .9f
// horizontal displacement
translationX = offset.x.minus(targetSize.width / 2)
// vertical displacement
translationY = offset.y.minus(targetSize.height / 2)
.onGloballyPositioned
targetSize = it.size
it.let coordinates ->
state.absolutePositionX = coordinates.positionInRoot().x
state.absolutePositionY = coordinates.positionInRoot().y
)
state.draggableComposable?.invoke()
}
}
Usage
We leverage the LongPressDraggable
function to make the entire horizontal pager and items draggable, providing users with the flexibility to move items to and from individual screens easily.
@Composable
fun HorizontalPagerContent()
val pagerState = rememberPagerState()// Wrap the entire horizontal pager with LongPressDraggable
LongPressDraggable
HorizontalPager(state = pagerState, count = 2) pageIndex ->
when (pageIndex)
0 -> Page1Content()
1 -> Page2Content()
The DragTarget
composable function plays a pivotal role in defining drag sources, representing items that can be dragged.
Code
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun <T> DragTarget(
context: Context,
pagerSize: Int,
verticalPagerState: PagerState? = null, // if you have nested / multi paged app
horizontalPagerState: PagerState? = null,
modifier: Modifier,
dataToDrop: Any? = null, // change type here to your data model class
content: @Composable (shouldAnimate: Boolean) -> Unit
) {
val coroutineScope = rememberCoroutineScope()var currentPosition by remember mutableStateOf(Offset.Zero)
val currentState = LocalDragTargetInfo.current
Box(modifier = modifier
.onGloballyPositioned
currentPosition = it.localToWindow(Offset.Zero)
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(onDragStart =
currentState.dataToDrop = dataToDrop
currentState.isDragging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content(false) // render scaled item without animation
, onDrag = { change, dragAmount ->
change.consume()
currentState.itemDropped =
false // used to prevent drop target from multiple re-renders
currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
val xOffset = abs(currentState.dragOffset.x)
val yOffset = abs(currentState.dragOffset.y)
coroutineScope.launch {
// this is a flag only for demo purposes, change as per your needs
val boundDragEnabled = false
if(boundDragEnabled)
// use this for dragging after the user has dragged the item outside a bound around the original item itself
if (xOffset > 20 && yOffset > 20)
verticalPagerState?.animateScrollToPage(
1,
animationSpec = tween(
durationMillis = 300,
easing = androidx.compose.animation.core.EaseOutCirc
)
)
else
// for dragging to and fro from different pages in the pager
val currentPage = horizontalPagerState?.currentPage
val dragPositionX = currentState.dragPosition.x + currentState.dragOffset.x
val dragPositionY = currentState.dragPosition.y + currentState.dragOffset.y
val displayMetrics = context.resources.displayMetrics
// if item is very close to left edge of page, move to previous page
if (dragPositionX < 60)
currentPage?.let
if (it > 1)
horizontalPagerState.animateScrollToPage(currentPage - 1)
else if (displayMetrics.widthPixels - dragPositionX < 60)
// if item is very close to right edge of page, move to next page
currentPage?.let
if (it < pagerSize)
horizontalPagerState.animateScrollToPage(currentPage + 1)
}
}, onDragEnd =
currentState.isDragging = false
currentState.dragOffset = Offset.Zero
, onDragCancel =
currentState.isDragging = false
currentState.dragOffset = Offset.Zero
)
}, contentAlignment = Alignment.Center
)
content(true) // render positioned content with animation
}
The DragTarget
composable function is a fundamental building block for enabling drag and drop interactions.
Let’s break down its key components —
context
— TheContext
parameter is used to access the application context. It is required to retrieve resources like display metrics, which can be useful in calculating dragging behavior.verticalPagerState
andhorizontalPagerState
— These optionalPagerState
parameters represent the state of the vertical and horizontal pagers, respectively. They are used to control the scroll positions and animate scrolling during the drag and drop operation.modifier
— Themodifier
parameter is used to detect gestures and update the current drag state.dataToDrop
— TheAny?
parameter represents the data that will be dropped during the drag operation. It allows you to associate specific data with the draggable item being moved.content
— The@Composable (shouldAnimate: Boolean) -> Unit
parameter represents the content of theDragTarget
. It’s a composable function that defines the UI elements to be displayed within theDragTarget
based on the state. Additionally, shouldAnimate helps the lambda block decide if the content should be animated or not while rendering. Example, one might not want the scaled composable to have animation.
How It Works —
- Local State and Event Handling: The
DragTarget
usesLocalDragTargetInfo.current
to access the current state of the drag and drop interactions. It also uses thepointerInput
modifier to handle drag gestures and respond to user interactions. - Initial Configuration: When the
DragTarget
is first created, it calculates and stores the initial position (currentPosition
) of theDragTarget
in the layout usingonGloballyPositioned
. - Drag Gesture Detection: The
detectDragGesturesAfterLongPress
function is used to detect drag gestures after a long-press is initiated on theDragTarget
. When the drag starts (onDragStart
), it sets theisDragging
flag to true and captures the initial drag position (dragPosition
) relative to the window’s coordinates. - Dragging Update: During the drag operation (
onDrag
), theonDrag
lambda is continuously called as the user moves their finger. It updates thedragOffset
, representing the current displacement from the initial drag position. The lambda skillfully manages various scenarios, such as detecting when the drag is beyond a specified boundary or reaches the ends of a page, elegantly initiating a smooth move to another page.. Feel free to change that block of code as per your needs. - End and Cancellation: When the drag ends (
onDragEnd
) or is canceled (onDragCancel
), theisDragging
flag is reset, and thedragOffset
is reset toOffset.Zero
.
Usage
@Composable
fun DragTargetWidgetItem(
data: Widget,
pagerState: PagerState
)
DragTarget(
context = LocalContext.current,
pagerSize = 2, // Assuming there are two pages in the horizontal pager
horizontalPagerState = pagerState,
modifier = modifier.wrapContentSize(),
dataToDrop = data,
) shouldAnimate ->
WidgetItem(data, shouldAnimate)
@Composable
fun WidgetItem(
data: Widget,
shouldAnimate: Boolean
)
// Add your custom implementation for the WidgetItem here.
// This composable will render the content of the draggable widget.
// You can use the 'data' parameter to extract necessary information and display it.
// The 'shouldAnimate' parameter can be used to control animations if needed.
// Example: Displaying a simple card with the widget's name
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.graphicsLayer
// Scale the card when shouldAnimate is true
scaleX = if (shouldAnimate) 1.2f else 1.0f
scaleY = if (shouldAnimate) 1.2f else 1.0f
,
elevation = 4.dp
)
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
)
Text(
text = data.widgetName,
style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 18.sp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = data.widgetDescription)