Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-15 17:24:36 +05:00
parent fa0c415aef
commit 3ee2426a9c
5 changed files with 467 additions and 0 deletions

View file

@ -0,0 +1,271 @@
package com.tangem.core.ui.ds.tabs
import android.content.res.Configuration
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
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.IntOffset
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Tangem segmented picker component.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8452-16487&m=dev)
*
* @param tangemSegmentedPickerUM Model representing the segmented picker.
* @param modifier Modifier to be applied to the segmented picker.
* @param onClick Lambda function to be invoked when a segment is clicked.
*/
@Composable
fun TangemSegmentedPicker(
tangemSegmentedPickerUM: TangemSegmentedPickerUM,
modifier: Modifier = Modifier,
onClick: (TangemSegmentUM) -> Unit,
) {
TangemSegmentedPicker(
items = tangemSegmentedPickerUM.items,
modifier = modifier,
initialSelectedItem = tangemSegmentedPickerUM.initialSelectedItem,
hasSeparator = tangemSegmentedPickerUM.hasSeparator,
isFixed = tangemSegmentedPickerUM.isFixed,
isAltSurface = tangemSegmentedPickerUM.isAltSurface,
onClick = onClick,
)
}
/**
* Tangem segmented picker component.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8452-16487&m=dev)
*
* @param items List of TangemSegmentUM representing the segments in the picker.
* @param modifier Modifier to be applied to the segmented picker.
* @param initialSelectedItem Optional TangemSegmentUM representing the initially selected segment.
* @param hasSeparator Boolean indicating whether there is a separator between segments.
* @param isFixed Boolean indicating whether the picker has a fixed width.
* @param isAltSurface Boolean indicating whether to use an alternative surface style.
* @param onClick Lambda function to be invoked when a segment is clicked.
*/
@Composable
fun TangemSegmentedPicker(
items: ImmutableList<TangemSegmentUM>,
modifier: Modifier = Modifier,
initialSelectedItem: TangemSegmentUM? = null,
hasSeparator: Boolean = false,
isFixed: Boolean = false,
isAltSurface: Boolean = false,
onClick: (TangemSegmentUM) -> Unit,
) {
if (items.isEmpty() || items.size == 1) return
val density = LocalDensity.current
val itemsWidths = remember { mutableStateListOf(*Array(items.size) { 0.dp }) }
val selectedIndex = remember { mutableStateOf(items.indexOfFirstOrNull { it == initialSelectedItem } ?: 0) }
val segmentHeight = remember { mutableStateOf(0.dp) }
val shape = RoundedCornerShape(TangemTheme.dimens2.x25)
val spacing = if (hasSeparator) {
TangemTheme.dimens2.x4
} else {
TangemTheme.dimens2.x1
}
Box(
modifier = modifier
.clip(shape)
.background(
color = if (isAltSurface) {
TangemTheme.colors2.tabs.backgroundQuaternary
} else {
TangemTheme.colors2.tabs.backgroundSecondary
},
)
.padding(TangemTheme.dimens2.x0_5),
) {
SegmentSelection(
itemsWidths = itemsWidths,
selectedIndex = selectedIndex.value,
segmentHeight = segmentHeight.value,
spacing = spacing,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(spacing),
) {
items.fastForEachIndexed { index, item ->
Segment(
item = item,
index = index,
isFixed = isFixed,
selectedIndex = selectedIndex,
onClick = { onClick(item) },
modifier = Modifier
.onGloballyPositioned {
with(density) {
itemsWidths[index] = it.size.width.toDp()
segmentHeight.value = it.size.height.toDp().coerceAtLeast(segmentHeight.value)
}
},
)
}
}
}
}
@Composable
private fun SegmentSelection(itemsWidths: SnapshotStateList<Dp>, selectedIndex: Int, segmentHeight: Dp, spacing: Dp) {
val indicatorOffset by animateDpAsState(
targetValue = itemsWidths.take(selectedIndex).fold(0.dp, Dp::plus) + spacing * selectedIndex,
animationSpec = tween(durationMillis = 300),
label = "indicatorOffset",
)
val indicatorWidth by animateDpAsState(
targetValue = itemsWidths[selectedIndex],
animationSpec = tween(durationMillis = 300),
label = "indicatorWidth",
)
Box(
modifier = Modifier
.offset {
IntOffset(x = indicatorOffset.roundToPx(), y = 0)
}
.size(width = indicatorWidth, height = segmentHeight)
.shadow(
elevation = TangemTheme.dimens2.x1,
shape = RoundedCornerShape(TangemTheme.dimens2.x25),
)
.background(
color = TangemTheme.colors2.tabs.backgroundTertiary,
),
)
}
@Composable
private fun RowScope.Segment(
item: TangemSegmentUM,
index: Int,
isFixed: Boolean,
selectedIndex: MutableState<Int>,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.conditional(isFixed) {
weight(1f)
}
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
) {
selectedIndex.value = index
onClick()
},
) {
Text(
text = item.title.resolveReference(),
style = TangemTheme.typography2.bodySemibold15,
color = if (selectedIndex.value == index) {
TangemTheme.colors2.tabs.textTertiary
} else {
TangemTheme.colors2.tabs.textSecondary
},
maxLines = 1,
modifier = Modifier
.align(Alignment.Center)
.padding(
horizontal = TangemTheme.dimens2.x3,
vertical = TangemTheme.dimens2.x2,
),
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemSegmentedPicker_Preview(@PreviewParameter(PreviewProvider::class) params: TangemSegmentedPickerUM) {
var selectedItem by remember { mutableStateOf(params.items.first()) }
TangemThemePreviewRedesign {
Box(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors2.surface.level1)
.padding(TangemTheme.dimens2.x4),
) {
TangemSegmentedPicker(
isFixed = params.isFixed,
hasSeparator = params.hasSeparator,
isAltSurface = params.isAltSurface,
items = params.items,
initialSelectedItem = params.items.last(),
onClick = { selectedItem = it },
)
}
}
}
private class PreviewProvider : PreviewParameterProvider<TangemSegmentedPickerUM> {
private val items = persistentListOf(
TangemSegmentUM(id = "1", title = TextReference.Str("Segment")),
TangemSegmentUM(id = "2", title = TextReference.Str("Segment Two")),
TangemSegmentUM(id = "3", title = TextReference.Str("Seg 3")),
)
override val values: Sequence<TangemSegmentedPickerUM>
get() = sequenceOf(
TangemSegmentedPickerUM(
items = items,
hasSeparator = false,
isFixed = false,
isAltSurface = false,
),
TangemSegmentedPickerUM(
items = items,
hasSeparator = true,
isFixed = false,
isAltSurface = true,
),
TangemSegmentedPickerUM(
items = items,
hasSeparator = false,
isFixed = true,
isAltSurface = false,
),
TangemSegmentedPickerUM(
items = items,
hasSeparator = true,
isFixed = true,
isAltSurface = true,
),
)
}
// endregion

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.ds.tabs
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/**
* Model for Tangem segmented picker component.
*
* @param items List of TangemSegmentUM representing the segments in the picker.
* @param initialSelectedItem Optional TangemSegmentUM representing the initially selected segment.
* @param hasSeparator Boolean indicating whether there is a separator between segments.
* @param isFixed Boolean indicating whether the picker has a fixed width.
* @param isAltSurface Boolean indicating whether to use an alternative surface style.
*/
data class TangemSegmentedPickerUM(
val items: ImmutableList<TangemSegmentUM>,
val initialSelectedItem: TangemSegmentUM? = null,
val hasSeparator: Boolean = false,
val isFixed: Boolean = false,
val isAltSurface: Boolean = false,
)
/**
* Model for a single segment in the Tangem segmented picker.
*
* @param id String representing the unique identifier of the segment.
* @param title TextReference representing the title of the segment.
*/
data class TangemSegmentUM(
val id: String,
val title: TextReference,
)

View file

@ -0,0 +1,105 @@
package com.tangem.core.ui.ds.tabs
import android.content.res.Configuration
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.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* Tangem tab component.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8448-74386&m=dev)
*
* @param text TextReference representing the text to be displayed on the tab.
* @param isChecked Boolean value representing the current state of the tab.
* @param onCheckedChange Lambda function to be invoked when the tab state changes.
* @param modifier Modifier to be applied to the tab.
* @param isEnabled Boolean value to determine if the tab is enabled or disabled.
*/
@Composable
fun TangemTab(
text: TextReference,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
val shape = RoundedCornerShape(TangemTheme.dimens2.x25)
val backgroundColor = if (isChecked) {
TangemTheme.colors2.tabs.backgroundPrimary
} else {
TangemTheme.colors2.tabs.textPrimary
}
val textColor = if (isChecked) {
TangemTheme.colors2.tabs.textPrimary
} else {
TangemTheme.colors2.tabs.textSecondary
}
Box(
modifier = modifier
.background(
color = backgroundColor,
shape = shape,
)
.clip(shape = shape)
.padding(all = TangemTheme.dimens2.x3)
.toggleable(
value = isChecked,
onValueChange = onCheckedChange,
enabled = isEnabled,
role = Role.Checkbox,
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
),
) {
Text(
text = text.resolveReference(),
style = TangemTheme.typography2.bodySemibold16,
color = textColor,
maxLines = 1,
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTab_Preview() {
TangemThemePreviewRedesign {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x6),
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
) {
TangemTab(
text = stringReference("Tab Title"),
isChecked = true,
onCheckedChange = {},
)
TangemTab(
text = stringReference("Tab Title"),
isChecked = false,
onCheckedChange = {},
)
}
}
}
// endregion

View file

@ -1,4 +1,5 @@
@file:Suppress("LongParameterList")
package com.tangem.core.ui.res
import androidx.compose.runtime.Stable
@ -20,6 +21,7 @@ class TangemColors2 internal constructor(
val fill: Fill,
val skeleton: Skeleton,
val markers: Markers,
val tabs: Tabs,
) {
@Stable
@ -524,6 +526,42 @@ class TangemColors2 internal constructor(
}
}
@Stable
class Tabs internal constructor(
textPrimary: Color,
textSecondary: Color,
textTertiary: Color,
backgroundPrimary: Color,
backgroundSecondary: Color,
backgroundTertiary: Color,
backgroundQuaternary: Color,
) {
var textPrimary by mutableStateOf(textPrimary)
private set
var textSecondary by mutableStateOf(textSecondary)
private set
var textTertiary by mutableStateOf(textTertiary)
private set
var backgroundPrimary by mutableStateOf(backgroundPrimary)
private set
var backgroundSecondary by mutableStateOf(backgroundSecondary)
private set
var backgroundTertiary by mutableStateOf(backgroundTertiary)
private set
var backgroundQuaternary by mutableStateOf(backgroundQuaternary)
private set
fun update(other: Tabs) {
textPrimary = other.textPrimary
textSecondary = other.textSecondary
textTertiary = other.textTertiary
backgroundPrimary = other.backgroundPrimary
backgroundSecondary = other.backgroundSecondary
backgroundTertiary = other.backgroundTertiary
backgroundQuaternary = other.backgroundQuaternary
}
}
fun update(other: TangemColors2) {
text.update(other.text)
graphic.update(other.graphic)
@ -536,5 +574,6 @@ class TangemColors2 internal constructor(
fill.update(other.fill)
skeleton.update(other.skeleton)
markers.update(other.markers)
tabs.update(other.tabs)
}
}

View file

@ -156,6 +156,15 @@ private fun lightThemeColors2(): TangemColors2 {
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
)
val tabs = TangemColors2.Tabs(
textPrimary = TangemColorPalette.Light2,
textSecondary = TangemColorPalette.Dark4,
textTertiary = TangemColorPalette.Black,
backgroundPrimary = TangemColorPalette.Dark6,
backgroundSecondary = TangemColorPalette.Dark_10,
backgroundTertiary = TangemColorPalette.White,
backgroundQuaternary = TangemColorPalette.Dark_20,
)
return TangemColors2(
text = text,
graphic = graphic,
@ -168,6 +177,7 @@ private fun lightThemeColors2(): TangemColors2 {
field = field,
skeleton = skeleton,
markers = markers,
tabs = tabs,
)
}
@ -296,6 +306,15 @@ private fun darkThemeColors2(): TangemColors2 {
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
)
val tabs = TangemColors2.Tabs(
textPrimary = TangemColorPalette.Dark4,
textSecondary = TangemColorPalette.Light5,
textTertiary = TangemColorPalette.White,
backgroundPrimary = TangemColorPalette.Light1,
backgroundSecondary = TangemColorPalette.Light_10,
backgroundTertiary = TangemColorPalette.Light_10,
backgroundQuaternary = TangemColorPalette.Light_10,
)
return TangemColors2(
text = text,
graphic = graphic,
@ -308,5 +327,6 @@ private fun darkThemeColors2(): TangemColors2 {
field = field,
skeleton = skeleton,
markers = markers,
tabs = tabs,
)
}