Source code

Revision control

Copy as Markdown

Other Tools

# [Android Components](../../../README.md) > Compose > Draggable
Draggable, reorderable Compose lists and grids.
## Usage
This component can be used to create a list or grid that supports drag actions:
1. scrolling up or down in the grid while dragging an item to the bottom of the screen
2. reordering items, either:
- 'live' so that the items swap as you drag one item over another
- after release of the drag, with a line indicator for where the reorder action is hovering
3. dragging one item onto another (drag and drop), to merge an item or place it in a target zone.
This component supports lists (single-column) and grid (multi-column) layouts.
### Grid
Set up a grid interaction state:
```kotlin
val gridState = rememberLazyGridState()
val gridInteractionState =
createGridInteractionState(
gridState = gridState, // a compose affordance
itemInteractionHandler = itemHandler, // handler interface for onMove, onDrop, etc, callbacks
onLongPress = { _ -> }, // (LazyGridItemInfo) -> Unit to respond to a long press.
liveReorderEnabled = false, // whether reordering should happen 'live' or after release
ignoredItems = emptySet(), // a set of items in your list that can't be dragged or targeted
)
```
Create a lazy grid in your compose view that passes in the interaction state.
You may wish to pass in collectionInfo here in the semantics block.
```kotlin
// Depending on your implementation, you may find it helpful to place the interactableGrid modifier
// on an outer box, or directly to the modifier inside the LazyVerticalGrid
Box(
modifier = Modifier.interactableGrid(
state = gridInteractionState,
isInMultiSelectMode = isInMultiSelectMode,
),
) {
LazyVerticalGrid(
columns = GridCells.Fixed(count = columns),
modifier = modifier
.semantics {
testTag = TEST_TAG
collectionInfo = collectionInfo
},
state = gridState,
userScrollEnabled = gridInteractionState.draggedItem == InteractionState.Grid.None,
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(space = verticalPadding),
horizontalArrangement = Arrangement.spacedBy(space = horizontalPadding),
) {
// optionally
renderHeaderItem()
itemsIndexed(
items = itemList,
// it is very important to define unique, stable keys, because by default
// the key is the position, which is updated if the dataset changes,
// causing data loss and unnecessary recompositions.
key = { _, item -> item.id },
) { index, item ->
// optional - if (and only if) you need to drag an item off the grid/list,
// you need to pin the composable so it stays rendered.
// it should be disposed as soon as it is no longer dragged.
val pinnableContainer = LocalPinnableContainer.current
val isDragged by
remember(item.id) {
derivedStateOf {
gridInteractionState.draggedItem.key == item.id
}
}
DisposableEffect(isDragged) {
val handle = if (isDragged) pinnableContainer?.pin() else null
onDispose { handle?.release() }
}
// If you also support clicking on the items, you may want to track whether drag is active
// and disable click listening when a drag is in progress.
val shouldClickListen = gridInteractionState.draggedItem.key != item.id
// Similarly, this component supports awareness of swipe-to-dismiss handling:
// you can safely pass "false" to swipingActive if you do not want to support this.
// rememberSwipeToDismissBoxState() is an app-specific call which you may re-implement.
val swipeToDismissBoxState = rememberSwipeToDismissBoxState(id = item.id)
val swipingActive by
remember(swipeToDismissBoxState) {
derivedStateOf {
swipeToDismissBoxState.dismissDirection != SwipeToDismissBoxValue.Settled
}
}
// Wrap your items in the interactable container,
// which handles animations and global layout coordinate tracking.
InteractableDragItemContainer(
state = gridInteractionState,
position = index, // likely your grid index, but you may increment this if you have headers
key = item.id,
swipingActive = swipingActive,
// optionally, you can declare when an item is entering composition
// and define an animation to play
enteringItemKey = keyOfItemEnteringComposition,
enteringItemDecoration = { interactionState ->
Modifier.specialEntranceAnimation()
},
) {
renderListItem(shouldClickListen = shouldClickListen)
}
}
// optionally
renderFooterItem()
}
}
```
### List
The list implementation is quite similar, but shown here for reference:
Set up a list interaction state:
```kotlin
val lazyListState = rememberLazyListState()
val listInteractionState =
createListInteractionState(
listState = lazyListState, // compose affordance
ignoredItems = ignoredItems, // a set of items in your list that can't be dragged or targeted
onLongPress = { _ -> }, // (LazyListItemInfo) -> Unit to respond to a long press.
itemInteractionHandler = itemInteractionHandler, // respond to onMove, onDrop, etc, events
dragAndDropEnabled = true, // should drag and drop (merging items) be enabled?
liveReorderEnabled = false, // should reorders happen live or after the drag is released?
)
```
Set up a LazyColumn with items wrapped in the InteractableDragItemContainer:
The list can similarly support some of the complexities described in the Grid implementation above.
```kotlin
Box(
modifier = Modifier.detectListPressAndDrag(
listState = lazyListState,
interactionState = listInteractionState,
shouldLongPressToDrag = true,
),
) {
LazyColumn(state = lazyListState, verticalArrangement = Arrangement.spacedBy(verticalPadding)) {
itemsIndexed(items = itemList, key = { _, item -> item.id }) { index, item ->
InteractableDragItemContainer(
state = listInteractionState,
position = index,
key = item.id,
) {
renderItem()
}
}
}
}
```
### Setting up the dependency
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
```Groovy
implementation "org.mozilla.components:compose-draggable:{latest-version}"
```
## License
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/