Updated on 2026-08-14
This commit is contained in:
parent
fe1d2342f4
commit
6e69e043c3
7 changed files with 224 additions and 36 deletions
|
|
@ -0,0 +1,111 @@
|
|||
package com.tangem.common.ui.charts
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.cartesian.*
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec
|
||||
import com.patrykandpatrick.vico.compose.common.shader.BrushShader
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
|
||||
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
||||
import com.patrykandpatrick.vico.core.common.shader.ColorShader
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun MarketChartMini(
|
||||
rawData: MarketChartRawData,
|
||||
modifier: Modifier = Modifier,
|
||||
type: MarketChartLook.Type = MarketChartLook.Type.Growing,
|
||||
growingColor: Color = TangemTheme.colors.icon.accent,
|
||||
fallingColor: Color = TangemTheme.colors.icon.warning,
|
||||
) {
|
||||
val model = remember(rawData) {
|
||||
CartesianChartModel(LineCartesianLayerModel.build { series(rawData.y) })
|
||||
}
|
||||
|
||||
val lineColor = when (type) {
|
||||
MarketChartLook.Type.Growing -> growingColor
|
||||
MarketChartLook.Type.Falling -> fallingColor
|
||||
}
|
||||
|
||||
val lineSpec = rememberLineSpec(
|
||||
shader = ColorShader(lineColor.toArgb()),
|
||||
thickness = 1.dp,
|
||||
backgroundShader = BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val layer = rememberLineCartesianLayer(listOf(lineSpec))
|
||||
val chart = rememberCartesianChart(layer)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = modifier,
|
||||
chart = chart,
|
||||
model = model,
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
scrollState = rememberVicoScrollState(scrollEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
val data = MarketChartRawData(
|
||||
x = List(20) { Random.nextFloat() },
|
||||
y = List(20) { Random.nextFloat() },
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
Column {
|
||||
MarketChartMini(rawData = data, type = MarketChartLook.Type.Growing)
|
||||
SpacerH16()
|
||||
MarketChartMini(rawData = data, type = MarketChartLook.Type.Falling)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewColumn() {
|
||||
val data = MarketChartRawData(
|
||||
x = List(20) { Random.nextFloat() },
|
||||
y = List(20) { Random.nextFloat() },
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
LazyColumn {
|
||||
items(100) {
|
||||
MarketChartMini(
|
||||
rawData = data,
|
||||
type = if (it % 3 == 0) MarketChartLook.Type.Growing else MarketChartLook.Type.Falling,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Preview
|
||||
|
|
@ -75,6 +75,7 @@ class TransactionSuspend(
|
|||
class MarketChartDataProducer private constructor(
|
||||
initialData: MarketChartData,
|
||||
initialLook: MarketChartLook,
|
||||
val pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
) {
|
||||
internal val startDrawingAnimation = MutableSharedFlow<Unit>()
|
||||
|
|
@ -110,13 +111,11 @@ class MarketChartDataProducer private constructor(
|
|||
startDrawingAnimation.emit(Unit)
|
||||
}
|
||||
withContext(dispatcher) {
|
||||
val minX = chartData.x.min()
|
||||
val minY = chartData.y.min()
|
||||
val rawData = pointsValuesConverter.convert(chartData)
|
||||
|
||||
val normY = chartData.y.map { normalize(it, minY) }
|
||||
val normX = chartData.x.map { normalize(it, minX) }
|
||||
val entriesLocal =
|
||||
rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) }
|
||||
|
||||
val entriesLocal = normX.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, normY[index]) }
|
||||
entries.value = entriesLocal
|
||||
|
||||
modelProducer.runTransaction {
|
||||
|
|
@ -160,6 +159,7 @@ class MarketChartDataProducer private constructor(
|
|||
* @return A MarketChartDataProducer.
|
||||
*/
|
||||
suspend fun buildSuspend(
|
||||
pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
block: TransactionSuspend.() -> Unit,
|
||||
): MarketChartDataProducer {
|
||||
|
|
@ -169,6 +169,7 @@ class MarketChartDataProducer private constructor(
|
|||
initialData = initialData,
|
||||
initialLook = initialLook,
|
||||
dispatcher = dispatcher,
|
||||
pointsValuesConverter = pointsValuesConverter,
|
||||
).apply {
|
||||
handleTransactionSuspend(transaction)
|
||||
}
|
||||
|
|
@ -183,6 +184,7 @@ class MarketChartDataProducer private constructor(
|
|||
* @return A MarketChartDataProducer.
|
||||
*/
|
||||
fun build(
|
||||
pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
block: Transaction.() -> Unit,
|
||||
): MarketChartDataProducer {
|
||||
|
|
@ -192,16 +194,8 @@ class MarketChartDataProducer private constructor(
|
|||
initialData = transaction.chartData ?: initialData,
|
||||
initialLook = transaction.chartLook ?: initialLook,
|
||||
dispatcher = dispatcher,
|
||||
pointsValuesConverter = pointsValuesConverter,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float {
|
||||
val n = value - min
|
||||
return if (scale > 2) {
|
||||
n.movePointRight(scale - 2).toFloat()
|
||||
} else {
|
||||
n.toFloat()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class MarketChartRawData(
|
||||
val y: List<Float>,
|
||||
val x: List<Float> = List(y.size) { 1f },
|
||||
)
|
||||
|
|
@ -75,15 +75,9 @@ class MarketChartState internal constructor(
|
|||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
|
||||
val sMin = state.x.min()
|
||||
val scale = sMin.scale()
|
||||
val bVal = if (scale > 2) {
|
||||
value.toBigDecimal().movePointLeft(scale - 2) + sMin
|
||||
} else {
|
||||
value.toBigDecimal() + sMin
|
||||
}
|
||||
|
||||
lookState.value.xAxisFormatter.format(bVal)
|
||||
lookState.value.xAxisFormatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,15 +86,9 @@ class MarketChartState internal constructor(
|
|||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
|
||||
val sMin = state.y.min()
|
||||
val scale = sMin.scale()
|
||||
val bVal = if (scale > 2) {
|
||||
value.toBigDecimal().movePointLeft(scale - 2) + sMin
|
||||
} else {
|
||||
value.toBigDecimal() + sMin
|
||||
}
|
||||
|
||||
lookState.value.yAxisFormatter.format(bVal)
|
||||
lookState.value.yAxisFormatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Interface to convert chart data values to Floats and backwards.
|
||||
*
|
||||
* We need to convert the values on the graph to floating point values in order to display them correctly on the canvas.
|
||||
* We also need to determine exactly which floating point value on the graph corresponds to the decimal point,
|
||||
* so that we can format the actual value and display on the x/y axis.
|
||||
*/
|
||||
interface PointValuesConverter {
|
||||
|
||||
fun convert(data: MarketChartData.Data): MarketChartRawData
|
||||
|
||||
fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal
|
||||
|
||||
fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal
|
||||
}
|
||||
|
||||
object DefaultPointValuesConverter : PointValuesConverter {
|
||||
|
||||
override fun convert(data: MarketChartData.Data): MarketChartRawData {
|
||||
val minX = data.x.min()
|
||||
val minY = data.y.min()
|
||||
|
||||
val normY = data.y.map { normalize(it, minY) }
|
||||
val normX = data.x.map { normalize(it, minX) }
|
||||
|
||||
return MarketChartRawData(
|
||||
x = normX,
|
||||
y = normY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal {
|
||||
val dataMin = data.x.min()
|
||||
val scale = dataMin.scale()
|
||||
val bVal = if (scale > 2) {
|
||||
rawX.toBigDecimal().movePointLeft(scale - 2) + dataMin
|
||||
} else {
|
||||
rawX.toBigDecimal() + dataMin
|
||||
}
|
||||
|
||||
return bVal
|
||||
}
|
||||
|
||||
override fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal {
|
||||
val dataMin = data.y.min()
|
||||
val scale = dataMin.scale()
|
||||
val bVal = if (scale > 2) {
|
||||
rawY.toBigDecimal().movePointLeft(scale - 2) + dataMin
|
||||
} else {
|
||||
rawY.toBigDecimal() + dataMin
|
||||
}
|
||||
|
||||
return bVal
|
||||
}
|
||||
|
||||
// TODO enhance algorithm for values with big difference between min and max, which cannot fit in Float
|
||||
private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float {
|
||||
val n = value - min
|
||||
return if (scale > 2) {
|
||||
n.movePointRight(scale - 2).toFloat()
|
||||
} else {
|
||||
n.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,30 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
object MockHapticManager : HapticManager {
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun MockHapticManager(mockHapticFeedback: HapticFeedback? = null): HapticManager =
|
||||
if (mockHapticFeedback == null) MockHapticManager else MockHapticManagerImpl(mockHapticFeedback)
|
||||
|
||||
val MockHapticManager: HapticManager = MockHapticManagerImpl()
|
||||
|
||||
private class MockHapticManagerImpl(
|
||||
private val mockHapticFeedback: HapticFeedback? = null,
|
||||
) : HapticManager {
|
||||
|
||||
override fun vibrateShort() {
|
||||
/** Intentionnaly do nothing */
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
|
||||
override fun vibrateMeduim() {
|
||||
/** Intentionnaly do nothing */
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
|
||||
override fun vibrateLong() {
|
||||
/** Intentionnaly do nothing */
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ package com.tangem.core.ui.res
|
|||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import com.tangem.core.ui.haptic.MockHapticManager
|
||||
import com.tangem.core.ui.windowsize.rememberWindowSizePreview
|
||||
|
||||
@Composable
|
||||
|
|
@ -20,6 +22,7 @@ fun TangemThemePreview(
|
|||
typography = typography,
|
||||
dimens = dimens,
|
||||
windowSize = rememberWindowSizePreview(maxWidth, maxHeight),
|
||||
hapticManager = MockHapticManager(LocalHapticFeedback.current),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue