Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE] Add combinedGesture Method #56

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ internal fun App() {
scene("SimpleReorderableLazyColumn") { SimpleReorderableLazyColumnScreen() }
scene("ComplexReorderableLazyColumn") { ComplexReorderableLazyColumnScreen() }
scene("SimpleLongPressHandleReorderableLazyColumn") { SimpleLongPressHandleReorderableLazyColumnScreen() }
scene("SimpleCombinedGestureHandleReorderableLazyColumn") { SimpleCombinedGestureHandleReorderableLazyColumnScreen() }
scene("SimpleReorderableLazyVerticalGrid") { SimpleReorderableLazyVerticalGridScreen() }
scene("SimpleReorderableLazyVerticalStaggeredGrid") { SimpleReorderableLazyVerticalStaggeredGridScreen() }
scene("ReorderableColumn") { ReorderableColumnScreen() }
Expand Down Expand Up @@ -117,6 +118,13 @@ fun MainScreen(navController: Navigator) {
textAlign = TextAlign.Center
)
}
Button(
onClick = { navController.navigate("SimpleCombinedGestureHandleReorderableLazyColumn") }) {
Text(
"\uD83D\uDEA7DEMO:\nSimple Reorderable LazyColumn with\n.combinedGestureHandle",
textAlign = TextAlign.Center
)
}
}

Column(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package sh.calvin.reorderable.demo.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.demo.ReorderHapticFeedbackType
import sh.calvin.reorderable.demo.items
import sh.calvin.reorderable.demo.rememberReorderHapticFeedback
import sh.calvin.reorderable.rememberReorderableLazyListState

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleCombinedGestureHandleReorderableLazyColumnScreen() {
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
var showBottomSheet by remember { mutableStateOf(false) }
var clickedItemId by remember { mutableStateOf<Int?>(null) }
var selectedItemId by remember { mutableStateOf<Int?>(null) }

val haptic = rememberReorderHapticFeedback()

var list by remember { mutableStateOf(items) }
val lazyListState = rememberLazyListState()
val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to ->
list = list.toMutableList().apply {
add(to.index, removeAt(from.index))
}

haptic.performHapticFeedback(ReorderHapticFeedbackType.MOVE)
}

Row {
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
state = lazyListState,
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(list, key = { _, item -> item.id }) { index, item ->
ReorderableItem(reorderableLazyColumnState, item.id) { isDragging ->
val interactionSource = remember { MutableInteractionSource() }

Box(
modifier = Modifier
.height(item.size.dp)
.fillMaxWidth()
.background(
color = Color.Gray,
shape = RoundedCornerShape(16.dp),
)
.combinedGestureHandle(
onClick = {
clickedItemId = item.id
},
onLongPress = {
selectedItemId = item.id
haptic.performHapticFeedback(ReorderHapticFeedbackType.START)
showBottomSheet = true
haptic.performHapticFeedback(ReorderHapticFeedbackType.END)
},
onDragStarted = {
showBottomSheet = false
haptic.performHapticFeedback(ReorderHapticFeedbackType.START)
},
onDragStopped = {
haptic.performHapticFeedback(ReorderHapticFeedbackType.END)
},
interactionSource = interactionSource,
)
.semantics {
customActions = listOf(
CustomAccessibilityAction(
label = "Move Up",
action = {
if (index > 0) {
list = list.toMutableList().apply {
add(index - 1, removeAt(index))
}
true
} else {
false
}
}
),
CustomAccessibilityAction(
label = "Move Down",
action = {
if (index < list.size - 1) {
list = list.toMutableList().apply {
add(index + 1, removeAt(index))
}
true
} else {
false
}
}
),
)
},
) {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(item.text, Modifier.padding(horizontal = 8.dp))
}
}
}
}
}
Card(
modifier = Modifier
.weight(3f)
.fillMaxHeight(),
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = clickedItemId?.let { "Item $it Content" } ?: "No Item Selected",
fontSize = 24.sp
)
}
}
}

if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
},
sheetState = sheetState
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Item ${selectedItemId ?: ""}", textAlign = TextAlign.Center)
Spacer(Modifier.height(24.dp))
Button(onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
}
}
}) {
Text("Pin Item")
}
Spacer(Modifier.height(16.dp))
Button(onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
}
}
}) {
Text("Remove Item")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,27 @@ interface ReorderableCollectionItemScope {
onDragStarted: (startedPosition: Offset) -> Unit = {},
onDragStopped: () -> Unit = {},
): Modifier

/**
* Make the UI element clickable, long-pressable, and draggable for the reorderable item.
*
* This modifier can only be used on the UI element that is a child of [ReorderableItem]. It allows the element to respond to click, long press, and drag gestures.
*
* @param enabled Whether the click, long press, and drag actions are enabled
* @param interactionSource [MutableInteractionSource] that will be used to emit interaction events
* @param onClick The function that is called when the element is clicked
* @param onLongPress The function that is called when the element is long-pressed
* @param onDragStarted The function that is called when the item starts being dragged
* @param onDragStopped The function that is called when the item stops being dragged
*/
fun Modifier.combinedGestureHandle(
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
onClick: () -> Unit = {},
onLongPress: () -> Unit = {},
onDragStarted: (startedPosition: Offset) -> Unit = {},
onDragStopped: () -> Unit = {},
): Modifier
}

internal class ReorderableCollectionItemScopeImpl(
Expand Down Expand Up @@ -772,6 +793,61 @@ internal class ReorderableCollectionItemScopeImpl(
},
)
}

/**
* Make the UI element the draggable handle for the reorderable item, combining click, long press, and drag interactions.
*
* @param enabled Whether or not drag is enabled
* @param interactionSource [MutableInteractionSource] that will be used to emit [DragInteraction.Start] when this draggable is being dragged
* @param onClick The function that is called when the item is clicked
* @param onLongPress The function that is called when the item is long pressed
* @param onDragStarted The function that is called when the item starts being dragged
* @param onDragStopped The function that is called when the item stops being dragged
*/
override fun Modifier.combinedGestureHandle(
enabled: Boolean,
interactionSource: MutableInteractionSource?,
onClick: () -> Unit,
onLongPress: () -> Unit,
onDragStarted: (startedPosition: Offset) -> Unit,
onDragStopped: () -> Unit
) = composed {
var handleOffset by remember { mutableStateOf(Offset.Zero) }
var handleSize by remember { mutableStateOf(IntSize.Zero) }

val coroutineScope = rememberCoroutineScope()

onGloballyPositioned {
handleOffset = it.positionInRoot()
handleSize = it.size
}.combinedGesture(
key1 = reorderableLazyCollectionState,
enabled = enabled && (reorderableLazyCollectionState.isItemDragging(key).value || !reorderableLazyCollectionState.isAnyItemDragging),
interactionSource = interactionSource,
onClick = onClick,
onLongPress = onLongPress,
onDragStarted = {
coroutineScope.launch {
val handleOffsetRelativeToItem = handleOffset - itemPositionProvider()
val handleCenter = Offset(
handleOffsetRelativeToItem.x + handleSize.width / 2f,
handleOffsetRelativeToItem.y + handleSize.height / 2f
)

reorderableLazyCollectionState.onDragStart(key, handleCenter)
}
onDragStarted(it)
},
onDragStopped = {
reorderableLazyCollectionState.onDragStop()
onDragStopped()
},
onDrag = { change, dragAmount ->
change.consume()
reorderableLazyCollectionState.onDrag(dragAmount)
},
)
}
}

/**
Expand Down
Loading