Updated on 2026-08-14
This commit is contained in:
parent
76c129f205
commit
a13a54accd
32 changed files with 665 additions and 35 deletions
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.components.token
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
|
|
@ -28,6 +30,7 @@ import com.tangem.core.ui.components.token.internal.*
|
|||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.PromoBannerState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
|
@ -44,7 +47,7 @@ private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3
|
|||
private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32
|
||||
|
||||
private enum class LayoutId {
|
||||
ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT
|
||||
ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT, PROMO_BANNER
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,6 +112,14 @@ fun TokenItem(
|
|||
.testTag(TokenElementsTestTags.TOKEN_ICON),
|
||||
)
|
||||
|
||||
YieldSupplyPromoBanner(
|
||||
state = state.promoBannerState,
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = LayoutId.PROMO_BANNER)
|
||||
.testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
TokenTitle(
|
||||
state = state.titleState,
|
||||
modifier = Modifier
|
||||
|
|
@ -220,6 +231,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
|
|||
|
||||
val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints)
|
||||
|
||||
val promoBanner = when (state.promoBannerState) {
|
||||
is PromoBannerState.Content -> measurables.measure(
|
||||
layoutId = LayoutId.PROMO_BANNER,
|
||||
constraints = constraints,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
var firstRowRemainingFreeSpace: Int? = null
|
||||
var secondRowRemainingFreeSpace: Int? = null
|
||||
|
||||
|
|
@ -283,10 +301,18 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
|
|||
)
|
||||
}
|
||||
|
||||
val promoBannerHeight = promoBanner?.height ?: 0
|
||||
val promoOffset = if (promoBannerHeight > 0) {
|
||||
promoBannerHeight - 8.dp.roundToPx()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
val layoutHeight = calculateLayoutHeight(
|
||||
state = state,
|
||||
minLayoutHeight = with(density) { dimens.size68.roundToPx() },
|
||||
layoutPadding = verticalPadding,
|
||||
promoOffset = promoOffset,
|
||||
title = title,
|
||||
fiatAmount = fiatAmount,
|
||||
cryptoAmount = cryptoAmount,
|
||||
|
|
@ -294,16 +320,22 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
|
|||
)
|
||||
|
||||
layout(width = constraints.maxWidth, height = layoutHeight) {
|
||||
icon.placeRelative(x = 0, y = (layoutHeight - icon.height).div(other = 2))
|
||||
promoBanner?.placeRelative(x = 0, y = 0)
|
||||
|
||||
icon.placeRelative(
|
||||
x = 0,
|
||||
y = promoOffset + (layoutHeight - promoOffset - icon.height)
|
||||
.div(other = 2),
|
||||
)
|
||||
|
||||
title.placeRelative(
|
||||
x = icon.width,
|
||||
y = when (state) {
|
||||
y = promoOffset + when (state) {
|
||||
is TokenItemState.NoAddress,
|
||||
is TokenItemState.Unreachable,
|
||||
-> {
|
||||
if (state.subtitleState == null) {
|
||||
(layoutHeight - title.height).div(other = 2)
|
||||
(layoutHeight - promoOffset - title.height).div(other = 2)
|
||||
} else {
|
||||
verticalPadding
|
||||
}
|
||||
|
|
@ -314,8 +346,8 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
|
|||
|
||||
fiatAmount?.placeRelative(
|
||||
x = layoutWidth - fiatAmount.width,
|
||||
y = when (state.subtitle2State) {
|
||||
null -> (layoutHeight - fiatAmount.height).div(other = 2)
|
||||
y = promoOffset + when (state.subtitle2State) {
|
||||
null -> (layoutHeight - promoOffset - fiatAmount.height).div(other = 2)
|
||||
else -> verticalPadding
|
||||
},
|
||||
)
|
||||
|
|
@ -335,7 +367,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
|
|||
|
||||
nonFiatContent.placeRelative(
|
||||
x = layoutWidth - nonFiatContent.width,
|
||||
y = (layoutHeight - nonFiatContent.height).div(other = 2),
|
||||
y = promoOffset + (layoutHeight - promoOffset - nonFiatContent.height).div(other = 2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -443,6 +475,7 @@ private fun calculateLayoutHeight(
|
|||
state: TokenItemState,
|
||||
minLayoutHeight: Int,
|
||||
layoutPadding: Int,
|
||||
promoOffset: Int,
|
||||
title: Placeable,
|
||||
fiatAmount: Placeable?,
|
||||
cryptoAmount: Placeable?,
|
||||
|
|
@ -468,7 +501,7 @@ private fun calculateLayoutHeight(
|
|||
}
|
||||
}
|
||||
|
||||
return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight)
|
||||
return (promoOffset + max(firstColumnHeight, secondColumnHeight)).coerceAtLeast(promoOffset + minLayoutHeight)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
|
|
@ -587,6 +620,11 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
|
|||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
promoBannerState = PromoBannerState.Content(
|
||||
title = TextReference.Str(value = "Trusted"),
|
||||
onPromoBannerClick = {},
|
||||
onCloseClick = {},
|
||||
),
|
||||
),
|
||||
TokenItemState.Loading(
|
||||
id = "Loading#1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
package com.tangem.core.ui.components.token.internal
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.PromoBannerState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
internal fun YieldSupplyPromoBanner(state: PromoBannerState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is PromoBannerState.Content -> YieldSupplyPromoBanner(state = state, modifier = modifier)
|
||||
is PromoBannerState.Empty -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: Modifier = Modifier) {
|
||||
val bgColor = TangemTheme.colors.control.unchecked
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(color = bgColor, shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.clickable(onClick = state.onPromoBannerClick)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens.spacing8)
|
||||
.size(TangemTheme.dimens.size16),
|
||||
)
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.secondary,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(bounded = false),
|
||||
onClick = { state.onCloseClick() },
|
||||
),
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_rectangle_bottom),
|
||||
contentDescription = null,
|
||||
tint = bgColor,
|
||||
modifier = Modifier
|
||||
.size(width = 12.dp, height = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_YieldSupplyPromoBanner() {
|
||||
TangemThemePreview {
|
||||
YieldSupplyPromoBanner(
|
||||
state = PromoBannerState.Content(
|
||||
title = TextReference.Str(value = "Earn up to 5% APY"),
|
||||
onPromoBannerClick = {},
|
||||
onCloseClick = {},
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,8 @@ sealed class TokenItemState {
|
|||
*/
|
||||
abstract val subtitle2State: Subtitle2State?
|
||||
|
||||
abstract val promoBannerState: PromoBannerState
|
||||
|
||||
/** Callback which will be called when an item is clicked */
|
||||
abstract val onItemClick: ((TokenItemState) -> Unit)?
|
||||
|
||||
|
|
@ -59,6 +61,7 @@ sealed class TokenItemState {
|
|||
) : TokenItemState() {
|
||||
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
|
||||
override val subtitle2State: Subtitle2State = Subtitle2State.Loading
|
||||
override val promoBannerState: PromoBannerState = PromoBannerState.Empty
|
||||
override val onItemClick: ((TokenItemState) -> Unit)? = null
|
||||
override val onItemLongClick: ((TokenItemState) -> Unit)? = null
|
||||
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
|
||||
|
|
@ -75,6 +78,7 @@ sealed class TokenItemState {
|
|||
override val subtitleState: SubtitleState = SubtitleState.Locked
|
||||
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
|
||||
override val subtitle2State: Subtitle2State = Subtitle2State.Locked
|
||||
override val promoBannerState: PromoBannerState = PromoBannerState.Empty
|
||||
override val onItemClick: ((TokenItemState) -> Unit)? = null
|
||||
override val onItemLongClick: ((TokenItemState) -> Unit)? = null
|
||||
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
|
||||
|
|
@ -99,6 +103,7 @@ sealed class TokenItemState {
|
|||
override val subtitleState: SubtitleState,
|
||||
override val fiatAmountState: FiatAmountState?,
|
||||
override val subtitle2State: Subtitle2State?,
|
||||
override val promoBannerState: PromoBannerState = PromoBannerState.Empty,
|
||||
override val onItemClick: ((TokenItemState) -> Unit)?,
|
||||
override val onItemLongClick: ((TokenItemState) -> Unit)?,
|
||||
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null,
|
||||
|
|
@ -120,6 +125,7 @@ sealed class TokenItemState {
|
|||
) : TokenItemState() {
|
||||
override val subtitleState: SubtitleState? = null
|
||||
override val fiatAmountState: FiatAmountState? = null
|
||||
override val promoBannerState: PromoBannerState = PromoBannerState.Empty
|
||||
override val onItemClick: ((TokenItemState) -> Unit)? = null
|
||||
override val onItemLongClick: ((TokenItemState) -> Unit)? = null
|
||||
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
|
||||
|
|
@ -146,6 +152,7 @@ sealed class TokenItemState {
|
|||
) : TokenItemState() {
|
||||
override val fiatAmountState: FiatAmountState? = null
|
||||
override val subtitle2State: Subtitle2State? = null
|
||||
override val promoBannerState: PromoBannerState = PromoBannerState.Empty
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,6 +175,7 @@ sealed class TokenItemState {
|
|||
override val subtitle2State: Subtitle2State? = null
|
||||
override val onItemClick: ((TokenItemState) -> Unit)? = null
|
||||
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
|
||||
override val promoBannerState: PromoBannerState = PromoBannerState.Empty
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
@ -254,4 +262,15 @@ sealed class TokenItemState {
|
|||
|
||||
data object Locked : Subtitle2State()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class PromoBannerState {
|
||||
data class Content(
|
||||
val title: TextReference,
|
||||
val onPromoBannerClick: () -> Unit,
|
||||
val onCloseClick: () -> Unit,
|
||||
) : PromoBannerState()
|
||||
|
||||
data object Empty : PromoBannerState()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,4 +7,5 @@ object TokenElementsTestTags {
|
|||
const val TOKEN_FIAT_AMOUNT = "TOKEN_FIAT_AMOUNT"
|
||||
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"
|
||||
}
|
||||
9
core/ui/src/main/res/drawable/ic_rectangle_bottom.xml
Normal file
9
core/ui/src/main/res/drawable/ic_rectangle_bottom.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="12dp"
|
||||
android:height="8dp"
|
||||
android:viewportWidth="12"
|
||||
android:viewportHeight="8">
|
||||
<path
|
||||
android:pathData="M0.198,1.178C0.034,0.976 -0.028,0.695 0.012,0.433C0.058,0.135 0.373,0 0.675,0H6.183H11.325C11.627,0 11.943,0.135 11.988,0.433C12.028,0.695 11.966,0.976 11.802,1.178L6.477,7.756C6.214,8.081 5.786,8.081 5.523,7.756L0.198,1.178Z"
|
||||
android:fillColor="#EBEBEB"/>
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue