Updated on 2026-08-14
This commit is contained in:
commit
af09ce9ca4
877 changed files with 16091 additions and 6613 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object AccountDetailsScreenTestTags {
|
||||
const val MANAGE_TOKENS_BUTTON = "ACCOUNT_DETAILS_SCREEN_MANAGE_TOKENS_BUTTON"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object SwitchTestTags {
|
||||
const val SWITCH = "SWITCH"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
9
core/ui/src/main/res/drawable/ic_fixed.xml
Normal file
9
core/ui/src/main/res/drawable/ic_fixed.xml
Normal 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>
|
||||
9
core/ui/src/main/res/drawable/ic_fixed_32.xml
Normal file
9
core/ui/src/main/res/drawable/ic_fixed_32.xml
Normal 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>
|
||||
13
core/ui/src/main/res/drawable/ic_floating.xml
Normal file
13
core/ui/src/main/res/drawable/ic_floating.xml
Normal 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>
|
||||
9
core/ui/src/main/res/drawable/ic_floating_32.xml
Normal file
9
core/ui/src/main/res/drawable/ic_floating_32.xml
Normal 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>
|
||||
9
core/ui/src/main/res/drawable/ic_return_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_return_24.xml
Normal 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>
|
||||
BIN
core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp
Normal file
BIN
core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
Loading…
Add table
Add a link
Reference in a new issue