Updated on 2026-08-14
This commit is contained in:
parent
f6d3003cb0
commit
2d8d9453bd
17 changed files with 763 additions and 55 deletions
|
|
@ -58,7 +58,7 @@ internal fun ContentIcon(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun CoinIcon(
|
||||
fun CoinIcon(
|
||||
url: String?,
|
||||
@DrawableRes fallbackResId: Int,
|
||||
alpha: Float,
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import android.content.res.Configuration
|
|||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -20,8 +18,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
|
||||
/**
|
||||
|
|
@ -116,7 +114,10 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) {
|
|||
) {
|
||||
Price(price = marketPriceBlockState.price, modifier = priceModifier)
|
||||
|
||||
PriceChangeInPercent(marketPriceBlockState.priceChangeConfig)
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = marketPriceBlockState.priceChangeConfig.valueInPercent,
|
||||
type = marketPriceBlockState.priceChangeConfig.type,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier)
|
||||
|
|
@ -136,47 +137,6 @@ private fun Price(price: String, modifier: Modifier = Modifier) {
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PriceChangeInPercent(config: PriceChangeState.Content) {
|
||||
AnimatedContent(
|
||||
targetState = config.type,
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
label = "Update price change",
|
||||
) { type ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size8),
|
||||
painter = painterResource(
|
||||
id = when (type) {
|
||||
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
|
||||
PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8
|
||||
PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8
|
||||
},
|
||||
),
|
||||
tint = when (type) {
|
||||
PriceChangeType.UP -> TangemTheme.colors.icon.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors.icon.warning
|
||||
PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = config.valueInPercent,
|
||||
color = when (type) {
|
||||
PriceChangeType.UP -> TangemTheme.colors.text.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
|
||||
PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled
|
||||
},
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingContent() {
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.core.ui.components.marketprice
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun PriceChangeInPercent(
|
||||
valueInPercent: String,
|
||||
type: PriceChangeType,
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle = TangemTheme.typography.body2,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size8)
|
||||
.align(Alignment.CenterVertically),
|
||||
imageVector = ImageVector.vectorResource(
|
||||
id = when (type) {
|
||||
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
|
||||
PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8
|
||||
PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8
|
||||
},
|
||||
),
|
||||
tint = when (type) {
|
||||
PriceChangeType.UP -> TangemTheme.colors.icon.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors.icon.warning
|
||||
PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = valueInPercent,
|
||||
color = when (type) {
|
||||
PriceChangeType.UP -> TangemTheme.colors.text.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
|
||||
PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled
|
||||
},
|
||||
style = textStyle,
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region Preview
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Column {
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = "52.00%",
|
||||
type = PriceChangeType.NEUTRAL,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = "52.00%",
|
||||
type = PriceChangeType.UP,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = "52.00%",
|
||||
type = PriceChangeType.DOWN,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = "52.00%",
|
||||
type = PriceChangeType.DOWN,
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Preview
|
||||
|
|
@ -46,18 +46,18 @@ data class WindowSize(
|
|||
fun heightAtMost(value: Dp): Boolean = this.height <= value
|
||||
|
||||
private fun getWidthWindowSizeType(windowDp: Dp): WindowSizeType = when {
|
||||
windowDp < 300.dp -> WindowSizeType.ExtraSmall
|
||||
windowDp < 380.dp -> WindowSizeType.Small
|
||||
windowDp < 540.dp -> WindowSizeType.Normal
|
||||
windowDp < 700.dp -> WindowSizeType.Large
|
||||
windowDp <= 320.dp -> WindowSizeType.ExtraSmall
|
||||
windowDp <= 360.dp -> WindowSizeType.Small
|
||||
windowDp <= 540.dp -> WindowSizeType.Normal
|
||||
windowDp <= 700.dp -> WindowSizeType.Large
|
||||
else -> WindowSizeType.ExtraLarge
|
||||
}
|
||||
|
||||
private fun getHeightWindowSizeType(heightDp: Dp): WindowSizeType = when {
|
||||
heightDp < 480.dp -> WindowSizeType.ExtraSmall
|
||||
heightDp < 640.dp -> WindowSizeType.Small
|
||||
heightDp < 860.dp -> WindowSizeType.Normal
|
||||
heightDp < 1100.dp -> WindowSizeType.Large
|
||||
heightDp <= 480.dp -> WindowSizeType.ExtraSmall
|
||||
heightDp <= 640.dp -> WindowSizeType.Small
|
||||
heightDp <= 860.dp -> WindowSizeType.Normal
|
||||
heightDp <= 1100.dp -> WindowSizeType.Large
|
||||
else -> WindowSizeType.ExtraLarge
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
core/ui/src/main/res/drawable/ic_plus_mini_28.xml
Normal file
10
core/ui/src/main/res/drawable/ic_plus_mini_28.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:pathData="M15.05,6.65C15.05,6.07 14.58,5.6 14,5.6C13.42,5.6 12.95,6.07 12.95,6.65V12.95H6.65C6.07,12.95 5.6,13.42 5.6,14C5.6,14.58 6.07,15.05 6.65,15.05H12.95V21.35C12.95,21.93 13.42,22.4 14,22.4C14.58,22.4 15.05,21.93 15.05,21.35V15.05H21.35C21.93,15.05 22.4,14.58 22.4,14C22.4,13.42 21.93,12.95 21.35,12.95H15.05V6.65Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
1
features/markets/api/.gitignore
vendored
Normal file
1
features/markets/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
18
features/markets/api/build.gradle.kts
Normal file
18
features/markets/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("kotlin-parcelize")
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.markets.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(deps.compose.foundation)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
interface MarketsListComponent {
|
||||
|
||||
// TODO
|
||||
|
||||
// @Composable
|
||||
// fun BottomSheetContent(state: State<BottomSheetState>, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier)
|
||||
}
|
||||
1
features/markets/impl/.gitignore
vendored
Normal file
1
features/markets/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
24
features/markets/impl/build.gradle.kts
Normal file
24
features/markets/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.markets.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.uiCharts)
|
||||
}
|
||||
|
|
@ -0,0 +1,448 @@
|
|||
package com.tangem.features.markets.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.Animatable
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.interaction.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import com.tangem.common.ui.charts.MarketChartMini
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.currency.tokenicon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.models.MarketsListItemModel
|
||||
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
|
||||
internal enum class DragValue { Start, End }
|
||||
|
||||
const val SWIPE_THRESHOLD_PERCENT = 0.8f
|
||||
const val SWIPE_VELOCITY_THRESHOLD = 20f
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MarketsListItem(
|
||||
model: MarketsListItemModel,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onSwipeToAction: () -> Unit = {},
|
||||
) {
|
||||
val actionWidth = TangemTheme.dimens.size68
|
||||
val actionWidthPx = with(LocalDensity.current) { actionWidth.toPx() }
|
||||
|
||||
val hapticManager = LocalHapticManager.current
|
||||
|
||||
val anchors = DraggableAnchors {
|
||||
DragValue.Start at 0f
|
||||
DragValue.End at -actionWidthPx
|
||||
}
|
||||
val state = remember {
|
||||
AnchoredDraggableState(
|
||||
initialValue = DragValue.Start,
|
||||
anchors = anchors,
|
||||
positionalThreshold = { it * (1 - SWIPE_THRESHOLD_PERCENT) },
|
||||
velocityThreshold = { SWIPE_VELOCITY_THRESHOLD },
|
||||
animationSpec = tween(easing = FastOutSlowInEasing),
|
||||
confirmValueChange = { it == DragValue.Start },
|
||||
)
|
||||
}
|
||||
val dragInteractionSource = remember { MutableInteractionSource() }
|
||||
val clickInteractionSource = remember { MutableInteractionSource() }
|
||||
val isInDraggedState by dragInteractionSource.collectIsDraggedAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
var actionPerformed = false
|
||||
var releasePerformed = true
|
||||
launch {
|
||||
snapshotFlow { state.offset }
|
||||
.collect {
|
||||
val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT
|
||||
if (it < border && actionPerformed.not()) {
|
||||
hapticManager.vibrateLong()
|
||||
actionPerformed = true
|
||||
releasePerformed = false
|
||||
}
|
||||
|
||||
if (it > border) {
|
||||
if (releasePerformed.not()) {
|
||||
hapticManager.vibrateShort()
|
||||
releasePerformed = true
|
||||
}
|
||||
actionPerformed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
snapshotFlow { isInDraggedState }
|
||||
.collect {
|
||||
if (it.not() && actionPerformed) {
|
||||
releasePerformed = true
|
||||
onSwipeToAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(intrinsicSize = IntrinsicSize.Min)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.fillMaxHeight()
|
||||
.offset {
|
||||
IntOffset(
|
||||
x = actionWidthPx.roundToInt() +
|
||||
state
|
||||
.requireOffset()
|
||||
.toInt(),
|
||||
y = 0,
|
||||
)
|
||||
}
|
||||
.width(actionWidth)
|
||||
.background(TangemTheme.colors.control.checked),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size28),
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_mini_28),
|
||||
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.primary2),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.clip(RectangleShape)
|
||||
.offset {
|
||||
IntOffset(
|
||||
x = state
|
||||
.requireOffset()
|
||||
.toInt(),
|
||||
y = 0,
|
||||
)
|
||||
}
|
||||
.anchoredDraggable(
|
||||
state = state,
|
||||
orientation = Orientation.Horizontal,
|
||||
interactionSource = dragInteractionSource,
|
||||
)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
interactionSource = clickInteractionSource,
|
||||
indication = rememberRipple(),
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
MarketsListItemContent(model = model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsListItemContent(model: MarketsListItemModel, modifier: Modifier = Modifier) {
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing15,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size36),
|
||||
url = model.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TokenTitle(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
name = model.name,
|
||||
currencySymbol = model.currencySymbol,
|
||||
)
|
||||
SpacerW8()
|
||||
TokenPriceText(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
price = model.price.text,
|
||||
priceChangeType = model.price.changeType,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
TokenSubtitle(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
ratingPosition = model.ratingPosition,
|
||||
marketCap = model.marketCap,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
type = model.trendType,
|
||||
valueInPercent = model.trendPercentText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
|
||||
|
||||
Chart(
|
||||
chartType = model.chartType,
|
||||
chartRawData = model.chardData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
text = name,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
SpacerW4()
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
text = currencySymbol,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TokenRatingPlace(ratingPosition = ratingPosition)
|
||||
SpacerW4()
|
||||
TokenMarketCapText(text = marketCap ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens.size16)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.size5), // TODO check
|
||||
) {
|
||||
Text(
|
||||
text = ratingPosition ?: "-",
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenMarketCapText(text: String) {
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
text = text,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
|
||||
val growColor = TangemTheme.colors.text.accent
|
||||
val fallColor = TangemTheme.colors.text.warning
|
||||
val generalColor = TangemTheme.colors.text.primary1
|
||||
|
||||
val color = remember { Animatable(generalColor) }
|
||||
|
||||
LaunchedEffect(price, growColor, fallColor, generalColor) {
|
||||
snapshotFlow { price }
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.collectLatest {
|
||||
val nextColor = when (priceChangeType) {
|
||||
PriceChangeType.NEUTRAL,
|
||||
PriceChangeType.UP,
|
||||
-> growColor
|
||||
PriceChangeType.DOWN -> fallColor
|
||||
null -> generalColor
|
||||
}
|
||||
|
||||
color.animateTo(nextColor, snap())
|
||||
color.animateTo(generalColor, tween(durationMillis = 500))
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = price,
|
||||
color = color.value,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) {
|
||||
val chartWidth = TangemTheme.dimens.size56
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing2)
|
||||
.size(height = TangemTheme.dimens.size32, width = chartWidth),
|
||||
) {
|
||||
if (chartRawData != null) {
|
||||
MarketChartMini(
|
||||
rawData = chartRawData,
|
||||
type = chartType,
|
||||
)
|
||||
} else {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size15) // TODO check
|
||||
.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 320, name = "small width")
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemModel) {
|
||||
TangemThemePreview {
|
||||
var state1 by remember { mutableStateOf(state) }
|
||||
var state2 by remember { mutableStateOf(state) }
|
||||
var prices by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
100 to PriceChangeType.NEUTRAL,
|
||||
200 to PriceChangeType.NEUTRAL,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
MarketsListItem(
|
||||
modifier = Modifier,
|
||||
model = state1,
|
||||
)
|
||||
MarketsListItem(
|
||||
modifier = Modifier,
|
||||
model = state2,
|
||||
)
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
state1 = state1.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
},
|
||||
) { Text(text = "trend") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
prices = prices.map {
|
||||
if (Random.nextBoolean()) {
|
||||
it.first.inc() to PriceChangeType.UP
|
||||
} else {
|
||||
it.first.dec() to PriceChangeType.DOWN
|
||||
}
|
||||
}
|
||||
state1 = state1.copy(
|
||||
price = MarketsListItemModel.Price(
|
||||
text = "0.${prices[0].first}023 $",
|
||||
changeType = prices[0].second,
|
||||
),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
price = MarketsListItemModel.Price(
|
||||
text = "0.${prices[1].first}023 $",
|
||||
changeType = prices[1].second,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { Text(text = "price") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion preview
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.markets.ui.models
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
|
||||
@Immutable
|
||||
data class MarketsListItemModel(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val currencySymbol: String,
|
||||
val iconUrl: String?,
|
||||
val ratingPosition: String?,
|
||||
val marketCap: String?,
|
||||
val price: Price,
|
||||
val trendPercentText: String,
|
||||
val trendType: PriceChangeType,
|
||||
val chardData: MarketChartRawData?,
|
||||
) {
|
||||
val chartType: MarketChartLook.Type = when (trendType) {
|
||||
PriceChangeType.UP,
|
||||
PriceChangeType.NEUTRAL,
|
||||
-> MarketChartLook.Type.Growing
|
||||
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Price(
|
||||
val text: String,
|
||||
val changeType: PriceChangeType = PriceChangeType.NEUTRAL,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
package com.tangem.features.markets.ui.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.features.markets.ui.models.MarketsListItemModel
|
||||
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemModel>(
|
||||
collection = listOf(
|
||||
MarketsListItemModel(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = "",
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
MarketsListItemModel(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
chardData = null,
|
||||
),
|
||||
MarketsListItemModel(
|
||||
id = "1",
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.23348172384781234 B",
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.DOWN,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
MarketsListItemModel(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = null,
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
MarketsListItemModel(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
MarketsListItemModel(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = null,
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -90,4 +90,5 @@ dependencies {
|
|||
implementation(projects.features.manageTokens.api)
|
||||
implementation(projects.features.details.api)
|
||||
implementation(projects.features.pushNotifications.api)
|
||||
implementation(projects.features.markets.api)
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ internal fun BaseExtension.configureCompose(project: Project) {
|
|||
contains(Regex(pattern = ":presentation\$")) ||
|
||||
contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA]
|
||||
contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function
|
||||
contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function
|
||||
contains(Regex(pattern = ":impl\$"))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,6 +186,9 @@ include(":features:push-notifications:impl")
|
|||
|
||||
include(":features:wallet-settings:api")
|
||||
include(":features:wallet-settings:impl")
|
||||
|
||||
include(":features:markets:api")
|
||||
include(":features:markets:impl")
|
||||
// endregion Feature modules
|
||||
|
||||
// region Domain modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue