Improve reordering folder experience.

This commit is contained in:
Michelle Tang 2024-10-22 13:05:46 -07:00 committed by Greyson Parrelli
parent 9e955e94d9
commit 422acde111
6 changed files with 350 additions and 105 deletions

View file

@ -1,23 +1,22 @@
package org.thoughtcrime.securesms.components.settings.app.chats.folders
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -31,6 +30,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
@ -42,7 +43,9 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Dividers
import org.signal.core.ui.DropdownMenus
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
@ -52,6 +55,7 @@ import org.signal.core.ui.copied.androidx.compose.rememberDragDropState
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
@ -83,6 +87,16 @@ class ChatFoldersFragment : ComposeFragment() {
Toast.makeText(requireContext(), getString(R.string.ChatFoldersFragment__folder_added, folder.name), Toast.LENGTH_SHORT).show()
viewModel.createFolder(requireContext(), folder)
},
onDeleteClicked = { folder ->
viewModel.setCurrentFolder(folder)
viewModel.showDeleteDialog(true)
},
onDeleteConfirmed = {
viewModel.deleteFolder(context = requireContext(), forceRefresh = true)
},
onDeleteDismissed = {
viewModel.showDeleteDialog(false)
},
onPositionUpdated = { fromIndex, toIndex -> viewModel.updatePosition(fromIndex, toIndex) }
)
}
@ -95,101 +109,125 @@ fun FoldersScreen(
modifier: Modifier = Modifier,
onFolderClicked: (ChatFolderRecord) -> Unit = {},
onAdd: (ChatFolderRecord) -> Unit = {},
onDeleteClicked: (ChatFolderRecord) -> Unit = {},
onDeleteConfirmed: () -> Unit = {},
onDeleteDismissed: () -> Unit = {},
onPositionUpdated: (Int, Int) -> Unit = { _, _ -> }
) {
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val isRtl = ViewUtil.isRtl(LocalContext.current)
val listState = rememberLazyListState()
val dragDropState =
rememberDragDropState(listState) { fromIndex, toIndex ->
rememberDragDropState(listState, includeHeader = true, includeFooter = true) { fromIndex, toIndex ->
onPositionUpdated(fromIndex, toIndex)
}
Column(modifier = modifier.verticalScroll(rememberScrollState())) {
Text(
text = stringResource(id = R.string.ChatFoldersFragment__organize_your_chats),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, end = 12.dp, start = 24.dp)
)
Text(
text = stringResource(id = R.string.ChatFoldersFragment__folders),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp, start = 24.dp)
)
FolderRow(
icon = R.drawable.symbol_plus_compact_16,
title = stringResource(R.string.ChatFoldersFragment__create_a_folder),
onClick = { onFolderClicked(ChatFolderRecord()) }
if (state.showDeleteDialog) {
Dialogs.SimpleAlertDialog(
title = "",
body = stringResource(id = R.string.CreateFoldersFragment__delete_this_chat_folder),
confirm = stringResource(id = R.string.delete),
onConfirm = onDeleteConfirmed,
dismiss = stringResource(id = android.R.string.cancel),
onDismiss = onDeleteDismissed
)
}
val columnHeight = dimensionResource(id = R.dimen.chat_folder_row_height).value * state.folders.size
LazyColumn(
modifier = Modifier
.height(columnHeight.dp)
.dragContainer(dragDropState),
state = listState
) {
itemsIndexed(state.folders) { index, folder ->
DraggableItem(dragDropState, index) { isDragging ->
val elevation = if (isDragging) 1.dp else 0.dp
val isAllChats = folder.folderType == ChatFolderRecord.FolderType.ALL
FolderRow(
icon = R.drawable.symbol_folder_24,
title = if (isAllChats) stringResource(R.string.ChatFoldersFragment__all_chats) else folder.name,
subtitle = getFolderDescription(folder),
onClick = if (!isAllChats) {
{ onFolderClicked(folder) }
} else null,
elevation = elevation,
showDragHandle = true
)
}
LazyColumn(
modifier = Modifier.dragContainer(
dragDropState = dragDropState,
leftDpOffset = if (isRtl) 0.dp else screenWidth - 48.dp,
rightDpOffset = if (isRtl) 48.dp else screenWidth
),
state = listState
) {
item {
DraggableItem(dragDropState, 0) {
Text(
text = stringResource(id = R.string.ChatFoldersFragment__organize_your_chats),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = modifier.padding(top = 12.dp, bottom = 12.dp, end = 12.dp, start = 24.dp)
)
Text(
text = stringResource(id = R.string.ChatFoldersFragment__folders),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp, start = 24.dp)
)
FolderRow(
icon = R.drawable.symbol_plus_compact_16,
title = stringResource(R.string.ChatFoldersFragment__create_a_folder),
onClick = { onFolderClicked(ChatFolderRecord()) }
)
}
}
if (state.suggestedFolders.isNotEmpty()) {
Dividers.Default()
Text(
text = stringResource(id = R.string.ChatFoldersFragment__suggested_folders),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp, start = 24.dp)
)
itemsIndexed(state.folders) { index, folder ->
DraggableItem(dragDropState, 1 + index) { isDragging ->
val elevation = if (isDragging) 1.dp else 0.dp
val isAllChats = folder.folderType == ChatFolderRecord.FolderType.ALL
FolderRow(
icon = R.drawable.symbol_folder_24,
title = if (isAllChats) stringResource(R.string.ChatFoldersFragment__all_chats) else folder.name,
subtitle = getFolderDescription(folder),
onClick = if (!isAllChats) {
{ onFolderClicked(folder) }
} else null,
onDelete = { onDeleteClicked(folder) },
elevation = elevation,
showDragHandle = true
)
}
}
state.suggestedFolders.forEach { chatFolder ->
when (chatFolder.folderType) {
ChatFolderRecord.FolderType.UNREAD -> {
val title: String = stringResource(R.string.ChatFoldersFragment__unread)
FolderRow(
icon = R.drawable.symbol_chat_badge_24,
title = title,
subtitle = stringResource(R.string.ChatFoldersFragment__unread_messages),
onAdd = { onAdd(chatFolder) }
item {
DraggableItem(dragDropState, 1 + state.folders.size) {
if (state.suggestedFolders.isNotEmpty()) {
Dividers.Default()
Text(
text = stringResource(id = R.string.ChatFoldersFragment__suggested_folders),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp, start = 24.dp)
)
}
ChatFolderRecord.FolderType.INDIVIDUAL -> {
val title: String = stringResource(R.string.ChatFoldersFragment__one_on_one_chats)
FolderRow(
icon = R.drawable.symbol_person_light_24,
title = title,
subtitle = stringResource(R.string.ChatFoldersFragment__only_direct_messages),
onAdd = { onAdd(chatFolder) }
)
}
ChatFolderRecord.FolderType.GROUP -> {
val title: String = stringResource(R.string.ChatFoldersFragment__groups)
FolderRow(
icon = R.drawable.symbol_group_light_20,
title = title,
subtitle = stringResource(R.string.ChatFoldersFragment__only_group_messages),
onAdd = { onAdd(chatFolder) }
)
}
ChatFolderRecord.FolderType.ALL -> {
throw IllegalStateException("All chats should not be suggested")
}
ChatFolderRecord.FolderType.CUSTOM -> {
throw IllegalStateException("Custom folders should not be suggested")
state.suggestedFolders.forEach { chatFolder ->
when (chatFolder.folderType) {
ChatFolderRecord.FolderType.UNREAD -> {
val title: String = stringResource(R.string.ChatFoldersFragment__unread)
FolderRow(
icon = R.drawable.symbol_chat_badge_24,
title = title,
subtitle = stringResource(R.string.ChatFoldersFragment__unread_messages),
onAdd = { onAdd(chatFolder) }
)
}
ChatFolderRecord.FolderType.INDIVIDUAL -> {
val title: String = stringResource(R.string.ChatFoldersFragment__one_on_one_chats)
FolderRow(
icon = R.drawable.symbol_person_light_24,
title = title,
subtitle = stringResource(R.string.ChatFoldersFragment__only_direct_messages),
onAdd = { onAdd(chatFolder) }
)
}
ChatFolderRecord.FolderType.GROUP -> {
val title: String = stringResource(R.string.ChatFoldersFragment__groups)
FolderRow(
icon = R.drawable.symbol_group_light_20,
title = title,
subtitle = stringResource(R.string.ChatFoldersFragment__only_group_messages),
onAdd = { onAdd(chatFolder) }
)
}
ChatFolderRecord.FolderType.ALL -> {
error("All chats should not be suggested")
}
ChatFolderRecord.FolderType.CUSTOM -> {
error("Custom folders should not be suggested")
}
}
}
}
}
@ -218,6 +256,7 @@ private fun getFolderDescription(folder: ChatFolderRecord): String {
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun FolderRow(
modifier: Modifier = Modifier,
@ -226,12 +265,26 @@ fun FolderRow(
subtitle: String = "",
onClick: (() -> Unit)? = null,
onAdd: (() -> Unit)? = null,
onDelete: (() -> Unit)? = null,
elevation: Dp = 0.dp,
showDragHandle: Boolean = false
) {
val menuController = remember { DropdownMenus.MenuController() }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = if (onClick != null) {
modifier = if (onClick != null && onDelete != null) {
modifier
.combinedClickable(
onClick = onClick,
onLongClick = { menuController.show() }
)
.fillMaxWidth()
.defaultMinSize(minHeight = dimensionResource(id = R.dimen.chat_folder_row_height))
.shadow(elevation = elevation)
.background(MaterialTheme.colorScheme.background)
.padding(start = 24.dp, end = 12.dp)
} else if (onClick != null) {
modifier
.clickable(onClick = onClick)
.fillMaxWidth()
@ -284,6 +337,47 @@ fun FolderRow(
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
DropdownMenus.Menu(controller = menuController, offsetX = 0.dp, offsetY = 4.dp) { menuController ->
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(onClick = {
onClick!!()
menuController.hide()
})
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_edit_24),
contentDescription = null
)
Text(
text = stringResource(R.string.ChatFoldersFragment__edit_folder),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(onClick = {
onDelete!!()
menuController.hide()
})
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_trash_24),
contentDescription = null
)
Text(
text = stringResource(R.string.CreateFoldersFragment__delete_folder),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
}
}
}

View file

@ -147,10 +147,13 @@ class ChatFoldersViewModel : ViewModel() {
}
}
fun deleteFolder() {
fun deleteFolder(context: Context, forceRefresh: Boolean = false) {
viewModelScope.launch(Dispatchers.IO) {
ChatFoldersRepository.deleteFolder(internalState.value.originalFolder)
if (forceRefresh) {
loadCurrentFolders(context)
}
internalState.update {
it.copy(showDeleteDialog = false)
}

View file

@ -129,7 +129,7 @@ class CreateFoldersFragment : ComposeFragment() {
onToggleShowMuted = { viewModel.toggleShowMutedChats(it) },
onDeleteClicked = { viewModel.showDeleteDialog(true) },
onDeleteConfirmed = {
viewModel.deleteFolder()
viewModel.deleteFolder(requireContext())
navController.popBackStack()
},
onDeleteDismissed = {

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import org.signal.core.ui.copied.androidx.compose.material3.DropdownMenu
@ -32,6 +33,8 @@ object DropdownMenus {
fun Menu(
controller: MenuController = remember { MenuController() },
modifier: Modifier = Modifier,
offsetX: Dp = dimensionResource(id = R.dimen.core_ui__gutter),
offsetY: Dp = 0.dp,
content: @Composable ColumnScope.(MenuController) -> Unit
) {
MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(18.dp))) {
@ -39,8 +42,8 @@ object DropdownMenus {
expanded = controller.isShown(),
onDismissRequest = controller::hide,
offset = DpOffset(
x = dimensionResource(id = R.dimen.core_ui__gutter),
y = 0.dp
x = offsetX,
y = offsetY
),
content = { content(controller) },
modifier = modifier

View file

@ -3,7 +3,6 @@ package org.signal.core.ui.copied.androidx.compose
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@ -23,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
@ -33,13 +33,14 @@ import kotlinx.coroutines.launch
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
*
* Allows for dragging and dropping to reorder within lazy columns
* Supports adding non-draggable headers and footers.
*/
@Composable
fun rememberDragDropState(lazyListState: LazyListState, onMove: (Int, Int) -> Unit): DragDropState {
fun rememberDragDropState(lazyListState: LazyListState, includeHeader: Boolean, includeFooter: Boolean, onMove: (Int, Int) -> Unit): DragDropState {
val scope = rememberCoroutineScope()
val state =
remember(lazyListState) {
DragDropState(state = lazyListState, onMove = onMove, scope = scope)
DragDropState(state = lazyListState, onMove = onMove, includeHeader = includeHeader, includeFooter = includeFooter, scope = scope)
}
LaunchedEffect(state) {
while (true) {
@ -54,6 +55,8 @@ class DragDropState
internal constructor(
private val state: LazyListState,
private val scope: CoroutineScope,
private val includeHeader: Boolean,
private val includeFooter: Boolean,
private val onMove: (Int, Int) -> Unit
) {
var draggingItemIndex by mutableStateOf<Int?>(null)
@ -80,7 +83,11 @@ internal constructor(
internal fun onDragStart(offset: Offset) {
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }
.firstOrNull { item ->
offset.y.toInt() in item.offset..(item.offset + item.size) &&
(!includeHeader || item.index != 0) &&
(!includeFooter || item.index != (state.layoutInfo.totalItemsCount - 1))
}
?.also {
draggingItemIndex = it.index
draggingItemInitialOffset = it.offset
@ -106,6 +113,10 @@ internal constructor(
}
internal fun onDrag(offset: Offset) {
if ((includeHeader && draggingItemIndex == 0) ||
(includeFooter && draggingItemIndex == (state.layoutInfo.totalItemsCount - 1))
) return
draggingItemDraggedDelta += offset.y
val draggingItem = draggingItemLayoutInfo ?: return
@ -116,19 +127,20 @@ internal constructor(
val targetItem =
state.layoutInfo.visibleItemsInfo.find { item ->
middleOffset.toInt() in item.offset..item.offsetEnd &&
draggingItem.index != item.index
item.index != draggingItem.index &&
(!includeHeader || item.index != 0) &&
(!includeFooter || item.index != (state.layoutInfo.totalItemsCount - 1))
}
if (targetItem != null) {
if (
draggingItem.index == state.firstVisibleItemIndex ||
targetItem.index == state.firstVisibleItemIndex
) {
state.requestScrollToItem(
state.firstVisibleItemIndex,
state.firstVisibleItemScrollOffset
)
if (targetItem != null &&
(!includeHeader || targetItem.index != 0) &&
(!includeFooter || targetItem.index != (state.layoutInfo.totalItemsCount - 1))
) {
if (includeHeader) {
onMove.invoke(draggingItem.index - 1, targetItem.index - 1)
} else {
onMove.invoke(draggingItem.index, targetItem.index)
}
onMove.invoke(draggingItem.index, targetItem.index)
draggingItemIndex = targetItem.index
} else {
val overscroll =
@ -149,16 +161,18 @@ internal constructor(
get() = this.offset + this.size
}
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
fun Modifier.dragContainer(dragDropState: DragDropState, leftDpOffset: Dp, rightDpOffset: Dp): Modifier {
return pointerInput(dragDropState) {
detectDragGesturesAfterLongPress(
detectDragGestures(
onDrag = { change, offset ->
change.consume()
dragDropState.onDrag(offset = offset)
},
onDragStart = { offset -> dragDropState.onDragStart(offset) },
onDragEnd = { dragDropState.onDragInterrupted() },
onDragCancel = { dragDropState.onDragInterrupted() }
onDragCancel = { dragDropState.onDragInterrupted() },
leftDpOffset = leftDpOffset,
rightDpOffset = rightDpOffset
)
}
}

View file

@ -0,0 +1,131 @@
package org.signal.core.ui.copied.androidx.compose
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.drag
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.isOutOfBounds
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.CancellationException
/**
* Modified version of detectDragGesturesAfterLongPress from [androidx.compose.foundation.gestures.DragGestureDetector]
* that allows you to optionally offset the starting and ending position of the draggable area
*/
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
leftDpOffset: Dp = 0.dp,
rightDpOffset: Dp
) {
awaitEachGesture {
try {
val down = awaitFirstDown(requireUnconsumed = false)
val drag = awaitLongPressOrCancellation(down.id)
if (drag != null && (drag.position.x > leftDpOffset.toPx()) && (drag.position.x < rightDpOffset.toPx())) {
onDragStart.invoke(drag.position)
if (
drag(drag.id) {
onDrag(it, it.positionChange())
it.consume()
}
) {
// consume up if we quit drag gracefully with the up
currentEvent.changes.fastForEach {
if (it.changedToUp()) it.consume()
}
onDragEnd()
} else {
onDragCancel()
}
}
} catch (c: CancellationException) {
onDragCancel()
throw c
}
}
}
/**
* Modified version of awaitLongPressOrCancellation from [androidx.compose.foundation.gestures.DragGestureDetector] with a reduced long press timeout
*/
suspend fun AwaitPointerEventScope.awaitLongPressOrCancellation(
pointerId: PointerId
): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
return null // The pointer has already been lifted, so the long press is cancelled.
}
val initialDown =
currentEvent.changes.fastFirstOrNull { it.id == pointerId } ?: return null
var longPress: PointerInputChange? = null
var currentDown = initialDown
val longPressTimeout = (viewConfiguration.longPressTimeoutMillis / 100)
return try {
// wait for first tap up or long press
withTimeout(longPressTimeout) {
var finished = false
while (!finished) {
val event = awaitPointerEvent(PointerEventPass.Main)
if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) {
// All pointers are up
finished = true
}
if (
event.changes.fastAny {
it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)
}
) {
finished = true // Canceled
}
// Check for cancel by position consumption. We can look on the Final pass of
// the existing pointer event because it comes after the Main pass we checked
// above.
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.fastAny { it.isConsumed }) {
finished = true
}
if (event.isPointerUp(currentDown.id)) {
val newPressed = event.changes.fastFirstOrNull { it.pressed }
if (newPressed != null) {
currentDown = newPressed
longPress = currentDown
} else {
// should technically never happen as we checked it above
finished = true
}
// Pointer (id) stayed down.
} else {
longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
}
}
}
null
} catch (_: PointerEventTimeoutCancellationException) {
longPress ?: initialDown
}
}
private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =
changes.fastFirstOrNull { it.id == pointerId }?.pressed != true