Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 10:24:41 +03:00
commit af09ce9ca4
877 changed files with 16091 additions and 6613 deletions

View file

@ -3,6 +3,8 @@ package com.tangem.core.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@ -10,6 +12,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.res.LocalPowerSavingState
import com.tangem.core.ui.res.TangemTheme
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeStyle
@ -61,6 +64,12 @@ fun BottomFade(gradientBrush: Brush, modifier: Modifier = Modifier) {
@Composable
fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val isPowerSavingState by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
if (isPowerSavingState) {
BottomFade(backgroundColor = backgroundColor, modifier = modifier)
return
}
Box(
modifier = modifier

View file

@ -3,10 +3,10 @@ package com.tangem.core.ui.components
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
@ -20,7 +20,7 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MarketsTestTags
import com.tangem.core.ui.test.SwitchTestTags
@Suppress("MagicNumber")
@Composable
@ -47,7 +47,8 @@ fun TangemSwitch(
Box(
modifier = modifier
.clickable(
.toggleable(
value = checked,
interactionSource = interactionSource,
indication = ripple(
bounded = false,
@ -55,10 +56,9 @@ fun TangemSwitch(
),
enabled = enabled,
role = Role.Switch,
onClick = {
onCheckedChange(!checked)
},
).testTag(MarketsTestTags.ADD_TO_PORTFOLIO_SWITCH),
onValueChange = onCheckedChange,
)
.testTag(SwitchTestTags.SWITCH),
) {
BoxWithConstraints(
modifier = Modifier

View file

@ -13,12 +13,14 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.utils.getGreyScaleColorFilter
/**
@ -147,7 +149,9 @@ private fun BoxScope.ContentIconContainer(
if (icon.shouldShowCustomBadge) {
CurrencyIconBottomBadge(
modifier = Modifier.align(Alignment.BottomEnd),
modifier = Modifier
.align(Alignment.BottomEnd)
.testTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON),
)
}
}

View file

@ -12,7 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_S
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.utils.defaultFormat
import com.tangem.core.ui.utils.formatWithThousands
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Locale
@ -78,7 +78,7 @@ class AmountVisualTransformation(
currency = formatterCurrency
}
val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
Timber.e("NumberFormat is null")
TangemLogger.e("NumberFormat is null")
return AnnotatedString(BigDecimalFormatConstants.EMPTY_BALANCE_SIGN)
}
return buildAnnotatedString {

View file

@ -42,29 +42,30 @@ fun Modifier.hazeEffectTangem(
return hazeEffect(state, style) {
fallbackTint = HazeTint(rootBackground.copy(alpha = 0.5f))
if (isGlobalBlurEnabled) {
configure()
}
configure()
blurEnabled = blurEnabled && isGlobalBlurEnabled
}
}
/**
* Applies a haze foreground effect to the [Modifier] with consideration of power saving mode.
* Applies a haze foreground effect to the [Modifier]
*
* @param style The [HazeStyle] to apply. Defaults to [HazeStyle.Unspecified].
* @param isBlurEnabled A Boolean indicating whether blur is enabled. Defaults to true.
* @param reactToPowerSavingMode A Boolean indicating whether the haze effect should react to power saving mode.
* Defaults to false.
* @param configure A lambda to configure the [HazeEffectScope].
* @return A [Modifier] with the configured haze foreground effect applied.
*/
@Composable
fun Modifier.hazeForegroundEffectTangem(
style: HazeStyle = HazeStyle.Unspecified,
reactToPowerSavingMode: Boolean = false,
isBlurEnabled: Boolean = true,
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
val isGlobalBlurEnabled = isBlurEnabled && !powerSavingEnabled.value
val isGlobalBlurEnabled = isBlurEnabled && (!reactToPowerSavingMode || !powerSavingEnabled.value)
return hazeEffect(
style = style,

View file

@ -5,11 +5,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.ManageTokensScreenTestTags
@Composable
inline fun RowContentContainer(
@ -27,18 +29,21 @@ inline fun RowContentContainer(
Box(
contentAlignment = Alignment.Center,
content = icon,
modifier = Modifier.testTag(ManageTokensScreenTestTags.NETWORK_ICON),
)
Box(
modifier = Modifier
.weight(1f)
.heightIn(min = TangemTheme.dimens.size22),
.heightIn(min = TangemTheme.dimens.size22)
.testTag(ManageTokensScreenTestTags.NETWORK_NAME),
contentAlignment = Alignment.CenterStart,
content = text,
)
Box(
modifier = Modifier
.requiredWidthIn(max = TangemTheme.dimens.size80)
.heightIn(min = TangemTheme.dimens.size24),
.heightIn(min = TangemTheme.dimens.size24)
.testTag(ManageTokensScreenTestTags.SWITCH),
contentAlignment = Alignment.CenterEnd,
content = action,
)

View file

@ -144,7 +144,7 @@ private fun ButtonContent(
) {
Column {
AnimatedVisibility(text != null) {
val wrappedText = remember(this) { text.orEmpty() }
val wrappedText = remember(this, text) { text.orEmpty() }
val textStyle = size.toTextStyle()
Text(
text = wrappedText.resolveReference(),

View file

@ -43,6 +43,7 @@ fun TangemContextMenu(
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset.Zero,
properties: PopupProperties = PopupProperties(focusable = true),
positionProvider: PopupPositionProvider? = null,
content: @Composable ColumnScope.() -> Unit,
) {
val expandedStates = remember { MutableTransitionState(false) }
@ -51,7 +52,7 @@ fun TangemContextMenu(
if (expandedStates.currentState || expandedStates.targetState) {
val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
val density = LocalDensity.current
val popupPositionProvider = DropdownMenuPositionProvider(
val popupPositionProvider = positionProvider ?: DropdownMenuPositionProvider(
offset,
density,
) { parentBounds, menuBounds ->
@ -249,6 +250,57 @@ internal data class DropdownMenuPositionProvider(
}
}
/**
* A [PopupPositionProvider] that centers the popup horizontally on the screen
* and positions it below the anchor. If there is not enough space below,
* it positions the popup above the anchor. If there is no space in either direction,
* it reports the required vertical shift via [onAnchorShiftRequired] so the caller
* can move the anchor upward to make room below.
*/
@Immutable
class CenteredContextMenuPositionProvider(
private val contentOffset: DpOffset,
private val density: Density,
private val onAnchorShiftRequired: (Int) -> Unit = {},
) : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize,
): IntOffset {
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
val x = (windowSize.width - popupContentSize.width) / 2
val yBelow = anchorBounds.bottom + contentOffsetY
val yAbove = anchorBounds.top - contentOffsetY - popupContentSize.height
val isFitsBelow = yBelow + popupContentSize.height <= windowSize.height
val isFitsAbove = yAbove >= 0
val y = when {
isFitsBelow -> {
onAnchorShiftRequired(0)
yBelow
}
isFitsAbove -> {
onAnchorShiftRequired(0)
yAbove
}
else -> {
// Neither fits — calculate how much the anchor must shift up
// so the popup fits below. Place popup at bottom edge of screen.
val desiredY = windowSize.height - popupContentSize.height
val shift = yBelow - desiredY
onAnchorShiftRequired(shift)
desiredY
}
}
return IntOffset(x, y)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)

View file

@ -143,7 +143,7 @@ fun TangemSearchField(
innerTextField = innerTextField,
focusManager = focusManager,
interactionSource = interactionSource,
color = TangemTheme.colors2.field.backgroundDefault,
color = TangemTheme.colors2.button.backgroundSecondary,
keyboardController = keyboardController,
)
},
@ -288,10 +288,9 @@ private fun CancelButton(
if (state.query.isNotEmpty()) {
state.onQueryChange("")
}
focusManager.clearFocus()
keyboardController?.hide()
state.onActiveChange(false)
state.onClearClick()
focusManager.clearFocus()
},
)
}

View file

@ -42,12 +42,14 @@ data class TangemMessageUM(
* @param text TextReference for the button label.
* @param type TangemButtonType defining the style type of the button.
* @param iconRes Drawable resource ID for the icon to be displayed in the button (optional).
* @param isLoading Boolean indicating whether the button should show a loading state.
* @param onClick Lambda to be invoked when the button is clicked.
*/
data class TangemMessageButtonUM(
val text: TextReference,
val type: TangemButtonType,
@DrawableRes val iconRes: Int? = null,
val isLoading: Boolean = false,
val onClick: () -> Unit,
) {
/** Creates a TangemButtonUM representation of this message button. */
@ -58,6 +60,7 @@ data class TangemMessageButtonUM(
iconRes = iconRes,
iconPosition = TangemButtonIconPosition.End,
type = type,
isLoading = isLoading,
onClick = onClick,
)
}

View file

@ -117,7 +117,6 @@ fun TangemHeaderRow(
* @param modifier Modifier for the composable
* @param subtitle Optional subtitle as a TextReference
* @param headTangemIconUM Optional TangemIconUM for the head icon
* @param footerTangemIconRes Optional drawable resource ID for the footer icon
* @param isEnabled Boolean indicating if the row is clickable
* @param onItemClick Optional click callback for the row
*/

View file

@ -1,18 +1,13 @@
package com.tangem.core.ui.ds.row.token
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -110,7 +105,7 @@ fun TangemTokenRow(
.fillMaxWidth(),
)
},
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
modifier = modifier,
)
}
@ -198,36 +193,10 @@ fun TangemTokenRow(
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
)
},
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
modifier = modifier,
)
}
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = composed {
val hapticFeedback = LocalHapticFeedback.current
val onClick = tokenRowUM.onItemClick
val onLongClick = tokenRowUM.onItemLongClick
val onHapticLongClick = if (onLongClick != null) {
{
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLongClick()
}
} else {
null
}
when {
onClick == null && onLongClick == null -> this
onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onHapticLongClick)
onClick != null && onLongClick == null -> combinedClickable(onClick = onClick)
onClick != null && onLongClick != null -> {
combinedClickable(onClick = onClick, onLongClick = onHapticLongClick)
}
else -> this
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.ds.row.token
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.ui.geometry.Offset
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.badge.TangemBadgeUM
@ -11,7 +12,9 @@ import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.serialization.Serializable
@Serializable
@Immutable
sealed class TangemTokenRowUM : TangemRowUM {
@ -43,11 +46,12 @@ sealed class TangemTokenRowUM : TangemRowUM {
abstract val onItemClick: (() -> Unit)?
/** Callback which will be called when an item is long clicked */
abstract val onItemLongClick: (() -> Unit)?
abstract val onItemLongClick: ((Offset, TangemTokenRowUM) -> Any)?
/**
* Content state of [TangemTokenRowUM]
*/
@Serializable
data class Content(
override val id: String,
override val headIconUM: TangemIconUM.Currency,
@ -58,12 +62,13 @@ sealed class TangemTokenRowUM : TangemRowUM {
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty,
override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty,
override val onItemClick: (() -> Unit)?,
override val onItemLongClick: (() -> Unit)?,
override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Any)?,
) : TangemTokenRowUM()
/**
* Loading state of [TangemTokenRowUM]
*/
@Serializable
data class Loading(
override val id: String,
override val headIconUM: TangemIconUM.Currency = TangemIconUM.Currency(CurrencyIconState.Loading),
@ -75,12 +80,13 @@ sealed class TangemTokenRowUM : TangemRowUM {
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)? = null
}
/**
* Loading state of [TangemTokenRowUM]
*/
@Serializable
data class Empty(
override val id: String,
) : TangemTokenRowUM() {
@ -92,12 +98,13 @@ sealed class TangemTokenRowUM : TangemRowUM {
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)? = null
}
/**
* Actionable state of [TangemTokenRowUM]
*/
@Serializable
data class Actionable(
override val id: String,
override val headIconUM: TangemIconUM.Currency,
@ -105,16 +112,17 @@ sealed class TangemTokenRowUM : TangemRowUM {
override val subtitleUM: SubtitleUM,
override val tailUM: TangemRowTailUM,
override val onItemClick: (() -> Unit)?,
override val onItemLongClick: (() -> Unit)?,
override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)?,
override val topEndContentUM: EndContentUM = EndContentUM.Empty,
override val bottomEndContentUM: EndContentUM = EndContentUM.Empty,
) : TangemTokenRowUM() {
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
}
@Serializable
@Immutable
sealed class TitleUM {
@Serializable
data class Content(
val text: TextReference,
val hasPending: Boolean = false,
@ -123,16 +131,20 @@ sealed class TangemTokenRowUM : TangemRowUM {
val onBadgeClick: (() -> Unit)? = null,
) : TitleUM()
@Serializable
data object Loading : TitleUM()
@Serializable
data object Placeholder : TitleUM()
@Serializable
data object Empty : TitleUM()
}
@Serializable
@Immutable
sealed class SubtitleUM {
@Serializable
data class Content(
val text: TextReference,
val isAvailable: Boolean = true,
@ -142,16 +154,20 @@ sealed class TangemTokenRowUM : TangemRowUM {
val badge: TangemBadgeUM? = null,
) : SubtitleUM()
@Serializable
data object Loading : SubtitleUM()
@Serializable
data object Placeholder : SubtitleUM()
@Serializable
data object Empty : SubtitleUM()
}
@Serializable
@Immutable
sealed class EndContentUM {
@Serializable
data class Content(
val text: TextReference,
val isAvailable: Boolean = true,
@ -161,13 +177,17 @@ sealed class TangemTokenRowUM : TangemRowUM {
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
) : EndContentUM()
@Serializable
data object Loading : EndContentUM()
@Serializable
data object Placeholder : EndContentUM()
@Serializable
data object Empty : EndContentUM()
}
@Serializable
@Immutable
sealed class PromoBannerUM {
data class Content(
@ -184,6 +204,7 @@ sealed class TangemTokenRowUM : TangemRowUM {
}
}
@Serializable
data object Empty : PromoBannerUM()
}
}

View file

@ -128,7 +128,7 @@ object TangemTokenRowPreviewData {
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
onItemLongClick = { _, _ -> },
)
val defaultEllipsisState: TangemTokenRowUM.Content
@ -155,7 +155,7 @@ object TangemTokenRowPreviewData {
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
onItemLongClick = { _, _ -> },
)
val tokenState: TangemTokenRowUM.Content
@ -169,7 +169,7 @@ object TangemTokenRowPreviewData {
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
onItemLongClick = { _, _ -> },
)
val customTokenState: TangemTokenRowUM.Content
@ -183,7 +183,7 @@ object TangemTokenRowPreviewData {
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
onItemLongClick = { _, _ -> },
)
val draggableState: TangemTokenRowUM.Actionable
@ -194,7 +194,7 @@ object TangemTokenRowPreviewData {
subtitleUM = subtitleUM,
tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24),
onItemClick = {},
onItemLongClick = {},
onItemLongClick = { _, _ -> },
)
val draggableStateV2: TangemTokenRowUM.Actionable
@ -207,7 +207,7 @@ object TangemTokenRowPreviewData {
bottomEndContentUM = bottomEndContentUM,
tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24),
onItemClick = {},
onItemLongClick = {},
onItemLongClick = { _, _ -> },
)
val loadingState: TangemTokenRowUM.Loading
@ -252,7 +252,7 @@ object TangemTokenRowPreviewData {
priceChangeUM = priceChangeState,
),
onItemClick = {},
onItemLongClick = {},
onItemLongClick = { _, _ -> },
)
val accountLetterState: TangemTokenRowUM.Content

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.ds.tabs
import android.content.res.Configuration
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -13,6 +14,7 @@ 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.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.onGloballyPositioned
@ -52,7 +54,6 @@ fun TangemSegmentedPicker(
items = tangemSegmentedPickerUM.items,
modifier = modifier,
initialSelectedItem = tangemSegmentedPickerUM.initialSelectedItem,
hasSeparator = tangemSegmentedPickerUM.hasSeparator,
isFixed = tangemSegmentedPickerUM.isFixed,
isAltSurface = tangemSegmentedPickerUM.isAltSurface,
onClick = onClick,
@ -66,7 +67,6 @@ fun TangemSegmentedPicker(
* @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.
@ -76,7 +76,6 @@ fun TangemSegmentedPicker(
items: ImmutableList<TangemSegmentUM>,
modifier: Modifier = Modifier,
initialSelectedItem: TangemSegmentUM? = null,
hasSeparator: Boolean = false,
isFixed: Boolean = false,
isAltSurface: Boolean = false,
minSegmentWidth: Dp = Dp.Unspecified,
@ -91,11 +90,6 @@ fun TangemSegmentedPicker(
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
@ -113,12 +107,8 @@ fun TangemSegmentedPicker(
itemsWidths = itemsWidths,
selectedIndex = selectedIndex.value,
segmentHeight = segmentHeight.value,
spacing = spacing,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(spacing),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
items.fastForEachIndexed { index, item ->
Segment(
item = item,
@ -135,15 +125,34 @@ fun TangemSegmentedPicker(
}
},
)
if (index != items.lastIndex) {
val selected by selectedIndex
val alpha by animateFloatAsState(
targetValue = if (selected == index || selected == index + 1) 0f else 1f,
animationSpec = tween(durationMillis = 300),
label = "separatorAlpha",
)
Box(
Modifier
.alpha(alpha)
.width(0.5.dp)
.height(20.dp)
.background(
color = TangemTheme.colors2.border.neutral.tertiary.copy(alpha = 0.1f),
),
)
}
}
}
}
}
@Composable
private fun SegmentSelection(itemsWidths: SnapshotStateList<Dp>, selectedIndex: Int, segmentHeight: Dp, spacing: Dp) {
private fun SegmentSelection(itemsWidths: SnapshotStateList<Dp>, selectedIndex: Int, segmentHeight: Dp) {
val indicatorOffset by animateDpAsState(
targetValue = itemsWidths.take(selectedIndex).fold(0.dp, Dp::plus) + spacing * selectedIndex,
targetValue = itemsWidths.take(selectedIndex).fold(0.dp, Dp::plus),
animationSpec = tween(durationMillis = 300),
label = "indicatorOffset",
)
@ -228,7 +237,6 @@ private fun TangemSegmentedPicker_Preview(@PreviewParameter(PreviewProvider::cla
) {
TangemSegmentedPicker(
isFixed = params.isFixed,
hasSeparator = params.hasSeparator,
isAltSurface = params.isAltSurface,
items = params.items,
initialSelectedItem = params.items.last(),
@ -248,25 +256,21 @@ private class PreviewProvider : PreviewParameterProvider<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,
),

View file

@ -8,14 +8,12 @@ import kotlinx.collections.immutable.ImmutableList
*
* @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,
)

View file

@ -102,6 +102,7 @@ fun TangemTopBar(
endContent = endContent,
content = {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
) {
@ -128,47 +129,56 @@ fun TangemTopBar(
* A top bar composable that displays a title and optional start and end icons.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev)
*
* @param modifier Modifier to be applied to the top bar.
* @param type Type of the top bar, which determines its size and padding.
* @param content Composable content to be displayed in the center of the top bar
* @param startContent Optional composable content to be displayed at the start (left) of the top bar.
* @param endContent Optional composable content to be displayed at the end (right) of the top bar.
* @param modifier Modifier to be applied to the top bar.
* @param type Type of the top bar, which determines its size and padding.
* @param content Composable content to be displayed in the center of the top bar
* @param startContent Optional composable content to be displayed at the start (left) of the top bar.
* @param endContent Optional composable content to be displayed at the end (right) of the top bar.
* @param reserveSlotSpace If true (default), slot space is always reserved even when start/end content is null,
* ensuring centered alignment of [content]. Set to false for edge-to-edge content like
* search fields, where empty slots should not consume space.
*/
@Composable
fun TangemTopBar(
modifier: Modifier = Modifier,
type: TangemTopBarType = TangemTopBarType.Default,
content: @Composable () -> Unit,
reserveSlotSpace: Boolean = true,
content: @Composable RowScope.() -> Unit,
startContent: @Composable (() -> Unit)? = null,
endContent: @Composable (() -> Unit)? = null,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier
.fillMaxWidth()
.heightIn(min = type.getSize())
.padding(type.getPadding()),
) {
AnimatedContent(
targetState = startContent != null,
modifier = Modifier.size(TangemTheme.dimens2.x11),
label = "Start Content Visibility",
) { isVisible ->
if (isVisible) {
startContent?.invoke()
if (reserveSlotSpace || startContent != null) {
AnimatedContent(
targetState = startContent != null,
modifier = Modifier.size(TangemTheme.dimens2.x11),
label = "Start Content Visibility",
) { isVisible ->
if (isVisible) {
startContent?.invoke()
}
}
}
content()
AnimatedContent(
targetState = endContent != null,
modifier = Modifier.size(TangemTheme.dimens2.x11),
label = "End Content Visibility",
) { isVisible ->
if (isVisible) {
endContent?.invoke()
if (reserveSlotSpace || endContent != null) {
AnimatedContent(
targetState = endContent != null,
modifier = Modifier
.height(TangemTheme.dimens2.x11)
.widthIn(min = TangemTheme.dimens2.x11),
label = "End Content Visibility",
) { isVisible ->
if (isVisible) {
endContent?.invoke()
}
}
}
}

View file

@ -17,7 +17,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState
import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBapScrollDirection
import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBarScrollDirection
import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState
import com.tangem.core.ui.utils.toPx
import kotlin.math.abs
@ -37,6 +37,7 @@ import kotlin.math.absoluteValue
*/
@Composable
fun rememberTangemExitUntilCollapsedScrollBehavior(
isTopOverscrollEnabled: Boolean = true,
expandedHeight: Dp = -Int.MAX_VALUE.dp,
partialCollapsedHeight: Dp = expandedHeight,
snapAnimationSpec: AnimationSpec<Float>? = spring(),
@ -45,6 +46,7 @@ fun rememberTangemExitUntilCollapsedScrollBehavior(
val topBarState = rememberTangemCollapsingAppBarState(
heightOffsetLimit = -expandedHeight.toPx(),
partialHeightLimit = partialCollapsedHeight.toPx(),
isTopOverscrollEnabled = isTopOverscrollEnabled,
)
return exitUntilCollapsedScrollBehavior(
state = topBarState,
@ -76,7 +78,7 @@ private fun exitUntilCollapsedScrollBehavior(
val dy = available.y
val consume = if (dy < 0) {
state.direction = TopBapScrollDirection.Collapsing
state.direction = TopBarScrollDirection.Collapsing
state.dispatchRawDelta(dy)
} else {
0f
@ -89,16 +91,57 @@ private fun exitUntilCollapsedScrollBehavior(
val dy = available.y
val consume = if (dy > 0) {
state.direction = TopBapScrollDirection.Expanding
state.direction = TopBarScrollDirection.Expanding
state.dispatchRawDelta(dy)
} else {
state.direction = TopBapScrollDirection.Collapsing
state.direction = TopBarScrollDirection.Collapsing
0f
}
return Offset(0f, consume)
}
@Suppress("MagicNumber")
override suspend fun onPreFling(available: Velocity): Velocity {
val vy = available.y
// Only handle upward fling (collapsing)
if (vy >= 0f) return Velocity.Zero
val effectiveLimit = if (state.isTopOverscrollEnabled) {
state.heightOffsetLimit + state.partialHeightLimit
} else {
state.heightOffsetLimit
}
// Already at the collapse limit — nothing to consume
if (state.heightOffset <= effectiveLimit) return Velocity.Zero
state.direction = TopBarScrollDirection.Collapsing
var remainingVelocity = vy
if (flingAnimationSpec != null) {
var lastValue = 0f
AnimationState(
initialValue = 0f,
initialVelocity = vy,
).animateDecay(flingAnimationSpec) {
val delta = value - lastValue
val prevOffset = state.heightOffset
state.heightOffset =
(prevOffset + delta).coerceAtLeast(effectiveLimit)
val consumed = abs(prevOffset - state.heightOffset)
lastValue = value
remainingVelocity = this.velocity
// Stop when the bar can't collapse any further
if (consumed < 0.5f && abs(delta) > 0.5f) {
cancelAnimation()
}
}
}
return Velocity(0f, available.y - remainingVelocity)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
val superConsumed = super.onPostFling(consumed, available)
return superConsumed + settleAppBar(
@ -179,7 +222,7 @@ private suspend fun settleAppBar(
val availableDelta = partialLimit - initialHeightOffset
state.heightOffset = if (delta < 0f && initialHeightOffset > partialLimit) {
state.heightOffset = if (delta < 0f && initialHeightOffset >= partialLimit) {
(initialHeightOffset + delta).coerceAtLeast(partialLimit)
} else {
initialHeightOffset + delta
@ -196,17 +239,17 @@ private suspend fun settleAppBar(
if (snapAnimationSpec != null && state.heightOffset > partialLimit && state.heightOffset < 0f) {
AnimationState(initialValue = state.heightOffset).animateTo(
when (state.direction) {
TopBapScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) {
TopBarScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) {
partialLimit
} else {
0f
}
TopBapScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) {
TopBarScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) {
0f
} else {
partialLimit
}
TopBapScrollDirection.Idle -> 0f
TopBarScrollDirection.Idle -> 0f
},
animationSpec = snapAnimationSpec,
) {

View file

@ -30,6 +30,7 @@ class TangemCollapsingAppBarState(
val initialHeightOffset: Float = 0f,
val heightOffsetLimit: Float = 0f,
val partialHeightLimit: Float = heightOffsetLimit,
var isTopOverscrollEnabled: Boolean = true,
) : ScrollableState {
private val _heightOffset = mutableFloatStateOf(initialHeightOffset)
@ -42,8 +43,7 @@ class TangemCollapsingAppBarState(
var heightOffset: Float
get() = _heightOffset.floatValue
set(newOffset) {
_heightOffset.floatValue =
newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f)
_heightOffset.floatValue = newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f)
}
/**
@ -60,11 +60,13 @@ class TangemCollapsingAppBarState(
/**
* The current scroll direction of the app bar, which can be Collapsing, Expanding, or Idle.
*/
var direction: TopBapScrollDirection = TopBapScrollDirection.Idle
var direction: TopBarScrollDirection = TopBarScrollDirection.Idle
private val scrollableState = ScrollableState { value ->
val effectiveLimit = if (isTopOverscrollEnabled) heightOffsetLimit + partialHeightLimit else heightOffsetLimit
val consume = if (value < 0) {
max(heightOffsetLimit - heightOffset, value)
// Already at or past the effective limit — don't consume collapsing scroll
if (heightOffset <= effectiveLimit) 0f else max(effectiveLimit - heightOffset, value)
} else {
min(0f - heightOffset, value)
}
@ -104,12 +106,20 @@ class TangemCollapsingAppBarState(
/** The default [Saver] implementation for [TangemCollapsingAppBarState]. */
val Saver: Saver<TangemCollapsingAppBarState, *> =
listSaver(
save = { state -> listOf(state.heightOffsetLimit, state.heightOffset, state.partialHeightLimit) },
save = { state ->
listOf(
state.heightOffsetLimit,
state.heightOffset,
state.partialHeightLimit,
state.isTopOverscrollEnabled,
)
},
restore = { state ->
TangemCollapsingAppBarState(
heightOffsetLimit = state[0],
partialHeightLimit = state[2],
initialHeightOffset = state[1],
heightOffsetLimit = state[0] as Float,
initialHeightOffset = state[1] as Float,
partialHeightLimit = state[2] as Float,
isTopOverscrollEnabled = state[3] as Boolean,
)
},
)
@ -121,6 +131,7 @@ class TangemCollapsingAppBarState(
*/
@Composable
fun rememberTangemCollapsingAppBarState(
isTopOverscrollEnabled: Boolean = true,
heightOffsetLimit: Float = -Float.MAX_VALUE,
partialHeightLimit: Float = -Float.MAX_VALUE,
initialHeightOffset: Float = 0f,
@ -130,13 +141,16 @@ fun rememberTangemCollapsingAppBarState(
initialHeightOffset = initialHeightOffset,
partialHeightLimit = partialHeightLimit,
heightOffsetLimit = heightOffsetLimit,
isTopOverscrollEnabled = isTopOverscrollEnabled,
)
}.also {
it.isTopOverscrollEnabled = isTopOverscrollEnabled
}
}
/**
* The scroll direction of the top app bar, which can be Collapsing, Expanding, or Idle.
*/
enum class TopBapScrollDirection {
enum class TopBarScrollDirection {
Collapsing, Expanding, Idle
}

View file

@ -13,6 +13,22 @@ import com.tangem.core.ui.message.EventMessageAction
*/
object Dialogs {
/**
* NFC feature is unavailable dialog
*/
fun nfcFeatureUnavailable(): DialogMessage = DialogMessage(
title = resourceReference(id = R.string.common_error),
message = resourceReference(R.string.nfc_error_unavailable),
)
/**
* Wrong wallet tapped dialog
*/
fun wrongWalletTapped(): DialogMessage = DialogMessage(
title = resourceReference(id = R.string.common_warning),
message = resourceReference(id = R.string.error_wrong_wallet_tapped),
)
/**
* Card verification failed dialog
*

View file

@ -31,6 +31,7 @@ fun TangemThemeRedesign(content: @Composable () -> Unit) {
LocalTangemColors provides themeColors,
LocalTangemColors2 provides if (LocalIsInDarkTheme.current) darkThemeColors2() else lightThemeColors2(),
LocalTangemTypography2 provides TangemTypography2(InterFamily),
LocalTangemTypography provides TangemTypography(InterFamily),
LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) },
) {
CompositionLocalProvider(
@ -93,7 +94,7 @@ private fun lightThemeColors2(): TangemColors2 {
neutral = TangemColors2.Border.Neutral(
primary = TangemColorPalette.Light3,
secondary = TangemColorPalette.Light5,
tertiary = TangemColorPalette.Light_10,
tertiary = TangemColorPalette.Dark_10,
quaternary = TangemColorPalette.Dark_10,
),
status = TangemColors2.Border.Status(

View file

@ -114,6 +114,18 @@ class TangemTypography2 internal constructor(
),
)
val headingSemibold22: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 22.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = 0.38f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 28f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingRegular20: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 20.sp,

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object AccountDetailsScreenTestTags {
const val MANAGE_TOKENS_BUTTON = "ACCOUNT_DETAILS_SCREEN_MANAGE_TOKENS_BUTTON"
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.ui.test
object ManageTokensScreenTestTags {
const val TOKEN_ITEM = "MANAGE_TOKENS_SCREEN_TOKEN_ITEM"
const val NETWORK_ICON = "MANAGE_TOKENS_SCREEN_NETWORK_ICON"
const val NETWORK_NAME = "MANAGE_TOKENS_SCREEN_NETWORK_NAME"
const val SWITCH = "MANAGE_TOKENS_SCREEN_SWITCH"
}

View file

@ -3,6 +3,5 @@ package com.tangem.core.ui.test
object MarketsTestTags {
const val TOKENS_LIST = "MARKETS_TOKENS_LIST"
const val TOKENS_LIST_ITEM = "MARKETS_TOKENS_LIST_ITEM"
const val ADD_TO_PORTFOLIO_SWITCH = "MARKETS_ADD_TO_PORTFOLIO_SWITCH"
const val LISTED_ON_EXCHANGES_COUNT = "MARKETS_LISTED_ON_EXCHANGES_COUNT"
}

View file

@ -16,7 +16,6 @@ object SwapTokenScreenTestTags {
const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON"
const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON"
const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT"
const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT"
const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON"
const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT"
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object SwitchTestTags {
const val SWITCH = "SWITCH"
}

View file

@ -9,4 +9,5 @@ object TokenElementsTestTags {
const val TOKEN_CRYPTO_AMOUNT = "TOKEN_CRYPTO_AMOUNT"
const val TOKEN_NON_FIAT_BLOCK = "TOKEN_NON_FIAT_BLOCK"
const val TOKEN_YIELD_PROMO_BANNER = "TOKEN_YIELD_PROMO_BANNER"
const val TOKEN_CUSTOM_DERIVATION_ICON = "TOKEN_CUSTOM_DERIVATION_ICON"
}

View file

@ -3,4 +3,5 @@ package com.tangem.core.ui.test
object WalletSettingsScreenTestTags {
const val SCREEN_CONTAINER = "WALLET_SETTINGS_SCREEN_CONTAINER"
const val SCREEN_ITEM = "WALLET_SETTINGS_SCREEN_ITEM"
const val USER_ACCOUNT_ITEM = "WALLET_SETTINGS_USER_ACCOUNT_ITEM"
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10"
android:viewportHeight="10">
<path
android:pathData="M7.834,5.222C7.834,4.946 7.61,4.722 7.334,4.722H2.667C2.391,4.722 2.167,4.946 2.167,5.222V7.834C2.167,8.11 2.391,8.334 2.667,8.334H7.334C7.61,8.333 7.834,8.11 7.834,7.834V5.222ZM6.444,3.111C6.444,2.313 5.798,1.667 5,1.667C4.202,1.667 3.556,2.313 3.556,3.111V3.722H6.444V3.111ZM7.444,3.727C8.221,3.784 8.834,4.431 8.834,5.222V7.834C8.834,8.662 8.162,9.333 7.334,9.334H2.667C1.839,9.334 1.167,8.662 1.167,7.834V5.222C1.167,4.431 1.779,3.784 2.556,3.727V3.111C2.556,1.761 3.65,0.667 5,0.667C6.35,0.667 7.444,1.761 7.444,3.111V3.727Z"
android:fillColor="#919191"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M9.348,29.329H22.648C23.551,29.329 24.222,29.09 24.66,28.612C25.107,28.143 25.33,27.406 25.33,26.402V15.869C25.33,14.874 25.107,14.143 24.66,13.674C24.222,13.196 23.551,12.956 22.648,12.956H9.628C8.734,12.956 8.016,13.196 7.476,13.674C6.936,14.152 6.666,14.884 6.666,15.869V26.402C6.666,27.406 6.89,28.143 7.337,28.612C7.784,29.09 8.454,29.329 9.348,29.329ZM8.887,13.918H11.137V9.053C11.137,7.848 11.36,6.839 11.807,6.025C12.254,5.203 12.845,4.586 13.581,4.174C14.326,3.753 15.132,3.543 15.998,3.543C16.874,3.543 17.679,3.753 18.415,4.174C19.151,4.586 19.742,5.203 20.189,6.025C20.636,6.839 20.86,7.848 20.86,9.053V13.918H23.109V9.355C23.109,8.006 22.913,6.829 22.522,5.825C22.131,4.82 21.6,3.988 20.93,3.328C20.259,2.658 19.495,2.161 18.638,1.835C17.791,1.501 16.911,1.333 15.998,1.333C15.085,1.333 14.205,1.501 13.358,1.835C12.51,2.161 11.751,2.658 11.081,3.328C10.41,3.988 9.874,4.82 9.474,5.825C9.083,6.829 8.887,8.006 8.887,9.355V13.918Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="11dp"
android:viewportWidth="10"
android:viewportHeight="11">
<path
android:pathData="M3.639,4.008C4.256,3.907 4.849,4.154 5.411,4.716C5.825,5.129 6.112,5.177 6.314,5.144C6.562,5.103 6.848,4.92 7.198,4.569C7.393,4.374 7.71,4.374 7.905,4.569C8.1,4.765 8.1,5.081 7.905,5.276C7.522,5.659 7.048,6.037 6.476,6.131C5.859,6.231 5.266,5.984 4.704,5.423C4.29,5.009 4.003,4.962 3.8,4.995C3.552,5.036 3.267,5.219 2.916,5.569C2.721,5.764 2.404,5.764 2.209,5.569C2.014,5.374 2.014,5.057 2.209,4.862C2.592,4.479 3.066,4.101 3.639,4.008Z"
android:fillColor="#919191"/>
<path
android:pathData="M5,0C7.761,0 10,2.239 10,5C10,7.761 7.761,10 5,10C2.239,10 0,7.761 0,5C0,2.239 2.239,0 5,0ZM5,1C2.791,1 1,2.791 1,5C1,7.209 2.791,9 5,9C7.209,9 9,7.209 9,5C9,2.791 7.209,1 5,1Z"
android:fillColor="#919191"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,2C23.732,2 30,8.268 30,16C30,23.732 23.732,30 16,30C8.268,30 2,23.732 2,16C2,8.268 8.268,2 16,2ZM17.042,15.184C15.549,13.691 13.972,13.035 12.332,13.303C10.812,13.551 9.551,14.556 8.533,15.573C8.015,16.092 8.015,16.933 8.533,17.452C9.052,17.97 9.894,17.97 10.412,17.452C11.344,16.521 12.103,16.033 12.761,15.926C13.3,15.838 14.064,15.963 15.163,17.063C16.656,18.555 18.232,19.212 19.871,18.944C21.391,18.696 22.652,17.691 23.67,16.674C24.188,16.155 24.188,15.314 23.67,14.795C23.151,14.276 22.31,14.276 21.791,14.795C20.86,15.726 20.1,16.213 19.442,16.32C18.904,16.408 18.14,16.282 17.042,15.184Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20,5C20.552,5 21,5.448 21,6V9.596C21,12.374 18.735,14.62 15.956,14.596L7.29,14.519L10.445,17.808C10.827,18.206 10.814,18.839 10.416,19.222C10.017,19.604 9.384,19.591 9.002,19.192L4.33,14.323C4.134,14.118 4.028,13.863 4.005,13.608L4,13.5L4.005,13.392C4.028,13.137 4.134,12.882 4.33,12.677L9.002,7.808C9.384,7.409 10.017,7.396 10.416,7.778C10.814,8.161 10.827,8.794 10.445,9.192L7.252,12.519L15.974,12.596C17.641,12.61 19,11.263 19,9.596V6C19,5.448 19.448,5 20,5Z"
android:fillColor="#919191"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB