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
|
@Composable
|
||||||
private fun CoinIcon(
|
fun CoinIcon(
|
||||||
url: String?,
|
url: String?,
|
||||||
@DrawableRes fallbackResId: Int,
|
@DrawableRes fallbackResId: Int,
|
||||||
alpha: Float,
|
alpha: Float,
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ import android.content.res.Configuration
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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 androidx.compose.ui.unit.Dp
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.RectangleShimmer
|
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.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -116,7 +114,10 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) {
|
||||||
) {
|
) {
|
||||||
Price(price = marketPriceBlockState.price, modifier = priceModifier)
|
Price(price = marketPriceBlockState.price, modifier = priceModifier)
|
||||||
|
|
||||||
PriceChangeInPercent(marketPriceBlockState.priceChangeConfig)
|
PriceChangeInPercent(
|
||||||
|
valueInPercent = marketPriceBlockState.priceChangeConfig.valueInPercent,
|
||||||
|
type = marketPriceBlockState.priceChangeConfig.type,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier)
|
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
|
@Composable
|
||||||
private fun LoadingContent() {
|
private fun LoadingContent() {
|
||||||
Row(
|
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
|
fun heightAtMost(value: Dp): Boolean = this.height <= value
|
||||||
|
|
||||||
private fun getWidthWindowSizeType(windowDp: Dp): WindowSizeType = when {
|
private fun getWidthWindowSizeType(windowDp: Dp): WindowSizeType = when {
|
||||||
windowDp < 300.dp -> WindowSizeType.ExtraSmall
|
windowDp <= 320.dp -> WindowSizeType.ExtraSmall
|
||||||
windowDp < 380.dp -> WindowSizeType.Small
|
windowDp <= 360.dp -> WindowSizeType.Small
|
||||||
windowDp < 540.dp -> WindowSizeType.Normal
|
windowDp <= 540.dp -> WindowSizeType.Normal
|
||||||
windowDp < 700.dp -> WindowSizeType.Large
|
windowDp <= 700.dp -> WindowSizeType.Large
|
||||||
else -> WindowSizeType.ExtraLarge
|
else -> WindowSizeType.ExtraLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHeightWindowSizeType(heightDp: Dp): WindowSizeType = when {
|
private fun getHeightWindowSizeType(heightDp: Dp): WindowSizeType = when {
|
||||||
heightDp < 480.dp -> WindowSizeType.ExtraSmall
|
heightDp <= 480.dp -> WindowSizeType.ExtraSmall
|
||||||
heightDp < 640.dp -> WindowSizeType.Small
|
heightDp <= 640.dp -> WindowSizeType.Small
|
||||||
heightDp < 860.dp -> WindowSizeType.Normal
|
heightDp <= 860.dp -> WindowSizeType.Normal
|
||||||
heightDp < 1100.dp -> WindowSizeType.Large
|
heightDp <= 1100.dp -> WindowSizeType.Large
|
||||||
else -> WindowSizeType.ExtraLarge
|
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.manageTokens.api)
|
||||||
implementation(projects.features.details.api)
|
implementation(projects.features.details.api)
|
||||||
implementation(projects.features.pushNotifications.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 = ":presentation\$")) ||
|
||||||
contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA]
|
contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA]
|
||||||
contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function
|
contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function
|
||||||
|
contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function
|
||||||
contains(Regex(pattern = ":impl\$"))
|
contains(Regex(pattern = ":impl\$"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,9 @@ include(":features:push-notifications:impl")
|
||||||
|
|
||||||
include(":features:wallet-settings:api")
|
include(":features:wallet-settings:api")
|
||||||
include(":features:wallet-settings:impl")
|
include(":features:wallet-settings:impl")
|
||||||
|
|
||||||
|
include(":features:markets:api")
|
||||||
|
include(":features:markets:impl")
|
||||||
// endregion Feature modules
|
// endregion Feature modules
|
||||||
|
|
||||||
// region Domain modules
|
// region Domain modules
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue