Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 19:28:21 +02:00
parent aecd523c70
commit 139b4ffdc6
9 changed files with 106 additions and 99 deletions

View file

@ -27,19 +27,20 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenElementsTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
/**
* UI model for header row component
*
* @param headerRowUM UI model for the header row
* @param modifier Modifier for the composable
* @param headerRowUM UI model for the header row
* @param modifier Modifier for the composable
* @param dragHandleModifier Modifier applied to the drag handle in the row's tail (e.g. a reorderable
* drag-handle modifier). Defaults to [Modifier] for non-reorderable rows.
*/
@Composable
fun TangemHeaderRow(
headerRowUM: TangemHeaderRowUM,
modifier: Modifier = Modifier,
reorderableState: ReorderableLazyListState? = null,
dragHandleModifier: Modifier = Modifier,
isBalanceHidden: Boolean = false,
) {
TangemHeaderRow(
@ -48,7 +49,7 @@ fun TangemHeaderRow(
title = headerRowUM.title,
subtitle = headerRowUM.subtitle,
isBalanceHidden = isBalanceHidden,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = modifier,
)
}
@ -129,7 +130,7 @@ fun TangemHeaderRow(
subtitle: TextReference? = null,
headTangemIconUM: TangemIconUM? = null,
tailUM: TangemRowTailUM = TangemRowTailUM.Empty,
reorderableState: ReorderableLazyListState? = null,
dragHandleModifier: Modifier = Modifier,
isEnabled: Boolean = false,
onItemClick: (() -> Unit)? = null,
) {
@ -181,7 +182,7 @@ fun TangemHeaderRow(
SpacerWMax()
TangemRowTail(
tangemRowTailUM = tailUM,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
)
}
}

View file

@ -19,14 +19,12 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
fun TangemRowTail(
tangemRowTailUM: TangemRowTailUM,
modifier: Modifier = Modifier,
reorderableState: ReorderableLazyListState? = null,
dragHandleModifier: Modifier = Modifier,
) {
AnimatedContent(
targetState = tangemRowTailUM,
@ -39,7 +37,7 @@ fun TangemRowTail(
TangemRowTailUM.Empty -> Unit
is TangemRowTailUM.Draggable -> DraggableImage(
iconRes = animatedState.iconRes,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = innerModifier,
)
is TangemRowTailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier)
@ -54,21 +52,11 @@ fun TangemRowTail(
}
@Composable
private fun DraggableImage(
@DrawableRes iconRes: Int,
reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
private fun DraggableImage(@DrawableRes iconRes: Int, dragHandleModifier: Modifier, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(size = TangemTheme.dimens2.x6)
.then(
other = if (reorderableState != null) {
Modifier.detectReorder(reorderableState)
} else {
Modifier
},
)
.then(dragHandleModifier)
.testTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE),
contentAlignment = Alignment.Center,
) {

View file

@ -21,24 +21,24 @@ import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenElementsTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
/**
* Composable function that represents a Tangem token row in a list.
*
* [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4)
*
* @param tokenRowUM The user model containing the data for the token row.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param reorderableState The state of the reorderable lazy list, if applicable.
* @param modifier The modifier to be applied to the row.
* @param tokenRowUM The user model containing the data for the token row.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param dragHandleModifier Modifier applied to the drag handle in the row's tail (e.g. a reorderable
* drag-handle modifier). Defaults to [Modifier] for non-reorderable rows.
* @param modifier The modifier to be applied to the row.
*/
@Composable
fun TangemTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
dragHandleModifier: Modifier = Modifier,
) {
TangemRowContainer(
content = {
@ -91,7 +91,7 @@ fun TangemTokenRow(
TangemRowTail(
tangemRowTailUM = tokenRowUM.tailUM,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
@ -116,18 +116,19 @@ fun TangemTokenRow(
* [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4)
*
* @param tokenRowUM The user model containing the data for the token row.
* @param headComponent The composable function representing the head component.
* @param titleComponent The composable function representing the title component.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param reorderableState The state of the reorderable lazy list, if applicable.
* @param modifier The modifier to be applied to the row.
* @param headComponent The composable function representing the head component.
* @param titleComponent The composable function representing the title component.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param dragHandleModifier Modifier applied to the drag handle in the row's tail (e.g. a reorderable
* drag-handle modifier). Defaults to [Modifier] for non-reorderable rows.
* @param modifier The modifier to be applied to the row.
*/
@Composable
fun TangemTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
dragHandleModifier: Modifier = Modifier,
headComponent: @Composable (Modifier) -> Unit,
titleComponent: @Composable (Modifier) -> Unit,
) {
@ -190,7 +191,7 @@ fun TangemTokenRow(
TangemRowTail(
tangemRowTailUM = tokenRowUM.tailUM,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
@ -210,7 +211,6 @@ private fun TangemTokenRow_Preview(
TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = false,
reorderableState = null,
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
)
}

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.markets.MarketsListItem
@ -405,6 +406,7 @@ private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Mod
Text(
text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k),
style = TangemTheme.typography2.subheadlineMedium14,
textAlign = TextAlign.Center,
color = TangemTheme.colors2.text.neutral.secondary,
)
TangemButton(

View file

@ -61,7 +61,6 @@ internal fun TangemTokenRowStory(state: TangemTokenRowStory, modifier: Modifier
TangemTokenRow(
tokenRowUM = um,
isBalanceHidden = state.isBalanceHidden,
reorderableState = null,
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
)
HorizontalDivider(

View file

@ -31,6 +31,7 @@ dependencies {
implementation(deps.compose.material3)
implementation(deps.compose.paging)
implementation(deps.compose.reorderable)
implementation(deps.compose.reorderableV2)
implementation(deps.compose.shimmer)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)

View file

@ -28,7 +28,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
@ -48,7 +48,6 @@ import com.tangem.core.ui.ds.topbar.TangemTopBarActionContent
import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.reordarable.ReorderableItem
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
@ -63,9 +62,9 @@ import com.tangem.feature.wallet.child.organizetokens.ui.preview.OrganizeTokensP
import com.tangem.feature.wallet.impl.R
import dev.chrisbanes.haze.rememberHazeState
import org.burnoutcrew.reorderable.ItemPosition
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.ReorderableLazyListState
import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
internal fun OrganizeTokensContent(
@ -134,26 +133,27 @@ private fun TokenList(
val hapticFeedback = LocalHapticFeedback.current
val tokenList = organizeTokensUM.tokenList
var draggingItem by remember { mutableStateOf<OrganizeRowItemUM?>(null) }
Box(
modifier = modifier.background(TangemTheme.colors2.surface.level2),
) {
val onDragEnd: (Int, Int) -> Unit = remember {
{ _, _ ->
dragAndDropIntents.onItemDraggingEnd()
}
}
val reorderableListState = rememberReorderableLazyListState(
onMove = dragAndDropIntents::onItemDragged,
listState = tokensListState,
canDragOver = dragAndDropIntents::canDragItemOver,
onDragEnd = onDragEnd,
)
val footerInset = LocalTangemBottomSheetContentBottomInset.current
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val reorderableState = rememberReorderableLazyListState(
lazyListState = tokensListState,
scrollThresholdPadding = PaddingValues(bottom = footerInset),
) { from, to ->
dragAndDropIntents.onItemDragged(
ItemPosition(index = from.index, key = from.key),
ItemPosition(index = to.index, key = to.key),
)
}
val listContentPadding = PaddingValues(
top = TangemTheme.dimens2.x1,
bottom = TangemTheme.dimens2.x1 + bottomBarHeight,
bottom = TangemTheme.dimens2.x1 + footerInset,
start = TangemTheme.dimens2.x3,
end = TangemTheme.dimens2.x3,
)
@ -161,60 +161,82 @@ private fun TokenList(
LazyColumn(
modifier = Modifier
.align(Alignment.TopCenter)
.reorderable(reorderableListState)
.testTag(OrganizeTokensScreenTestTags.TOKENS_LAZY_LIST),
state = reorderableListState.listState,
state = tokensListState,
contentPadding = listContentPadding,
) {
itemsIndexed(
items = tokenList,
key = { _, item -> item.id },
) { index, item ->
val onDragStart = remember(item) {
{
dragAndDropIntents.onItemDraggingStart(item)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
DraggableItem(
index = index,
item = item,
reorderableState = reorderableListState,
onDragStart = onDragStart,
reorderableState = reorderableState,
isValidDropTarget = isValidDropTarget(
item = item,
dragging = draggingItem,
isGrouped = organizeTokensUM.isGrouped,
isAccountsMode = organizeTokensUM.isAccountsMode,
),
isBalanceHidden = organizeTokensUM.isBalanceHidden,
onDragStart = {
draggingItem = item
dragAndDropIntents.onItemDraggingStart(item)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
},
onDragStop = {
draggingItem = null
dragAndDropIntents.onItemDraggingEnd()
},
)
}
item {
SpacerH(TangemTheme.dimens2.x20)
}
}
}
}
private fun isValidDropTarget(
item: OrganizeRowItemUM,
dragging: OrganizeRowItemUM?,
isGrouped: Boolean,
isAccountsMode: Boolean,
): Boolean {
return when {
dragging == null -> true
item.id == dragging.id -> true
dragging is OrganizeRowItemUM.Network ->
item is OrganizeRowItemUM.Placeholder && item.accountId == dragging.accountId
dragging is OrganizeRowItemUM.Token -> when (item) {
is OrganizeRowItemUM.Token -> when {
isGrouped -> item.groupId == dragging.groupId
isAccountsMode -> item.accountId == dragging.accountId
else -> true
}
else -> false
}
else -> false
}
}
@Suppress("LongParameterList")
@Composable
private fun LazyItemScope.DraggableItem(
index: Int,
item: OrganizeRowItemUM,
reorderableState: ReorderableLazyListState,
onDragStart: () -> Unit,
isValidDropTarget: Boolean,
isBalanceHidden: Boolean,
onDragStart: () -> Unit,
onDragStop: () -> Unit,
) {
var isDragging by remember {
mutableStateOf(value = false)
}
val itemModifier = Modifier.applyShapeAndShadow(item.roundingModeUM, item.isShowShadow)
ReorderableItem(
reorderableState = reorderableState,
index = index,
state = reorderableState,
key = item.id,
) { isItemDragging ->
isDragging = isItemDragging
// Only same-group/account items are eligible drop targets while dragging (see TokenList).
enabled = isValidDropTarget,
) { _ ->
val modifierWithBackground = itemModifier
.background(color = TangemTheme.colors.background.primary)
.semantics { lazyListItemPosition = index }
@ -222,7 +244,10 @@ private fun LazyItemScope.DraggableItem(
when (item) {
is OrganizeRowItemUM.Network -> TangemHeaderRow(
modifier = modifierWithBackground.testTag(OrganizeTokensScreenTestTags.GROUP_TITLE_ITEM),
reorderableState = reorderableState,
dragHandleModifier = Modifier.draggableHandle(
onDragStarted = { onDragStart() },
onDragStopped = { onDragStop() },
),
headerRowUM = item.headerRowUM,
)
is OrganizeRowItemUM.Portfolio -> TangemHeaderRow(
@ -233,21 +258,16 @@ private fun LazyItemScope.DraggableItem(
is OrganizeRowItemUM.Token -> OrganizeTokenRow(
modifier = modifierWithBackground.testTag(OrganizeTokensScreenTestTags.TOKEN_LIST_ITEM),
tokenRowUM = item.tokenRowUM,
reorderableState = reorderableState,
dragHandleModifier = Modifier.draggableHandle(
onDragStarted = { onDragStart() },
onDragStopped = { onDragStop() },
),
isBalanceHidden = isBalanceHidden,
)
// Should be presented in the list but remain invisible
is OrganizeRowItemUM.Placeholder -> Box(modifier = Modifier.fillMaxWidth())
}
}
DisposableEffect(isDragging) {
onDispose {
if (isDragging) {
onDragStart()
}
}
}
}
@Composable
@ -310,7 +330,7 @@ private fun Modifier.applyShapeAndShadow(roundingMode: RoundingModeUM, showShado
private fun OrganizeTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableState: ReorderableLazyListState?,
dragHandleModifier: Modifier,
modifier: Modifier = Modifier,
) {
TangemRowContainer(
@ -345,7 +365,7 @@ private fun OrganizeTokenRow(
TangemRowTail(
tangemRowTailUM = tokenRowUM.tailUM,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),

View file

@ -59,7 +59,6 @@ internal fun TokenActionContent(
TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
reorderableState = null,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens2.x3)
.clip(RoundedCornerShape(18.dp))

View file

@ -167,7 +167,6 @@ private fun LazyListScope.tokenItem(
is TangemTokenRowUM -> TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
reorderableState = null,
modifier = itemModifier
.onGloballyPositioned { position = it.positionOnScreen() }
.combinedClickable(
@ -238,7 +237,6 @@ private fun LazyListScope.portfolioItem(
is TangemTokenRowUM -> TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
reorderableState = null,
modifier = itemModifier
.onGloballyPositioned {
position = it.positionInWindow()
@ -450,7 +448,6 @@ internal fun PortfolioRowItem(
headComponent = composables.icon,
titleComponent = composables.title,
isBalanceHidden = isBalanceHidden,
reorderableState = null,
)
}
}