Updated on 2026-08-14
This commit is contained in:
parent
45db7c879f
commit
e20f932081
52 changed files with 1676 additions and 813 deletions
|
|
@ -94,6 +94,11 @@ val generateComposeMetrics by tasks.registering {
|
|||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$outputDirectory",
|
||||
)
|
||||
// Compose strong skipping mode
|
||||
// freeCompilerArgs.addAll(
|
||||
// "-P",
|
||||
// "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ dependencies {
|
|||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.common.ui.charts
|
|||
import android.content.res.Configuration
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -12,10 +11,11 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontSynthesis
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -23,26 +23,32 @@ import androidx.compose.ui.text.font.resolveAsTypeface
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.cartesian.*
|
||||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
|
||||
import com.patrykandpatrick.vico.compose.common.of
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
|
||||
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.*
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.BaseAxis
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
||||
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget
|
||||
import com.patrykandpatrick.vico.core.common.Dimensions
|
||||
import com.patrykandpatrick.vico.core.common.component.LineComponent
|
||||
import com.patrykandpatrick.vico.core.common.shape.Shape
|
||||
import com.tangem.common.ui.charts.layer.TimeItemPlacer
|
||||
import com.tangem.common.ui.charts.layer.rememberMarketChartLayer
|
||||
import com.tangem.common.ui.charts.marker.rememberTangemChartMarker
|
||||
import com.tangem.common.ui.charts.layer.rememberTangemChartMarker
|
||||
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
|
||||
import com.tangem.common.ui.charts.state.*
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
|
|
@ -50,6 +56,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
|
@ -73,70 +80,72 @@ fun MarketChart(
|
|||
splitChartSegmentColor: Color = TangemTheme.colors.icon.inactive,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float = 0.24f,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float = 0.24f,
|
||||
noChartContent: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
var canvasWidth by remember { mutableIntStateOf(0) }
|
||||
var canvasHeight by remember { mutableIntStateOf(0) }
|
||||
var chartHeight by remember { mutableIntStateOf(0) }
|
||||
|
||||
val layer = rememberLayerFromState(
|
||||
state = state,
|
||||
splitChartSegmentColor = splitChartSegmentColor,
|
||||
backgroundColorAlpha = backgroundColorAlpha,
|
||||
backgroundSplitChartSegmentColorAlpha = backgroundSplitChartSegmentColorAlpha,
|
||||
canvasHeight = canvasHeight,
|
||||
val layer = rememberMarketChartLayer(
|
||||
lineColor = state.chartColor,
|
||||
backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha),
|
||||
secondLineColor = splitChartSegmentColor,
|
||||
backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha),
|
||||
secondColorOnTheRightSide = state.markerHighlightRightSide.not(),
|
||||
markerFraction = state.markerFraction,
|
||||
axisValueOverrider = AxisValueOverrider.fixed(),
|
||||
canvasHeight = chartHeight,
|
||||
)
|
||||
|
||||
val marker = rememberTangemChartMarker(color = state.chartColor)
|
||||
|
||||
val chart = rememberCartesianChart(
|
||||
layer,
|
||||
startAxis = rememberMarketChartStartAxis(
|
||||
yValueFormatter = state.yValueFormatter,
|
||||
),
|
||||
bottomAxis = rememberMarketChartBottomAxis(
|
||||
xValueFormatter = state.xValueFormatter,
|
||||
),
|
||||
startAxis = rememberMarketChartStartAxis(state.yValueFormatter),
|
||||
bottomAxis = rememberMarketChartBottomAxis(state.xValueFormatter),
|
||||
horizontalLayout = HorizontalLayout.FullWidth(),
|
||||
markerVisibilityListener = rememberMarketVisibilityListener(canvasWidth, state),
|
||||
marker = marker,
|
||||
)
|
||||
val marker = rememberTangemChartMarker(
|
||||
color = state.chartColor,
|
||||
innerCircleColor = Color.White,
|
||||
)
|
||||
val density = LocalDensity.current
|
||||
|
||||
// we need to calculate what the overall height should be in order to get the correct height of the graph
|
||||
val bottomAxisHeight = with(LocalDensity.current) {
|
||||
TangemTheme.typography.caption2.fontSize.toPx().toInt() + TangemTheme.dimens.spacing26.toPx().toInt()
|
||||
}
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = modifier.onGloballyPositioned {
|
||||
with(density) {
|
||||
modifier = modifier
|
||||
.onGloballyPositioned {
|
||||
canvasWidth = it.size.width
|
||||
canvasHeight = if (it.size.height != 0) {
|
||||
// FIXME get height bounded to min max chart points
|
||||
it.size.height - 20.dp.toPx().toInt() - 27.dp.toPx().toInt()
|
||||
chartHeight = if (it.size.height != 0) {
|
||||
it.size.height - bottomAxisHeight
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
chart = chart,
|
||||
modelProducer = state.modelProducer,
|
||||
scrollState = rememberVicoScrollState(scrollEnabled = false),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
markerVisibilityListener = state.rememberMarketVisibilityListener(canvasWidth = canvasWidth),
|
||||
diffAnimationSpec = null,
|
||||
marker = marker,
|
||||
placeholder = noChartContent,
|
||||
animationSpec = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int): CartesianMarkerVisibilityListener {
|
||||
val state = this
|
||||
return remember(state.markerVisibilityListener, canvasWidth) {
|
||||
private fun rememberMarketVisibilityListener(
|
||||
canvasWidth: Int,
|
||||
state: MarketChartState,
|
||||
): CartesianMarkerVisibilityListener {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
return remember(state, canvasWidth) {
|
||||
val maxCanvasXFloat = canvasWidth.toFloat().takeIf { it != 0f }
|
||||
|
||||
object : CartesianMarkerVisibilityListener {
|
||||
override fun onShown(marker: CartesianMarker, targets: List<CartesianMarker.Target>) {
|
||||
state.stopDrawingAnimation()
|
||||
val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX
|
||||
|
||||
state.markerFraction = maxCanvasXFloat?.let { xCanvas / it }
|
||||
state.markerVisibilityListener.onShown(marker, targets)
|
||||
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
}
|
||||
|
||||
override fun onHidden(marker: CartesianMarker) {
|
||||
|
|
@ -149,38 +158,29 @@ private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int):
|
|||
|
||||
state.markerFraction = maxCanvasXFloat?.let { xCanvas / it }
|
||||
state.markerVisibilityListener.onUpdated(marker, targets)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberLayerFromState(
|
||||
state: MarketChartState,
|
||||
splitChartSegmentColor: Color,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float,
|
||||
canvasHeight: Int,
|
||||
): LineCartesianLayer {
|
||||
return rememberMarketChartLayer(
|
||||
lineColor = state.chartColor,
|
||||
backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha),
|
||||
secondLineColor = splitChartSegmentColor,
|
||||
backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha),
|
||||
secondColorOnTheRightSide = state.markerHighlightRightSide.not(),
|
||||
startDrawingAnimation = state.startDrawingAnimationState,
|
||||
markerFraction = state.markerFraction,
|
||||
axisValueOverrider = AxisValueOverrider.adaptiveYValues(yFraction = 1.2f, round = true), // FIXME ?
|
||||
canvasHeight = canvasHeight,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberMarketChartStartAxis(
|
||||
yValueFormatter: CartesianValueFormatter,
|
||||
): VerticalAxis<AxisPosition.Vertical.Start> {
|
||||
val textStyle = TangemTheme.typography.caption2
|
||||
val resolver = LocalFontFamilyResolver.current
|
||||
val typeface by remember(resolver, textStyle) {
|
||||
resolver.resolveAsTypeface(
|
||||
fontFamily = textStyle.fontFamily,
|
||||
fontWeight = textStyle.fontWeight ?: FontWeight.Normal,
|
||||
fontStyle = textStyle.fontStyle ?: FontStyle.Normal,
|
||||
fontSynthesis = textStyle.fontSynthesis ?: FontSynthesis.All,
|
||||
)
|
||||
}
|
||||
|
||||
return rememberCustomStartAxis(
|
||||
axis = null,
|
||||
line = null,
|
||||
tick = null,
|
||||
guideline = null,
|
||||
labelGuideline = rememberChartAxisGuidelineComponent(
|
||||
|
|
@ -194,38 +194,44 @@ private fun rememberMarketChartStartAxis(
|
|||
end = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
textSize = TangemTheme.typography.caption2.fontSize,
|
||||
typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(),
|
||||
typeface = typeface,
|
||||
),
|
||||
horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside,
|
||||
verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center,
|
||||
itemPlacer = AxisItemPlacer.Vertical.count({ GUIDELINES_COUNT }, false),
|
||||
itemPlacer = VerticalAxis.ItemPlacer.count({ GUIDELINES_COUNT }, false),
|
||||
valueFormatter = yValueFormatter,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberMarketChartBottomAxis(
|
||||
private fun rememberMarketChartBottomAxis(
|
||||
xValueFormatter: CartesianValueFormatter,
|
||||
): HorizontalAxis<AxisPosition.Horizontal.Bottom> {
|
||||
val textStyle = TangemTheme.typography.caption2
|
||||
|
||||
val resolver = LocalFontFamilyResolver.current
|
||||
|
||||
val typeface by remember(resolver, textStyle) {
|
||||
resolver.resolveAsTypeface(
|
||||
fontFamily = textStyle.fontFamily,
|
||||
fontWeight = textStyle.fontWeight ?: FontWeight.Normal,
|
||||
fontStyle = textStyle.fontStyle ?: FontStyle.Normal,
|
||||
fontSynthesis = textStyle.fontSynthesis ?: FontSynthesis.All,
|
||||
)
|
||||
}
|
||||
|
||||
return rememberBottomAxis(
|
||||
label = rememberAxisLabelComponent(
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textSize = TangemTheme.typography.caption2.fontSize,
|
||||
padding = Dimensions.of(top = TangemTheme.dimens.spacing20),
|
||||
typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(),
|
||||
padding = Dimensions.of(top = TangemTheme.dimens.spacing26),
|
||||
typeface = typeface,
|
||||
),
|
||||
tick = null,
|
||||
axis = null,
|
||||
line = null,
|
||||
guideline = null,
|
||||
sizeConstraint = BaseAxis.SizeConstraint.Exact(sizeDp = 37f), // FIXME ?
|
||||
itemPlacer = remember {
|
||||
AxisItemPlacer.Horizontal.default(
|
||||
spacing = 25, // FIXME ?
|
||||
offset = 60, // FIXME ?
|
||||
shiftExtremeTicks = false,
|
||||
addExtremeLabelPadding = false,
|
||||
)
|
||||
},
|
||||
sizeConstraint = BaseAxis.SizeConstraint.Auto(),
|
||||
itemPlacer = remember { TimeItemPlacer() },
|
||||
valueFormatter = xValueFormatter,
|
||||
)
|
||||
}
|
||||
|
|
@ -245,19 +251,6 @@ private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent {
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TextStyle.toGraphicsTypeFace(): android.graphics.Typeface {
|
||||
val resolver = LocalFontFamilyResolver.current
|
||||
return remember(resolver, this) {
|
||||
resolver.resolveAsTypeface(
|
||||
fontFamily = this.fontFamily,
|
||||
fontWeight = this.fontWeight ?: FontWeight.Normal,
|
||||
fontStyle = this.fontStyle ?: FontStyle.Normal,
|
||||
fontSynthesis = this.fontSynthesis ?: FontSynthesis.All,
|
||||
)
|
||||
}.value
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -275,7 +268,6 @@ private fun MarketChartPreview(
|
|||
chartLook = MarketChartLook(
|
||||
type = MarketChartLook.Type.Growing,
|
||||
markerHighlightRightSide = true,
|
||||
animationOnDataChange = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -283,8 +275,8 @@ private fun MarketChartPreview(
|
|||
LaunchedEffect(key1 = Unit) {
|
||||
dataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data(
|
||||
x = x,
|
||||
y = y,
|
||||
x = x.toImmutableList(),
|
||||
y = y.toImmutableList(),
|
||||
)
|
||||
updateLook {
|
||||
it.copy(
|
||||
|
|
@ -338,13 +330,9 @@ private fun MarketChartPreview(
|
|||
splitChartSegmentColor = TangemTheme.colors.icon.inactive,
|
||||
backgroundSplitChartSegmentColorAlpha = 0.24f,
|
||||
backgroundColorAlpha = 0.24f,
|
||||
noChartContent = { },
|
||||
)
|
||||
SpacerH16()
|
||||
|
||||
Button(onClick = { chartState.startDrawingAnimation() }) {
|
||||
Text("Start drawing animation")
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
dataProducer.runTransaction {
|
||||
|
|
@ -365,7 +353,7 @@ private fun MarketChartPreview(
|
|||
updateData {
|
||||
MarketChartData.Data(
|
||||
x = it.x,
|
||||
y = it.y.reversed(),
|
||||
y = it.y.reversed().toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -374,15 +362,6 @@ private fun MarketChartPreview(
|
|||
) {
|
||||
Text("Change Data")
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
dataProducer.runTransaction {
|
||||
updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) }
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text("Change animationOnDataChange = ${look.animationOnDataChange}")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
|
|
|
|||
|
|
@ -12,19 +12,21 @@ 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.rememberLine
|
||||
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.compose.common.shader.toDynamicShader
|
||||
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.cartesian.layer.LineCartesianLayer
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
|
|
@ -44,18 +46,19 @@ fun MarketChartMini(
|
|||
MarketChartLook.Type.Falling -> fallingColor
|
||||
}
|
||||
|
||||
val lineSpec = rememberLineSpec(
|
||||
val lineSpec = rememberLine(
|
||||
shader = ColorShader(lineColor.toArgb()),
|
||||
thickness = 1.dp,
|
||||
backgroundShader = BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent),
|
||||
),
|
||||
),
|
||||
backgroundShader = Brush.verticalGradient(
|
||||
colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent),
|
||||
).toDynamicShader(),
|
||||
)
|
||||
|
||||
val layer = rememberLineCartesianLayer(listOf(lineSpec))
|
||||
val chart = rememberCartesianChart(layer)
|
||||
val layer = rememberLineCartesianLayer(LineCartesianLayer.LineProvider.series(lineSpec))
|
||||
val chart = rememberCartesianChart(
|
||||
layer,
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = modifier,
|
||||
|
|
@ -63,7 +66,6 @@ fun MarketChartMini(
|
|||
model = model,
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
scrollState = rememberVicoScrollState(scrollEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -74,8 +76,8 @@ fun MarketChartMini(
|
|||
@Composable
|
||||
private fun Preview() {
|
||||
val data = MarketChartRawData(
|
||||
x = List(20) { Random.nextFloat() },
|
||||
y = List(20) { Random.nextFloat() },
|
||||
x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
|
||||
y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
|
|
@ -92,8 +94,8 @@ private fun Preview() {
|
|||
@Composable
|
||||
private fun PreviewColumn() {
|
||||
val data = MarketChartRawData(
|
||||
x = List(20) { Random.nextFloat() },
|
||||
y = List(20) { Random.nextFloat() },
|
||||
x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
|
||||
y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,246 @@
|
|||
package com.tangem.common.ui.charts.downsample
|
||||
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* =========================================================
|
||||
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* =========================================================
|
||||
*
|
||||
* Downsamples the given data points to the desired number of buckets (points + 2).
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object LTThreeBuckets {
|
||||
|
||||
fun downsample(x: List<Double>, y: List<Double>, desiredBuckets: Int): Result {
|
||||
require(x.size == y.size) { "X and Y must have the same size" }
|
||||
require(desiredBuckets > 0) { "Desired buckets must be greater than 0" }
|
||||
|
||||
val points = x.zip(y).mapIndexed { index, (x, y) -> Point(index, x, y) }
|
||||
val results = mutableListOf<Point>()
|
||||
|
||||
points.onPassBucketize(desiredBuckets)
|
||||
.sliding(size = 3, step = 1)
|
||||
.map { buckets -> Triangle.of(buckets) }
|
||||
.fastForEach { triangle ->
|
||||
if (results.isEmpty()) {
|
||||
results.add(triangle.getFirst())
|
||||
}
|
||||
|
||||
results.add(triangle.getResult())
|
||||
|
||||
if (results.size == desiredBuckets + 1) {
|
||||
results.add(triangle.getLast())
|
||||
}
|
||||
}
|
||||
|
||||
val xRes = ArrayList<Double>(points.size)
|
||||
val yRes = ArrayList<Double>(points.size)
|
||||
val indexesRes = ArrayList<Int>(points.size)
|
||||
|
||||
results.fastForEach {
|
||||
xRes.add(it.x)
|
||||
yRes.add(it.y)
|
||||
indexesRes.add(it.originalIndex!!)
|
||||
}
|
||||
|
||||
return Result(
|
||||
originalIndexes = indexesRes,
|
||||
x = xRes,
|
||||
y = yRes,
|
||||
)
|
||||
}
|
||||
|
||||
data class Result(
|
||||
val originalIndexes: List<Int>,
|
||||
val x: List<Double>,
|
||||
val y: List<Double>,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Point>.onPassBucketize(desiredBucketsCount: Int): List<Bucket> {
|
||||
val middleSize = size - 2
|
||||
val bucketSize = middleSize / desiredBucketsCount
|
||||
val remainingElements = middleSize % desiredBucketsCount
|
||||
|
||||
require(bucketSize != 0) {
|
||||
"Can't produce $desiredBucketsCount buckets from an input series of ${middleSize + 2} elements"
|
||||
}
|
||||
|
||||
val buckets = mutableListOf<Bucket>()
|
||||
|
||||
// Add first point as the only point in the first bucket
|
||||
buckets.add(Bucket.of(this[0]))
|
||||
|
||||
var rest = this.subList(1, this.lastIndex)
|
||||
|
||||
// Add middle buckets.
|
||||
// When inputSize is not a multiple of desiredBuckets,
|
||||
// remaining elements are equally distributed on the first buckets.
|
||||
while (buckets.size < desiredBucketsCount + 1) {
|
||||
val size = if (buckets.size <= remainingElements) bucketSize + 1 else bucketSize
|
||||
buckets.add(Bucket.of(rest.subList(0, size)))
|
||||
rest = rest.subList(size, rest.size)
|
||||
}
|
||||
|
||||
// Add last point as the only point in the last bucket
|
||||
buckets.add(Bucket.of(this.last()))
|
||||
|
||||
return buckets
|
||||
}
|
||||
|
||||
private fun List<Bucket>.sliding(size: Int, step: Int): List<List<Bucket>> {
|
||||
val window = max(size, step)
|
||||
val buffer = ArrayDeque<Bucket>()
|
||||
var totalIn = 0
|
||||
|
||||
val lists = mutableListOf<List<Bucket>>()
|
||||
|
||||
fastForEach { p ->
|
||||
buffer.add(p)
|
||||
++totalIn
|
||||
if (buffer.size == window) {
|
||||
val batch = buffer.take(size)
|
||||
lists.add(batch)
|
||||
|
||||
repeat(step) {
|
||||
buffer.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.isNotEmpty()) {
|
||||
val totalOut = max(0, (totalIn + step - size - 1) / step) + 1
|
||||
if (totalOut > lists.size) {
|
||||
val batch = buffer.take(size)
|
||||
lists.add(batch)
|
||||
}
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
|
||||
private data class Point(
|
||||
val originalIndex: Int? = null,
|
||||
val x: Double,
|
||||
val y: Double,
|
||||
)
|
||||
|
||||
private data class Bucket(
|
||||
val data: List<Point>,
|
||||
val center: Point,
|
||||
val result: Point,
|
||||
val first: Point,
|
||||
val last: Point,
|
||||
) {
|
||||
companion object {
|
||||
private fun centerBetweenPoints(a: Point, b: Point): Point {
|
||||
val vector = Point(
|
||||
x = b.x - a.x,
|
||||
y = b.y - a.y,
|
||||
)
|
||||
val halfVector = Point(
|
||||
x = vector.x / 2,
|
||||
y = vector.y / 2,
|
||||
)
|
||||
|
||||
return Point(
|
||||
x = a.x + halfVector.x,
|
||||
y = a.y + halfVector.y,
|
||||
)
|
||||
}
|
||||
|
||||
fun of(points: List<Point>): Bucket {
|
||||
val first = points.first()
|
||||
val last = points.last()
|
||||
|
||||
return Bucket(
|
||||
data = points,
|
||||
center = centerBetweenPoints(first, last),
|
||||
result = first,
|
||||
first = first,
|
||||
last = last,
|
||||
)
|
||||
}
|
||||
|
||||
fun of(point: Point): Bucket {
|
||||
return Bucket(
|
||||
data = listOf(point),
|
||||
center = point,
|
||||
result = point,
|
||||
first = point,
|
||||
last = point,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Triangle(
|
||||
val left: Bucket,
|
||||
val center: Bucket,
|
||||
val right: Bucket,
|
||||
) {
|
||||
fun getResult(): Point {
|
||||
return center.data.map { Area.ofTriangle(left.result, it, right.center) }
|
||||
.maxByOrNull { it.value }
|
||||
?.generator
|
||||
?: error("Can't obtain max area triangle")
|
||||
}
|
||||
|
||||
fun getFirst(): Point {
|
||||
return left.first
|
||||
}
|
||||
|
||||
fun getLast(): Point {
|
||||
return right.last
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun of(buckets: List<Bucket>): Triangle {
|
||||
return Triangle(
|
||||
left = buckets[0],
|
||||
center = buckets[1],
|
||||
right = buckets[2],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Area(
|
||||
val generator: Point,
|
||||
val value: Double,
|
||||
) {
|
||||
companion object {
|
||||
fun ofTriangle(a: Point, b: Point, c: Point): Area {
|
||||
val addends = listOf(
|
||||
a.x * (b.y - c.y),
|
||||
b.x * (c.y - a.y),
|
||||
c.x * (a.y - b.y),
|
||||
)
|
||||
val sum = addends.sum()
|
||||
val value = kotlin.math.abs(sum / 2)
|
||||
|
||||
return Area(b, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
|
||||
for (index in indices) {
|
||||
val item = get(index)
|
||||
action(item)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.common.ui.charts.layer
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberUnboundedLineComponent
|
||||
import com.patrykandpatrick.vico.compose.common.component.shapeComponent
|
||||
import com.patrykandpatrick.vico.compose.common.of
|
||||
import com.patrykandpatrick.vico.compose.common.shape.dashed
|
||||
import com.patrykandpatrick.vico.core.cartesian.*
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerValueFormatter
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker
|
||||
import com.patrykandpatrick.vico.core.common.Dimensions
|
||||
import com.patrykandpatrick.vico.core.common.LayeredComponent
|
||||
import com.patrykandpatrick.vico.core.common.component.Component
|
||||
import com.patrykandpatrick.vico.core.common.component.TextComponent
|
||||
import com.patrykandpatrick.vico.core.common.shape.Shape
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* @param color The color of the indicator and guideline.
|
||||
* @param innerCircleColor The color of the inner circle of the indicator.
|
||||
*
|
||||
* @return A [CartesianMarker] that consists of a dashed guideline and a layered indicator with a shadow effect.
|
||||
*/
|
||||
@Composable
|
||||
internal fun rememberTangemChartMarker(color: Color): CartesianMarker {
|
||||
val guideline = rememberUnboundedLineComponent(
|
||||
color = color,
|
||||
verticalAddDrawSpace = TangemTheme.dimens.spacing24,
|
||||
shape = remember { Shape.dashed(Shape.Rectangle, 4.dp, 4.dp) },
|
||||
)
|
||||
|
||||
return remember(guideline) {
|
||||
val outColor = guideline.color
|
||||
|
||||
object : DefaultCartesianMarker(
|
||||
label = TextComponent(textSizeSp = 0f),
|
||||
indicator = ::indicator,
|
||||
indicatorSizeDp = INDICATOR_SIZE_DP,
|
||||
guideline = guideline,
|
||||
valueFormatter = object : CartesianMarkerValueFormatter {
|
||||
override fun format(
|
||||
context: CartesianDrawContext,
|
||||
targets: List<CartesianMarker.Target>,
|
||||
): CharSequence = ""
|
||||
},
|
||||
) {
|
||||
override fun updateInsets(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
model: CartesianChartModel,
|
||||
insets: Insets,
|
||||
) {
|
||||
with(context) {
|
||||
super.updateInsets(context, horizontalDimensions, model, insets)
|
||||
val baseShadowInsetDp =
|
||||
CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP
|
||||
val topInset = (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels
|
||||
val bottomInset = (baseShadowInsetDp + LABEL_BACKGROUND_SHADOW_DY_DP).pixels
|
||||
insets.ensureValuesAtLeast(top = topInset, bottom = bottomInset)
|
||||
}
|
||||
}
|
||||
|
||||
override fun CartesianDrawContext.drawIndicator(x: Float, y: Float, color: Int, halfIndicatorSize: Float) {
|
||||
val indicator = indicator ?: return
|
||||
cacheStore
|
||||
.getOrSet(keyNamespace, indicator, outColor) { indicator.invoke(outColor) }
|
||||
.draw(
|
||||
this,
|
||||
x - halfIndicatorSize,
|
||||
y - halfIndicatorSize,
|
||||
x + halfIndicatorSize,
|
||||
y + halfIndicatorSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun indicator(color: Int): Component {
|
||||
val composeColor = Color(color)
|
||||
|
||||
return LayeredComponent(
|
||||
rear = shapeComponent(
|
||||
color = composeColor.copy(alpha = INDICATOR_REAR_COLOR_ALPHA),
|
||||
shape = Shape.Pill,
|
||||
),
|
||||
front = LayeredComponent(
|
||||
rear = shapeComponent(
|
||||
color = composeColor,
|
||||
shape = Shape.Pill,
|
||||
),
|
||||
front = shapeComponent(
|
||||
color = Color.White,
|
||||
shape = Shape.Pill,
|
||||
),
|
||||
padding = indicatorPadding,
|
||||
),
|
||||
padding = indicatorPadding,
|
||||
)
|
||||
}
|
||||
|
||||
private val indicatorPadding = Dimensions.of(3.dp)
|
||||
private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f
|
||||
private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f
|
||||
private const val CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER = 1.4f
|
||||
private const val INDICATOR_SIZE_DP = 16f
|
||||
private const val INDICATOR_REAR_COLOR_ALPHA = .24f
|
||||
|
|
@ -2,9 +2,6 @@ package com.tangem.common.ui.charts.layer
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -20,22 +17,22 @@ import androidx.compose.ui.unit.dp
|
|||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||
import com.patrykandpatrick.vico.compose.cartesian.fullWidth
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberSplitLineSpec
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberSplitLine
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
|
||||
import com.patrykandpatrick.vico.compose.common.shader.BrushShader
|
||||
import com.patrykandpatrick.vico.compose.common.shader.toDynamicShader
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
|
||||
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.core.common.shader.ColorShader
|
||||
import com.patrykandpatrick.vico.core.common.shader.DynamicShader
|
||||
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -59,117 +56,50 @@ internal fun rememberMarketChartLayer(
|
|||
backgroundLineColor: Color,
|
||||
secondLineColor: Color,
|
||||
backgroundSecondLineColor: Color,
|
||||
startDrawingAnimation: MutableState<Boolean>,
|
||||
axisValueOverrider: AxisValueOverrider,
|
||||
secondColorOnTheRightSide: Boolean,
|
||||
@FloatRange(from = 0.0, to = 1.0) markerFraction: Float?,
|
||||
canvasHeight: Int,
|
||||
): LineCartesianLayer {
|
||||
var animationFraction: Float? by remember { mutableStateOf(null) }
|
||||
val backgroundColorLineGradient = persistentListOf(backgroundLineColor, Color.Transparent)
|
||||
val backgroundSecondLineColorGradient = persistentListOf(backgroundSecondLineColor, Color.Transparent)
|
||||
|
||||
LaunchedEffect(startDrawingAnimation.value) {
|
||||
animationFraction = null
|
||||
if (startDrawingAnimation.value) {
|
||||
animate(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(easing = LinearEasing, durationMillis = 1000),
|
||||
) { start, _ ->
|
||||
if (start == 1f) {
|
||||
animationFraction = null
|
||||
startDrawingAnimation.value = false
|
||||
} else {
|
||||
animationFraction = start
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val markerSet = markerFraction != null
|
||||
|
||||
return rememberRawMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = backgroundLineColor,
|
||||
secondLineColor = secondLineColor,
|
||||
backgroundSecondLineColor = backgroundSecondLineColor,
|
||||
return rememberLayer(
|
||||
fractionValue = markerFraction ?: 0f,
|
||||
axisValueOverrider = axisValueOverrider,
|
||||
secondColorOnTheRightSide = secondColorOnTheRightSide,
|
||||
markerFraction = markerFraction,
|
||||
animationFraction = animationFraction,
|
||||
canvasHeight = canvasHeight,
|
||||
lineColor = if (markerFraction != null) {
|
||||
secondLineColor
|
||||
} else {
|
||||
lineColor
|
||||
},
|
||||
backLineColor = if (markerSet && !secondColorOnTheRightSide) {
|
||||
backgroundSecondLineColorGradient
|
||||
} else {
|
||||
backgroundColorLineGradient
|
||||
},
|
||||
lineColorRight = when {
|
||||
markerSet && secondColorOnTheRightSide -> secondLineColor
|
||||
else -> lineColor
|
||||
},
|
||||
backLineColorRight = when {
|
||||
markerSet && secondColorOnTheRightSide -> backgroundSecondLineColorGradient
|
||||
else -> backgroundColorLineGradient
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun rememberRawMarketChartLayer(
|
||||
lineColor: Color,
|
||||
backgroundLineColor: Color,
|
||||
secondLineColor: Color,
|
||||
backgroundSecondLineColor: Color,
|
||||
axisValueOverrider: AxisValueOverrider,
|
||||
canvasHeight: Int,
|
||||
secondColorOnTheRightSide: Boolean = false,
|
||||
@FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null,
|
||||
@FloatRange(from = 0.0, to = 1.0) animationFraction: Float? = null,
|
||||
): LineCartesianLayer {
|
||||
val backgroundColorLineGradient = listOf(backgroundLineColor, Color.Transparent)
|
||||
val backgroundSecondLineColorGradient = listOf(backgroundSecondLineColor, Color.Transparent)
|
||||
|
||||
val markerSet = markerFraction != null
|
||||
val animationRunning = animationFraction != null && animationFraction != 1f
|
||||
|
||||
val layerColors = when {
|
||||
!animationRunning && markerSet && secondColorOnTheRightSide -> {
|
||||
LayerColors(
|
||||
lineColor = lineColor,
|
||||
backLineColor = backgroundColorLineGradient,
|
||||
lineColorRight = secondLineColor,
|
||||
backLineColorRight = backgroundSecondLineColorGradient,
|
||||
)
|
||||
}
|
||||
!animationRunning && markerSet && !secondColorOnTheRightSide -> {
|
||||
LayerColors(
|
||||
lineColor = secondLineColor,
|
||||
backLineColor = backgroundSecondLineColorGradient,
|
||||
lineColorRight = lineColor,
|
||||
backLineColorRight = backgroundColorLineGradient,
|
||||
)
|
||||
}
|
||||
animationRunning -> {
|
||||
LayerColors(
|
||||
lineColor = lineColor,
|
||||
backLineColor = backgroundColorLineGradient,
|
||||
lineColorRight = Color.Transparent,
|
||||
backLineColorRight = listOf(Color.Transparent, Color.Transparent),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
LayerColors(
|
||||
lineColor = lineColor,
|
||||
backLineColor = backgroundColorLineGradient,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return rememberLayer(
|
||||
fractionValue = animationFraction ?: markerFraction,
|
||||
axisValueOverrider = axisValueOverrider,
|
||||
layerColors = layerColors,
|
||||
canvasHeight = canvasHeight,
|
||||
)
|
||||
}
|
||||
|
||||
private data class LayerColors(
|
||||
val lineColor: Color,
|
||||
val backLineColor: List<Color>,
|
||||
val lineColorRight: Color? = null,
|
||||
val backLineColorRight: List<Color>? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun rememberLayer(
|
||||
fractionValue: Float?,
|
||||
fractionValue: Float,
|
||||
axisValueOverrider: AxisValueOverrider,
|
||||
layerColors: LayerColors,
|
||||
lineColor: Color,
|
||||
backLineColor: ImmutableList<Color>,
|
||||
lineColorRight: Color,
|
||||
backLineColorRight: ImmutableList<Color>,
|
||||
canvasHeight: Int,
|
||||
): LineCartesianLayer {
|
||||
val endGradientColorPosition = if (canvasHeight != 0) {
|
||||
|
|
@ -178,47 +108,27 @@ private fun rememberLayer(
|
|||
Float.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
val alineColor = remember(lineColor) { lineColor.toArgb() }
|
||||
val alineColorRight = remember(lineColorRight) { lineColorRight.toArgb() }
|
||||
|
||||
return rememberLineCartesianLayer(
|
||||
listOf(
|
||||
if (layerColors.lineColorRight == null || layerColors.backLineColorRight == null || fractionValue == null) {
|
||||
rememberLineSpec(
|
||||
shader = remember(layerColors.lineColor) { ColorShader(color = layerColors.lineColor.toArgb()) },
|
||||
backgroundShader = remember(layerColors.backLineColor, endGradientColorPosition) {
|
||||
BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = layerColors.backLineColor,
|
||||
endY = endGradientColorPosition,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
rememberSplitLineSpec(
|
||||
shader = remember(layerColors.lineColor, layerColors.lineColorRight, fractionValue) {
|
||||
DynamicShader.Companion.horizontalGradient(
|
||||
colors = intArrayOf(layerColors.lineColor.toArgb(), layerColors.lineColorRight.toArgb()),
|
||||
positions = floatArrayOf(fractionValue, fractionValue),
|
||||
)
|
||||
},
|
||||
backgroundShaderFirst = remember(layerColors.backLineColor, endGradientColorPosition) {
|
||||
BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = layerColors.backLineColor,
|
||||
endY = endGradientColorPosition,
|
||||
),
|
||||
)
|
||||
},
|
||||
backgroundShaderSecond = remember(layerColors.backLineColorRight, endGradientColorPosition) {
|
||||
BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = layerColors.backLineColorRight,
|
||||
endY = endGradientColorPosition,
|
||||
),
|
||||
)
|
||||
},
|
||||
xSplitFraction = fractionValue,
|
||||
)
|
||||
},
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
rememberSplitLine(
|
||||
shader = DynamicShader.Companion.horizontalGradient(
|
||||
colors = intArrayOf(alineColor, alineColorRight),
|
||||
positions = floatArrayOf(fractionValue, fractionValue),
|
||||
),
|
||||
backgroundShaderFirst = Brush.verticalGradient(
|
||||
colors = backLineColor,
|
||||
endY = endGradientColorPosition,
|
||||
).toDynamicShader(),
|
||||
backgroundShaderSecond = Brush.verticalGradient(
|
||||
colors = backLineColorRight,
|
||||
endY = endGradientColorPosition,
|
||||
).toDynamicShader(),
|
||||
xSplitFraction = fractionValue,
|
||||
thickness = 1.dp,
|
||||
),
|
||||
),
|
||||
axisValueOverrider = axisValueOverrider,
|
||||
)
|
||||
|
|
@ -250,25 +160,26 @@ private fun LayerChartPreview(
|
|||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberRawMarketChartLayer(
|
||||
rememberMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = lineColor.copy(alpha = 0.24f),
|
||||
secondLineColor = Color.Gray,
|
||||
backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f),
|
||||
secondColorOnTheRightSide = true,
|
||||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
markerFraction = 0.35f,
|
||||
canvasHeight = 495,
|
||||
),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
model = model,
|
||||
)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberRawMarketChartLayer(
|
||||
rememberMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = lineColor.copy(alpha = 0.24f),
|
||||
secondLineColor = Color.Gray,
|
||||
|
|
@ -278,16 +189,16 @@ private fun LayerChartPreview(
|
|||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
canvasHeight = 495,
|
||||
),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
model = model,
|
||||
)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberRawMarketChartLayer(
|
||||
rememberMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = lineColor.copy(alpha = 0.24f),
|
||||
secondLineColor = Color.Gray,
|
||||
|
|
@ -297,29 +208,9 @@ private fun LayerChartPreview(
|
|||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
canvasHeight = 495,
|
||||
),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
model = model,
|
||||
)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberRawMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = lineColor.copy(alpha = 0.24f),
|
||||
secondLineColor = lineColor,
|
||||
backgroundSecondLineColor = lineColor.copy(alpha = 0.24f),
|
||||
markerFraction = 0.35f,
|
||||
secondColorOnTheRightSide = true,
|
||||
animationFraction = 0.7f,
|
||||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
canvasHeight = 495,
|
||||
),
|
||||
),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
model = model,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.common.ui.charts.layer
|
||||
|
||||
import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext
|
||||
import com.patrykandpatrick.vico.core.cartesian.CartesianMeasureContext
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalDimensions
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.ChartValues
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class TimeItemPlacer : HorizontalAxis.ItemPlacer {
|
||||
|
||||
private val ChartValues.measuredLabelValues
|
||||
get() = buildList {
|
||||
// produce exactly 6 values distributed evenly
|
||||
val xLength = maxX - minX
|
||||
val xStep = xLength / 7
|
||||
|
||||
repeat(times = 6) {
|
||||
add(minX + xStep * (it + 1))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEndHorizontalAxisInset(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
tickThickness: Float,
|
||||
maxLabelWidth: Float,
|
||||
): Float = 0f
|
||||
|
||||
override fun getStartHorizontalAxisInset(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
tickThickness: Float,
|
||||
maxLabelWidth: Float,
|
||||
): Float = 0f
|
||||
|
||||
override fun getHeightMeasurementLabelValues(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
fullXRange: ClosedFloatingPointRange<Double>,
|
||||
maxLabelWidth: Float,
|
||||
): List<Double> = context.chartValues.measuredLabelValues
|
||||
|
||||
override fun getLabelValues(
|
||||
context: CartesianDrawContext,
|
||||
visibleXRange: ClosedFloatingPointRange<Double>,
|
||||
fullXRange: ClosedFloatingPointRange<Double>,
|
||||
maxLabelWidth: Float,
|
||||
): List<Double> = context.chartValues.measuredLabelValues
|
||||
|
||||
override fun getWidthMeasurementLabelValues(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
fullXRange: ClosedFloatingPointRange<Double>,
|
||||
): List<Double> = context.chartValues.measuredLabelValues
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
package com.tangem.common.ui.charts.marker
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||
import com.patrykandpatrick.vico.compose.cartesian.fullWidth
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberLayeredComponent
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberUnboundedLineComponent
|
||||
import com.patrykandpatrick.vico.compose.common.of
|
||||
import com.patrykandpatrick.vico.compose.common.shader.color
|
||||
import com.patrykandpatrick.vico.compose.common.shape.dashed
|
||||
import com.patrykandpatrick.vico.core.cartesian.*
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker
|
||||
import com.patrykandpatrick.vico.core.common.Dimensions
|
||||
import com.patrykandpatrick.vico.core.common.component.TextComponent
|
||||
import com.patrykandpatrick.vico.core.common.shader.DynamicShader
|
||||
import com.patrykandpatrick.vico.core.common.shape.Shape
|
||||
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* @param color The color of the indicator and guideline.
|
||||
* @param innerCircleColor The color of the inner circle of the indicator.
|
||||
*
|
||||
* @return A [CartesianMarker] that consists of a dashed guideline and a layered indicator with a shadow effect.
|
||||
*/
|
||||
@Composable
|
||||
internal fun rememberTangemChartMarker(color: Color, innerCircleColor: Color): CartesianMarker {
|
||||
val indicatorFrontComponent = rememberShapeComponent(
|
||||
shape = Shape.Pill,
|
||||
color = innerCircleColor,
|
||||
)
|
||||
val indicatorCenterComponent = rememberShapeComponent(
|
||||
shape = Shape.Pill,
|
||||
color = color,
|
||||
)
|
||||
val indicatorRearComponent = rememberShapeComponent(
|
||||
shape = Shape.Pill,
|
||||
color = if (color == Color.Transparent) {
|
||||
Color.Transparent
|
||||
} else {
|
||||
color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA)
|
||||
},
|
||||
)
|
||||
val indicator = rememberLayeredComponent(
|
||||
rear = indicatorRearComponent,
|
||||
front = rememberLayeredComponent(
|
||||
rear = indicatorCenterComponent,
|
||||
front = indicatorFrontComponent,
|
||||
padding = indicatorPadding,
|
||||
),
|
||||
padding = indicatorPadding,
|
||||
)
|
||||
val guideline = rememberUnboundedLineComponent(
|
||||
color = color,
|
||||
verticalAddDrawSpace = TangemTheme.dimens.spacing24,
|
||||
shape = remember { Shape.dashed(Shape.Rectangle, 4.dp, 4.dp) },
|
||||
)
|
||||
return remember(indicator, guideline) {
|
||||
object : DefaultCartesianMarker(
|
||||
label = TextComponent.build { textSizeSp = 0f },
|
||||
indicator = indicator,
|
||||
indicatorSizeDp = INDICATOR_SIZE_DP,
|
||||
guideline = guideline,
|
||||
) {
|
||||
override fun getInsets(
|
||||
context: CartesianMeasureContext,
|
||||
outInsets: Insets,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
) {
|
||||
with(context) {
|
||||
super.getInsets(context, outInsets, horizontalDimensions)
|
||||
val baseShadowInsetDp =
|
||||
CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP
|
||||
outInsets.top += (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels
|
||||
outInsets.bottom += (baseShadowInsetDp + LABEL_BACKGROUND_SHADOW_DY_DP).pixels
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val indicatorPadding = Dimensions.of(3.dp)
|
||||
private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f
|
||||
private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f
|
||||
private const val CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER = 1.4f
|
||||
private const val INDICATOR_SIZE_DP = 16f
|
||||
private const val INDICATOR_REAR_COLOR_ALPHA = .24f
|
||||
|
||||
// region Preview
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TangemChartMarkerPreview(
|
||||
@PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair<List<BigDecimal>, List<BigDecimal>>,
|
||||
) {
|
||||
val marker = rememberTangemChartMarker(Color.Red, Color.White)
|
||||
val y = previewData.second.map { it.toFloat() }
|
||||
val x = List(y.size) { it.toFloat() }
|
||||
val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) })
|
||||
|
||||
val centerAprx = (model.models[0].minX + model.models[0].maxX) / 2f
|
||||
val center = model.models[0].getXDeltaGcd().let { centerAprx - centerAprx % it }
|
||||
|
||||
TangemThemePreview {
|
||||
Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberLineCartesianLayer(
|
||||
listOf(rememberLineSpec(shader = DynamicShader.color(Color.Blue))),
|
||||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
),
|
||||
persistentMarkers = mapOf(center to marker),
|
||||
),
|
||||
model = model,
|
||||
marker = marker,
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Preview
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Immutable
|
||||
|
|
@ -30,7 +32,7 @@ sealed interface MarketChartData {
|
|||
*/
|
||||
@Immutable
|
||||
data class Data(
|
||||
val x: List<BigDecimal> = listOf(),
|
||||
val y: List<BigDecimal> = listOf(),
|
||||
val x: ImmutableList<BigDecimal> = persistentListOf(),
|
||||
val y: ImmutableList<BigDecimal> = persistentListOf(),
|
||||
) : MarketChartData
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@ package com.tangem.common.ui.charts.state
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
||||
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import com.tangem.common.ui.charts.state.converter.PointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.formatter.FormatterWrapWithCache
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* This class represents a transaction for updating the state and look of a Market Chart.
|
||||
|
|
@ -25,7 +25,12 @@ class Transaction(
|
|||
var chartData: MarketChartData.NoData? = null
|
||||
|
||||
fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) {
|
||||
chartLook = block(currentLook)
|
||||
val newLook = block(currentLook)
|
||||
|
||||
chartLook = newLook.copy(
|
||||
xAxisFormatter = FormatterWrapWithCache(newLook.xAxisFormatter),
|
||||
yAxisFormatter = FormatterWrapWithCache(newLook.yAxisFormatter),
|
||||
)
|
||||
}
|
||||
|
||||
fun updateState(block: (prev: MarketChartData) -> MarketChartData.NoData) {
|
||||
|
|
@ -56,7 +61,12 @@ class TransactionSuspend(
|
|||
}
|
||||
|
||||
fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) {
|
||||
chartLook = block(currentLook)
|
||||
val newLook = block(currentLook)
|
||||
|
||||
chartLook = newLook.copy(
|
||||
xAxisFormatter = FormatterWrapWithCache(newLook.xAxisFormatter),
|
||||
yAxisFormatter = FormatterWrapWithCache(newLook.yAxisFormatter),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun updateState(block: (prev: MarketChartData) -> MarketChartData) {
|
||||
|
|
@ -75,21 +85,24 @@ class TransactionSuspend(
|
|||
class MarketChartDataProducer private constructor(
|
||||
initialData: MarketChartData,
|
||||
initialLook: MarketChartLook,
|
||||
val pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
val pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
) {
|
||||
internal val startDrawingAnimation = MutableSharedFlow<Unit>()
|
||||
internal val dataState = MutableStateFlow(initialData)
|
||||
internal val lookState = MutableStateFlow(initialLook)
|
||||
internal val entries = MutableStateFlow<List<LineCartesianLayerModel.Entry>>(emptyList())
|
||||
|
||||
internal val modelProducer = CartesianChartModelProducer.build(dispatcher = dispatcher)
|
||||
internal val modelProducer = CartesianChartModelProducer(dispatcher = dispatcher)
|
||||
internal val rawData = MutableStateFlow<MarketChartRawData?>(null)
|
||||
private val mutex = Mutex()
|
||||
|
||||
/**
|
||||
* This function runs a suspending transaction block to update the state and look of the Market Chart.
|
||||
*/
|
||||
suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) =
|
||||
handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block))
|
||||
suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) = withContext(dispatcher) {
|
||||
mutex.withLock {
|
||||
handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function runs a non-suspending transaction block to update the state and look of the Market Chart.
|
||||
|
|
@ -102,32 +115,30 @@ class MarketChartDataProducer private constructor(
|
|||
val chartData = transaction.chartData
|
||||
val oldData = dataState.value
|
||||
|
||||
if (chartData != null) {
|
||||
dataState.value = chartData
|
||||
}
|
||||
|
||||
if (chartData is MarketChartData.Data && (oldData !is MarketChartData.Data || oldData != chartData)) {
|
||||
if (lookState.value.animationOnDataChange) {
|
||||
startDrawingAnimation.emit(Unit)
|
||||
}
|
||||
withContext(dispatcher) {
|
||||
val rawData = pointsValuesConverter.convert(chartData)
|
||||
(lookState.value.xAxisFormatter as? FormatterWrapWithCache)?.clearCache()
|
||||
(lookState.value.yAxisFormatter as? FormatterWrapWithCache)?.clearCache()
|
||||
|
||||
val entriesLocal =
|
||||
rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) }
|
||||
val rawData = pointsValuesConverter.convert(chartData)
|
||||
|
||||
entries.value = entriesLocal
|
||||
val entriesLocal =
|
||||
rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) }
|
||||
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
runCatching {
|
||||
modelProducer.runTransaction {
|
||||
add(LineCartesianLayerModel.Partial(series = listOf(entriesLocal)))
|
||||
|
||||
updateExtras {
|
||||
it[entriesKey] = entriesLocal
|
||||
it[xKey] = chartData.x
|
||||
it[yKey] = chartData.y
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
}
|
||||
|
||||
entries.value = entriesLocal
|
||||
dataState.value = chartData
|
||||
this.rawData.value = rawData
|
||||
|
||||
delay(timeMillis = 200)
|
||||
} else if (chartData != null) {
|
||||
dataState.value = chartData
|
||||
}
|
||||
|
||||
nonSuspendTransaction?.let { handleTransaction(it) }
|
||||
|
|
@ -143,10 +154,6 @@ class MarketChartDataProducer private constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
internal val entriesKey = ExtraStore.Key<List<LineCartesianLayerModel.Entry>>()
|
||||
internal val xKey = ExtraStore.Key<List<BigDecimal>>()
|
||||
internal val yKey = ExtraStore.Key<List<BigDecimal>>()
|
||||
|
||||
private val initialData: MarketChartData = MarketChartData.NoData.Empty
|
||||
private val initialLook: MarketChartLook = MarketChartLook()
|
||||
|
||||
|
|
@ -159,7 +166,7 @@ class MarketChartDataProducer private constructor(
|
|||
* @return A MarketChartDataProducer.
|
||||
*/
|
||||
suspend fun buildSuspend(
|
||||
pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
block: TransactionSuspend.() -> Unit,
|
||||
): MarketChartDataProducer {
|
||||
|
|
@ -184,7 +191,7 @@ class MarketChartDataProducer private constructor(
|
|||
* @return A MarketChartDataProducer.
|
||||
*/
|
||||
fun build(
|
||||
pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
block: Transaction.() -> Unit,
|
||||
): MarketChartDataProducer {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.formatter.AxisLabelFormatter
|
||||
|
||||
/**
|
||||
* This class represents the look and feel of a Market Chart.
|
||||
* It includes properties for type, marker highlight, animation on data change, animate data appearance,
|
||||
|
|
@ -7,16 +10,13 @@ package com.tangem.common.ui.charts.state
|
|||
*
|
||||
* @property type The type of the chart, can be either Growing or Falling.
|
||||
* @property markerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart.
|
||||
* @property animationOnDataChange A boolean indicating whether to animate on data change.
|
||||
* @property animateDataAppearance A boolean indicating whether to animate data appearance.
|
||||
* @property xAxisFormatter A formatter for the x-axis labels.
|
||||
* @property yAxisFormatter A formatter for the y-axis labels.
|
||||
*/
|
||||
@Immutable
|
||||
data class MarketChartLook(
|
||||
val type: Type = Type.Growing,
|
||||
val markerHighlightRightSide: Boolean = true,
|
||||
val animationOnDataChange: Boolean = false,
|
||||
val animateDataAppearance: Boolean = false,
|
||||
val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() },
|
||||
val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() },
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* This class represents raw data for a Market Chart. Used for drawing the chart.
|
||||
*
|
||||
* @property originalIndexes If the source data has the original representation (due to reduced sampling),
|
||||
* this list contains the original indexes of the data points.
|
||||
* @property y The list of y-values.
|
||||
* @property x The list of x-values.
|
||||
*/
|
||||
@Immutable
|
||||
data class MarketChartRawData(
|
||||
val y: List<Float>,
|
||||
val x: List<Float> = List(y.size) { 1f },
|
||||
val originalIndexes: ImmutableList<Int>? = null,
|
||||
val y: ImmutableList<Double>,
|
||||
val x: ImmutableList<Double> = List(y.size) { 1.0 }.toImmutableList(),
|
||||
)
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.common.ui.charts.state
|
|||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener
|
||||
|
|
@ -20,26 +19,22 @@ import java.math.BigDecimal
|
|||
@Composable
|
||||
fun rememberMarketChartState(
|
||||
dataProducer: MarketChartDataProducer = remember { MarketChartDataProducer.build {} },
|
||||
colorMapper: (MarketChartLook.Type) -> Color = {
|
||||
when (it) {
|
||||
MarketChartLook.Type.Growing -> Color.Green
|
||||
MarketChartLook.Type.Falling -> Color.Red
|
||||
colorMapper: (MarketChartLook.Type) -> Color = remember {
|
||||
{
|
||||
when (it) {
|
||||
MarketChartLook.Type.Growing -> Color.Green
|
||||
MarketChartLook.Type.Falling -> Color.Red
|
||||
}
|
||||
}
|
||||
},
|
||||
onMarkerShown: (x: BigDecimal?, y: BigDecimal?) -> Unit = { _, _ -> },
|
||||
): MarketChartState {
|
||||
val lookState = dataProducer.lookState.collectAsStateWithLifecycle()
|
||||
val lookState = dataProducer.lookState.collectAsState()
|
||||
|
||||
val state = remember(dataProducer, lookState, colorMapper, onMarkerShown) {
|
||||
MarketChartState(dataProducer, lookState, colorMapper, onMarkerShown)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
dataProducer.startDrawingAnimation.collect {
|
||||
state.startDrawingAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +54,6 @@ class MarketChartState internal constructor(
|
|||
private val colorMapper: (MarketChartLook.Type) -> Color,
|
||||
private val markerCallback: (x: BigDecimal?, y: BigDecimal?) -> Unit,
|
||||
) {
|
||||
internal val startDrawingAnimationState = mutableStateOf(false)
|
||||
internal val modelProducer = dataProducer.modelProducer
|
||||
|
||||
internal val chartColor by derivedStateOf {
|
||||
|
|
@ -70,29 +64,29 @@ class MarketChartState internal constructor(
|
|||
lookState.value.markerHighlightRightSide
|
||||
}
|
||||
|
||||
internal val xValueFormatter by derivedStateOf {
|
||||
CartesianValueFormatter { value, _, _ ->
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
internal val xValueFormatter = CartesianValueFormatter { value, _, _ ->
|
||||
val formatter = dataProducer.lookState.value.xAxisFormatter
|
||||
|
||||
lookState.value.xAxisFormatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state),
|
||||
)
|
||||
}
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
|
||||
formatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state),
|
||||
)
|
||||
}
|
||||
|
||||
internal val yValueFormatter by derivedStateOf {
|
||||
CartesianValueFormatter { value, _, _ ->
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
internal val yValueFormatter = CartesianValueFormatter { value, _, _ ->
|
||||
val formatter = dataProducer.lookState.value.yAxisFormatter
|
||||
|
||||
lookState.value.yAxisFormatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state),
|
||||
)
|
||||
}
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
|
||||
formatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state),
|
||||
)
|
||||
}
|
||||
|
||||
internal var markerFraction: Float? by mutableStateOf(null)
|
||||
internal var markerFraction by mutableStateOf<Float?>(null)
|
||||
|
||||
internal val markerVisibilityListener = object : CartesianMarkerVisibilityListener {
|
||||
override fun onShown(marker: CartesianMarker, targets: List<CartesianMarker.Target>) {
|
||||
|
|
@ -116,24 +110,17 @@ class MarketChartState internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val isDrawingAnimationInProgress: Boolean by derivedStateOf {
|
||||
startDrawingAnimationState.value
|
||||
}
|
||||
|
||||
private fun getPoint(targets: List<CartesianMarker.Target>): Pair<BigDecimal, BigDecimal>? {
|
||||
val entry = (targets[0] as LineCartesianLayerMarkerTarget).points[0].entry
|
||||
val entryIndex = dataProducer.entries.value.indexOf(entry).takeIf { it != -1 } ?: return null
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data ?: return null
|
||||
val x = state.x.getOrNull(entryIndex) ?: return null
|
||||
val y = state.y.getOrNull(entryIndex) ?: return null
|
||||
val rawData = dataProducer.rawData.value ?: return null
|
||||
|
||||
val originalIndex = rawData.originalIndexes?.getOrNull(entryIndex)
|
||||
val index = originalIndex ?: entryIndex
|
||||
|
||||
val x = state.x.getOrNull(index) ?: return null
|
||||
val y = state.y.getOrNull(index) ?: return null
|
||||
return x to y
|
||||
}
|
||||
|
||||
fun startDrawingAnimation() {
|
||||
startDrawingAnimationState.value = true
|
||||
}
|
||||
|
||||
fun stopDrawingAnimation() {
|
||||
startDrawingAnimationState.value = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.common.ui.charts.state.converter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
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.
|
||||
*
|
||||
* **[prepareRawXForFormat] and [prepareRawYForFormat] must be very fast because they are called in the onDraw method**
|
||||
*/
|
||||
interface PointValuesConverter {
|
||||
|
||||
fun convert(data: MarketChartData.Data): MarketChartRawData
|
||||
|
||||
fun prepareRawXForFormat(rawX: Double, data: MarketChartData.Data): BigDecimal
|
||||
|
||||
fun prepareRawYForFormat(rawY: Double, data: MarketChartData.Data): BigDecimal
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.tangem.common.ui.charts.state.converter
|
||||
|
||||
import com.tangem.common.ui.charts.downsample.LTThreeBuckets
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class PriceAndTimePointValuesConverter(
|
||||
private val needToFormatAxis: Boolean,
|
||||
) : PointValuesConverter {
|
||||
|
||||
private data class MinMaxCache(
|
||||
val minX: BigDecimal,
|
||||
val maxX: BigDecimal,
|
||||
val minY: BigDecimal,
|
||||
val maxY: BigDecimal,
|
||||
)
|
||||
|
||||
private var minMaxCache = MinMaxCache(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)
|
||||
private val formatYValuesCache = mutableMapOf<Double, BigDecimal>()
|
||||
private val formatXValuesCache = mutableMapOf<Double, BigDecimal>()
|
||||
|
||||
override fun convert(data: MarketChartData.Data): MarketChartRawData {
|
||||
formatYValuesCache.clear()
|
||||
formatXValuesCache.clear()
|
||||
val cache = MinMaxCache(
|
||||
minY = data.y.minOrNull() ?: BigDecimal.ZERO,
|
||||
maxY = data.y.maxOrNull() ?: BigDecimal.ZERO,
|
||||
minX = data.x.minOrNull() ?: BigDecimal.ZERO,
|
||||
maxX = data.x.maxOrNull() ?: BigDecimal.ZERO,
|
||||
)
|
||||
minMaxCache = cache
|
||||
|
||||
val normY = data.y.normalizeToDouble(min = cache.minY, max = cache.maxY)
|
||||
val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX)
|
||||
|
||||
return if (normX.size > MAX_POINTS) {
|
||||
LTThreeBuckets
|
||||
.downsample(normX, normY, MAX_POINTS - 2)
|
||||
.let {
|
||||
MarketChartRawData(
|
||||
originalIndexes = it.originalIndexes.toImmutableList(),
|
||||
x = it.x.toImmutableList(),
|
||||
y = it.y.toImmutableList(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
MarketChartRawData(
|
||||
x = normX.toImmutableList(),
|
||||
y = normY.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun prepareRawXForFormat(rawX: Double, data: MarketChartData.Data): BigDecimal {
|
||||
if (!needToFormatAxis) return BigDecimal.ZERO
|
||||
if (formatXValuesCache.containsKey(rawX)) return formatXValuesCache[rawX]!!
|
||||
|
||||
val result = (rawX * MINUTE).toBigDecimal()
|
||||
|
||||
formatXValuesCache[rawX] = result
|
||||
return result
|
||||
}
|
||||
|
||||
override fun prepareRawYForFormat(rawY: Double, data: MarketChartData.Data): BigDecimal {
|
||||
if (!needToFormatAxis) return BigDecimal.ZERO
|
||||
if (formatYValuesCache.containsKey(rawY)) return formatYValuesCache[rawY]!!
|
||||
|
||||
val min = minMaxCache.minY
|
||||
val max = minMaxCache.maxY
|
||||
val length = max - min
|
||||
|
||||
val result = when {
|
||||
rawY < 0.01f -> min
|
||||
rawY < 0.55f && rawY > 0.45f -> min + length / 2.toBigDecimal()
|
||||
rawY > 0.97f && rawY < 1.01f -> max
|
||||
else -> length * rawY.toBigDecimal() + min
|
||||
}
|
||||
formatYValuesCache[rawY] = result
|
||||
return result
|
||||
}
|
||||
|
||||
private fun List<BigDecimal>.normalizeToDouble(min: BigDecimal, max: BigDecimal): List<Double> {
|
||||
if (min == max) {
|
||||
return List(size) { 0.5 }
|
||||
}
|
||||
|
||||
return map { ((it - min) / (max - min)).toDouble() }
|
||||
}
|
||||
|
||||
private fun List<BigDecimal>.normalizeTime(min: BigDecimal, max: BigDecimal): List<Double> {
|
||||
if (min == max) {
|
||||
return List(size) { 0.5 }
|
||||
}
|
||||
|
||||
return map {
|
||||
(it / MINUTE_BIG).toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val MAX_POINTS = 502
|
||||
private const val MINUTE = 60000L
|
||||
private val MINUTE_BIG = 60000L.toBigDecimal()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
package com.tangem.common.ui.charts.state.formatter
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -7,6 +7,8 @@ import java.math.BigDecimal
|
|||
* Used for formatting the axis labels in a chart.
|
||||
* It takes a BigDecimal value and returns a CharSequence that represents the formatted label.
|
||||
*
|
||||
* [format] has to be very fast because it is called in the onDraw method.
|
||||
*
|
||||
* @param value The value to be formatted.
|
||||
* @return The formatted label as a CharSequence.
|
||||
*/
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.common.ui.charts.state.formatter
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class FormatterWrapWithCache(private val formatter: AxisLabelFormatter) : AxisLabelFormatter {
|
||||
private val cache = mutableMapOf<BigDecimal, CharSequence>()
|
||||
|
||||
override fun format(value: BigDecimal): CharSequence {
|
||||
return cache.getOrPut(value) { formatter.format(value) }
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ interface TangemTechMarketsApi {
|
|||
|
||||
@GET("coins/{coin_id}/history")
|
||||
suspend fun getCoinChart(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
): ApiResponse<TokenMarketChartResponse>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ fun TangemTheme(
|
|||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalTangemShimmer provides TangemShimmer,
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) },
|
||||
) {
|
||||
ProvideTextStyle(
|
||||
value = TangemTheme.typography.body1,
|
||||
|
|
@ -246,4 +247,8 @@ val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
|
|||
|
||||
val LocalTangemShimmer = staticCompositionLocalOf<Shimmer> {
|
||||
error("No TangemShimmer provided")
|
||||
}
|
||||
|
||||
val LocalMainBottomSheetColor = staticCompositionLocalOf<MutableState<Color>> {
|
||||
error("No MainBottomSheetColor provided")
|
||||
}
|
||||
|
|
@ -61,7 +61,14 @@ object DateTimeFormatters {
|
|||
*/
|
||||
val dateMMMMd: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))
|
||||
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM"))
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
||||
val dateYYYY: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyy"))
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,9 +91,14 @@ internal class DefaultMarketsTokenRepository(
|
|||
).toBatchFlow()
|
||||
}
|
||||
|
||||
override suspend fun getChart(interval: PriceChangeInterval, tokenId: String): TokenChart {
|
||||
override suspend fun getChart(
|
||||
fiatCurrencyCode: String,
|
||||
interval: PriceChangeInterval,
|
||||
tokenId: String,
|
||||
): TokenChart {
|
||||
val response = marketsApi.getCoinChart(
|
||||
currency = tokenId,
|
||||
currency = fiatCurrencyCode,
|
||||
coinId = tokenId,
|
||||
interval = interval.toRequestParam(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
|
|||
fun PriceChangeInterval.toRequestParam(): String = when (this) {
|
||||
PriceChangeInterval.H24 -> "24h"
|
||||
PriceChangeInterval.WEEK -> "1w"
|
||||
PriceChangeInterval.MONTH -> "30d"
|
||||
PriceChangeInterval.MONTH -> "1m"
|
||||
PriceChangeInterval.MONTH3 -> "3m"
|
||||
PriceChangeInterval.MONTH6 -> "6m"
|
||||
PriceChangeInterval.YEAR -> "1y"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.markets.converters
|
|||
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
|
||||
class TokenMarketChartsConverter(
|
||||
private val tokenChartConverter: TokenChartConverter,
|
||||
|
|
@ -11,23 +12,29 @@ class TokenMarketChartsConverter(
|
|||
fun convert(
|
||||
chartsToCopy: TokenMarket.Charts,
|
||||
tokenId: String,
|
||||
interval: PriceChangeInterval,
|
||||
interval: TokenMarketListConfig.Interval,
|
||||
value: TokenMarketChartListResponse,
|
||||
): TokenMarket.Charts {
|
||||
val prices = requireNotNull(value[tokenId]) {
|
||||
"$tokenId is not found in the response. This shouldn't have happened."
|
||||
}
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> chartsToCopy.copy(
|
||||
h24 = tokenChartConverter.convert(interval, prices),
|
||||
TokenMarketListConfig.Interval.H24 -> chartsToCopy.copy(
|
||||
h24 = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
|
||||
)
|
||||
PriceChangeInterval.WEEK -> chartsToCopy.copy(
|
||||
week = tokenChartConverter.convert(interval, prices),
|
||||
TokenMarketListConfig.Interval.WEEK -> chartsToCopy.copy(
|
||||
week = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
|
||||
)
|
||||
PriceChangeInterval.MONTH -> chartsToCopy.copy(
|
||||
month = tokenChartConverter.convert(interval, prices),
|
||||
TokenMarketListConfig.Interval.MONTH -> chartsToCopy.copy(
|
||||
month = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
|
||||
)
|
||||
else -> error("unsupported interval=$interval. This shouldn't have happened.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarketListConfig.Interval.toPriceChangeInterval(): PriceChangeInterval = when (this) {
|
||||
TokenMarketListConfig.Interval.H24 -> PriceChangeInterval.H24
|
||||
TokenMarketListConfig.Interval.WEEK -> PriceChangeInterval.WEEK
|
||||
TokenMarketListConfig.Interval.MONTH -> PriceChangeInterval.MONTH
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(deps.kotlin.serialization)
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.domain.appcurrency.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AppCurrency(
|
||||
val code: String,
|
||||
val name: String,
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ android {
|
|||
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.markets.models)
|
||||
api(projects.domain.appCurrency.models)
|
||||
api(projects.domain.core)
|
||||
api(projects.core.pagination)
|
||||
api(projects.domain.markets.models)
|
||||
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sealed class TokenMarketUpdateRequest {
|
|||
) : TokenMarketUpdateRequest()
|
||||
|
||||
data class UpdateChart(
|
||||
val interval: PriceChangeInterval,
|
||||
val interval: TokenMarketListConfig.Interval,
|
||||
val currency: String,
|
||||
) : TokenMarketUpdateRequest()
|
||||
}
|
||||
|
|
@ -1,15 +1,24 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
|
||||
class GetTokenPriceChartUseCase(
|
||||
private val marketsTokenRepository: MarketsTokenRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(interval: PriceChangeInterval, tokenId: String): Either<Unit, TokenChart> {
|
||||
suspend operator fun invoke(
|
||||
appCurrency: AppCurrency,
|
||||
interval: PriceChangeInterval,
|
||||
tokenId: String,
|
||||
): Either<Unit, TokenChart> {
|
||||
return Either.catch {
|
||||
marketsTokenRepository.getChart(interval = interval, tokenId = tokenId)
|
||||
marketsTokenRepository.getChart(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
interval = interval,
|
||||
tokenId = tokenId,
|
||||
)
|
||||
}.mapLeft {}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,5 +10,5 @@ interface MarketsTokenRepository {
|
|||
nextBatchSize: Int,
|
||||
): TokenListBatchFlow
|
||||
|
||||
suspend fun getChart(interval: PriceChangeInterval, tokenId: String): TokenChart
|
||||
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
|
||||
}
|
||||
|
|
@ -1,36 +1,38 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.animation.Animatable
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.push
|
||||
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.*
|
||||
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.*
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.component.MarketsEntryComponent
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.api.toSerializable
|
||||
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
|
||||
import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Stable
|
||||
internal class DefaultMarketsEntryComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
private val marketsEntryChildFactory: MarketsEntryChildFactory,
|
||||
) : MarketsEntryComponent, AppComponentContext by context {
|
||||
|
||||
private val model: MarketsListModel = getOrCreateModel()
|
||||
|
||||
private val stackNavigation = StackNavigation<MarketsEntryChildFactory.Child>()
|
||||
|
||||
val stack: Value<ChildStack<MarketsEntryChildFactory.Child, Any>> = childStack(
|
||||
|
|
@ -44,20 +46,36 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
|
|||
child = configuration,
|
||||
appComponentContext = childByContext(componentContext),
|
||||
onTokenSelected = ::marketsListTokenSelected,
|
||||
onDetailsBack = ::onDetailsBack,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Children(stack) {
|
||||
val primary = TangemTheme.colors.background.primary
|
||||
val secondary = TangemTheme.colors.background.secondary
|
||||
val backgroundColor = remember { Animatable(primary) }
|
||||
val stackState = stack.subscribeAsState()
|
||||
|
||||
LocalMainBottomSheetColor.current.value = backgroundColor.value
|
||||
|
||||
Children(
|
||||
stack = stackState.value,
|
||||
animation = stackAnimation(slide()),
|
||||
) {
|
||||
when (it.configuration) {
|
||||
is MarketsEntryChildFactory.Child.TokenDetails -> {
|
||||
(it.instance as MarketsTokenDetailsComponent).Content(modifier)
|
||||
(it.instance as MarketsTokenDetailsComponent).BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
(it.instance as MarketsTokenListComponent).BottomSheetContent(
|
||||
|
|
@ -68,16 +86,75 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val activeChild = stackState.value.active.configuration
|
||||
|
||||
LaunchedEffect(bottomSheetState.value) {
|
||||
if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) {
|
||||
when (bottomSheetState.value) {
|
||||
BottomSheetState.EXPANDED -> {
|
||||
backgroundColor.animateTo(
|
||||
secondary,
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
)
|
||||
}
|
||||
BottomSheetState.COLLAPSED -> {
|
||||
backgroundColor.animateTo(
|
||||
primary,
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(activeChild) {
|
||||
when (activeChild) {
|
||||
is MarketsEntryChildFactory.Child.TokenDetails -> {
|
||||
backgroundColor.animateTo(
|
||||
secondary,
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
backgroundColor.animateTo(
|
||||
primary,
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(primary, secondary) {
|
||||
if (backgroundColor.isRunning) return@LaunchedEffect
|
||||
|
||||
when (activeChild) {
|
||||
is MarketsEntryChildFactory.Child.TokenDetails -> {
|
||||
backgroundColor.snapTo(secondary)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
backgroundColor.snapTo(primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun marketsListTokenSelected(token: TokenMarket) {
|
||||
stackNavigation.push(
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) {
|
||||
stackNavigation.pushNew(
|
||||
configuration = MarketsEntryChildFactory.Child.TokenDetails(
|
||||
params = MarketsTokenDetailsComponent.Params(token.toSerializable()),
|
||||
params = MarketsTokenDetailsComponent.Params(
|
||||
token = token.toSerializable(),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDetailsBack() {
|
||||
stackNavigation.popWhile { it != MarketsEntryChildFactory.Child.TokenList }
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsEntryComponent.Factory {
|
||||
override fun create(context: AppComponentContext): DefaultMarketsEntryComponent
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
|
||||
|
|
@ -13,25 +15,30 @@ internal class MarketsEntryChildFactory @Inject constructor(
|
|||
) {
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
sealed interface Child {
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
data object TokenList : Child
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child
|
||||
}
|
||||
|
||||
fun createChild(
|
||||
child: Child,
|
||||
appComponentContext: AppComponentContext,
|
||||
onTokenSelected: (TokenMarket) -> Unit,
|
||||
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
|
||||
onDetailsBack: () -> Unit,
|
||||
): Any {
|
||||
return when (child) {
|
||||
is Child.TokenDetails -> {
|
||||
tokenDetailsComponentFactory.create(
|
||||
context = appComponentContext,
|
||||
params = child.params,
|
||||
onBack = onDetailsBack,
|
||||
)
|
||||
}
|
||||
is Child.TokenList -> {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,32 @@
|
|||
package com.tangem.features.markets.details.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenDetailsComponent : ComposableContentComponent {
|
||||
interface MarketsTokenDetailsComponent {
|
||||
|
||||
@Serializable
|
||||
data class Params(val token: TokenMarketSerializable)
|
||||
data class Params(
|
||||
val token: TokenMarketSerializable,
|
||||
val appCurrency: AppCurrency,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, MarketsTokenDetailsComponent>
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params, onBack: () -> Unit): MarketsTokenDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,14 @@ package com.tangem.features.markets.details.impl
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
|
||||
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
|
||||
|
|
@ -18,17 +21,23 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: MarketsTokenDetailsComponent.Params,
|
||||
@Assisted private val onBack: () -> Unit,
|
||||
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
|
||||
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
MarketsTokenDetailsContent(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onBackClick = { onBack() },
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -38,6 +47,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
|||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: MarketsTokenDetailsComponent.Params,
|
||||
onBack: () -> Unit,
|
||||
): DefaultMarketsTokenDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +1,254 @@
|
|||
package com.tangem.features.markets.details.impl.model
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.charts.state.*
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
internal class MarketsTokenDetailsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = params.appCurrency,
|
||||
)
|
||||
|
||||
private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = getChartTypeByPercent(params.token.tokenQuotes.h24Percent),
|
||||
xAxisFormatter = { value ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter)
|
||||
},
|
||||
yAxisFormatter = { value ->
|
||||
BigDecimalFormatter.formatFiatAmountUncapped(
|
||||
fiatAmount = value,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = "",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val state = MutableStateFlow(
|
||||
MarketsTokenDetailsUM(
|
||||
tokenName = params.token.name,
|
||||
dateTimeText = "datetime",
|
||||
priceChangePercentText = params.token.tokenQuotes.h24Percent.toString(),
|
||||
priceText = BigDecimalFormatter.formatFiatAmountUncapped(
|
||||
fiatAmount = params.token.tokenQuotes.currentPrice,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
dateTimeText = resourceReference(R.string.common_today),
|
||||
priceChangePercentText = BigDecimalFormatter.formatPercent(
|
||||
percent = params.token.tokenQuotes.h24Percent,
|
||||
useAbsoluteValue = true,
|
||||
),
|
||||
priceChangeType = if (params.token.tokenQuotes.h24Percent < BigDecimal.ZERO) {
|
||||
PriceChangeType.DOWN
|
||||
} else {
|
||||
PriceChangeType.UP
|
||||
},
|
||||
iconUrl = params.token.imageUrl,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = chartDataProducer,
|
||||
chartLook = MarketChartLook(),
|
||||
onLoadRetryClick = {},
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = ::onMarkerPointSelected,
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = ::onSelectedIntervalChange,
|
||||
),
|
||||
)
|
||||
|
||||
private val loadChartJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
loadChart(PriceChangeInterval.H24)
|
||||
}
|
||||
|
||||
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
|
||||
if (state.value.selectedInterval == interval) return
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
selectedInterval = interval,
|
||||
priceChangeType = PriceChangeType.UP,
|
||||
)
|
||||
}
|
||||
|
||||
loadChart(interval)
|
||||
}
|
||||
|
||||
private fun loadChart(interval: PriceChangeInterval) {
|
||||
modelScope.launch {
|
||||
val chart = getTokenPriceChartUseCase.invoke(PriceChangeInterval.H24, params.token.id)
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
}
|
||||
|
||||
val chart = getTokenPriceChartUseCase.invoke(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
interval = interval,
|
||||
tokenId = params.token.id,
|
||||
)
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
selectedInterval = interval,
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val xAxisFormatter = getFormatterByInterval(state.value.selectedInterval)
|
||||
|
||||
chart.onRight {
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data()
|
||||
chartData = MarketChartData.Data(
|
||||
x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
y = it.priceY.toImmutableList(),
|
||||
)
|
||||
|
||||
updateLook {
|
||||
it.copy(
|
||||
xAxisFormatter = xAxisFormatter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.DATA,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.onLeft {
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.ERROR,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.saveIn(loadChartJobHolder)
|
||||
}
|
||||
|
||||
private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> { value: BigDecimal ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter)
|
||||
}
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
PriceChangeInterval.MONTH6,
|
||||
-> { value ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
|
||||
}
|
||||
PriceChangeInterval.YEAR -> { value ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
|
||||
}
|
||||
PriceChangeInterval.ALL_TIME -> { value ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.dateYYYY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun onMarkerPointSelected(time: BigDecimal?, price: BigDecimal?) {
|
||||
val timeText = time?.toLong()?.toTimeFormat(DateTimeFormatters.dateTimeFormatter)?.let {
|
||||
resourceReference(R.string.common_range, wrappedList(it, resourceReference(R.string.common_now)))
|
||||
} ?: resourceReference(R.string.common_today)
|
||||
|
||||
val percent = price?.subtract(params.token.tokenQuotes.currentPrice)
|
||||
?.divide(params.token.tokenQuotes.currentPrice, 4, RoundingMode.HALF_UP)
|
||||
?.multiply(BigDecimal(-100))
|
||||
?: params.token.tokenQuotes.h24Percent
|
||||
|
||||
val percentText = BigDecimalFormatter.formatPercent(
|
||||
percent = percent,
|
||||
useAbsoluteValue = true,
|
||||
)
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
dateTimeText = timeText,
|
||||
priceText = BigDecimalFormatter.formatFiatAmountUncapped(
|
||||
fiatAmount = price ?: params.token.tokenQuotes.currentPrice,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
priceChangePercentText = percentText,
|
||||
priceChangeType = when {
|
||||
percent < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
percent > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = getChartTypeByPercent(percent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChartTypeByPercent(percent: BigDecimal): MarketChartLook.Type {
|
||||
return if (percent >= BigDecimal.ZERO) {
|
||||
MarketChartLook.Type.Growing
|
||||
} else {
|
||||
MarketChartLook.Type.Falling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,223 @@
|
|||
package com.tangem.features.markets.details.impl.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.draw.drawBehind
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart
|
||||
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
internal fun MarketsTokenDetailsContent(
|
||||
state: MarketsTokenDetailsUM,
|
||||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// TODO
|
||||
Content(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
private fun Content(state: MarketsTokenDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column {
|
||||
private fun Content(
|
||||
state: MarketsTokenDetailsUM,
|
||||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backgroundColor = LocalMainBottomSheetColor.current.value
|
||||
val density = LocalDensity.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
if (it.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(it.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
title = state.tokenName,
|
||||
startButton = TopAppBarButtonUM.Back(onBackClick),
|
||||
)
|
||||
}
|
||||
SpacerH4()
|
||||
|
||||
// TODO
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
SpacerH32()
|
||||
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = state.priceText,
|
||||
style = TangemTheme.typography.head,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
Text(
|
||||
text = state.dateTimeText.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = state.priceChangePercentText,
|
||||
type = state.priceChangeType,
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerW4()
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size48),
|
||||
url = state.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IntervalSelector(
|
||||
trendInterval: PriceChangeInterval,
|
||||
onIntervalClick: (PriceChangeInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
PriceChangeInterval.MONTH6,
|
||||
PriceChangeInterval.YEAR,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
),
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
initialSelectedItem = trendInterval,
|
||||
onClick = onIntervalClick,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PriceChangeInterval.getText(): TextReference {
|
||||
return when (this) {
|
||||
PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title)
|
||||
PriceChangeInterval.WEEK -> resourceReference(R.string.markets_selector_interval_7d_title)
|
||||
PriceChangeInterval.MONTH -> resourceReference(R.string.markets_selector_interval_1m_title)
|
||||
PriceChangeInterval.MONTH3 -> resourceReference(R.string.markets_selector_interval_3m_title)
|
||||
PriceChangeInterval.MONTH6 -> resourceReference(R.string.markets_selector_interval_6m_title)
|
||||
PriceChangeInterval.YEAR -> resourceReference(R.string.markets_selector_interval_1y_title)
|
||||
PriceChangeInterval.ALL_TIME -> resourceReference(R.string.markets_selector_interval_all_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Content(
|
||||
state = MarketsTokenDetailsUM(
|
||||
tokenName = "Token Name",
|
||||
priceText = "Price",
|
||||
dateTimeText = stringReference("Date Time"),
|
||||
priceChangePercentText = "Price Change",
|
||||
iconUrl = "",
|
||||
priceChangeType = PriceChangeType.UP,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = MarketChartDataProducer.build { },
|
||||
chartLook = MarketChartLook(),
|
||||
onLoadRetryClick = {},
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = { _, _ -> },
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = { },
|
||||
),
|
||||
onHeaderSizeChange = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +1,76 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import com.tangem.common.ui.charts.MarketChart
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.rememberMarketChartState
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
|
||||
|
||||
@Composable
|
||||
fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) {
|
||||
val chartState = rememberMarketChartState(state.dataProducer)
|
||||
val growingColor = TangemTheme.colors.icon.accent
|
||||
val fallingColor = TangemTheme.colors.icon.warning
|
||||
|
||||
MarketChart(
|
||||
modifier = modifier,
|
||||
state = chartState,
|
||||
noChartContent = {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12)
|
||||
.align(Alignment.Center),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
val chartState = rememberMarketChartState(
|
||||
dataProducer = state.dataProducer,
|
||||
colorMapper = {
|
||||
when (it) {
|
||||
MarketChartLook.Type.Growing -> growingColor
|
||||
MarketChartLook.Type.Falling -> fallingColor
|
||||
}
|
||||
},
|
||||
onMarkerShown = state.onMarkerPointSelected,
|
||||
)
|
||||
|
||||
val backgroundColor = LocalMainBottomSheetColor.current.value
|
||||
|
||||
Box(modifier) {
|
||||
MarketChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = chartState,
|
||||
)
|
||||
|
||||
if (state.status != MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
Box(
|
||||
Modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.matchParentSize(),
|
||||
) {
|
||||
when (state.status) {
|
||||
MarketsTokenDetailsUM.ChartState.Status.LOADING -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.align(Alignment.Center),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
MarketsTokenDetailsUM.ChartState.Status.ERROR -> {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.align(Alignment.Center),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,18 +3,31 @@ package com.tangem.features.markets.details.impl.ui.entity
|
|||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class MarketsTokenDetailsUM(
|
||||
val tokenName: String,
|
||||
val dateTimeText: String,
|
||||
val priceText: String,
|
||||
val iconUrl: String,
|
||||
val dateTimeText: TextReference,
|
||||
val priceChangePercentText: String,
|
||||
val priceChangeType: PriceChangeType,
|
||||
val selectedInterval: PriceChangeInterval,
|
||||
val chartState: ChartState,
|
||||
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
|
||||
) {
|
||||
|
||||
data class ChartState(
|
||||
val status: Status,
|
||||
val dataProducer: MarketChartDataProducer,
|
||||
val chartLook: MarketChartLook,
|
||||
val onLoadRetryClick: () -> Unit,
|
||||
)
|
||||
val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit,
|
||||
) {
|
||||
enum class Status {
|
||||
LOADING, ERROR, DATA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
|
||||
|
|
@ -20,6 +21,9 @@ interface MarketsTokenListComponent {
|
|||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, onTokenSelected: (TokenMarket) -> Unit): MarketsTokenListComponent
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
|
||||
): MarketsTokenListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
|
||||
|
|
@ -22,14 +23,14 @@ import kotlinx.coroutines.flow.onEach
|
|||
|
||||
class DefaultMarketsTokenListComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val onTokenSelected: (TokenMarket) -> Unit,
|
||||
@Assisted private val onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
|
||||
) : AppComponentContext by appComponentContext, MarketsTokenListComponent {
|
||||
|
||||
private val model: MarketsListModel = getOrCreateModel()
|
||||
|
||||
init {
|
||||
model.tokenSelected
|
||||
.onEach { onTokenSelected(it) }
|
||||
.onEach { onTokenSelected(it.first, it.second) }
|
||||
.launchIn(componentScope)
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor(
|
|||
interface Factory : MarketsTokenListComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
onTokenSelected: (TokenMarket) -> Unit,
|
||||
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
|
||||
): DefaultMarketsTokenListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
|
||||
private var activeListManager: MarketsListBatchFlowManager = mainMarketsListManager
|
||||
|
||||
private val _tokenSelected = MutableSharedFlow<TokenMarket>()
|
||||
private val _tokenSelected = MutableSharedFlow<Pair<TokenMarket, AppCurrency>>()
|
||||
|
||||
val tokenSelected = _tokenSelected.asSharedFlow()
|
||||
|
||||
|
|
@ -223,7 +223,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
private fun onTokenUIClicked(token: MarketsListItemUM) {
|
||||
modelScope.launch {
|
||||
activeListManager.getTokenById(token.id)?.let { found ->
|
||||
_tokenSelected.emit(found)
|
||||
_tokenSelected.emit(found to currentAppCurrency.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.markets.tokenlist.impl.model.converters
|
||||
|
||||
import com.tangem.common.ui.charts.state.DefaultPointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
|
|
@ -10,6 +10,7 @@ import com.tangem.domain.markets.TokenMarket
|
|||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
|
|
@ -18,6 +19,8 @@ internal class MarketsTokenItemConverter(
|
|||
private val appCurrency: AppCurrency,
|
||||
) : Converter<TokenMarket, MarketsListItemUM> {
|
||||
|
||||
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false)
|
||||
|
||||
override fun convert(value: TokenMarket): MarketsListItemUM {
|
||||
return MarketsListItemUM(
|
||||
id = value.id,
|
||||
|
|
@ -107,10 +110,10 @@ internal class MarketsTokenItemConverter(
|
|||
}
|
||||
|
||||
return chart?.let { ct ->
|
||||
DefaultPointValuesConverter.convert(
|
||||
priceAndTimePointValuesConverter.convert(
|
||||
MarketChartData.Data(
|
||||
y = ct.priceY,
|
||||
x = ct.timeStamps.map { it.toBigDecimal() },
|
||||
y = ct.priceY.toImmutableList(),
|
||||
x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ internal class MarketsListBatchFlowManager(
|
|||
BatchAction.UpdateBatches(
|
||||
keys = batchesKeysToLoad,
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateChart(
|
||||
interval = interval.toRequestInterval(),
|
||||
interval = interval.toBatchRequestInterval(),
|
||||
currency = currentAppCurrency().code,
|
||||
),
|
||||
async = true,
|
||||
|
|
@ -303,14 +303,6 @@ internal class MarketsListBatchFlowManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun TrendInterval.toRequestInterval(): PriceChangeInterval {
|
||||
return when (this) {
|
||||
TrendInterval.H24 -> PriceChangeInterval.H24
|
||||
TrendInterval.D7 -> PriceChangeInterval.WEEK
|
||||
TrendInterval.M1 -> PriceChangeInterval.MONTH
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Flow<T>.onEachWithPrevious(operation: suspend (prev: T?, value: T) -> Unit): Flow<T> = flow {
|
||||
var prev: T? = null
|
||||
collect { value ->
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
package com.tangem.features.markets.tokenlist.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
|
|
@ -32,6 +31,7 @@ import com.tangem.core.ui.components.keyboardAsState
|
|||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
|
|
@ -68,23 +68,28 @@ internal fun MarketsList(
|
|||
@Composable
|
||||
private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
.drawBehind { drawRect(background) },
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
with(density) { onHeaderSizeChange(it.size.height.toDp()) }
|
||||
if (it.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(it.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
state = state.searchBar,
|
||||
)
|
||||
|
|
@ -225,45 +230,52 @@ private fun KeyboardEvents(isSortByBottomSheetShown: Boolean, bottomSheetState:
|
|||
//region: Preview
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
MarketsList(
|
||||
state = MarketsListUM(
|
||||
list = ListUM.Content(
|
||||
items = MarketChartListItemPreviewDataProvider().values
|
||||
.flatMap { item -> List(size = 10) { item } }
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(id = index.toString())
|
||||
}
|
||||
.toImmutableList(),
|
||||
showUnder100kTokens = false,
|
||||
loadMore = {},
|
||||
visibleIdsChanged = {},
|
||||
onShowTokensUnder100kClicked = {},
|
||||
triggerScrollReset = consumedEvent(),
|
||||
onItemClick = {},
|
||||
TangemThemePreview(alwaysShowBottomSheets = false) {
|
||||
val primaryBackground = TangemTheme.colors.background.primary
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(primaryBackground) },
|
||||
) {
|
||||
MarketsList(
|
||||
state = MarketsListUM(
|
||||
list = ListUM.Content(
|
||||
items = MarketChartListItemPreviewDataProvider().values
|
||||
.flatMap { item -> List(size = 10) { item } }
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(id = index.toString())
|
||||
}
|
||||
.toImmutableList(),
|
||||
showUnder100kTokens = false,
|
||||
loadMore = {},
|
||||
visibleIdsChanged = {},
|
||||
onShowTokensUnder100kClicked = {},
|
||||
triggerScrollReset = consumedEvent(),
|
||||
onItemClick = {},
|
||||
),
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
selectedSortBy = SortByTypeUM.Rating,
|
||||
selectedInterval = MarketsListUM.TrendInterval.H24,
|
||||
onIntervalClick = {},
|
||||
onSortByButtonClick = {},
|
||||
sortByBottomSheet = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
||||
),
|
||||
),
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
selectedSortBy = SortByTypeUM.Rating,
|
||||
selectedInterval = MarketsListUM.TrendInterval.H24,
|
||||
onIntervalClick = {},
|
||||
onSortByButtonClick = {},
|
||||
sortByBottomSheet = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
||||
),
|
||||
),
|
||||
onHeaderSizeChange = {},
|
||||
bottomSheetState = BottomSheetState.EXPANDED,
|
||||
)
|
||||
onHeaderSizeChange = {},
|
||||
bottomSheetState = BottomSheetState.EXPANDED,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ internal fun MarketsListLazyColumn(
|
|||
is ListUM.Content -> {
|
||||
items(
|
||||
items = state.items,
|
||||
key = { it.id },
|
||||
key = { it.id + it.marketCap.toString() },
|
||||
) { item ->
|
||||
MarketsListItem(
|
||||
model = item,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
|
|||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
collection = listOf(
|
||||
|
|
@ -19,7 +20,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
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),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
MarketsListItemUM(
|
||||
|
|
@ -45,7 +46,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
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),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
MarketsListItemUM(
|
||||
|
|
@ -59,7 +60,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
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),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
MarketsListItemUM(
|
||||
|
|
@ -73,7 +74,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
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),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
MarketsListItemUM(
|
||||
|
|
@ -87,7 +88,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
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),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation
|
|||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.defaultComponentContext
|
||||
import com.tangem.core.decompose.context.DefaultAppComponentContext
|
||||
|
|
@ -68,7 +69,11 @@ internal class WalletFragment : ComposeFragment() {
|
|||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
_walletRouter.Initialize(
|
||||
onFinish = requireActivity()::finish,
|
||||
onFinish = remember(requireActivity()) {
|
||||
{
|
||||
requireActivity().finish()
|
||||
}
|
||||
},
|
||||
marketsEntryComponent = marketsEntryComponent,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class WalletScreenState(
|
||||
val onBackClick: () -> Unit,
|
||||
val topBarConfig: WalletTopBarConfig,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
|
|||
import com.tangem.core.ui.components.snackbar.TangemSnackbar
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
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.test.TestTags
|
||||
|
|
@ -70,6 +72,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balances
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.component.BottomSheetState.*
|
||||
import com.tangem.features.markets.component.MarketsEntryComponent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -316,15 +319,15 @@ private fun BaseScaffold(
|
|||
@Suppress("LongParameterList", "LongMethod")
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BaseScaffoldWithMarkets(
|
||||
private inline fun BaseScaffoldWithMarkets(
|
||||
state: WalletScreenState,
|
||||
selectedWallet: WalletState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
bottomSheetContent: @Composable () -> Unit,
|
||||
crossinline bottomSheetContent: @Composable () -> Unit,
|
||||
alertConfig: WalletAlertState?,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
crossinline content: @Composable () -> Unit,
|
||||
) {
|
||||
// show the bottom sheet if there is at least one multicurrency wallet
|
||||
val showManageTokensBottomSheet = remember(state.wallets) {
|
||||
|
|
@ -332,11 +335,13 @@ private fun BaseScaffoldWithMarkets(
|
|||
}
|
||||
val bottomSheetState = rememberSheetStateEnhanced(
|
||||
initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden,
|
||||
confirmValueChange = { sheetValue ->
|
||||
when {
|
||||
sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false
|
||||
sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false
|
||||
else -> true
|
||||
confirmValueChange = remember(showManageTokensBottomSheet) {
|
||||
{ sheetValue ->
|
||||
when {
|
||||
sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false
|
||||
sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
},
|
||||
skipHiddenState = showManageTokensBottomSheet,
|
||||
|
|
@ -344,14 +349,6 @@ private fun BaseScaffoldWithMarkets(
|
|||
|
||||
val keyboardShown = keyboardAsState()
|
||||
|
||||
BottomSheetStateEffects(
|
||||
bottomSheetState = bottomSheetState,
|
||||
showManageTokensBottomSheet = showManageTokensBottomSheet,
|
||||
alertConfig = alertConfig,
|
||||
keyboardShown = keyboardShown,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
)
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = bottomSheetState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
|
|
@ -360,77 +357,89 @@ private fun BaseScaffoldWithMarkets(
|
|||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight
|
||||
val maxHeight = LocalWindowSize.current.height
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val backgroundPrimary = TangemTheme.colors.background.primary
|
||||
|
||||
BottomSheetScaffold(
|
||||
snackbarHost = {
|
||||
WalletSnackbarHost(
|
||||
snackbarHostState = it,
|
||||
event = state.event,
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing4)
|
||||
.navigationBarsPadding(),
|
||||
)
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
sheetContainerColor = TangemTheme.colors.background.primary,
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = peekHeight,
|
||||
sheetDragHandle = {
|
||||
Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary))
|
||||
},
|
||||
sheetTonalElevation = 8.dp,
|
||||
sheetShadowElevation = 8.dp,
|
||||
sheetContent = {
|
||||
BoxWithConstraints {
|
||||
Box(
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) },
|
||||
) {
|
||||
val backgroundColor = LocalMainBottomSheetColor.current
|
||||
|
||||
BottomSheetStateEffects(
|
||||
bottomSheetState = bottomSheetState,
|
||||
showManageTokensBottomSheet = showManageTokensBottomSheet,
|
||||
alertConfig = alertConfig,
|
||||
keyboardShown = keyboardShown,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
)
|
||||
|
||||
BottomSheetScaffold(
|
||||
snackbarHost = {
|
||||
WalletSnackbarHost(
|
||||
snackbarHostState = it,
|
||||
event = state.event,
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = maxHeight - statusBarHeight)
|
||||
.align(Alignment.BottomCenter),
|
||||
.padding(bottom = TangemTheme.dimens.spacing4)
|
||||
.navigationBarsPadding(),
|
||||
)
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
sheetContainerColor = backgroundColor.value,
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = peekHeight,
|
||||
sheetDragHandle = {
|
||||
Hand(modifier = Modifier.background(color = backgroundColor.value))
|
||||
},
|
||||
sheetTonalElevation = 8.dp,
|
||||
sheetShadowElevation = 8.dp,
|
||||
sheetContent = {
|
||||
Box(
|
||||
modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight - handComposableComponentHeight),
|
||||
) {
|
||||
bottomSheetContent()
|
||||
}
|
||||
}
|
||||
|
||||
// hide bottom sheet when back pressed
|
||||
BackHandler(
|
||||
keyboardShown.value is Keyboard.Closed &&
|
||||
bottomSheetState.currentValue == SheetValue.Expanded,
|
||||
) {
|
||||
coroutineScope.launch { bottomSheetState.partialExpand() }
|
||||
}
|
||||
},
|
||||
content = { _ ->
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
onRefresh = {
|
||||
selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true))
|
||||
},
|
||||
)
|
||||
|
||||
Column {
|
||||
WalletTopBar(config = state.topBarConfig)
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(pullRefreshState),
|
||||
// hide bottom sheet when back pressed
|
||||
BackHandler(
|
||||
keyboardShown.value is Keyboard.Closed &&
|
||||
bottomSheetState.currentValue == SheetValue.Expanded,
|
||||
) {
|
||||
content()
|
||||
|
||||
WalletPullToRefreshIndicator(
|
||||
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
)
|
||||
coroutineScope.launch { bottomSheetState.partialExpand() }
|
||||
}
|
||||
}
|
||||
},
|
||||
content = { _ ->
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
onRefresh = {
|
||||
selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true))
|
||||
},
|
||||
)
|
||||
|
||||
BottomSheetScrim(
|
||||
color = BottomSheetDefaults.ScrimColor,
|
||||
visible = bottomSheetState.targetValue == SheetValue.Expanded,
|
||||
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
|
||||
)
|
||||
},
|
||||
)
|
||||
Column {
|
||||
WalletTopBar(config = state.topBarConfig)
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(pullRefreshState),
|
||||
) {
|
||||
content()
|
||||
|
||||
WalletPullToRefreshIndicator(
|
||||
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetScrim(
|
||||
color = BottomSheetDefaults.ScrimColor,
|
||||
visible = bottomSheetState.targetValue == SheetValue.Expanded,
|
||||
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -545,9 +554,9 @@ private fun BottomSheetStateEffects(
|
|||
LaunchedEffect(isSheetHidden) {
|
||||
onBottomSheetStateChange(
|
||||
if (isSheetHidden) {
|
||||
BottomSheetState.COLLAPSED
|
||||
COLLAPSED
|
||||
} else {
|
||||
BottomSheetState.EXPANDED
|
||||
EXPANDED
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -44,6 +45,7 @@ import timber.log.Timber
|
|||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Stable
|
||||
@HiltViewModel
|
||||
internal class WalletViewModel @Inject constructor(
|
||||
private val stateHolder: WalletStateController,
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ tangemBlockchainSdk = "develop-713"
|
|||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "develop-375"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
tangemVico = "2.0.0-alpha.21-tangem14"
|
||||
tangemVico = "2.0.0-alpha.25-tangem16"
|
||||
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
|
||||
# endregion Tangem
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue