Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-18 10:52:08 +05:00
parent fa948bd91a
commit 6369a3f33a
11 changed files with 544 additions and 30 deletions

View file

@ -0,0 +1,78 @@
package com.tangem.core.ui.components.atoms.radiobutton
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* [Radio button](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=101-119&t=FH84ljLBk1vmGAei-4)
*
* @param isSelected Whether the radio button is selected
* @param onClick Called when the user clicks the button
* @param modifier Modifier to be applied to the button
*/
@Composable
fun TangemRadioButton(isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
Box(
modifier = modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16),
onClick = onClick,
),
) {
val color = TangemTheme.colors.stroke.secondary
val radius = with(LocalDensity.current) { TangemTheme.dimens.size9.toPx() }
val width = with(LocalDensity.current) { TangemTheme.dimens.size2.toPx() }
Canvas(
modifier = Modifier
.size(TangemTheme.dimens.size24)
.padding(TangemTheme.dimens.spacing2),
) {
drawCircle(
color = color,
radius = radius,
style = Stroke(width),
)
}
AnimatedVisibility(
visible = isSelected,
label = "Radio button animation",
modifier = modifier
.size(TangemTheme.dimens.size24),
) {
Icon(
painter = painterResource(id = R.drawable.ic_check_circle_24),
contentDescription = null,
tint = TangemTheme.colors.control.checked,
)
}
}
}
// region Preview
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemRadioButton_Preview() {
var isSelected by remember { mutableStateOf(false) }
TangemThemePreview {
TangemRadioButton(isSelected, onClick = { isSelected = !isSelected })
}
}
// endregion

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
@ -14,18 +13,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
@ -63,7 +59,7 @@ fun InputRowBestRate(
modifier = Modifier
.padding(TangemTheme.dimens.spacing12),
) {
InnerIcon(imageUrl = imageUrl)
InputRowAsyncImage(imageUrl = imageUrl, modifier = Modifier.size(TangemTheme.dimens.spacing40))
Column(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
@ -131,29 +127,6 @@ private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag:
}
}
@Composable
private fun InnerIcon(imageUrl: String) {
SubcomposeAsyncImage(
modifier = Modifier.size(TangemTheme.dimens.size40),
model = ImageRequest.Builder(context = LocalContext.current)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = { LoadingIcon() },
error = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,
shape = CircleShape,
),
)
},
contentDescription = null,
)
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -0,0 +1,54 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.layout.*
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.Color
import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun InputRowImageBase(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
extraContent: @Composable RowScope.() -> Unit = {},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
InputRowAsyncImage(
imageUrl = imageUrl,
modifier = Modifier
.size(TangemTheme.dimens.spacing36)
.padding(vertical = TangemTheme.dimens.size1),
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = TangemTheme.dimens.spacing12),
) {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
Text(
text = caption.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
extraContent()
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
/**
* Input row component with selector
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2100-842&t=hoBXmDX8NeLrp4p6-4)
*
* @param subtitle subtitle text
* @param caption caption text
* @param imageUrl icon to load
* @param modifier modifier
* @param subtitleColor subtitle text color
* @param captionColor caption text color
*/
@Composable
fun InputRowImageChevron(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
) {
InputRowImageBase(
subtitle = subtitle,
caption = caption,
imageUrl = imageUrl,
modifier = modifier,
subtitleColor = subtitleColor,
captionColor = captionColor,
) {
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}

View file

@ -0,0 +1,131 @@
package com.tangem.core.ui.components.inputrow
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.atoms.radiobutton.TangemRadioButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
import java.math.BigDecimal
/**
* Input row component with selector
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2772-905&t=FH84ljLBk1vmGAei-4)
*
* @param subtitle subtitle text
* @param caption caption text
* @param imageUrl icon to load
* @param onSelect callback when selected
* @param modifier modifier
* @param subtitleColor subtitle text color
* @param captionColor caption text color
* @param isSelected true if selected
*/
@Composable
fun InputRowImageSelector(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
onSelect: () -> Unit,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
isSelected: Boolean = false,
) {
InputRowImageBase(
subtitle = subtitle,
caption = caption,
imageUrl = imageUrl,
subtitleColor = subtitleColor,
captionColor = captionColor,
modifier = modifier
.clickable(
onClick = onSelect,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
)
.padding(TangemTheme.dimens.spacing12),
) {
TangemRadioButton(isSelected = isSelected, isEnabled = false, onClick = onSelect)
}
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun InputRowImageSelectorPreview(
@PreviewParameter(InputRowImageSelectorPreviewDataProvider::class) data: InputRowImageSelectorPreviewData,
) {
TangemThemePreview {
InputRowImageSelector(
modifier = Modifier.background(TangemTheme.colors.background.action),
subtitle = data.subtitle,
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) {
append(
BigDecimalFormatter.formatPercent(BigDecimal.ZERO, true),
)
}
},
),
),
imageUrl = "",
isSelected = false,
onSelect = {},
)
}
}
private data class InputRowImageSelectorPreviewData(
val subtitle: TextReference,
val caption: TextReference,
val showDivider: Boolean,
val actionIconRes: Int?,
val isSelected: Boolean,
)
private class InputRowImageSelectorPreviewDataProvider :
PreviewParameterProvider<InputRowImageSelectorPreviewData> {
override val values: Sequence<InputRowImageSelectorPreviewData>
get() = sequenceOf(
InputRowImageSelectorPreviewData(
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
actionIconRes = null,
showDivider = false,
isSelected = false,
),
InputRowImageSelectorPreviewData(
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
actionIconRes = R.drawable.ic_chevron_right_24,
showDivider = true,
isSelected = true,
),
)
}
//endregion

