From 4bf2d405ceb0b9bb06cb82b01fb06c9f59885f54 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sun, 30 Jun 2024 08:05:59 -0300 Subject: [PATCH] feat: add drag-and-drop to channel editor --- .../com/geeksville/mesh/ui/ChannelFragment.kt | 49 ++++++++++++++----- .../config/ChannelSettingsItemList.kt | 35 +++++++++++-- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index 02b6e5b7b..78cb84d1f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -8,13 +8,15 @@ import android.view.View import android.view.ViewGroup import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth 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.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ButtonDefaults @@ -51,6 +53,7 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -89,6 +92,9 @@ import com.geeksville.mesh.ui.components.DropDownPreference import com.geeksville.mesh.ui.components.PreferenceFooter import com.geeksville.mesh.ui.components.config.ChannelCard import com.geeksville.mesh.ui.components.config.EditChannelDialog +import com.geeksville.mesh.ui.components.dragContainer +import com.geeksville.mesh.ui.components.dragDropItemsIndexed +import com.geeksville.mesh.ui.components.rememberDragDropState import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.journeyapps.barcodescanner.ScanContract @@ -159,6 +165,19 @@ fun ChannelScreen( } } + fun updateSettingsList(update: MutableList.() -> Unit) { + try { + val list = channelSet.settingsList.toMutableList() + list.update() + channelSet = channelSet.copy { + settings.clear() + settings.addAll(list) + } + } catch (ex: Exception) { + errormsg("Error updating ChannelSettings list:", ex) + } + } + fun zxingScan() { debug("Starting zxing QR code scanner") val zxingScan = ScanOptions() @@ -273,10 +292,18 @@ fun ChannelScreen( ) } + val listState = rememberLazyListState() + val dragDropState = rememberDragDropState(listState) { from, to -> + updateSettingsList { add(to.index, removeAt(from.index)) } + } + LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp, vertical = 16.dp), + modifier = Modifier.dragContainer( + dragDropState = dragDropState, + haptics = LocalHapticFeedback.current, + ), + state = listState, + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp), ) { if (!showChannelEditor) item { ClickableTextField( @@ -288,18 +315,18 @@ fun ChannelScreen( modifier = Modifier.fillMaxWidth(), ) } else { - itemsIndexed(channelSet.settingsList) { index, channel -> + dragDropItemsIndexed( + items = channelSet.settingsList, + dragDropState = dragDropState, + ) { index, channel, isDragging -> + val elevation by animateDpAsState(if (isDragging) 8.dp else 4.dp, label = "drag") ChannelCard( + elevation = elevation, index = index, title = channel.name.ifEmpty { modemPresetName }, enabled = enabled, onEditClick = { showEditChannelDialog = index }, - onDeleteClick = { - channelSet = channelSet.copy { - settings.clear() - settings.addAll(channelSet.settingsList.filterIndexed { i, _ -> i != index }) - } - } + onDeleteClick = { updateSettingsList { removeAt(index) } } ) } item { diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/ChannelSettingsItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/ChannelSettingsItemList.kt index f61c89a8a..916a48592 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/config/ChannelSettingsItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/ChannelSettingsItemList.kt @@ -2,18 +2,20 @@ package com.geeksville.mesh.ui.components.config import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Card import androidx.compose.material.Chip import androidx.compose.material.ContentAlpha @@ -36,8 +38,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.geeksville.mesh.ChannelProtos.ChannelSettings import com.geeksville.mesh.R @@ -45,6 +49,9 @@ import com.geeksville.mesh.channelSettings import com.geeksville.mesh.model.Channel import com.geeksville.mesh.ui.components.PreferenceCategory import com.geeksville.mesh.ui.components.PreferenceFooter +import com.geeksville.mesh.ui.components.dragContainer +import com.geeksville.mesh.ui.components.dragDropItemsIndexed +import com.geeksville.mesh.ui.components.rememberDragDropState @OptIn(ExperimentalMaterialApi::class) @Composable @@ -54,13 +61,14 @@ fun ChannelCard( enabled: Boolean, onEditClick: () -> Unit, onDeleteClick: () -> Unit, + elevation: Dp = 4.dp, ) { Card( modifier = Modifier .fillMaxWidth() .padding(vertical = 2.dp) .clickable(enabled = enabled) { onEditClick() }, - elevation = 4.dp + elevation = elevation, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -96,6 +104,15 @@ fun ChannelSettingsItemList( val focusManager = LocalFocusManager.current val settingsListInput = remember(settingsList) { settingsList.toMutableStateList() } + val listState = rememberLazyListState() + val dragDropState = rememberDragDropState(listState) { from, to -> + settingsListInput.apply { + val fromIndex = indexOfFirst { it.hashCode() == from.key } + val toIndex = indexOfFirst { it.hashCode() == to.key } + add(toIndex, removeAt(fromIndex)) + } + } + val isEditing: Boolean = settingsList.size != settingsListInput.size || settingsList.zip(settingsListInput).any { (item1, item2) -> item1 != item2 } @@ -123,12 +140,22 @@ fun ChannelSettingsItemList( .clickable(onClick = { }, enabled = false) ) { LazyColumn( - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier.dragContainer( + dragDropState = dragDropState, + haptics = LocalHapticFeedback.current, + ), + state = listState, + contentPadding = PaddingValues(horizontal = 16.dp), ) { item { PreferenceCategory(text = "Channels") } - itemsIndexed(settingsListInput) { index, channel -> + dragDropItemsIndexed( + items = settingsListInput, + dragDropState = dragDropState, + ) { index, channel, isDragging -> + val elevation by animateDpAsState(if (isDragging) 8.dp else 4.dp, label = "drag") ChannelCard( + elevation = elevation, index = index, title = channel.name.ifEmpty { modemPresetName }, enabled = enabled,