View file

@ -0,0 +1,41 @@
package com.tangem.core.ui.components.inputrow.inner
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
import com.tangem.core.ui.res.TangemTheme
/**
* Loads image by url for icon in the input row
*
* @param imageUrl url of the image
* @param modifier modifier
*/
@Composable
internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier) {
SubcomposeAsyncImage(
modifier = modifier,
model = ImageRequest.Builder(context = LocalContext.current)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = { LoadingIcon() },
error = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,
shape = CircleShape,
),
)
},
contentDescription = null,
)
}

View file

@ -48,6 +48,13 @@ sealed interface TextReference {
*/
data class Str(val value: String) : TextReference
/**
* Annotated text
*
* @property value annotated string
*/
data class Annotated(val value: AnnotatedString) : TextReference
/**
* Combined reference. It concatenates all [refs].
*
@ -83,6 +90,16 @@ fun stringReference(value: String): TextReference {
return TextReference.Str(value)
}
/**
* Creates a [TextReference] using an annotated string value.
*
* @param value The annotated string value.
* @return A [TextReference] representing the provided annotated string value.
*/
fun annotatedReference(value: AnnotatedString): TextReference {
return TextReference.Annotated(value)
}
/**
* Creates a [TextReference] using a plural string resource ID with count and optional format arguments.
*
@ -133,6 +150,7 @@ fun TextReference.resolveReference(): String {
}
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Annotated -> value.text
is TextReference.Combined -> {
buildString {
refs.forEach {
@ -155,6 +173,7 @@ fun TextReference.resolveReference(resources: Resources): String {
}
is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Annotated -> value.text
is TextReference.Combined -> {
buildString {
refs.forEach {
@ -180,9 +199,10 @@ fun TextReference.resolveAnnotatedReference(): AnnotatedString {
pluralStringResource(id, count, *formatArgs.toTypedArray()),
)
is TextReference.Str -> formatAnnotated(value)
is TextReference.Annotated -> value
is TextReference.Combined -> buildAnnotatedString {
refs.forEach {
append(formatAnnotated(it.resolveReference()))
append(it.resolveAnnotatedReference())
}
}
}
@ -195,6 +215,7 @@ operator fun TextReference.plus(ref: TextReference): TextReference {
is TextReference.PluralRes,
is TextReference.Res,
is TextReference.Str,
is TextReference.Annotated,
-> TextReference.Combined(refs = wrappedList(this, ref))
}
}

View file

@ -47,6 +47,7 @@ data class TangemDimens internal constructor(
val size5: Dp = 5.dp,
val size7: Dp = 7.dp,
val size8: Dp = 8.dp,
val size9: Dp = 9.dp,
val size10: Dp = 10.dp,
val size11: Dp = 11.dp,
val size12: Dp = 12.dp,

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#1E1E1E" android:pathData="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2ZM10,17L5,12L6.41,10.59L10,14.17L17.59,6.58L19,8L10,17Z"/>
</vector>

View file

@ -0,0 +1,37 @@
package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.state.StakingStates
internal object StakingValidatorStateStub {
private val validator = Yield.Validator(
address = "address",
status = "status",
name = "Binance",
image = null,
website = null,
apr = "0.0354".toBigDecimal(),
commission = null,
stakedBalance = null,
votingPower = null,
preferred = false,
)
val state = StakingStates.ValidatorState.Data(
isPrimaryButtonEnabled = false,
validators = listOf(
validator,
validator.copy(
address = "TRONLink",
name = "TRONLink",
apr = "0.0506".toBigDecimal(),
),
validator.copy(
address = "1inch",
name = "1inch",
apr = "0.0306".toBigDecimal(),
),
),
selectedValidator = validator,
)
}

View file

@ -0,0 +1,127 @@
package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.inputrow.InputRowImageSelector
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.state.stub.StakingValidatorStateStub
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import java.math.BigDecimal
/**
* Staking screen with validators
*/
@Composable
internal fun StakingValidatorListContent(
state: StakingStates.ValidatorState,
clickIntents: StakingClickIntents,
modifier: Modifier = Modifier,
) {
if (state !is StakingStates.ValidatorState.Data) return
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
contentPadding = PaddingValues(bottom = bottomBarHeight),
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
item(key = "HEADER") {
Text(
text = stringResource(R.string.staking_validator),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.clip(CornersToRound.TOP_2.getShape())
.background(TangemTheme.colors.background.action)
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing8,
),
)
}
items(
count = state.validators.size,
key = { state.validators[it] },
contentType = { state.validators[it]::class.java },
) { index ->
val item = state.validators[index]
InputRowImageSelector(
subtitle = stringReference(item.name),
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) {
append(
BigDecimalFormatter.formatPercent(item.apr ?: BigDecimal.ZERO, true),
)
}
},
),
),
imageUrl = item.image.orEmpty(),
isSelected = item == state.selectedValidator,
onSelect = { clickIntents.onValidatorSelect(item) },
modifier = Modifier
.clip(
if (index == state.validators.lastIndex) {
CornersToRound.BOTTOM_2
} else {
CornersToRound.ZERO
}.getShape(),
)
.background(TangemTheme.colors.background.action),
)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun StakingValidatorListContent_Preview(
@PreviewParameter(StakingValidatorListContentPreviewProvider::class)
data: StakingStates.ValidatorState.Data,
) {
TangemThemePreview {
StakingValidatorListContent(
state = data,
clickIntents = StakingClickIntentsStub,
)
}
}
private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider<StakingStates.ValidatorState.Data> {
override val values: Sequence<StakingStates.ValidatorState.Data>
get() = sequenceOf(StakingValidatorStateStub.state)
}
// endregion