Updated on 2026-08-14
This commit is contained in:
commit
304bf0df87
928 changed files with 23076 additions and 29831 deletions
|
|
@ -1,60 +0,0 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating
|
||||
* elements and floating button at the bottom of the screen.
|
||||
*/
|
||||
@Composable
|
||||
fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size100 + bottomBarHeight)
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect. Used on screens with a list of repeating
|
||||
* elements and floating button at the bottom of the screen.
|
||||
*/
|
||||
@Composable
|
||||
fun Fade(
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = TangemTheme.colors.background.secondary,
|
||||
height: Dp = 32.dp,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
144
core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt
Normal file
144
core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
import dev.chrisbanes.haze.HazeTint
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating
|
||||
* elements and floating button at the bottom of the screen.
|
||||
*/
|
||||
@Composable
|
||||
fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size100 + bottomBarHeight)
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect at the right end of the screen. Same as [BottomFade]
|
||||
* but with a horizontal gradient.
|
||||
*/
|
||||
@Composable
|
||||
fun HorizontalFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect at the bottom of the screen. Same as [BottomFade]
|
||||
* but with a vertical blur.
|
||||
*/
|
||||
@Composable
|
||||
fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size100 + bottomBarHeight)
|
||||
.hazeEffectTangem(
|
||||
style = HazeStyle(
|
||||
blurRadius = 20.dp,
|
||||
tint = HazeTint(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
progressive =
|
||||
HazeProgressive.verticalGradient(startIntensity = 0f, endIntensity = 1f)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect at the right end of the screen. Same as [HorizontalFade]
|
||||
* but with blur.
|
||||
*/
|
||||
@Composable
|
||||
fun HorizontalFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.hazeEffectTangem(
|
||||
style = HazeStyle(
|
||||
blurRadius = 20.dp,
|
||||
tint = HazeTint(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor = Color.Transparent,
|
||||
),
|
||||
) {
|
||||
progressive =
|
||||
HazeProgressive.horizontalGradient(startIntensity = 0f, endIntensity = 1f)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect. Used on screens with a list of repeating
|
||||
* elements and floating button at the bottom of the screen.
|
||||
*/
|
||||
@Composable
|
||||
fun Fade(
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = TangemTheme.colors.background.secondary,
|
||||
height: Dp = 32.dp,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -8,16 +8,29 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
@Composable
|
||||
fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
UnableToLoadDataV2(onRetryClick, modifier)
|
||||
} else {
|
||||
UnableToLoadDataV1(onRetryClick, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnableToLoadDataV1(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
|
|
@ -37,11 +50,43 @@ fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnableToLoadDataV2(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_loading_error_title),
|
||||
style = TangemTheme.typography2.bodyRegular14,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
)
|
||||
TangemButton(
|
||||
buttonUM = TangemButtonUM(
|
||||
text = resourceReference(R.string.try_to_load_data_again_button_title),
|
||||
onClick = onRetryClick,
|
||||
type = TangemButtonType.Secondary,
|
||||
size = TangemButtonSize.X8,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
UnableToLoadDataV2(onRetryClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
private fun PreviewV1() {
|
||||
TangemThemePreview {
|
||||
UnableToLoadData(onRetryClick = {})
|
||||
UnableToLoadDataV1(onRetryClick = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
|
|
@ -30,6 +31,7 @@ import com.tangem.core.ui.components.SpacerWMax
|
|||
import com.tangem.core.ui.components.TangemTextFieldsDefault
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.AppBarWithSearchTestTags
|
||||
|
||||
/**
|
||||
* App bar with close icon and search functionality
|
||||
|
|
@ -135,7 +137,8 @@ private fun CollapsedSearchView(
|
|||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clickable { onExpandedChange(true) }
|
||||
.padding(end = TangemTheme.dimens.spacing16),
|
||||
.padding(end = TangemTheme.dimens.spacing16)
|
||||
.testTag(AppBarWithSearchTestTags.SEARCH_ICON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -210,7 +213,8 @@ private fun ExpandedSearchView(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(textFieldFocusRequester)
|
||||
.onFocusChanged { onFocusChange(it.hasFocus) },
|
||||
.onFocusChanged { onFocusChange(it.hasFocus) }
|
||||
.testTag(AppBarWithSearchTestTags.TEXT_FIELD),
|
||||
placeholder = {
|
||||
Text(text = placeholderSearchText)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
@file:Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||
package com.tangem.core.ui.components.background
|
||||
|
||||
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import com.tangem.core.ui.shader.TangemShader
|
||||
import com.tangem.core.ui.shader.runtime.buildEffect
|
||||
import kotlin.math.round
|
||||
|
||||
@Composable
|
||||
fun Modifier.shaderBackground(
|
||||
shader: TangemShader,
|
||||
speed: Float = 1f,
|
||||
fallback: () -> Brush = {
|
||||
Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||
},
|
||||
): Modifier {
|
||||
val runtimeEffect = remember(shader) { buildEffect(shader) }
|
||||
var size: Size by remember { mutableStateOf(Size(-1f, -1f)) }
|
||||
val speedModifier = shader.speedModifier
|
||||
|
||||
val time by if (runtimeEffect.isSupported) {
|
||||
var startMillis = remember(shader) { -1L }
|
||||
produceState(0f, speedModifier) {
|
||||
while (true) {
|
||||
withInfiniteAnimationFrameMillis { frameTimeMillis ->
|
||||
if (startMillis < 0) startMillis = frameTimeMillis
|
||||
value = ((frameTimeMillis - startMillis) / 16.6f) / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remember { mutableFloatStateOf(-1f) }
|
||||
}
|
||||
|
||||
return this then Modifier.onGloballyPositioned {
|
||||
size = Size(it.size.width.toFloat(), it.size.height.toFloat())
|
||||
}.drawBehind {
|
||||
runtimeEffect.update(
|
||||
shader = shader,
|
||||
time = (time * speed * speedModifier).round(3),
|
||||
width = size.width,
|
||||
height = size.height,
|
||||
) // set uniforms for the shaders
|
||||
|
||||
if (runtimeEffect.isReady) {
|
||||
drawRect(brush = runtimeEffect.build())
|
||||
} else {
|
||||
drawRect(brush = fallback())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Float.round(decimals: Int): Float {
|
||||
var multiplier = 1.0f
|
||||
repeat(decimals) { multiplier *= 10 }
|
||||
return round(this * multiplier) / multiplier
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
package com.tangem.core.ui.components.background.northernlights
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import android.graphics.BlurMaskFilter
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
|
||||
val transition = rememberInfiniteTransition(label = "FluidMeshGradient")
|
||||
|
||||
// ── Circle 1 (left) ──────────────────────────────────────────────────────
|
||||
val color1 by transition.animateColor(
|
||||
initialValue = Color(0xFF3355EE),
|
||||
targetValue = Color(0xFF5577FF),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(4_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "color1",
|
||||
)
|
||||
val x1 by transition.animateFloat(
|
||||
initialValue = 0.05f,
|
||||
targetValue = 0.28f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(5_500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "x1",
|
||||
)
|
||||
val y1 by transition.animateFloat(
|
||||
initialValue = 0.0f,
|
||||
targetValue = 0.18f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(6_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "y1",
|
||||
)
|
||||
|
||||
// ── Circle 2 (right) ─────────────────────────────────────────────────────
|
||||
val color2 by transition.animateColor(
|
||||
initialValue = Color(0xFF7733CC),
|
||||
targetValue = Color(0xFF4455EE),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(5_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(1_500),
|
||||
),
|
||||
label = "color2",
|
||||
)
|
||||
val x2 by transition.animateFloat(
|
||||
initialValue = 0.68f,
|
||||
targetValue = 0.92f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(7_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "x2",
|
||||
)
|
||||
val y2 by transition.animateFloat(
|
||||
initialValue = 0.02f,
|
||||
targetValue = 0.20f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(5_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(2_000),
|
||||
),
|
||||
label = "y2",
|
||||
)
|
||||
|
||||
// ── Oval (center) ────────────────────────────────────────────────────────
|
||||
val ovalColor by transition.animateColor(
|
||||
initialValue = Color(0xFF5533CC),
|
||||
targetValue = Color(0xFF8844EE),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(7_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(2_500),
|
||||
),
|
||||
label = "ovalColor",
|
||||
)
|
||||
// ── Circle 3 (center) ────────────────────────────────────────────────────
|
||||
val color3 by transition.animateColor(
|
||||
initialValue = Color(0xFF9933BB),
|
||||
targetValue = Color(0xFFBB44DD),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(6_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(3_000),
|
||||
),
|
||||
label = "color3",
|
||||
)
|
||||
val x3 by transition.animateFloat(
|
||||
initialValue = 0.35f,
|
||||
targetValue = 0.58f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(6_500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(1_000),
|
||||
),
|
||||
label = "x3",
|
||||
)
|
||||
val y3 by transition.animateFloat(
|
||||
initialValue = 0.0f,
|
||||
targetValue = 0.15f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(4_500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(500),
|
||||
),
|
||||
label = "y3",
|
||||
)
|
||||
|
||||
var blurRadiusState by remember { mutableFloatStateOf(0f) }
|
||||
val circlePaint1 = remember { Paint() }
|
||||
val circlePaint2 = remember { Paint() }
|
||||
val circlePaint3 = remember { Paint() }
|
||||
val ovalPaint = remember { Paint() }
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
val blurRadius = (size.minDimension * 0.28f).coerceIn(60f, 300f)
|
||||
val circleRadius = size.width * 0.52f
|
||||
|
||||
// Update maskFilter only when blur radius changes meaningfully
|
||||
if (blurRadiusState != blurRadius) {
|
||||
blurRadiusState = blurRadius
|
||||
val mf = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
|
||||
circlePaint1.asFrameworkPaint().maskFilter = mf
|
||||
circlePaint2.asFrameworkPaint().maskFilter = mf
|
||||
circlePaint3.asFrameworkPaint().maskFilter = mf
|
||||
ovalPaint.asFrameworkPaint().maskFilter = mf
|
||||
}
|
||||
|
||||
circlePaint1.color = color1.copy(alpha = 0.85f)
|
||||
circlePaint2.color = color2.copy(alpha = 0.85f)
|
||||
circlePaint3.color = color3.copy(alpha = 0.85f)
|
||||
ovalPaint.color = ovalColor.copy(alpha = 0.80f)
|
||||
|
||||
drawIntoCanvas { canvas ->
|
||||
canvas.drawCircle(Offset(x1 * size.width, y1 * size.height), circleRadius, circlePaint1)
|
||||
canvas.drawCircle(Offset(x2 * size.width, y2 * size.height), circleRadius, circlePaint2)
|
||||
canvas.drawCircle(Offset(x3 * size.width, y3 * size.height), circleRadius, circlePaint3)
|
||||
|
||||
val halfW = size.width * 0.68f
|
||||
val halfH = size.width * 0.24f
|
||||
val ovalCx = size.width * 0.50f
|
||||
val ovalCy = 0f
|
||||
|
||||
canvas.drawOval(
|
||||
Rect(left = ovalCx - halfW, top = ovalCy - halfH, right = ovalCx + halfW, bottom = ovalCy + halfH),
|
||||
ovalPaint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.core.ui.components.background.northernlights
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.background.shaderBackground
|
||||
import com.tangem.core.ui.res.LocalPowerSavingState
|
||||
import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader
|
||||
|
||||
/**
|
||||
* Animated northern lights background.
|
||||
* Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode.
|
||||
*/
|
||||
@Composable
|
||||
fun NorthernLightsBackground(
|
||||
containerColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
forceSimpleVersion: Boolean = false,
|
||||
) {
|
||||
val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
|
||||
if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) {
|
||||
NorthernLightsBackgroundWithShader(containerColor, modifier)
|
||||
} else {
|
||||
MovingColorfulBlubsBackground(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NorthernLightsBackgroundWithShader(containerColor: Color, modifier: Modifier = Modifier) {
|
||||
val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2")
|
||||
|
||||
// Each track cycles through 4 states (matching the screenshot frames):
|
||||
// deep/dark → saturated+bright → light/pastel → vibrant/vivid → back
|
||||
// 16 s total per track, staggered so no two tracks peak simultaneously.
|
||||
|
||||
// ── Color 1 – indigo → bright blue → lavender → hot violet ──────────────
|
||||
val color1 by transition.animateColor(
|
||||
initialValue = Color(0xFF2A1480),
|
||||
targetValue = Color(0xFF2A1480),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF2A1480) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "color1",
|
||||
)
|
||||
|
||||
// ── Color 2 – dark blue → cyan-blue → sky → teal ─────────────────────────
|
||||
val color2 by transition.animateColor(
|
||||
initialValue = Color(0xFF1444AA),
|
||||
targetValue = Color(0xFF1444AA),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF1444AA) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF22AADD) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFF99BBDD) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFF44DDCC) at 12_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
initialStartOffset = StartOffset(4_000),
|
||||
),
|
||||
label = "color2",
|
||||
)
|
||||
|
||||
// ── Color 3 – dark purple → medium purple → rose pink → magenta ──────────
|
||||
val color3 by transition.animateColor(
|
||||
initialValue = Color(0xFF4422BB),
|
||||
targetValue = Color(0xFF4422BB),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF4422BB) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF7733CC) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFFDD88BB) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFFEE44AA) at 12_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
initialStartOffset = StartOffset(8_000),
|
||||
),
|
||||
label = "color3",
|
||||
)
|
||||
|
||||
// ── Color 4 – dark violet → medium violet → light pink → hot pink ────────
|
||||
val color4 by transition.animateColor(
|
||||
initialValue = Color(0xFF331199),
|
||||
targetValue = Color(0xFF331199),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF331199) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF6644CC) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFFCC77DD) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFFFF66CC) at 12_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
initialStartOffset = StartOffset(2_000),
|
||||
),
|
||||
label = "color4",
|
||||
)
|
||||
|
||||
// Keep a stable shader instance so the RuntimeShader is never recreated.
|
||||
// Colors are pushed each recomposition via updateColors().
|
||||
val shader = remember {
|
||||
NorthernLightsMeshGradientShader(
|
||||
colors = arrayOf(
|
||||
Color(0xFF2A1480),
|
||||
Color(0xFF1444AA),
|
||||
Color(0xFF4422BB),
|
||||
Color(0xFF331199),
|
||||
containerColor,
|
||||
),
|
||||
speed = 0.5f,
|
||||
scale = 4f,
|
||||
)
|
||||
}
|
||||
val colorsArray = remember { Array(5) { Color.Unspecified } }
|
||||
colorsArray[0] = color1
|
||||
colorsArray[1] = color2
|
||||
colorsArray[2] = color3
|
||||
colorsArray[3] = color4
|
||||
colorsArray[4] = containerColor
|
||||
shader.updateColors(colorsArray)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(containerColor)
|
||||
.fillMaxSize()
|
||||
.shaderBackground(shader),
|
||||
)
|
||||
}
|
||||
|
|
@ -5,13 +5,7 @@ import androidx.compose.animation.animateColorAsState
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
|
|
@ -72,7 +66,7 @@ fun Chip(state: ChipUM, modifier: Modifier = Modifier) {
|
|||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ChipPreview() {
|
||||
private fun ChipPreviewV() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
fun TangemPullToRefreshContainer(
|
||||
config: PullToRefreshConfig,
|
||||
modifier: Modifier = Modifier,
|
||||
indicatorModifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val state = rememberPullToRefreshState()
|
||||
|
|
@ -32,7 +33,7 @@ fun TangemPullToRefreshContainer(
|
|||
modifier = modifier,
|
||||
indicator = {
|
||||
Indicator(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
modifier = indicatorModifier.align(Alignment.TopCenter),
|
||||
isRefreshing = config.isRefreshing,
|
||||
state = state,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
|
|
|
|||
|
|
@ -32,15 +32,15 @@ internal fun ProvideHaze(content: @Composable () -> Unit) {
|
|||
*/
|
||||
@Composable
|
||||
fun Modifier.hazeEffectTangem(
|
||||
state: HazeState = LocalHazeState.current,
|
||||
style: HazeStyle = HazeStyle.Unspecified,
|
||||
configure: HazeEffectScope.() -> Unit = {},
|
||||
): Modifier {
|
||||
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
|
||||
val hazeState = LocalHazeState.current
|
||||
val isGlobalBlurEnabled = hazeState.blurEnabled && !powerSavingEnabled.value
|
||||
val isGlobalBlurEnabled = state.blurEnabled && !powerSavingEnabled.value
|
||||
val rootBackground by LocalRootBackgroundColor.current
|
||||
|
||||
return hazeEffect(hazeState, style) {
|
||||
return hazeEffect(state, style) {
|
||||
fallbackTint = HazeTint(rootBackground)
|
||||
if (isGlobalBlurEnabled) {
|
||||
configure()
|
||||
|
|
@ -78,5 +78,5 @@ fun Modifier.hazeForegroundEffectTangem(
|
|||
* Applies a haze source to the [Modifier] using the current global haze state.
|
||||
*/
|
||||
@Composable
|
||||
fun Modifier.hazeSourceTangem(zIndex: Float = 0f, key: Any? = null) =
|
||||
this.hazeSource(LocalHazeState.current, zIndex, key)
|
||||
fun Modifier.hazeSourceTangem(state: HazeState = LocalHazeState.current, zIndex: Float = 0f, key: Any? = null) =
|
||||
this.hazeSource(state, zIndex, key)
|
||||
|
|
@ -45,18 +45,32 @@ fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle {
|
|||
override fun createShader(size: Size): Shader {
|
||||
val center = Offset(size.width / 2f, size.height / 2f)
|
||||
val diagonal = sqrt(size.width * size.width + size.height * size.height)
|
||||
val direction = Offset(x = 1f, y = 0.5f)
|
||||
val halfDist = diagonal / 2f
|
||||
val baseStart = center - direction * halfDist
|
||||
val baseEnd = center + direction * halfDist
|
||||
val shift = direction * offset * diagonal
|
||||
// Subtle diagonal angle, similar to iOS shimmer
|
||||
val direction = Offset(x = 1f, y = 0.3f)
|
||||
|
||||
// Half-width of the blob (80% of diagonal total — wide, soft sweep)
|
||||
val bandHalf = diagonal * 0.40f
|
||||
|
||||
// Sweep the highlight center from left-of-element to right-of-element.
|
||||
// offset 0..1 maps to a full pass including off-screen padding on both sides.
|
||||
val shift = direction * ((offset - 0.5f) * diagonal * 1.5f)
|
||||
val highlightCenter = center + shift
|
||||
|
||||
// Full color text with a wide, gradual low-alpha dip sweeping left → right
|
||||
return LinearGradientShader(
|
||||
colors = listOf(textColor.copy(alpha = 0.2f), textColor),
|
||||
from = baseStart + shift,
|
||||
to = baseEnd + shift,
|
||||
colorStops = listOf(0.0f, 0.15f),
|
||||
tileMode = TileMode.Mirror,
|
||||
colors = listOf(
|
||||
textColor,
|
||||
textColor.copy(alpha = 0.75f),
|
||||
textColor.copy(alpha = 0.45f),
|
||||
textColor.copy(alpha = 0.3f),
|
||||
textColor.copy(alpha = 0.45f),
|
||||
textColor.copy(alpha = 0.75f),
|
||||
textColor,
|
||||
),
|
||||
from = highlightCenter - direction * bandHalf,
|
||||
to = highlightCenter + direction * bandHalf,
|
||||
colorStops = listOf(0f, 0.15f, 0.35f, 0.5f, 0.65f, 0.85f, 1f),
|
||||
tileMode = TileMode.Clamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
|||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/** Tokens list item state */
|
||||
@Immutable
|
||||
|
|
@ -45,15 +46,34 @@ sealed interface TokensListItemUM {
|
|||
val tokenItemUM: TokenItemState,
|
||||
val isExpanded: Boolean,
|
||||
val isCollapsable: Boolean,
|
||||
val tokens: ImmutableList<PortfolioTokensListItemUM>,
|
||||
val content: PortfolioItemContentUM,
|
||||
) : TokensListItemUM {
|
||||
override val id: String = tokenItemUM.id
|
||||
|
||||
val tokens: ImmutableList<PortfolioTokensListItemUM>
|
||||
get() = when (content) {
|
||||
is PortfolioItemContentUM.Tokens -> content.tokens
|
||||
is PortfolioItemContentUM.Empty -> persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
data class Text(override val id: Any, val text: TextReference) : TokensListItemUM
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface PortfolioTokensListItemUM {
|
||||
/** Unique ID */
|
||||
val id: Any
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface PortfolioItemContentUM {
|
||||
data class Tokens(val tokens: ImmutableList<PortfolioTokensListItemUM>) : PortfolioItemContentUM
|
||||
data class Empty(val action: Action? = null) : PortfolioItemContentUM {
|
||||
|
||||
data class Action(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
package com.tangem.core.ui.ds
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val ANIMATION_DURATION = 300
|
||||
private const val MAX_VISIBLE_DOTS = 5
|
||||
private const val MIN_HIDDEN_FOR_SMALL_DOT = 2
|
||||
private const val MIN_DISTANCE_FOR_SMALL_DOT = 3
|
||||
private const val MIN_DISTANCE_FOR_HINT_DOT = 2
|
||||
|
||||
private val SPACING = 4.dp
|
||||
private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp)
|
||||
private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp)
|
||||
private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
|
||||
private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp)
|
||||
|
||||
/**
|
||||
* // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation.
|
||||
*
|
||||
* A pager indicator that adapts to the number of pages and the current page index.
|
||||
*
|
||||
* For 5 or fewer pages, it shows all dots with the current page highlighted.
|
||||
* For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position.
|
||||
*
|
||||
* @param pagerState state of the pager to observe
|
||||
* @param activeIndicatorColor color for the active page indicator
|
||||
* @param inactiveIndicatorColor color for the inactive page indicators
|
||||
* @param modifier modifier for styling
|
||||
*/
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun TangemPagerIndicator(
|
||||
pagerState: PagerState,
|
||||
modifier: Modifier = Modifier,
|
||||
activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary,
|
||||
inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary,
|
||||
) {
|
||||
val totalPages = pagerState.pageCount
|
||||
val currentIndex = pagerState.currentPage
|
||||
|
||||
if (totalPages == 0) return
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex)
|
||||
|
||||
var displayLower by remember { mutableIntStateOf(targetLower) }
|
||||
var displayUpper by remember { mutableIntStateOf(targetUpper) }
|
||||
var prevTargetLower by remember { mutableIntStateOf(targetLower) }
|
||||
|
||||
val slideOffset = remember { Animatable(0f) }
|
||||
var isSliding by remember { mutableStateOf(false) }
|
||||
var slideDirection by remember { mutableIntStateOf(0) }
|
||||
val fadeProgress = remember { Animatable(0f) }
|
||||
var fadeJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
LaunchedEffect(targetLower) {
|
||||
if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) {
|
||||
fadeJob?.cancel()
|
||||
slideOffset.stop()
|
||||
fadeProgress.stop()
|
||||
|
||||
val dir = if (targetLower > prevTargetLower) 1 else -1
|
||||
val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
|
||||
val halfEdge = edgeDotSize / 2
|
||||
|
||||
isSliding = true
|
||||
slideDirection = dir
|
||||
fadeProgress.snapTo(0f)
|
||||
|
||||
if (dir > 0) {
|
||||
displayLower = prevTargetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(halfEdge)
|
||||
} else {
|
||||
displayLower = targetLower
|
||||
displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
|
||||
slideOffset.snapTo(-halfEdge)
|
||||
}
|
||||
|
||||
prevTargetLower = targetLower
|
||||
|
||||
fadeJob = launch {
|
||||
fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
|
||||
}
|
||||
slideOffset.animateTo(
|
||||
if (dir > 0) -halfEdge else halfEdge,
|
||||
tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
displayLower = targetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(0f)
|
||||
isSliding = false
|
||||
slideDirection = 0
|
||||
}
|
||||
}
|
||||
val visibleIndices = (displayLower until displayUpper).toList()
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(slideOffset.value.roundToInt(), 0)
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(SPACING),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
visibleIndices.forEach { index ->
|
||||
val dotAlpha = when {
|
||||
!isSliding -> 1f
|
||||
slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value
|
||||
slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value
|
||||
slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value
|
||||
slideDirection < 0 && index == displayLower -> fadeProgress.value
|
||||
else -> 1f
|
||||
}
|
||||
|
||||
key(index) {
|
||||
Dot(
|
||||
index = index,
|
||||
currentIndex = currentIndex,
|
||||
totalPages = totalPages,
|
||||
activeColor = activeIndicatorColor,
|
||||
inactiveColor = inactiveIndicatorColor,
|
||||
modifier = Modifier.graphicsLayer { alpha = dotAlpha },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair<Int, Int> {
|
||||
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return 0 to totalPages
|
||||
}
|
||||
val lowerBound = when {
|
||||
currentIndex <= 1 -> 0
|
||||
currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
|
||||
else -> currentIndex - 2
|
||||
}
|
||||
val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
|
||||
return lowerBound to upperBound
|
||||
}
|
||||
|
||||
private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
|
||||
if (index == currentIndex) {
|
||||
return CURRENT_DOT_SIZE
|
||||
}
|
||||
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return NORMAL_DOT_SIZE
|
||||
}
|
||||
val params = DotSizeParams.create(index, currentIndex, totalPages)
|
||||
return params.calculateSize()
|
||||
}
|
||||
|
||||
private class DotSizeParams private constructor(
|
||||
val posInWindow: Int,
|
||||
val currentPosInWindow: Int,
|
||||
val hiddenLeft: Int,
|
||||
val hiddenRight: Int,
|
||||
val distanceFromCurrent: Int,
|
||||
) {
|
||||
private val lastPos = MAX_VISIBLE_DOTS - 1
|
||||
private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1
|
||||
|
||||
fun calculateSize(): DpSize = when {
|
||||
isCentered -> getCenteredSize()
|
||||
hiddenRight >= 1 -> getRightEdgeSize()
|
||||
hiddenLeft >= 1 -> getLeftEdgeSize()
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
|
||||
private fun getCenteredSize(): DpSize = when (posInWindow) {
|
||||
0, lastPos -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
|
||||
private fun getRightEdgeSize(): DpSize {
|
||||
val isLastPos = posInWindow == lastPos
|
||||
val isSecondToLast = posInWindow == lastPos - 1
|
||||
val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT
|
||||
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
|
||||
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
|
||||
|
||||
return when {
|
||||
isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
|
||||
isLastPos && isModerateDistance -> HINT_DOT_SIZE
|
||||
isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLeftEdgeSize(): DpSize {
|
||||
val isFirstPos = posInWindow == 0
|
||||
val isSecondPos = posInWindow == 1
|
||||
val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT
|
||||
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
|
||||
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
|
||||
|
||||
return when {
|
||||
isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
|
||||
isFirstPos && isModerateDistance -> HINT_DOT_SIZE
|
||||
isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams {
|
||||
val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex)
|
||||
val posInWindow = index - windowStart
|
||||
val currentPosInWindow = currentIndex - windowStart
|
||||
return DotSizeParams(
|
||||
posInWindow = posInWindow,
|
||||
currentPosInWindow = currentPosInWindow,
|
||||
hiddenLeft = windowStart,
|
||||
hiddenRight = totalPages - windowEnd,
|
||||
distanceFromCurrent = abs(posInWindow - currentPosInWindow),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Dot(
|
||||
index: Int,
|
||||
currentIndex: Int,
|
||||
totalPages: Int,
|
||||
activeColor: Color,
|
||||
inactiveColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isActive = index == currentIndex
|
||||
val size = getDotSize(index, currentIndex, totalPages)
|
||||
|
||||
val animSpec = tween<Dp>(ANIMATION_DURATION)
|
||||
val colorSpec = tween<Color>(ANIMATION_DURATION)
|
||||
|
||||
val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
|
||||
val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
|
||||
val animatedColor by animateColorAsState(
|
||||
targetValue = if (isActive) activeColor else inactiveColor,
|
||||
animationSpec = colorSpec,
|
||||
label = "c$index",
|
||||
)
|
||||
|
||||
val shape = RoundedCornerShape(animatedHeight / 2)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(animatedWidth)
|
||||
.height(animatedHeight)
|
||||
.background(animatedColor, shape),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4).forEach { page ->
|
||||
TangemPagerIndicator(rememberPagerState(page) { 5 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator6ItemsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5).forEach { page ->
|
||||
TangemPagerIndicator(rememberPagerState(page) { 6 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator7ItemsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5, 6).forEach { page ->
|
||||
TangemPagerIndicator(rememberPagerState(page) { 7 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator10ItemsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page ->
|
||||
TangemPagerIndicator(rememberPagerState(page) { 10 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorSmallCountsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
TangemPagerIndicator(rememberPagerState(0) { 1 })
|
||||
TangemPagerIndicator(rememberPagerState(1) { 2 })
|
||||
TangemPagerIndicator(rememberPagerState(1) { 3 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
package com.tangem.core.ui.ds.badge
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
|
|
@ -15,18 +13,17 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeSize.*
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
|
|
@ -43,7 +40,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
|||
fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
|
||||
TangemBadge(
|
||||
text = badgeUM.text,
|
||||
iconRes = badgeUM.iconRes,
|
||||
tangemIconUM = badgeUM.tangemIconUM,
|
||||
size = badgeUM.size,
|
||||
shape = badgeUM.shape,
|
||||
color = badgeUM.color,
|
||||
|
|
@ -60,7 +57,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
|
|||
*
|
||||
* @param text TextReference for the badge label.
|
||||
* @param modifier Modifier to be applied to the badge.
|
||||
* @param iconRes Drawable resource ID for the icon to be displayed in the badge.
|
||||
* @param tangemIconUM Model of representation for the icon to be displayed in the badge.
|
||||
* @param size [TangemBadgeSize] defining the size of the badge.
|
||||
* @param shape [TangemBadgeShape] defining the shape of the badge.
|
||||
* @param color [TangemBadgeColor] defining the color scheme of the badge.
|
||||
|
|
@ -72,14 +69,14 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
|
|||
*/
|
||||
@Composable
|
||||
fun TangemBadge(
|
||||
text: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
@DrawableRes iconRes: Int? = null,
|
||||
text: TextReference? = null,
|
||||
tangemIconUM: TangemIconUM? = null,
|
||||
size: TangemBadgeSize = X9,
|
||||
shape: TangemBadgeShape = TangemBadgeShape.Default,
|
||||
color: TangemBadgeColor = TangemBadgeColor.Gray,
|
||||
type: TangemBadgeType = TangemBadgeType.Solid,
|
||||
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
|
||||
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val iconColor = getIconColor(type = type, color = color)
|
||||
|
|
@ -93,37 +90,84 @@ fun TangemBadge(
|
|||
.padding(size.toPaddingDp(position = iconPosition))
|
||||
.clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
label = "Start Icon Visibility",
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
Icon(
|
||||
painter = painterResource(id = wrappedIconRes),
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
style = size.toTextStyle(),
|
||||
maxLines = 1,
|
||||
color = getTextColor(type = type, color = color),
|
||||
StartIcon(
|
||||
tangemIconUM = tangemIconUM,
|
||||
iconPosition = iconPosition,
|
||||
size = size,
|
||||
iconColor = iconColor,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
label = "End Icon Visibility",
|
||||
visible = text != null,
|
||||
label = "Text Visibility",
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
Icon(
|
||||
painter = painterResource(id = wrappedIconRes),
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
val wrappedText = remember(this) { requireNotNull(text) }
|
||||
Text(
|
||||
text = wrappedText.resolveReference(),
|
||||
style = size.toTextStyle(),
|
||||
maxLines = 1,
|
||||
color = getTextColor(type = type, color = color),
|
||||
)
|
||||
}
|
||||
EndIcon(
|
||||
tangemIconUM = tangemIconUM,
|
||||
iconPosition = iconPosition,
|
||||
size = size,
|
||||
iconColor = iconColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartIcon(
|
||||
iconPosition: TangemBadgeIconPosition,
|
||||
size: TangemBadgeSize,
|
||||
iconColor: Color,
|
||||
tangemIconUM: TangemIconUM? = null,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = tangemIconUM != null && iconPosition != TangemBadgeIconPosition.End,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
label = "Start Icon Visibility",
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(tangemIconUM) }
|
||||
TangemIcon(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
tangemIconUM = when (wrappedIconRes) {
|
||||
is TangemIconUM.Currency,
|
||||
is TangemIconUM.Ident,
|
||||
is TangemIconUM.Image,
|
||||
is TangemIconUM.Url,
|
||||
-> wrappedIconRes
|
||||
is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EndIcon(
|
||||
iconPosition: TangemBadgeIconPosition,
|
||||
size: TangemBadgeSize,
|
||||
iconColor: Color,
|
||||
tangemIconUM: TangemIconUM? = null,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = tangemIconUM != null && iconPosition == TangemBadgeIconPosition.End,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
label = "End Icon Visibility",
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(tangemIconUM) }
|
||||
TangemIcon(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
tangemIconUM = when (wrappedIconRes) {
|
||||
is TangemIconUM.Currency,
|
||||
is TangemIconUM.Ident,
|
||||
is TangemIconUM.Image,
|
||||
is TangemIconUM.Url,
|
||||
-> wrappedIconRes
|
||||
is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,14 +222,17 @@ enum class TangemBadgeSize {
|
|||
X4 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp)
|
||||
TangemBadgeIconPosition.None -> PaddingValues(start = 6.dp, end = 6.dp)
|
||||
}
|
||||
X6 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp)
|
||||
TangemBadgeIconPosition.None -> PaddingValues(start = 12.dp, end = 12.dp)
|
||||
}
|
||||
X9 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp)
|
||||
TangemBadgeIconPosition.None -> PaddingValues(start = 16.dp, end = 16.dp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +269,7 @@ enum class TangemBadgeSize {
|
|||
enum class TangemBadgeIconPosition {
|
||||
Start,
|
||||
End,
|
||||
None,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -240,6 +288,7 @@ enum class TangemBadgeColor {
|
|||
Blue,
|
||||
Red,
|
||||
Gray,
|
||||
Green,
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
|
|
@ -258,6 +307,12 @@ private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when
|
|||
-> TangemTheme.colors2.markers.iconRed
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||
}
|
||||
TangemBadgeColor.Green -> when (type) {
|
||||
TangemBadgeType.Outline,
|
||||
TangemBadgeType.Tinted,
|
||||
-> TangemTheme.colors2.markers.iconGreen
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||
}
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
|
|
@ -276,8 +331,15 @@ private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when
|
|||
-> TangemTheme.colors2.markers.textRed
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
||||
}
|
||||
TangemBadgeColor.Green -> when (type) {
|
||||
TangemBadgeType.Outline,
|
||||
TangemBadgeType.Tinted,
|
||||
-> TangemTheme.colors2.markers.textGreen
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) {
|
||||
|
|
@ -286,6 +348,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
|||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed
|
||||
TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundSolidGreen
|
||||
},
|
||||
)
|
||||
TangemBadgeType.Tinted -> background(
|
||||
|
|
@ -293,6 +356,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
|||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed
|
||||
TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundTintedGreen
|
||||
},
|
||||
)
|
||||
TangemBadgeType.Outline -> {
|
||||
|
|
@ -301,6 +365,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
|||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed
|
||||
TangemBadgeColor.Green -> TangemTheme.colors2.markers.borderTintedGreen
|
||||
},
|
||||
shape = shape,
|
||||
width = 1.dp,
|
||||
|
|
@ -320,16 +385,16 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl
|
|||
.background(TangemTheme.colors2.surface.level1)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
repeat(2) { yIndex ->
|
||||
repeat(3) { yIndex ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
repeat(TangemBadgeType.entries.size) { index ->
|
||||
TangemBadge(
|
||||
text = stringReference("Title"),
|
||||
iconRes = R.drawable.ic_information_24,
|
||||
text = stringReference("Title").takeIf { yIndex < 2 },
|
||||
tangemIconUM = TangemIconUM.Icon(R.drawable.ic_information_24),
|
||||
type = TangemBadgeType.entries[index],
|
||||
color = params,
|
||||
shape = TangemBadgeShape.entries[yIndex % 2],
|
||||
iconPosition = TangemBadgeIconPosition.entries[yIndex % 2],
|
||||
iconPosition = TangemBadgeIconPosition.entries[yIndex],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -344,6 +409,7 @@ private class TangemBadgePreviewProvider : PreviewParameterProvider<TangemBadgeC
|
|||
TangemBadgeColor.Gray,
|
||||
TangemBadgeColor.Blue,
|
||||
TangemBadgeColor.Red,
|
||||
TangemBadgeColor.Green,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
package com.tangem.core.ui.ds.badge
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeSize.X9
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* UI model for [TangemBadge] component
|
||||
*
|
||||
* @param text TextReference for the badge label.
|
||||
* @param iconRes Drawable resource ID for the icon to be displayed in the badge.
|
||||
* @param tangemIconUM Model of representation for the icon to be displayed in the badge.
|
||||
* @param size [TangemBadgeSize] defining the size of the badge.
|
||||
* @param shape [TangemBadgeShape] defining the shape of the badge.
|
||||
* @param color [TangemBadgeColor] defining the color scheme of the badge.
|
||||
|
|
@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
*/
|
||||
class TangemBadgeUM(
|
||||
val text: TextReference,
|
||||
@DrawableRes val iconRes: Int? = null,
|
||||
val tangemIconUM: TangemIconUM? = null,
|
||||
val size: TangemBadgeSize = X9,
|
||||
val shape: TangemBadgeShape = TangemBadgeShape.Default,
|
||||
val color: TangemBadgeColor = TangemBadgeColor.Gray,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
|
@ -36,6 +37,7 @@ fun GhostTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
|
|||
enabled = buttonUM.isEnabled,
|
||||
size = buttonUM.size,
|
||||
state = buttonUM.state,
|
||||
shape = buttonUM.shape,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +65,7 @@ fun GhostTangemButton(
|
|||
size: TangemButtonSize = TangemButtonSize.X15,
|
||||
state: TangemButtonState = TangemButtonState.Default,
|
||||
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
|
||||
shape: TangemButtonShape = TangemButtonShape.Default,
|
||||
) {
|
||||
val contentColor = when (state) {
|
||||
TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled
|
||||
|
|
@ -70,7 +73,8 @@ fun GhostTangemButton(
|
|||
}
|
||||
TangemButtonInternal(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.clip(shape = shape.toShape(size)),
|
||||
text = text,
|
||||
contentColor = contentColor,
|
||||
enabled = enabled,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import androidx.compose.animation.animateContentSize
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.ripple.RippleAlpha
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -60,72 +60,91 @@ internal fun TangemButtonInternal(
|
|||
size: TangemButtonSize = TangemButtonSize.X15,
|
||||
state: TangemButtonState = TangemButtonState.Default,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.testTag(BaseButtonTestTags.BUTTON)
|
||||
.height(size.toHeightDp())
|
||||
.conditionalCompose(text == null) {
|
||||
width(size.toHeightDp())
|
||||
ProvideButtonRippleConfiguration {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.testTag(BaseButtonTestTags.BUTTON)
|
||||
.clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button)
|
||||
.height(size.toHeightDp())
|
||||
.conditionalCompose(text == null) {
|
||||
width(size.toHeightDp())
|
||||
}
|
||||
.conditionalCompose(text != null) {
|
||||
padding(horizontal = size.toPaddingDp())
|
||||
}
|
||||
.animateContentSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
|
||||
}
|
||||
.clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button)
|
||||
.conditionalCompose(text != null) {
|
||||
padding(horizontal = size.toPaddingDp())
|
||||
|
||||
AnimatedVisibility(text != null && state != TangemButtonState.Loading) {
|
||||
val wrappedText = remember(this) { requireNotNull(text) }
|
||||
val textStyle = size.toTextStyle()
|
||||
Text(
|
||||
text = wrappedText.resolveReference(),
|
||||
style = textStyle,
|
||||
color = contentColor,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
minFontSize = 12.sp,
|
||||
maxFontSize = textStyle.fontSize,
|
||||
),
|
||||
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
|
||||
)
|
||||
}
|
||||
.animateContentSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) {
|
||||
val wrappedText = remember(this) { requireNotNull(descriptionText) }
|
||||
val textStyle = TangemTheme.typography2.captionSemibold12
|
||||
Text(
|
||||
text = wrappedText.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors2.text.status.disabled,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
minFontSize = 12.sp,
|
||||
maxFontSize = textStyle.fontSize,
|
||||
),
|
||||
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemButtonIconPosition.End,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun ProvideButtonRippleConfiguration(crossinline content: @Composable () -> Unit) {
|
||||
CompositionLocalProvider(
|
||||
LocalRippleConfiguration provides RippleConfiguration(
|
||||
color = TangemTheme.colors2.overlay.overlaySecondary,
|
||||
RippleAlpha(
|
||||
pressedAlpha = 0.4f,
|
||||
focusedAlpha = 0.4f,
|
||||
draggedAlpha = 0.4f,
|
||||
hoveredAlpha = 0.4f,
|
||||
),
|
||||
),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
|
||||
}
|
||||
|
||||
AnimatedVisibility(text != null && state != TangemButtonState.Loading) {
|
||||
val wrappedText = remember(this) { requireNotNull(text) }
|
||||
val textStyle = size.toTextStyle()
|
||||
Text(
|
||||
text = wrappedText.resolveReference(),
|
||||
style = textStyle,
|
||||
color = contentColor,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
minFontSize = 12.sp,
|
||||
maxFontSize = textStyle.fontSize,
|
||||
),
|
||||
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) {
|
||||
val wrappedText = remember(this) { requireNotNull(descriptionText) }
|
||||
val textStyle = TangemTheme.typography2.captionSemibold12
|
||||
Text(
|
||||
text = wrappedText.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors2.text.status.disabled,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
minFontSize = 12.sp,
|
||||
maxFontSize = textStyle.fontSize,
|
||||
),
|
||||
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemButtonIconPosition.End,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.core.ui.ds.button
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
|
|
@ -19,6 +20,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Stable
|
||||
data class TangemButtonUM(
|
||||
val text: TextReference? = null,
|
||||
val descriptionText: TextReference? = null,
|
||||
|
|
|
|||
202
core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt
Normal file
202
core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
package com.tangem.core.ui.ds.image
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
/**
|
||||
* Composable function for displaying a wallet icon based on the provided [DeviceIconUM] state.
|
||||
*
|
||||
* The icon can represent different types of devices, such as cards, rings, stubs, or mobile wallet,
|
||||
* with customizable colors and styles.
|
||||
*
|
||||
* @param state The state of the device icon, which determines its appearance.
|
||||
* @param modifier Optional [Modifier] for styling the composable.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemDeviceIcon(state: DeviceIconUM, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is DeviceIconUM.Card -> DeviceIcon(
|
||||
modifier = modifier,
|
||||
isRing = false,
|
||||
mainColor = state.mainColor,
|
||||
secondColor = state.secondColor,
|
||||
thirdColor = state.thirdColor,
|
||||
tColor = null,
|
||||
)
|
||||
is DeviceIconUM.Ring -> DeviceIcon(
|
||||
modifier = modifier,
|
||||
isRing = true,
|
||||
mainColor = state.mainColor,
|
||||
secondColor = state.cardColor,
|
||||
thirdColor = state.secondCardColor,
|
||||
tColor = null,
|
||||
)
|
||||
is DeviceIconUM.Stub -> DeviceIcon(
|
||||
modifier = modifier,
|
||||
isRing = false,
|
||||
mainColor = Color.Unspecified,
|
||||
secondColor = Color.Unspecified.takeIf { state.cardsCount > 1 },
|
||||
thirdColor = Color.Unspecified.takeIf { state.cardsCount > 2 },
|
||||
tColor = TangemTheme.colors2.graphic.neutral.secondary,
|
||||
)
|
||||
DeviceIconUM.Mobile -> Icon(
|
||||
modifier = modifier,
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_shield_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.status.attention,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceIcon(
|
||||
isRing: Boolean,
|
||||
mainColor: Color,
|
||||
secondColor: Color?,
|
||||
thirdColor: Color?,
|
||||
tColor: Color?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val main = mainColor.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
|
||||
val second = secondColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
|
||||
val third = thirdColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
|
||||
val borderColor = TangemTheme.colors2.border.walletIcon
|
||||
|
||||
val imageVector = remember(isRing, main, second, third, borderColor, tColor) {
|
||||
when {
|
||||
isRing && second != null && third != null -> WalletIconVectorBuilders.buildRingWithCard2(
|
||||
mainColor = main,
|
||||
cardColor = second,
|
||||
secondCardColor = third,
|
||||
borderColor = borderColor,
|
||||
)
|
||||
!isRing && second != null && third != null -> WalletIconVectorBuilders.buildCard3(
|
||||
mainColor = main,
|
||||
secondColor = second,
|
||||
thirdColor = third,
|
||||
tColor = tColor,
|
||||
borderColor = borderColor,
|
||||
)
|
||||
isRing && second != null -> WalletIconVectorBuilders.buildRingWithCard(
|
||||
mainColor = main,
|
||||
cardColor = second,
|
||||
borderColor = borderColor,
|
||||
)
|
||||
!isRing && second != null -> WalletIconVectorBuilders.buildCard2(
|
||||
mainColor = main,
|
||||
secondColor = second,
|
||||
tColor = tColor,
|
||||
borderColor = borderColor,
|
||||
)
|
||||
isRing -> WalletIconVectorBuilders.buildRing(
|
||||
mainColor = main,
|
||||
borderColor = borderColor,
|
||||
)
|
||||
else -> WalletIconVectorBuilders.buildCard(
|
||||
mainColor = main,
|
||||
borderColor = borderColor,
|
||||
tColor = tColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
private val previewCardBlue
|
||||
get() = Color(0xFF1C5FBF)
|
||||
private val previewCardGold
|
||||
get() = Color(0xFFD4A017)
|
||||
private val previewCardPurple
|
||||
get() = Color(0xFF7B2FBE)
|
||||
private val previewRingGreen
|
||||
get() = Color(0xFF2ECC71)
|
||||
|
||||
private val previewStates: List<Pair<String, DeviceIconUM>>
|
||||
get() = listOf(
|
||||
"Card 1" to DeviceIconUM.Card(
|
||||
mainColor = previewCardBlue,
|
||||
secondColor = null,
|
||||
),
|
||||
"Card 2" to DeviceIconUM.Card(
|
||||
mainColor = previewCardBlue,
|
||||
secondColor = previewCardGold,
|
||||
),
|
||||
"Card 3" to DeviceIconUM.Card(
|
||||
mainColor = previewCardBlue,
|
||||
secondColor = previewCardGold,
|
||||
thirdColor = previewCardPurple,
|
||||
),
|
||||
"Ring" to DeviceIconUM.Ring(
|
||||
mainColor = previewRingGreen,
|
||||
),
|
||||
"Ring + Card" to DeviceIconUM.Ring(
|
||||
mainColor = previewRingGreen,
|
||||
cardColor = previewCardBlue,
|
||||
),
|
||||
"Ring + 2 Cards" to DeviceIconUM.Ring(
|
||||
mainColor = previewRingGreen,
|
||||
cardColor = previewCardBlue,
|
||||
secondCardColor = previewCardGold,
|
||||
),
|
||||
"Stub 1" to DeviceIconUM.Stub(cardsCount = 1),
|
||||
"Stub 2" to DeviceIconUM.Stub(cardsCount = 2),
|
||||
"Stub 3" to DeviceIconUM.Stub(cardsCount = 3),
|
||||
"Mobile" to DeviceIconUM.Mobile,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun TangemDeviceIcon_Preview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
previewStates.forEach { (label, state) ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TangemDeviceIcon(
|
||||
modifier = Modifier.size(40.dp),
|
||||
state = state,
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.core.ui.ds.image
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
sealed interface DeviceIconUM {
|
||||
|
||||
data class Card(
|
||||
val mainColor: Color,
|
||||
val secondColor: Color?,
|
||||
val thirdColor: Color? = null,
|
||||
) : DeviceIconUM
|
||||
|
||||
data class Ring(
|
||||
val mainColor: Color = Color.Unspecified,
|
||||
val cardColor: Color? = null,
|
||||
val secondCardColor: Color? = null,
|
||||
) : DeviceIconUM
|
||||
|
||||
data class Stub(val cardsCount: Int) : DeviceIconUM
|
||||
|
||||
data object Mobile : DeviceIconUM
|
||||
}
|
||||
|
|
@ -2,12 +2,19 @@ package com.tangem.core.ui.ds.image
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
|
|
@ -40,6 +47,11 @@ sealed interface TangemIconUM {
|
|||
data class Ident(
|
||||
val text: String,
|
||||
) : TangemIconUM
|
||||
|
||||
/** Image represented from network by url */
|
||||
data class Url(
|
||||
val url: String,
|
||||
) : TangemIconUM
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -72,5 +84,24 @@ fun TangemIcon(tangemIconUM: TangemIconUM, modifier: Modifier = Modifier) {
|
|||
address = tangemIconUM.text,
|
||||
modifier = modifier,
|
||||
)
|
||||
is TangemIconUM.Url -> SubcomposeAsyncImage(
|
||||
modifier = modifier,
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(tangemIconUM.url)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(enable = false)
|
||||
.build(),
|
||||
loading = { CircleShimmer() },
|
||||
error = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors2.surface.level3,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -22,11 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.flicker
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
|
@ -58,7 +58,15 @@ fun TangemMessage(
|
|||
if (messageUM.iconUM != null) {
|
||||
TangemIcon(
|
||||
tangemIconUM = messageUM.iconUM,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x8),
|
||||
modifier = Modifier
|
||||
.align(
|
||||
if (messageUM.buttonsUM.isEmpty()) {
|
||||
Alignment.CenterVertically
|
||||
} else {
|
||||
Alignment.Top
|
||||
},
|
||||
)
|
||||
.size(TangemTheme.dimens2.x7),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -342,6 +350,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider<TangemMess
|
|||
id = "1",
|
||||
title = stringReference("Title text"),
|
||||
subtitle = stringReference("Subtext"),
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
isCentered = true,
|
||||
),
|
||||
|
|
@ -350,6 +359,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider<TangemMess
|
|||
title = stringReference("Title text"),
|
||||
subtitle = stringReference("Subtext"),
|
||||
messageEffect = TangemMessageEffect.Magic,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
isCentered = false,
|
||||
buttonsUM = persistentListOf(
|
||||
TangemMessageButtonUM(
|
||||
|
|
@ -405,9 +415,8 @@ private fun TangemMessage2_Preview() {
|
|||
content = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x10)
|
||||
.size(TangemTheme.dimens2.x7)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens2.x2))
|
||||
.flicker(isFlickering = true)
|
||||
.background(TangemTheme.colors2.text.neutral.primary),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -106,7 +106,10 @@ enum class TangemMessageEffect(val isAnimatable: Boolean) {
|
|||
Color(0x1AFFFFFF),
|
||||
)
|
||||
} else {
|
||||
persistentListOf()
|
||||
persistentListOf(
|
||||
Color(0xFFE1E1E1),
|
||||
Color(0xFFE1E1E1),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -192,7 +195,10 @@ enum class TangemMessageEffect(val isAnimatable: Boolean) {
|
|||
Color(0x17E44848),
|
||||
)
|
||||
None -> if (isInDarkTheme) {
|
||||
persistentListOf()
|
||||
persistentListOf(
|
||||
Color(0x1AFFFFFF),
|
||||
Color(0x1AFFFFFF),
|
||||
)
|
||||
} else {
|
||||
persistentListOf(
|
||||
Color(0x0d000000),
|
||||
|
|
@ -238,9 +244,10 @@ internal fun Modifier.messageEffectBackground(
|
|||
val isInDarkTheme = LocalIsInDarkTheme.current
|
||||
val borderGradientColors = remember { messageEffect.getBorderGradient(isInDarkTheme) }
|
||||
val gradientColors = remember { messageEffect.getColorGradient(isInDarkTheme) }
|
||||
val gradientTint = remember { messageEffect.getGradientTint(isInDarkTheme) }
|
||||
|
||||
val angle by rememberAnimationAngle(messageEffect.isAnimatable)
|
||||
val brush = Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme))
|
||||
val brush = remember { Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme)) }
|
||||
val padding = 1.dp.toPx()
|
||||
|
||||
return this
|
||||
|
|
@ -254,14 +261,14 @@ internal fun Modifier.messageEffectBackground(
|
|||
border(
|
||||
width = 1.dp,
|
||||
brush = Brush.sweepGradient(
|
||||
colors = messageEffect.getBorderGradient(isInDarkTheme),
|
||||
colors = borderGradientColors,
|
||||
center = Offset.Infinite,
|
||||
),
|
||||
shape = RoundedCornerShape(radius),
|
||||
)
|
||||
}
|
||||
.hazeForegroundEffectTangem(
|
||||
style = HazeStyle(tints = messageEffect.getGradientTint(isInDarkTheme)),
|
||||
style = HazeStyle(tints = gradientTint),
|
||||
isBlurEnabled = true,
|
||||
) {
|
||||
fallbackTint = HazeTint(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
package com.tangem.core.ui.ds.opportunities
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.draw.innerShadow
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.drawOutline
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.shadow.Shadow
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
|
||||
/**
|
||||
* Container that draws a blurred background (from URL or solid color) and
|
||||
* applies a semi‑transparent overlay on top of it, then renders foreground content.
|
||||
*
|
||||
* Figma https://www.figma.com/design/X0IMgSMOT5rWWgiSIeZQwC/Bottom-sheet--Redesign-?node-id=3360-64755&m=dev
|
||||
*
|
||||
* @param icon Background configuration (URL, solid color or none).
|
||||
* @param modifier Modifier applied to the outer container.
|
||||
* @param content Foreground content rendered on top of the overlay.
|
||||
* @param shape Shape used for inner shadow and border (e.g. rounded corners).
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun OpportunitiesBG(
|
||||
icon: TangemIconUM,
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = RoundedCornerShape(16.dp),
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val isInDarkTheme = LocalIsInDarkTheme.current
|
||||
val overlayColor = remember(isInDarkTheme) {
|
||||
if (isInDarkTheme) {
|
||||
Color(OVERLAY_DARK)
|
||||
} else {
|
||||
Color.White
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
BackgroundLayer(icon = icon)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.innerShadow(
|
||||
shape = shape,
|
||||
shadow = Shadow(
|
||||
radius = 30.dp,
|
||||
spread = 5.dp,
|
||||
color = Color(INNER_SHADOW_COLOR_START).copy(alpha = .3f),
|
||||
offset = DpOffset(0.dp, 0.dp),
|
||||
),
|
||||
)
|
||||
.innerShadow(
|
||||
shape = shape,
|
||||
shadow = Shadow(
|
||||
radius = 100.dp,
|
||||
spread = (-39).dp,
|
||||
color = Color(INNER_SHADOW_COLOR_END).copy(.3f),
|
||||
offset = DpOffset(0.dp, (-56).dp),
|
||||
),
|
||||
)
|
||||
.innerShadow(
|
||||
shape = shape,
|
||||
shadow = Shadow(
|
||||
radius = 40.dp,
|
||||
spread = (-19).dp,
|
||||
color = Color(INNER_SHADOW_COLOR_END).copy(alpha = .25f),
|
||||
offset = DpOffset(0.dp, (-16).dp),
|
||||
),
|
||||
)
|
||||
.drawWithContent {
|
||||
drawRect(color = overlayColor.copy(alpha = .7f))
|
||||
drawContent()
|
||||
val outline = shape.createOutline(size, layoutDirection, this)
|
||||
drawOutline(outline, Color(BORDER_COLOR).copy(alpha = .1f), style = Stroke(width = 1.dp.toPx()))
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@Composable
|
||||
private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp) {
|
||||
when (icon) {
|
||||
is TangemIconUM.Currency -> CurrencyIconBackgroundLayer(icon.currencyIconState, blurRadius)
|
||||
is TangemIconUM.Icon -> SolidColorBackground(icon.tintReference(), blurRadius)
|
||||
is TangemIconUM.Ident -> Unit
|
||||
is TangemIconUM.Image -> ResBackground(icon.imageRes, blurRadius)
|
||||
is TangemIconUM.Url -> UrlColorBackground(icon.url, blurRadius)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@Composable
|
||||
private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurRadius: Dp) {
|
||||
when (state) {
|
||||
is CurrencyIconState.CryptoPortfolio.Icon -> SolidColorBackground(
|
||||
color = state.color,
|
||||
blurRadius = blurRadius,
|
||||
)
|
||||
is CurrencyIconState.CryptoPortfolio.Letter -> SolidColorBackground(
|
||||
color = state.color,
|
||||
blurRadius = blurRadius,
|
||||
)
|
||||
is CurrencyIconState.CustomTokenIcon -> SolidColorBackground(
|
||||
color = state.background,
|
||||
blurRadius = blurRadius,
|
||||
)
|
||||
is CurrencyIconState.Empty -> ResBackground(res = state.resId, blurRadius = blurRadius)
|
||||
is CurrencyIconState.CoinIcon -> {
|
||||
state.url?.let {
|
||||
UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
|
||||
} ?: run {
|
||||
ResBackground(res = state.fallbackResId, blurRadius = blurRadius)
|
||||
}
|
||||
}
|
||||
is CurrencyIconState.FiatIcon -> state.url?.let {
|
||||
UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
|
||||
} ?: run {
|
||||
ResBackground(res = state.fallbackResId, blurRadius = blurRadius)
|
||||
}
|
||||
is CurrencyIconState.TokenIcon -> state.url?.let {
|
||||
UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
|
||||
} ?: run {
|
||||
SolidColorBackground(
|
||||
color = state.fallbackBackground,
|
||||
blurRadius = blurRadius,
|
||||
)
|
||||
}
|
||||
CurrencyIconState.Loading -> Unit
|
||||
CurrencyIconState.Locked -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val imageRequest = remember(imageUrl) {
|
||||
if (imageUrl.isNullOrBlank()) {
|
||||
null
|
||||
} else {
|
||||
ImageRequest.Builder(context)
|
||||
.data(imageUrl)
|
||||
.crossfade(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
if (imageRequest != null) {
|
||||
AsyncImage(
|
||||
model = imageRequest,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.scale(SCALE_FACTOR)
|
||||
.hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) {
|
||||
Image(
|
||||
painter = painterResource(res),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.scale(SCALE_FACTOR)
|
||||
.hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.SolidColorBackground(color: Color, blurRadius: Dp) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(color = color)
|
||||
.hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.UrlColorBackground(url: String, blurRadius: Dp) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(url)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(enable = false)
|
||||
.build(),
|
||||
loading = { CircleShimmer() },
|
||||
error = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors2.surface.level3,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
private const val SCALE_FACTOR = 1.5f
|
||||
private const val INNER_SHADOW_COLOR_START = 0x00000000
|
||||
private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF
|
||||
|
||||
private const val BORDER_COLOR = 0xFFF0F0F0
|
||||
private const val OVERLAY_DARK = 0xFF141414
|
||||
|
||||
// region Previews
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun OpportunitiesBGPreview() {
|
||||
TangemThemePreview {
|
||||
OpportunitiesBG(
|
||||
modifier = Modifier.size(400.dp),
|
||||
icon = TangemIconUM.Currency(
|
||||
CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_solana_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
),
|
||||
content = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -16,8 +16,8 @@ import kotlin.math.max
|
|||
/**
|
||||
* A custom layout composable that arranges its children in a row with specific layout IDs.
|
||||
*/
|
||||
internal enum class TangemRowLayoutId {
|
||||
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP
|
||||
enum class TangemRowLayoutId {
|
||||
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP, EXTRA_BOTTOM
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -29,7 +29,7 @@ internal enum class TangemRowLayoutId {
|
|||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun TangemRowContainer(
|
||||
fun TangemRowContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3),
|
||||
content: @Composable () -> Unit,
|
||||
|
|
@ -37,6 +37,7 @@ internal fun TangemRowContainer(
|
|||
val density = LocalDensity.current
|
||||
val localDirection = LocalLayoutDirection.current
|
||||
val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() }
|
||||
val extraContentPadding = with(density) { TangemTheme.dimens2.x2.roundToPx() }
|
||||
val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||
val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||
val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() }
|
||||
|
|
@ -45,7 +46,7 @@ internal fun TangemRowContainer(
|
|||
content = content,
|
||||
modifier = modifier,
|
||||
) { measurables, constraints ->
|
||||
val layoutWidth = constraints.maxWidth - contentStartPadding - contentEndPadding
|
||||
val layoutWidth = max(0, constraints.maxWidth - contentStartPadding - contentEndPadding)
|
||||
|
||||
val startTopMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
|
||||
val startBottomMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt()
|
||||
|
|
@ -110,6 +111,10 @@ internal fun TangemRowContainer(
|
|||
layoutId = TangemRowLayoutId.EXTRA_TOP,
|
||||
constraints = constraints,
|
||||
)
|
||||
val extraBottomPlaceable = measurables.measure(
|
||||
layoutId = TangemRowLayoutId.EXTRA_BOTTOM,
|
||||
constraints = constraints,
|
||||
)
|
||||
|
||||
val mainLayoutHeight = maxOf(
|
||||
headPlaceable.heightOrZero(),
|
||||
|
|
@ -124,7 +129,13 @@ internal fun TangemRowContainer(
|
|||
contentTopPadding
|
||||
}
|
||||
|
||||
val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding
|
||||
val mainContentBottomPadding = if (extraBottomPlaceable != null) {
|
||||
extraBottomPlaceable.heightOrZero() + contentBottomPadding
|
||||
} else {
|
||||
contentBottomPadding
|
||||
}
|
||||
|
||||
val layoutHeight = mainLayoutHeight + mainContentTopPadding + mainContentBottomPadding
|
||||
|
||||
layout(width = constraints.maxWidth, height = layoutHeight) {
|
||||
extraTopPlaceable?.placeRelative(x = 0, y = 0)
|
||||
|
|
@ -174,6 +185,11 @@ internal fun TangemRowContainer(
|
|||
x = layoutWidth - tailPlaceable.width + contentEndPadding,
|
||||
y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2),
|
||||
)
|
||||
|
||||
extraBottomPlaceable?.placeRelative(
|
||||
x = 0,
|
||||
y = mainContentTopPadding + mainLayoutHeight + extraContentPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,7 @@ import com.tangem.core.ui.components.SpacerWMax
|
|||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
|
|
@ -40,12 +37,13 @@ import com.tangem.core.ui.test.TokenElementsTestTags
|
|||
* @param modifier Modifier for the composable
|
||||
*/
|
||||
@Composable
|
||||
fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier) {
|
||||
fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier, isBalanceHidden: Boolean = false) {
|
||||
TangemHeaderRow(
|
||||
headTangemIconUM = headerRowUM.startIconUM,
|
||||
footerTangemIconRes = headerRowUM.endIconRes,
|
||||
title = headerRowUM.title,
|
||||
subtitle = headerRowUM.subtitle,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -63,6 +61,7 @@ fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifie
|
|||
@Composable
|
||||
fun TangemHeaderRow(
|
||||
modifier: Modifier = Modifier,
|
||||
isBalanceHidden: Boolean = false,
|
||||
subtitle: TextReference? = null,
|
||||
onItemClick: (() -> Unit)? = null,
|
||||
@DrawableRes footerTangemIconRes: Int? = null,
|
||||
|
|
@ -92,7 +91,7 @@ fun TangemHeaderRow(
|
|||
) {
|
||||
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
|
||||
Text(
|
||||
text = wrappedSubtitle.resolveAnnotatedReference(),
|
||||
text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
|
|
@ -131,6 +130,7 @@ fun TangemHeaderRow(
|
|||
fun TangemHeaderRow(
|
||||
title: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
isBalanceHidden: Boolean = false,
|
||||
subtitle: TextReference? = null,
|
||||
headTangemIconUM: TangemIconUM? = null,
|
||||
@DrawableRes footerTangemIconRes: Int? = null,
|
||||
|
|
@ -172,7 +172,7 @@ fun TangemHeaderRow(
|
|||
) {
|
||||
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
|
||||
Text(
|
||||
text = wrappedSubtitle.resolveAnnotatedReference(),
|
||||
text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
|
|
|
|||
|
|
@ -52,15 +52,6 @@ fun TangemTokenRow(
|
|||
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
|
||||
)
|
||||
|
||||
TokenRowPromoBanner(
|
||||
promoBannerUM = tokenRowUM.promoBannerUM,
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP)
|
||||
.testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
|
||||
.padding(horizontal = TangemTheme.dimens2.x3)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
TokenRowTitle(
|
||||
titleUM = tokenRowUM.titleUM,
|
||||
modifier = Modifier
|
||||
|
|
@ -77,17 +68,21 @@ fun TangemTokenRow(
|
|||
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
|
||||
)
|
||||
|
||||
TokenRowEndTopContent(
|
||||
TokenRowEndContent(
|
||||
endContentUM = tokenRowUM.topEndContentUM,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
textStyle = TangemTheme.typography2.bodySemibold16,
|
||||
textColor = TangemTheme.colors2.text.neutral.primary,
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
|
||||
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
|
||||
)
|
||||
|
||||
TokenRowEndBottomContent(
|
||||
TokenRowEndContent(
|
||||
endContentUM = tokenRowUM.bottomEndContentUM,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
textStyle = TangemTheme.typography2.captionSemibold12,
|
||||
textColor = TangemTheme.colors2.text.neutral.secondary,
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
|
||||
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
|
||||
|
|
@ -100,6 +95,15 @@ fun TangemTokenRow(
|
|||
.layoutId(layoutId = TangemRowLayoutId.TAIL)
|
||||
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
|
||||
)
|
||||
|
||||
TokenRowPromoBanner(
|
||||
promoBannerUM = tokenRowUM.promoBannerUM,
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.EXTRA_BOTTOM)
|
||||
.testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
|
||||
.padding(start = TangemTheme.dimens2.x10, bottom = TangemTheme.dimens2.x2)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
|
||||
)
|
||||
|
|
@ -159,17 +163,21 @@ fun TangemTokenRow(
|
|||
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
|
||||
)
|
||||
|
||||
TokenRowEndTopContent(
|
||||
TokenRowEndContent(
|
||||
endContentUM = tokenRowUM.topEndContentUM,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
textStyle = TangemTheme.typography2.bodySemibold16,
|
||||
textColor = TangemTheme.colors2.text.neutral.primary,
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
|
||||
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
|
||||
)
|
||||
|
||||
TokenRowEndBottomContent(
|
||||
TokenRowEndContent(
|
||||
endContentUM = tokenRowUM.bottomEndContentUM,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
textStyle = TangemTheme.typography2.captionSemibold12,
|
||||
textColor = TangemTheme.colors2.text.neutral.secondary,
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
|
||||
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
|
||||
|
|
@ -218,7 +226,7 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co
|
|||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun TangemTokenRow_Preview(
|
||||
@PreviewParameter(TangemTokenRowPreviewProvider::class) tokenRowUM: TangemTokenRowUM,
|
||||
@PreviewParameter(TangemTokenRow_PreviewProvider::class) tokenRowUM: TangemTokenRowUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
TangemTokenRow(
|
||||
|
|
@ -230,7 +238,8 @@ private fun TangemTokenRow_Preview(
|
|||
}
|
||||
}
|
||||
|
||||
private class TangemTokenRowPreviewProvider : CollectionPreviewParameterProvider<TangemTokenRowUM>(
|
||||
@Suppress("ClassNaming")
|
||||
class TangemTokenRow_PreviewProvider : CollectionPreviewParameterProvider<TangemTokenRowUM>(
|
||||
collection = listOf(
|
||||
TangemTokenRowPreviewData.defaultState,
|
||||
TangemTokenRowPreviewData.defaultEllipsisState,
|
||||
|
|
|
|||
|
|
@ -133,7 +133,8 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
val text: TextReference,
|
||||
val isAvailable: Boolean = true,
|
||||
val isFlickering: Boolean = false,
|
||||
val icons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
|
||||
val startIcons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
|
||||
val endIcons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
|
||||
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
|
||||
) : EndContentUM()
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ internal object TangemTokenRowPreviewData {
|
|||
)
|
||||
}),
|
||||
),
|
||||
icons = persistentListOf(
|
||||
startIcons = persistentListOf(
|
||||
TangemIconUM.Icon(R.drawable.ic_staking_mini_10),
|
||||
TangemIconUM.Icon(R.drawable.ic_attention_12),
|
||||
TangemIconUM.Icon(R.drawable.ic_error_sync_24),
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
package com.tangem.core.ui.ds.row.token.internal
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
@Composable
|
||||
internal fun TokenRowEndBottomContent(
|
||||
endContentUM: TangemTokenRowUM.EndContentUM,
|
||||
isBalanceHidden: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (endContentUM) {
|
||||
is TangemTokenRowUM.EndContentUM.Content -> Content(
|
||||
modifier = modifier,
|
||||
endContentUM = endContentUM,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
TangemTokenRowUM.EndContentUM.Empty -> Unit
|
||||
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
modifier = modifier.width(TangemTheme.dimens2.x10),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
endContentUM: TangemTokenRowUM.EndContentUM.Content,
|
||||
isBalanceHidden: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
|
||||
isEnabled = endContentUM.isFlickering,
|
||||
textColor = if (endContentUM.isAvailable) {
|
||||
TangemTheme.colors2.text.neutral.secondary
|
||||
} else {
|
||||
TangemTheme.colors2.text.status.disabled
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
when (val priceChangeUM = endContentUM.priceChangeUM) {
|
||||
is PriceChangeState.Content -> TokenRowPriceChangeContent(
|
||||
priceChangeState = priceChangeUM,
|
||||
isFlickering = endContentUM.isFlickering,
|
||||
isAvailable = endContentUM.isAvailable,
|
||||
)
|
||||
PriceChangeState.Unknown -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun TokenRowEndBottomContent_Preview(
|
||||
@PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
TokenRowEndBottomContent(
|
||||
endContentUM = params,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
|
||||
override val values: Sequence<TangemTokenRowUM.EndContentUM>
|
||||
get() = sequenceOf(
|
||||
TangemTokenRowPreviewData.bottomEndContentUM,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -8,15 +8,18 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
|
|
@ -25,9 +28,11 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
@Composable
|
||||
internal fun TokenRowEndTopContent(
|
||||
internal fun TokenRowEndContent(
|
||||
endContentUM: TangemTokenRowUM.EndContentUM,
|
||||
isBalanceHidden: Boolean,
|
||||
textStyle: TextStyle,
|
||||
textColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (endContentUM) {
|
||||
|
|
@ -35,11 +40,13 @@ internal fun TokenRowEndTopContent(
|
|||
modifier = modifier,
|
||||
endContentUM = endContentUM,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
textStyle = textStyle,
|
||||
textColor = textColor,
|
||||
)
|
||||
TangemTokenRowUM.EndContentUM.Empty -> Unit
|
||||
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
modifier = modifier.width(TangemTheme.dimens2.x18),
|
||||
style = textStyle,
|
||||
modifier = modifier.width(TangemTheme.dimens2.x10),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
|
|
@ -48,6 +55,8 @@ internal fun TokenRowEndTopContent(
|
|||
@Composable
|
||||
private fun Content(
|
||||
endContentUM: TangemTokenRowUM.EndContentUM.Content,
|
||||
textStyle: TextStyle,
|
||||
textColor: Color,
|
||||
isBalanceHidden: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -56,14 +65,14 @@ private fun Content(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = endContentUM.icons.isNotEmpty(),
|
||||
visible = endContentUM.startIcons.isNotEmpty(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
) {
|
||||
endContentUM.icons.fastForEach { icon ->
|
||||
endContentUM.startIcons.fastForEach { icon ->
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x3),
|
||||
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
|
||||
|
|
@ -75,11 +84,11 @@ private fun Content(
|
|||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier,
|
||||
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography2.bodySemibold16.applyBladeBrush(
|
||||
color = textColor,
|
||||
style = textStyle.applyBladeBrush(
|
||||
isEnabled = endContentUM.isFlickering,
|
||||
textColor = if (endContentUM.isAvailable) {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
|
|
@ -88,6 +97,34 @@ private fun Content(
|
|||
},
|
||||
),
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = endContentUM.endIcons.isNotEmpty(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
) {
|
||||
endContentUM.endIcons.fastForEach { icon ->
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x3),
|
||||
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
|
||||
tint = icon.tintReference(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val priceChangeUM = endContentUM.priceChangeUM) {
|
||||
is PriceChangeState.Content -> TokenRowPriceChangeContent(
|
||||
priceChangeState = priceChangeUM,
|
||||
isFlickering = endContentUM.isFlickering,
|
||||
isAvailable = endContentUM.isAvailable,
|
||||
)
|
||||
PriceChangeState.Unknown -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,13 +132,15 @@ private fun Content(
|
|||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun TokenRowEndTopContent_Preview(
|
||||
private fun TokenRowEndContent_Preview(
|
||||
@PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
TokenRowEndTopContent(
|
||||
TokenRowEndContent(
|
||||
endContentUM = params,
|
||||
isBalanceHidden = false,
|
||||
textColor = TangemTheme.colors2.text.neutral.primary,
|
||||
textStyle = TangemTheme.typography2.captionSemibold12,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +148,7 @@ private fun TokenRowEndTopContent_Preview(
|
|||
private class TokenRowEndContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
|
||||
override val values: Sequence<TangemTokenRowUM.EndContentUM>
|
||||
get() = sequenceOf(
|
||||
TangemTokenRowPreviewData.topEndContentUM,
|
||||
TangemTokenRowPreviewData.bottomEndContentUM,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -24,6 +24,12 @@ internal fun RowScope.TokenRowPriceChangeContent(
|
|||
isFlickering: Boolean,
|
||||
isAvailable: Boolean = true,
|
||||
) {
|
||||
val color = when (priceChangeState.type) {
|
||||
PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning
|
||||
PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.tertiary
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = priceChangeState.type,
|
||||
label = "Update the price change's arrow",
|
||||
|
|
@ -39,11 +45,7 @@ internal fun RowScope.TokenRowPriceChangeContent(
|
|||
},
|
||||
),
|
||||
),
|
||||
tint = when (animatedType) {
|
||||
PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning
|
||||
PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.secondary
|
||||
},
|
||||
tint = color,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x3),
|
||||
)
|
||||
|
|
@ -61,7 +63,7 @@ internal fun RowScope.TokenRowPriceChangeContent(
|
|||
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
|
||||
isEnabled = isFlickering,
|
||||
textColor = if (isAvailable) {
|
||||
TangemTheme.colors2.text.neutral.secondary
|
||||
color
|
||||
} else {
|
||||
TangemTheme.colors2.text.status.disabled
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,15 +3,12 @@ package com.tangem.core.ui.ds.row.token.internal
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
|
@ -19,6 +16,8 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds.badge.*
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -40,55 +39,52 @@ internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.C
|
|||
LaunchedEffect(promoBannerUM) {
|
||||
promoBannerUM.onPromoShown()
|
||||
}
|
||||
val bgColor = TangemTheme.colors.control.default
|
||||
Column(modifier = modifier) {
|
||||
val bgColor = TangemTheme.colors2.markers.backgroundTintedGreen
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.shape_triangular),
|
||||
contentDescription = null,
|
||||
tint = bgColor,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens2.x5),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4))
|
||||
.clickable(onClick = promoBannerUM.onPromoBannerClick)
|
||||
.padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2)
|
||||
.fillMaxWidth(),
|
||||
.padding(
|
||||
start = TangemTheme.dimens2.x2_5,
|
||||
end = TangemTheme.dimens2.x0_5,
|
||||
top = TangemTheme.dimens2.x0_5,
|
||||
bottom = TangemTheme.dimens2.x0_5,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
tint = TangemTheme.colors2.markers.textGreen,
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens2.x2)
|
||||
.size(TangemTheme.dimens2.x4),
|
||||
.padding(vertical = TangemTheme.dimens2.x0_5)
|
||||
.size(TangemTheme.dimens2.x3),
|
||||
)
|
||||
Text(
|
||||
text = promoBannerUM.title.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
style = TangemTheme.typography2.captionSemibold11,
|
||||
color = TangemTheme.colors2.markers.textGreen,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = TangemTheme.dimens2.x2),
|
||||
.padding(vertical = TangemTheme.dimens2.x0_5),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.text.neutral.secondary,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x4)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(bounded = false),
|
||||
onClick = { promoBannerUM.onCloseClick() },
|
||||
),
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_rectangle_bottom),
|
||||
contentDescription = null,
|
||||
tint = bgColor,
|
||||
modifier = Modifier
|
||||
.size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2),
|
||||
TangemBadge(
|
||||
size = TangemBadgeSize.X4,
|
||||
shape = TangemBadgeShape.Rounded,
|
||||
color = TangemBadgeColor.Green,
|
||||
type = TangemBadgeType.Tinted,
|
||||
tangemIconUM = TangemIconUM.Icon(R.drawable.ic_close_24),
|
||||
iconPosition = TangemBadgeIconPosition.None,
|
||||
onClick = promoBannerUM.onCloseClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ fun TangemTab(
|
|||
val backgroundColor = if (isChecked) {
|
||||
TangemTheme.colors2.tabs.backgroundPrimary
|
||||
} else {
|
||||
TangemTheme.colors2.tabs.textPrimary
|
||||
TangemTheme.colors2.tabs.backgroundSecondary
|
||||
}
|
||||
val textColor = if (isChecked) {
|
||||
TangemTheme.colors2.tabs.textPrimary
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.core.ui.ds.topbar
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -55,6 +55,7 @@ fun TangemTopBar(
|
|||
modifier = modifier,
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
|
||||
) {
|
||||
|
|
@ -95,11 +96,17 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes:
|
|||
AnimatedVisibility(
|
||||
visible = title != null,
|
||||
label = "Title Visibility",
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
val wrappedTitle = remember(this) { requireNotNull(title) }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = TangemTheme.dimens2.x1,
|
||||
alignment = Alignment.CenterHorizontally,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
package com.tangem.core.ui.ds.topbar.collapsing
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@Composable
|
||||
fun TangemCollapsingTopBar(
|
||||
state: TangemCollapsingAppBarState,
|
||||
collapsingPart: @Composable () -> Unit,
|
||||
body: @Composable () -> Unit,
|
||||
) {
|
||||
Layout(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
content = {
|
||||
collapsingPart()
|
||||
body()
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
|
||||
val collapsingConstraints = constraints.copy(
|
||||
minWidth = 0,
|
||||
minHeight = 0,
|
||||
)
|
||||
val collapsingPlaceable = measurables[0].measure(collapsingConstraints)
|
||||
|
||||
val bodyConstraints = constraints.copy(
|
||||
minWidth = 0,
|
||||
minHeight = 0,
|
||||
maxHeight = (constraints.maxHeight - collapsingConstraints.minHeight).coerceAtLeast(0),
|
||||
)
|
||||
val bodyPlaceable = measurables[1].measure(bodyConstraints)
|
||||
|
||||
val minHeight = 0.dp.roundToPx()
|
||||
val maxHeight = collapsingPlaceable.height + minHeight
|
||||
|
||||
val offset = state.heightOffset.roundToInt().coerceAtLeast(-maxHeight)
|
||||
|
||||
val width = max(
|
||||
collapsingPlaceable.width,
|
||||
bodyPlaceable.width,
|
||||
).coerceIn(constraints.minWidth, constraints.maxWidth)
|
||||
val height = max(
|
||||
collapsingPlaceable.height,
|
||||
bodyPlaceable.height,
|
||||
).coerceIn(constraints.minHeight, constraints.maxHeight)
|
||||
|
||||
layout(width = width, height = height) {
|
||||
bodyPlaceable.placeRelative(0, collapsingPlaceable.height + offset)
|
||||
collapsingPlaceable.placeRelative(0, offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down.
|
||||
*
|
||||
* @property state The state of the collapsing app bar.
|
||||
* @property snapAnimationSpec The animation spec used for snapping the app bar to its collapsed or
|
||||
* expanded state after a fling. If null, no snapping will occur.
|
||||
* @property flingAnimationSpec The decay animation spec used for fling gestures.
|
||||
* If null, fling gestures will not be handled.
|
||||
* @property nestedScrollConnection Nested scroll connection
|
||||
*/
|
||||
@Stable
|
||||
data class TangemCollapsingAppBarBehavior(
|
||||
val state: TangemCollapsingAppBarState,
|
||||
val snapAnimationSpec: AnimationSpec<Float>?,
|
||||
val flingAnimationSpec: DecayAnimationSpec<Float>?,
|
||||
val nestedScrollConnection: NestedScrollConnection,
|
||||
)
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun TangemCollapsingTopBar_Preview() {
|
||||
TangemThemePreviewRedesign {
|
||||
val collapsingHeight = 200.dp
|
||||
val behavior = rememberTangemExitUntilCollapsedScrollBehavior(
|
||||
expandedHeight = collapsingHeight,
|
||||
)
|
||||
TangemCollapsingTopBar(
|
||||
state = behavior.state,
|
||||
collapsingPart = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(collapsingHeight)
|
||||
.background(Color.Red),
|
||||
)
|
||||
},
|
||||
body = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Blue)
|
||||
.nestedScroll(behavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
package com.tangem.core.ui.ds.topbar.collapsing
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBapScrollDirection
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState
|
||||
import com.tangem.core.ui.utils.toPx
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/**
|
||||
* A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down.
|
||||
* When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state
|
||||
* based on the current collapsed fraction and scroll direction.
|
||||
*
|
||||
* @param expandedHeight The height of the app bar when it is fully expanded.
|
||||
* @param partialCollapsedHeight The height of the app bar when it is partially collapsed.
|
||||
* @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the
|
||||
* user stops scrolling. If null, no snapping will occur.
|
||||
* @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar.
|
||||
* If null, no fling behavior will occur.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberTangemExitUntilCollapsedScrollBehavior(
|
||||
expandedHeight: Dp = -Int.MAX_VALUE.dp,
|
||||
partialCollapsedHeight: Dp = expandedHeight,
|
||||
snapAnimationSpec: AnimationSpec<Float>? = spring(),
|
||||
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay(),
|
||||
): TangemCollapsingAppBarBehavior {
|
||||
val topBarState = rememberTangemCollapsingAppBarState(
|
||||
heightOffsetLimit = -expandedHeight.toPx(),
|
||||
partialHeightLimit = partialCollapsedHeight.toPx(),
|
||||
)
|
||||
return exitUntilCollapsedScrollBehavior(
|
||||
state = topBarState,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
flingAnimationSpec = flingAnimationSpec,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down.
|
||||
* When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state
|
||||
* based on the current collapsed fraction and scroll direction.
|
||||
*
|
||||
* @param state The state of the collapsing app bar, which controls the height offset and scroll behavior.
|
||||
* @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the
|
||||
* user stops scrolling. If null, no snapping will occur.
|
||||
* @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar.
|
||||
* If null, no fling behavior will occur.
|
||||
*/
|
||||
@Composable
|
||||
private fun exitUntilCollapsedScrollBehavior(
|
||||
state: TangemCollapsingAppBarState = rememberTangemCollapsingAppBarState(),
|
||||
snapAnimationSpec: AnimationSpec<Float>? = spring(),
|
||||
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay(),
|
||||
): TangemCollapsingAppBarBehavior {
|
||||
val nestedScrollConnection = remember(state) {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val dy = available.y
|
||||
|
||||
val consume = if (dy < 0) {
|
||||
state.direction = TopBapScrollDirection.Collapsing
|
||||
state.dispatchRawDelta(dy)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
return Offset(0f, consume)
|
||||
}
|
||||
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
val dy = available.y
|
||||
|
||||
val consume = if (dy > 0) {
|
||||
state.direction = TopBapScrollDirection.Expanding
|
||||
state.dispatchRawDelta(dy)
|
||||
} else {
|
||||
state.direction = TopBapScrollDirection.Collapsing
|
||||
0f
|
||||
}
|
||||
|
||||
return Offset(0f, consume)
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
val superConsumed = super.onPostFling(consumed, available)
|
||||
return superConsumed + settleAppBar(
|
||||
state = state,
|
||||
velocity = available.y,
|
||||
flingAnimationSpec = flingAnimationSpec,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return remember(state, nestedScrollConnection, snapAnimationSpec, flingAnimationSpec) {
|
||||
TangemCollapsingAppBarBehavior(
|
||||
state = state,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
flingAnimationSpec = flingAnimationSpec,
|
||||
nestedScrollConnection = nestedScrollConnection,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.snapToExitUntilCollapsed(behavior: TangemCollapsingAppBarBehavior): Modifier {
|
||||
return draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { delta ->
|
||||
behavior.state.heightOffset += delta
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
settleAppBar(
|
||||
state = behavior.state,
|
||||
velocity = velocity,
|
||||
flingAnimationSpec = behavior.flingAnimationSpec,
|
||||
snapAnimationSpec = behavior.snapAnimationSpec,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Settles the app bar to either fully collapsed or fully expanded state
|
||||
* based on the current collapsed fraction and scroll direction.
|
||||
*/
|
||||
@Suppress("MagicNumber", "CyclomaticComplexMethod")
|
||||
private suspend fun settleAppBar(
|
||||
state: TangemCollapsingAppBarState,
|
||||
velocity: Float,
|
||||
flingAnimationSpec: DecayAnimationSpec<Float>?,
|
||||
snapAnimationSpec: AnimationSpec<Float>?,
|
||||
snapCollapseThreshold: Float = 0.3f,
|
||||
snapExpandThreshold: Float = 0.7f,
|
||||
): Velocity {
|
||||
val partialLimit = state.heightOffsetLimit + state.partialHeightLimit
|
||||
var remainingVelocity = velocity
|
||||
|
||||
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
|
||||
// and just return Zero Velocity.
|
||||
// Note that we don't check for 0f due to float precision with the collapsedFraction
|
||||
// calculation.
|
||||
if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
|
||||
return Velocity.Zero
|
||||
}
|
||||
|
||||
// Fling
|
||||
if (flingAnimationSpec != null && velocity.absoluteValue > 1f) {
|
||||
var lastValue = 0f
|
||||
AnimationState(
|
||||
initialValue = 0f,
|
||||
initialVelocity = velocity,
|
||||
).animateDecay(flingAnimationSpec) {
|
||||
val delta = value - lastValue
|
||||
val initialHeightOffset = state.heightOffset
|
||||
|
||||
val availableDelta = partialLimit - initialHeightOffset
|
||||
|
||||
state.heightOffset = if (delta < 0f && initialHeightOffset > partialLimit) {
|
||||
(initialHeightOffset + delta).coerceAtLeast(partialLimit)
|
||||
} else {
|
||||
initialHeightOffset + delta
|
||||
}
|
||||
|
||||
val consumed = abs(initialHeightOffset - state.heightOffset)
|
||||
lastValue = value
|
||||
remainingVelocity = this.velocity
|
||||
// avoid rounding errors and stop if anything is unconsumed
|
||||
if (abs(maxOf(delta, availableDelta) - consumed) > 0.5f) this.cancelAnimation()
|
||||
}
|
||||
}
|
||||
// Snap
|
||||
if (snapAnimationSpec != null && state.heightOffset > partialLimit && state.heightOffset < 0f) {
|
||||
AnimationState(initialValue = state.heightOffset).animateTo(
|
||||
when (state.direction) {
|
||||
TopBapScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) {
|
||||
partialLimit
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
TopBapScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) {
|
||||
0f
|
||||
} else {
|
||||
partialLimit
|
||||
}
|
||||
TopBapScrollDirection.Idle -> 0f
|
||||
},
|
||||
animationSpec = snapAnimationSpec,
|
||||
) {
|
||||
state.heightOffset = value
|
||||
}
|
||||
}
|
||||
return Velocity(0f, remainingVelocity)
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.core.ui.ds.topbar.collapsing.entity
|
||||
|
||||
import androidx.compose.animation.core.AnimationState
|
||||
import androidx.compose.animation.core.animateTo
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.MutatePriority
|
||||
import androidx.compose.foundation.gestures.ScrollScope
|
||||
import androidx.compose.foundation.gestures.ScrollableState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState.Companion.Saver
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* State of the collapsing top app bar.
|
||||
* It contains the current height offset, the limits for collapsing and expanding, and the scroll direction.
|
||||
*
|
||||
* @property initialHeightOffset The initial height offset of the app bar. Default is 0f.
|
||||
* @property heightOffsetLimit The height offset limit for full collapse.
|
||||
* @property partialHeightLimit The height offset limit for partial collapse. Default is the same as [heightOffsetLimit]
|
||||
*/
|
||||
@Stable
|
||||
class TangemCollapsingAppBarState(
|
||||
val initialHeightOffset: Float = 0f,
|
||||
val heightOffsetLimit: Float = 0f,
|
||||
val partialHeightLimit: Float = heightOffsetLimit,
|
||||
) : ScrollableState {
|
||||
|
||||
private val _heightOffset = mutableFloatStateOf(initialHeightOffset)
|
||||
private var deferredConsumption: Float = 0f
|
||||
|
||||
/**
|
||||
* The current height offset of the app bar.
|
||||
* This value is updated as the user scrolls, and is constrained between [heightOffsetLimit] and 0f.
|
||||
*/
|
||||
var heightOffset: Float
|
||||
get() = _heightOffset.floatValue
|
||||
set(newOffset) {
|
||||
_heightOffset.floatValue =
|
||||
newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f)
|
||||
}
|
||||
|
||||
/**
|
||||
* The fraction of the app bar that is collapsed, calculated as the ratio of [heightOffset] to [heightOffsetLimit].
|
||||
*/
|
||||
val collapsedFraction: Float
|
||||
get() =
|
||||
if (heightOffsetLimit != 0f) {
|
||||
heightOffset / heightOffsetLimit
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
/**
|
||||
* The current scroll direction of the app bar, which can be Collapsing, Expanding, or Idle.
|
||||
*/
|
||||
var direction: TopBapScrollDirection = TopBapScrollDirection.Idle
|
||||
|
||||
private val scrollableState = ScrollableState { value ->
|
||||
val consume = if (value < 0) {
|
||||
max(heightOffsetLimit - heightOffset, value)
|
||||
} else {
|
||||
min(0f - heightOffset, value)
|
||||
}
|
||||
|
||||
val current = consume + deferredConsumption
|
||||
val currentInt = current.toInt()
|
||||
|
||||
if (current.absoluteValue > 0) {
|
||||
heightOffset += currentInt
|
||||
deferredConsumption = current - currentInt
|
||||
}
|
||||
|
||||
consume
|
||||
}
|
||||
|
||||
override val isScrollInProgress: Boolean
|
||||
get() = scrollableState.isScrollInProgress
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
suspend fun collapse() {
|
||||
AnimationState(initialValue = heightOffset).animateTo(
|
||||
targetValue = heightOffsetLimit + partialHeightLimit,
|
||||
animationSpec = tween(),
|
||||
) {
|
||||
heightOffset = value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit) =
|
||||
scrollableState.scroll(scrollPriority, block)
|
||||
|
||||
override fun dispatchRawDelta(delta: Float) = scrollableState.dispatchRawDelta(delta)
|
||||
|
||||
companion object {
|
||||
/** The default [Saver] implementation for [TangemCollapsingAppBarState]. */
|
||||
val Saver: Saver<TangemCollapsingAppBarState, *> =
|
||||
listSaver(
|
||||
save = { state -> listOf(state.heightOffsetLimit, state.heightOffset, state.partialHeightLimit) },
|
||||
restore = { state ->
|
||||
TangemCollapsingAppBarState(
|
||||
heightOffsetLimit = state[0],
|
||||
partialHeightLimit = state[2],
|
||||
initialHeightOffset = state[1],
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers and saves the state of the collapsing top app bar across recompositions and configuration changes.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberTangemCollapsingAppBarState(
|
||||
heightOffsetLimit: Float = -Float.MAX_VALUE,
|
||||
partialHeightLimit: Float = -Float.MAX_VALUE,
|
||||
initialHeightOffset: Float = 0f,
|
||||
): TangemCollapsingAppBarState {
|
||||
return rememberSaveable(saver = Saver) {
|
||||
TangemCollapsingAppBarState(
|
||||
initialHeightOffset = initialHeightOffset,
|
||||
partialHeightLimit = partialHeightLimit,
|
||||
heightOffsetLimit = heightOffsetLimit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The scroll direction of the top app bar, which can be Collapsing, Expanding, or Idle.
|
||||
*/
|
||||
enum class TopBapScrollDirection {
|
||||
Collapsing, Expanding, Idle
|
||||
}
|
||||
|
|
@ -100,6 +100,7 @@ fun getActiveIconRes(blockchainId: String): Int {
|
|||
"arbitrum-nova" -> R.drawable.img_arbitrum_nova_22
|
||||
"plasma", "plasma/test" -> R.drawable.img_plasma_22
|
||||
"monad", "monad/test" -> R.drawable.img_monad_22
|
||||
"berachain", "berachain/test" -> R.drawable.img_berachain_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -198,6 +199,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
|
|||
"arbitrum-nova" -> R.drawable.img_arbitrum_nova_22
|
||||
"plasma", "plasma/test" -> R.drawable.img_plasma_22
|
||||
"monad", "monad/test" -> R.drawable.img_monad_22
|
||||
"berachain-bera" -> R.drawable.img_berachain_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -299,6 +301,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
|
|||
"arbitrum-nova" -> R.drawable.ic_arbitrum_nova_22
|
||||
"plasma", "plasma/test" -> R.drawable.ic_plasma_22
|
||||
"monad", "monad/test" -> R.drawable.ic_monad_22
|
||||
"berachain", "berachain/test" -> R.drawable.ic_berachain_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
|
||||
|
|
@ -9,6 +10,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
|
|||
import com.tangem.utils.extensions.isNotWhitespace
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
|
@ -34,6 +36,17 @@ class BigDecimalCryptoFormatFull(
|
|||
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
||||
}
|
||||
|
||||
open class BigDecimalCryptoFormatStyled(
|
||||
val symbol: String,
|
||||
val decimals: Int,
|
||||
val spanStyleReference: SpanStyleReference,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
val shouldIgnoreSymbolPosition: Boolean = false,
|
||||
) : BigDecimalFormatStyled {
|
||||
|
||||
override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value)
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.crypto(
|
||||
|
|
@ -61,6 +74,35 @@ fun BigDecimalFormatScope.crypto(
|
|||
)
|
||||
}
|
||||
|
||||
fun BigDecimalFormatScope.cryptoStyled(
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
spanStyleReference: SpanStyleReference,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalCryptoFormatStyled {
|
||||
return BigDecimalCryptoFormatStyled(
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
spanStyleReference = spanStyleReference,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
fun BigDecimalFormatScope.cryptoStyled(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spanStyleReference: SpanStyleReference,
|
||||
ignoreSymbolPosition: Boolean = false,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalCryptoFormatStyled {
|
||||
return BigDecimalCryptoFormatStyled(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
spanStyleReference = spanStyleReference,
|
||||
shouldIgnoreSymbolPosition = ignoreSymbolPosition,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
|
||||
|
|
@ -88,6 +130,51 @@ fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
|
|||
}
|
||||
}
|
||||
|
||||
fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) =
|
||||
BigDecimalFormatStyled { value ->
|
||||
if (shouldIgnoreSymbolPosition) {
|
||||
val formatter = NumberFormat.getInstance(locale).apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
val formattedAmount = formatter.format(value)
|
||||
|
||||
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
|
||||
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
|
||||
|
||||
combinedReference(
|
||||
stringReference(formattedAmount.take(separatorIndex)),
|
||||
styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference),
|
||||
stringReference(NON_BREAKING_SPACE + symbol),
|
||||
)
|
||||
} else {
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
val formattedAmount = formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
|
||||
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
|
||||
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
|
||||
|
||||
combinedReference(
|
||||
stringReference(formattedAmount.take(separatorIndex)),
|
||||
styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value ->
|
||||
val formatter = if (value.isMoreThanThreshold()) {
|
||||
NumberFormat.getCurrencyInstance(locale).apply {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
||||
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
|
@ -15,8 +17,16 @@ open class BigDecimalFiatFormat(
|
|||
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
open class BigDecimalFiatFormatStyled(
|
||||
val fiatCurrencyCode: String,
|
||||
val fiatCurrencySymbol: String,
|
||||
val spanStyleReference: SpanStyleReference,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormatStyled {
|
||||
override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value)
|
||||
}
|
||||
|
||||
//region == Initializers ==
|
||||
fun BigDecimalFormatScope.fiat(
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
|
|
@ -29,7 +39,20 @@ fun BigDecimalFormatScope.fiat(
|
|||
)
|
||||
}
|
||||
|
||||
// == Formatters ==
|
||||
fun BigDecimalFormatScope.fiat(
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
spanStyleReference: SpanStyleReference,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalFiatFormatStyled {
|
||||
return BigDecimalFiatFormatStyled(
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
spanStyleReference = spanStyleReference,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
// endregion == Formatters ==
|
||||
|
||||
/**
|
||||
* Formats fiat amount with default precision.
|
||||
|
|
@ -58,6 +81,38 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat {
|
|||
}
|
||||
}
|
||||
|
||||
fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value ->
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
val formattingAmount = if (value.isLessThanThreshold()) {
|
||||
FIAT_FORMAT_THRESHOLD
|
||||
} else {
|
||||
value
|
||||
}
|
||||
|
||||
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
|
||||
val formattedAmount = formatter.format(formattingAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
|
||||
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
|
||||
|
||||
val wholePart = formattedAmount.take(separatorIndex)
|
||||
val fractionalPart = formattedAmount.drop(separatorIndex)
|
||||
|
||||
combinedReference(
|
||||
if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY,
|
||||
stringReference(wholePart),
|
||||
styledStringReference(fractionalPart, spanStyleReference),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats fiat amount with default precision and adds tilde sign
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,17 +1,27 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface BigDecimalFormatScope {
|
||||
companion object { val Empty = object : BigDecimalFormatScope {} }
|
||||
companion object {
|
||||
val Empty = object : BigDecimalFormatScope {}
|
||||
}
|
||||
}
|
||||
|
||||
fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope
|
||||
|
||||
fun interface BigDecimalFormatStyled : (BigDecimal) -> TextReference, BigDecimalFormatScope
|
||||
|
||||
inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String {
|
||||
return BigDecimalFormatScope.Empty.block()(this)
|
||||
}
|
||||
|
||||
inline fun BigDecimal.formatStyled(block: BigDecimalFormatScope.() -> BigDecimalFormatStyled): TextReference {
|
||||
return BigDecimalFormatScope.Empty.block()(this)
|
||||
}
|
||||
|
||||
inline fun BigDecimal?.format(
|
||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||
block: BigDecimalFormatScope.() -> BigDecimalFormat,
|
||||
|
|
@ -20,10 +30,26 @@ inline fun BigDecimal?.format(
|
|||
return BigDecimalFormatScope.Empty.block()(this)
|
||||
}
|
||||
|
||||
inline fun BigDecimal?.formatStyled(
|
||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||
block: BigDecimalFormatScope.() -> BigDecimalFormatStyled,
|
||||
): TextReference {
|
||||
if (this == null) return stringReference(fallbackString)
|
||||
return BigDecimalFormatScope.Empty.block()(this)
|
||||
}
|
||||
|
||||
fun BigDecimal?.format(
|
||||
format: BigDecimalFormat,
|
||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||
): String {
|
||||
if (this == null) return fallbackString
|
||||
return format(this)
|
||||
}
|
||||
|
||||
fun BigDecimal?.format(
|
||||
format: BigDecimalFormatStyled,
|
||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||
): TextReference {
|
||||
if (this == null) return stringReference(fallbackString)
|
||||
return format(this)
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ object TangemColorPalette {
|
|||
val Dark4 = Color(0xFF3B3B3B)
|
||||
val Dark5 = Color(0xFF303030)
|
||||
val Dark6 = Color(0xFF1E1E1E)
|
||||
val Dark7 = Color(0xFF171717)
|
||||
// endregion Dark
|
||||
|
||||
// region Dark Alpha
|
||||
|
|
@ -58,20 +59,45 @@ object TangemColorPalette {
|
|||
val DarkGreen = Color(0xFF06311F)
|
||||
// endregion Green
|
||||
|
||||
// region Blue
|
||||
// region Azure
|
||||
val Azure = Color(0xFF0099FF)
|
||||
// endregion Blue
|
||||
val Azure_50 = Color(0x800099FF)
|
||||
val Azure_10 = Color(0x1A0099FF)
|
||||
// endregion Azure
|
||||
|
||||
// region Red
|
||||
// region Amaranth
|
||||
val Amaranth = Color(0xFFFF3333)
|
||||
val Amaranth_50 = Color(0x80FF3333)
|
||||
val Amaranth_20 = Color(0x33FF3333)
|
||||
val Amaranth_10 = Color(0x1AFF3333)
|
||||
// endregion Amaranth
|
||||
|
||||
// region Flamingo
|
||||
val Flamingo = Color(0xFFFF5B5B)
|
||||
// endregion Red
|
||||
val Flamingo_50 = Color(0x80FF5B5B)
|
||||
val Flamingo_20 = Color(0x33FF5B5B)
|
||||
val Flamingo_10 = Color(0x1AFF5B5B)
|
||||
// endregion Flamingo
|
||||
|
||||
// region Yellow
|
||||
val Tangerine = Color(0xFFFFB71B)
|
||||
val Mustard = Color(0xFFFDDE55)
|
||||
// endregion Yellow
|
||||
|
||||
// region Emerald
|
||||
val Emerald = Color(0xFF34DF12)
|
||||
val Emerald_50 = Color(0x8034DF12)
|
||||
val Emerald_20 = Color(0x3334DF12)
|
||||
val Emerald_10 = Color(0x1A34DF12)
|
||||
// endregion Emerald
|
||||
|
||||
// region Eucalyptus
|
||||
val Eucalyptus = Color(0xFF0C9F3D)
|
||||
val Eucalyptus_50 = Color(0x800C9F3D)
|
||||
val Eucalyptus_20 = Color(0x330C9F3D)
|
||||
val Eucalyptus_10 = Color(0x1A0C9F3D)
|
||||
// endregion Eucalyptus
|
||||
|
||||
// region Overlay
|
||||
val Overlay1 = Color(0x66000000)
|
||||
val Overlay2 = Color(0xB2000000)
|
||||
|
|
|
|||
|
|
@ -326,7 +326,10 @@ class TangemColors2 internal constructor(
|
|||
class Border internal constructor(
|
||||
val neutral: Neutral,
|
||||
val status: Status,
|
||||
walletIcon: Color,
|
||||
) {
|
||||
var walletIcon by mutableStateOf(walletIcon)
|
||||
private set
|
||||
|
||||
@Stable
|
||||
class Neutral internal constructor(
|
||||
|
|
@ -367,6 +370,7 @@ class TangemColors2 internal constructor(
|
|||
fun update(other: Border) {
|
||||
neutral.update(other.neutral)
|
||||
status.update(other.status)
|
||||
walletIcon = other.walletIcon
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -448,24 +452,36 @@ class TangemColors2 internal constructor(
|
|||
|
||||
@Stable
|
||||
class Markers internal constructor(
|
||||
backgroundSolidGray: Color,
|
||||
backgroundDisabled: Color,
|
||||
backgroundSolidBlue: Color,
|
||||
textGray: Color,
|
||||
textDisabled: Color,
|
||||
iconGray: Color,
|
||||
iconDisabled: Color,
|
||||
backgroundDisabled: Color,
|
||||
textGray: Color,
|
||||
iconGray: Color,
|
||||
borderGray: Color,
|
||||
backgroundTintedBlue: Color,
|
||||
backgroundSolidGray: Color,
|
||||
backgroundTintedGray: Color,
|
||||
textBlue: Color,
|
||||
iconBlue: Color,
|
||||
borderTintedBlue: Color,
|
||||
backgroundSolidBlue: Color,
|
||||
backgroundTintedBlue: Color,
|
||||
textRed: Color,
|
||||
iconRed: Color,
|
||||
borderTintedRed: Color,
|
||||
backgroundSolidRed: Color,
|
||||
backgroundTintedRed: Color,
|
||||
iconBlue: Color,
|
||||
iconRed: Color,
|
||||
textRed: Color,
|
||||
backgroundTintedGray: Color,
|
||||
borderTintedBlue: Color,
|
||||
borderTintedRed: Color,
|
||||
textGreen: Color,
|
||||
iconGreen: Color,
|
||||
borderTintedGreen: Color,
|
||||
borderSolidColor: Color,
|
||||
backgroundTintedGreen: Color,
|
||||
backgroundSolidGreen: Color,
|
||||
textGreenAlt: Color,
|
||||
iconGreenAlt: Color,
|
||||
borderTintedGreenAlt: Color,
|
||||
borderSolidColorAlt: Color,
|
||||
backgroundTintedGreenAlt: Color,
|
||||
backgroundSolidGreenAlt: Color,
|
||||
) {
|
||||
var backgroundSolidGray by mutableStateOf(backgroundSolidGray)
|
||||
private set
|
||||
|
|
@ -504,6 +520,32 @@ class TangemColors2 internal constructor(
|
|||
var borderTintedRed by mutableStateOf(borderTintedRed)
|
||||
private set
|
||||
|
||||
var textGreen by mutableStateOf(textGreen)
|
||||
private set
|
||||
var iconGreen by mutableStateOf(iconGreen)
|
||||
private set
|
||||
var borderTintedGreen by mutableStateOf(borderTintedGreen)
|
||||
private set
|
||||
var borderSolidColor by mutableStateOf(borderSolidColor)
|
||||
private set
|
||||
var backgroundTintedGreen by mutableStateOf(backgroundTintedGreen)
|
||||
private set
|
||||
var backgroundSolidGreen by mutableStateOf(backgroundSolidGreen)
|
||||
private set
|
||||
var textGreenAlt by mutableStateOf(textGreenAlt)
|
||||
private set
|
||||
var iconGreenAlt by mutableStateOf(iconGreenAlt)
|
||||
private set
|
||||
var borderTintedGreenAlt by mutableStateOf(borderTintedGreenAlt)
|
||||
private set
|
||||
var borderSolidColorAlt by mutableStateOf(borderSolidColorAlt)
|
||||
private set
|
||||
|
||||
var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreenAlt)
|
||||
private set
|
||||
var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreenAlt)
|
||||
private set
|
||||
|
||||
fun update(other: Markers) {
|
||||
backgroundSolidGray = other.backgroundSolidGray
|
||||
backgroundDisabled = other.backgroundDisabled
|
||||
|
|
@ -523,6 +565,18 @@ class TangemColors2 internal constructor(
|
|||
backgroundTintedGray = other.backgroundTintedGray
|
||||
borderTintedBlue = other.borderTintedBlue
|
||||
borderTintedRed = other.borderTintedRed
|
||||
textGreen = other.textGreen
|
||||
iconGreen = other.iconGreen
|
||||
borderTintedGreen = other.borderTintedGreen
|
||||
borderSolidColor = other.borderSolidColor
|
||||
backgroundTintedGreen = other.backgroundTintedGreen
|
||||
backgroundSolidGreen = other.backgroundSolidGreen
|
||||
textGreenAlt = other.textGreenAlt
|
||||
iconGreenAlt = other.iconGreenAlt
|
||||
borderTintedGreenAlt = other.borderTintedGreenAlt
|
||||
borderSolidColorAlt = other.borderSolidColorAlt
|
||||
backgroundTintedGreenAlt = other.backgroundTintedGreenAlt
|
||||
backgroundSolidGreenAlt = other.backgroundSolidGreenAlt
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ private fun lightThemeColors2(): TangemColors2 {
|
|||
warning = TangemColorPalette.Amaranth,
|
||||
attention = TangemColorPalette.Tangerine,
|
||||
),
|
||||
walletIcon = TangemColorPalette.Dark_10,
|
||||
)
|
||||
val overlay = TangemColors2.Overlay(
|
||||
overlayPrimary = TangemColorPalette.Overlay1,
|
||||
|
|
@ -122,8 +123,8 @@ private fun lightThemeColors2(): TangemColors2 {
|
|||
val surface = TangemColors2.Surface(
|
||||
level1 = TangemColorPalette.White,
|
||||
level2 = TangemColorPalette.Light1V2,
|
||||
level3 = TangemColorPalette.Light1V2,
|
||||
level4 = TangemColorPalette.White,
|
||||
level3 = TangemColorPalette.White,
|
||||
level4 = TangemColorPalette.Light1V2,
|
||||
)
|
||||
val controls = TangemColors2.Controls(
|
||||
backgroundChecked = TangemColorPalette.Dark6,
|
||||
|
|
@ -154,16 +155,28 @@ private fun lightThemeColors2(): TangemColors2 {
|
|||
iconGray = TangemColorPalette.Dark1,
|
||||
iconDisabled = TangemColorPalette.Light2,
|
||||
borderGray = TangemColorPalette.Light3,
|
||||
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
|
||||
backgroundTintedBlue = TangemColorPalette.Azure_10,
|
||||
textBlue = text.status.accent,
|
||||
backgroundSolidRed = TangemColorPalette.Amaranth,
|
||||
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
||||
backgroundTintedRed = TangemColorPalette.Amaranth_10,
|
||||
iconBlue = TangemColorPalette.Azure,
|
||||
iconRed = TangemColorPalette.Amaranth,
|
||||
textRed = TangemColorPalette.Amaranth,
|
||||
backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f),
|
||||
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
|
||||
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
||||
borderTintedBlue = TangemColorPalette.Azure_10,
|
||||
borderTintedRed = TangemColorPalette.Amaranth_10,
|
||||
textGreen = TangemColorPalette.Emerald,
|
||||
iconGreen = TangemColorPalette.Emerald,
|
||||
borderTintedGreen = TangemColorPalette.Emerald_10,
|
||||
borderSolidColor = TangemColorPalette.Emerald_50,
|
||||
backgroundTintedGreen = TangemColorPalette.Emerald_10,
|
||||
backgroundSolidGreen = TangemColorPalette.Emerald,
|
||||
textGreenAlt = TangemColorPalette.Eucalyptus,
|
||||
iconGreenAlt = TangemColorPalette.Eucalyptus,
|
||||
borderTintedGreenAlt = TangemColorPalette.Eucalyptus_10,
|
||||
borderSolidColorAlt = TangemColorPalette.Eucalyptus_50,
|
||||
backgroundTintedGreenAlt = TangemColorPalette.Eucalyptus_10,
|
||||
backgroundSolidGreenAlt = TangemColorPalette.Eucalyptus,
|
||||
)
|
||||
val tabs = TangemColors2.Tabs(
|
||||
textPrimary = TangemColorPalette.Light2,
|
||||
|
|
@ -235,6 +248,7 @@ private fun darkThemeColors2(): TangemColors2 {
|
|||
warning = TangemColorPalette.Flamingo,
|
||||
attention = TangemColorPalette.Mustard,
|
||||
),
|
||||
walletIcon = TangemColorPalette.Light_10,
|
||||
)
|
||||
val overlay = TangemColors2.Overlay(
|
||||
overlayPrimary = TangemColorPalette.Overlay1,
|
||||
|
|
@ -270,8 +284,8 @@ private fun darkThemeColors2(): TangemColors2 {
|
|||
borderPrimary = TangemColorPalette.Light4,
|
||||
)
|
||||
val surface = TangemColors2.Surface(
|
||||
level1 = TangemColorPalette.Dark6,
|
||||
level2 = TangemColorPalette.Black,
|
||||
level1 = TangemColorPalette.Black,
|
||||
level2 = TangemColorPalette.Dark7,
|
||||
level3 = TangemColorPalette.Dark6,
|
||||
level4 = TangemColorPalette.Dark5,
|
||||
)
|
||||
|
|
@ -304,7 +318,7 @@ private fun darkThemeColors2(): TangemColors2 {
|
|||
iconGray = TangemColorPalette.Dark2,
|
||||
iconDisabled = TangemColorPalette.Dark5,
|
||||
borderGray = TangemColorPalette.White.copy(alpha = 0.2f),
|
||||
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
|
||||
backgroundTintedBlue = TangemColorPalette.Azure_10,
|
||||
textBlue = text.status.accent,
|
||||
backgroundSolidRed = TangemColorPalette.Amaranth,
|
||||
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
||||
|
|
@ -312,8 +326,20 @@ private fun darkThemeColors2(): TangemColors2 {
|
|||
iconRed = TangemColorPalette.Flamingo,
|
||||
textRed = TangemColorPalette.Flamingo,
|
||||
backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f),
|
||||
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
|
||||
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
||||
borderTintedBlue = TangemColorPalette.Azure_10,
|
||||
borderTintedRed = TangemColorPalette.Amaranth_10,
|
||||
textGreen = TangemColorPalette.Emerald,
|
||||
iconGreen = TangemColorPalette.Emerald,
|
||||
borderTintedGreen = TangemColorPalette.Emerald_10,
|
||||
borderSolidColor = TangemColorPalette.Emerald_50,
|
||||
backgroundTintedGreen = TangemColorPalette.Emerald_10,
|
||||
backgroundSolidGreen = TangemColorPalette.Emerald,
|
||||
textGreenAlt = TangemColorPalette.Emerald,
|
||||
iconGreenAlt = TangemColorPalette.Emerald,
|
||||
borderTintedGreenAlt = TangemColorPalette.Emerald_10,
|
||||
borderSolidColorAlt = TangemColorPalette.Emerald_50,
|
||||
backgroundTintedGreenAlt = TangemColorPalette.Emerald_10,
|
||||
backgroundSolidGreenAlt = TangemColorPalette.Emerald,
|
||||
)
|
||||
val tabs = TangemColors2.Tabs(
|
||||
textPrimary = TangemColorPalette.Dark4,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemeRedesign
|
||||
|
||||
/**
|
||||
* Interface representing a Compose screen with common theming and content composition properties.
|
||||
|
|
@ -61,7 +62,9 @@ internal fun ComposeScreen.createComposeView(
|
|||
uiDependencies = uiDependencies,
|
||||
overrideSystemBarColors = overrideSystemBarColors,
|
||||
) {
|
||||
ScreenContent(modifier = screenModifier)
|
||||
TangemThemeRedesign {
|
||||
ScreenContent(modifier = screenModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.core.ui.shader
|
||||
|
||||
class GlossyShader : TangemShader {
|
||||
override val sksl: String =
|
||||
"""
|
||||
// The MIT License
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
uniform float uTime;
|
||||
uniform vec3 uResolution;
|
||||
|
||||
vec4 main( vec2 fragCoord )
|
||||
{
|
||||
float mr = min(uResolution.x, uResolution.y);
|
||||
vec2 uv = (fragCoord * 2.0 - uResolution.xy) / mr;
|
||||
|
||||
float d = -uTime * 0.5;
|
||||
float a = 0.0;
|
||||
for (float i = 0.0; i < 8.0; ++i) {
|
||||
a += cos(i - d - a * uv.x);
|
||||
d += sin(uv.y * i + a);
|
||||
}
|
||||
d += uTime * 0.5;
|
||||
vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);
|
||||
col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5);
|
||||
return vec4(col,1.0);
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
package com.tangem.core.ui.shader
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.shader.runtime.RuntimeEffect
|
||||
|
||||
/**
|
||||
* A shader that creates a colorful, flowing "northern lights" effect.
|
||||
* @param colors The colors to display. The last provided color acts like a "background"
|
||||
* @param speed Adjust the speed of the movement
|
||||
* @param scale Adjusts the scale of the board. Higher number -> larger billboard -> smaller color blobs
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class NorthernLightsMeshGradientShader(
|
||||
colors: Array<Color>,
|
||||
speed: Float = 1f,
|
||||
scale: Float = 2f,
|
||||
) : TangemShader {
|
||||
|
||||
private val colorCount = colors.size
|
||||
private val colorUniforms = colors.flatMap {
|
||||
listOf(it.red, it.green, it.blue)
|
||||
}.toTypedArray().toFloatArray()
|
||||
private val ambientUniform = FloatArray(3)
|
||||
|
||||
init {
|
||||
recomputeAmbient()
|
||||
}
|
||||
|
||||
override val sksl = """
|
||||
uniform float uTime;
|
||||
uniform vec3 uResolution;
|
||||
uniform vec3 uAmbient;
|
||||
|
||||
const int MAX_COLORS = $colorCount;
|
||||
uniform vec3 uColor[MAX_COLORS];
|
||||
|
||||
// Simplex 3D Noise
|
||||
// by Ian McEwan, Ashima Arts
|
||||
// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
|
||||
//
|
||||
vec4 permute(vec4 x) {
|
||||
return mod(((x * 34.0) + 1.0) * x, 289.0);
|
||||
}
|
||||
vec4 taylorInvSqrt(vec4 r) {
|
||||
return 1.79284291400159 - 0.85373472095314 * r;
|
||||
}
|
||||
|
||||
float snoise(vec3 v) {
|
||||
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
|
||||
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
||||
|
||||
// First corner
|
||||
vec3 i = floor(v + dot(v, C.yyy));
|
||||
vec3 x0 = v - i + dot(i, C.xxx);
|
||||
|
||||
// Other corners
|
||||
vec3 g = step(x0.yzx, x0.xyz);
|
||||
vec3 l = 1.0 - g;
|
||||
vec3 i1 = min(g.xyz, l.zxy);
|
||||
vec3 i2 = max(g.xyz, l.zxy);
|
||||
|
||||
// x0 = x0 - 0. + 0.0 * C
|
||||
vec3 x1 = x0 - i1 + 1.0 * C.xxx;
|
||||
vec3 x2 = x0 - i2 + 2.0 * C.xxx;
|
||||
vec3 x3 = x0 - 1. + 3.0 * C.xxx;
|
||||
|
||||
// Permutations
|
||||
i = mod(i, 289.0);
|
||||
vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
|
||||
|
||||
// Gradients
|
||||
// ( N*N points uniformly over a square, mapped onto an octahedron.)
|
||||
float n_ = 1.0 / 7.0; // N=7
|
||||
vec3 ns = n_ * D.wyz - D.xzx;
|
||||
|
||||
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N)
|
||||
|
||||
vec4 x_ = floor(j * ns.z);
|
||||
vec4 y_ = floor(j - 7.0 * x_); // mod(j,N)
|
||||
|
||||
vec4 x = x_ * ns.x + ns.yyyy;
|
||||
vec4 y = y_ * ns.x + ns.yyyy;
|
||||
vec4 h = 1.0 - abs(x) - abs(y);
|
||||
|
||||
vec4 b0 = vec4(x.xy, y.xy);
|
||||
vec4 b1 = vec4(x.zw, y.zw);
|
||||
|
||||
vec4 s0 = floor(b0) * 2.0 + 1.0;
|
||||
vec4 s1 = floor(b1) * 2.0 + 1.0;
|
||||
vec4 sh = -step(h, vec4(0.0));
|
||||
|
||||
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
|
||||
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
|
||||
|
||||
vec3 p0 = vec3(a0.xy, h.x);
|
||||
vec3 p1 = vec3(a0.zw, h.y);
|
||||
vec3 p2 = vec3(a1.xy, h.z);
|
||||
vec3 p3 = vec3(a1.zw, h.w);
|
||||
|
||||
//Normalise gradients
|
||||
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
|
||||
p0 *= norm.x;
|
||||
p1 *= norm.y;
|
||||
p2 *= norm.z;
|
||||
p3 *= norm.w;
|
||||
|
||||
// Mix final noise value
|
||||
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
|
||||
m = m * m;
|
||||
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
|
||||
}
|
||||
|
||||
vec4 main( vec2 fragCoord ) {
|
||||
float mr = min(uResolution.x, uResolution.y);
|
||||
vec2 uv = (fragCoord * $scale - uResolution.xy) / mr;
|
||||
|
||||
vec2 base = uv / 2;
|
||||
|
||||
vec3 vColor = uColor[MAX_COLORS - 1];
|
||||
|
||||
const vec2 frequency = vec2(0.7, 0.3);
|
||||
const float noiseFloor = 0.00001;
|
||||
float t = uTime * 0.005;
|
||||
|
||||
for(int i = 0; i < MAX_COLORS - 1; i++) {
|
||||
float fi = float(i);
|
||||
float flow = 5. + fi * 0.3;
|
||||
float speed = 6. * $speed + fi * 0.3;
|
||||
float seed = 1. + fi * 4.;
|
||||
float noiseCeil = 0.6 + fi * 0.07;
|
||||
|
||||
float noise = smoothstep(noiseFloor, noiseCeil, snoise(vec3(base.x * frequency.x, base.y * frequency.y - t * flow, t * speed + seed)));
|
||||
|
||||
vColor = mix(vColor, uColor[i], noise);
|
||||
}
|
||||
|
||||
vColor = max(vColor, uAmbient);
|
||||
|
||||
// Elliptical falloff centred at the very top of the screen.
|
||||
// Using fragCoord directly (pixels) and uResolution for screen size.
|
||||
// Horizontal radius ~ 80 % of screen width → wide enough to cover corners.
|
||||
// Vertical radius ~ 45 % of screen height → controls how far down the glow reaches.
|
||||
vec2 topCenter = vec2(uResolution.x * 0.5, 0.0);
|
||||
vec2 delta = fragCoord - topCenter;
|
||||
vec2 radii = vec2(uResolution.x * 0.9, uResolution.y * 0.65);
|
||||
float normDist = length(delta / radii);
|
||||
float alpha = pow(1.0 - smoothstep(0.0, 1.0, normDist), 1.5);
|
||||
|
||||
// Pre-multiplied alpha so the shader composites correctly over the dark background.
|
||||
return vec4(vColor * alpha, alpha);
|
||||
}
|
||||
"""
|
||||
|
||||
/** Updates the animated colors in-place without recreating the shader. */
|
||||
fun updateColors(colors: Array<Color>) {
|
||||
colors.forEachIndexed { i, color ->
|
||||
colorUniforms[i * 3 + 0] = color.red
|
||||
colorUniforms[i * 3 + 1] = color.green
|
||||
colorUniforms[i * 3 + 2] = color.blue
|
||||
}
|
||||
recomputeAmbient()
|
||||
}
|
||||
|
||||
private fun recomputeAmbient() {
|
||||
val count = colorCount - 1
|
||||
var r = 0f
|
||||
var g = 0f
|
||||
var b = 0f
|
||||
for (i in 0 until count) {
|
||||
r += colorUniforms[i * 3]
|
||||
g += colorUniforms[i * 3 + 1]
|
||||
b += colorUniforms[i * 3 + 2]
|
||||
}
|
||||
val scale = 0.5f / count
|
||||
ambientUniform[0] = r * scale
|
||||
ambientUniform[1] = g * scale
|
||||
ambientUniform[2] = b * scale
|
||||
}
|
||||
|
||||
override fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) {
|
||||
super.applyUniforms(runtimeEffect = runtimeEffect, time = time, width = width, height = height)
|
||||
|
||||
runtimeEffect.setFloatUniform(name = "uColor", values = colorUniforms)
|
||||
runtimeEffect.setFloatUniform(
|
||||
name = "uAmbient",
|
||||
value1 = ambientUniform[0],
|
||||
value2 = ambientUniform[1],
|
||||
value3 = ambientUniform[2],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.core.ui.shader
|
||||
|
||||
import com.tangem.core.ui.shader.runtime.RuntimeEffect
|
||||
|
||||
interface TangemShader {
|
||||
val speedModifier: Float
|
||||
get() = 0.5f
|
||||
|
||||
val sksl: String
|
||||
|
||||
/** Applies the uniforms required for this shader to the effect */
|
||||
fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) {
|
||||
runtimeEffect.setFloatUniform(name = "uResolution", value1 = width, value2 = height, value3 = width / height)
|
||||
runtimeEffect.setFloatUniform(name = "uTime", value1 = time)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.core.ui.shader.runtime
|
||||
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
internal class FallbackRuntimeEffect : RuntimeEffect {
|
||||
override val isSupported: Boolean = false
|
||||
override val isReady: Boolean = false
|
||||
|
||||
override fun build(): Brush {
|
||||
return Brush.horizontalGradient(listOf(Color.White, Color.White))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.core.ui.shader.runtime
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import com.tangem.core.ui.shader.TangemShader
|
||||
|
||||
interface RuntimeEffect {
|
||||
|
||||
val isSupported: Boolean
|
||||
val isReady: Boolean
|
||||
|
||||
/** Sets a float array uniform for this shader */
|
||||
fun setFloatUniform(name: String, value1: Float) {}
|
||||
|
||||
/** Sets a float array uniform for this shader */
|
||||
fun setFloatUniform(name: String, value1: Float, value2: Float) {}
|
||||
|
||||
/** Sets a float array uniform for this shader */
|
||||
fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {}
|
||||
|
||||
/** Sets a float array uniform for this shader */
|
||||
fun setFloatUniform(name: String, values: FloatArray) {}
|
||||
|
||||
fun update(shader: TangemShader, time: Float, width: Float, height: Float) {}
|
||||
|
||||
fun build(): Brush
|
||||
}
|
||||
|
||||
internal fun buildEffect(shader: TangemShader): RuntimeEffect {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
RuntimeShaderEffect(shader)
|
||||
} else {
|
||||
FallbackRuntimeEffect()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.core.ui.shader.runtime
|
||||
|
||||
import android.graphics.RuntimeShader
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import com.tangem.core.ui.shader.TangemShader
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
internal class RuntimeShaderEffect(tangemShader: TangemShader) : RuntimeEffect {
|
||||
private val compositeRuntimeEffect = RuntimeShader(tangemShader.sksl)
|
||||
|
||||
override val isSupported: Boolean = true
|
||||
override var isReady: Boolean = false
|
||||
|
||||
override fun setFloatUniform(name: String, value1: Float) {
|
||||
compositeRuntimeEffect.setFloatUniform(name, value1)
|
||||
}
|
||||
|
||||
override fun setFloatUniform(name: String, value1: Float, value2: Float) {
|
||||
compositeRuntimeEffect.setFloatUniform(name, value1, value2)
|
||||
}
|
||||
|
||||
override fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {
|
||||
compositeRuntimeEffect.setFloatUniform(name, value1, value2, value3)
|
||||
}
|
||||
|
||||
override fun setFloatUniform(name: String, values: FloatArray) {
|
||||
compositeRuntimeEffect.setFloatUniform(name, values)
|
||||
}
|
||||
|
||||
override fun update(shader: TangemShader, time: Float, width: Float, height: Float) {
|
||||
shader.applyUniforms(runtimeEffect = this, time = time, width = width, height = height)
|
||||
isReady = width > 0 && height > 0
|
||||
}
|
||||
|
||||
override fun build(): Brush {
|
||||
return ShaderBrush(compositeRuntimeEffect)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object AppBarWithSearchTestTags {
|
||||
const val SEARCH_ICON = "APP_BAR_WITH_SEARCH_SEARCH_ICON"
|
||||
const val TEXT_FIELD = "APP_BAR_WITH_SEARCH_TEXT_FIELD"
|
||||
}
|
||||
|
|
@ -16,5 +16,7 @@ object SwapTokenScreenTestTags {
|
|||
const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON"
|
||||
const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON"
|
||||
const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT"
|
||||
const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT"
|
||||
const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON"
|
||||
const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
9
core/ui/src/main/res/drawable/ic_arrow_back_28.xml
Normal file
9
core/ui/src/main/res/drawable/ic_arrow_back_28.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:pathData="M15.631,7.455C16.024,7.067 16.657,7.071 17.045,7.464C17.433,7.857 17.429,8.491 17.036,8.879L11.851,14L17.036,19.122C17.429,19.51 17.433,20.144 17.045,20.537C16.657,20.93 16.024,20.933 15.631,20.545L9.848,14.833C9.384,14.375 9.384,13.626 9.848,13.168L15.631,7.455Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
15
core/ui/src/main/res/drawable/ic_berachain_22.xml
Normal file
15
core/ui/src/main/res/drawable/ic_berachain_22.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M14.1523,13.2142V12.3734C14.6582,12.1959 15.0266,11.6448 15.0266,10.9924C15.0266,10.34 14.6582,9.7873 14.1523,9.6113V8.7705C14.6287,8.6039 14.984,8.1041 15.0233,7.5H14.4438V7.7569C14.4438,7.9547 14.3259,8.129 14.1523,8.2162V8.0963C14.1523,7.9204 14.0017,7.7772 13.8167,7.7772H13.7873C13.6023,7.7772 13.4516,7.9204 13.4516,8.0963V8.2162C13.2781,8.129 13.1602,7.9562 13.1602,7.7569V7.5H12.5807C12.62,8.1041 12.9752,8.6039 13.4516,8.7705V9.6113C12.9474,9.7873 12.5774,10.3384 12.5774,10.9924C12.5774,11.6463 12.9457,12.1975 13.4516,12.3734V13.2142C12.9752,13.3808 12.62,13.8807 12.5807,14.4847H13.1602V14.2278C13.1602,14.0301 13.2781,13.8557 13.4516,13.7685V13.8884C13.4516,14.0644 13.6023,14.2076 13.7873,14.2076H13.8167C14.0017,14.2076 14.1523,14.0644 14.1523,13.8884V13.7685C14.3259,13.8557 14.4438,14.0285 14.4438,14.2278V14.4847H15.0233C14.984,13.8807 14.6287,13.3808 14.1523,13.2142ZM13.1602,11.3598V10.6234C13.1602,10.4256 13.2781,10.2512 13.4516,10.164V10.2839C13.4516,10.4599 13.6023,10.6031 13.7873,10.6031H13.8167C14.0017,10.6031 14.1523,10.4599 14.1523,10.2839V10.164C14.3259,10.2512 14.4438,10.4241 14.4438,10.6234V11.3598C14.4438,11.5576 14.3259,11.732 14.1523,11.8191V11.6993C14.1523,11.5233 14.0017,11.3801 13.8167,11.3801H13.7873C13.6023,11.3801 13.4516,11.5233 13.4516,11.6993V11.8191C13.2797,11.732 13.1602,11.5591 13.1602,11.3598Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M17.7083,11.4392V10.544C18.2142,10.3665 18.5826,9.8153 18.5826,9.1629C18.5826,8.5105 18.2142,7.9578 17.7083,7.7818V7.5H17.0092V7.7818C16.5033,7.9593 16.135,8.5105 16.135,9.1629C16.135,9.8153 16.5033,10.368 17.0092,10.544V11.4392C16.5033,11.6167 16.135,12.1679 16.135,12.8203C16.135,13.4727 16.5033,14.0254 17.0092,14.2014V14.4832H17.7083V14.2014C18.2142,14.0239 18.5826,13.4727 18.5826,12.8203C18.5826,12.1679 18.2142,11.6167 17.7083,11.4392ZM16.7162,9.5303V8.7939C16.7162,8.5961 16.8341,8.4218 17.0076,8.3346V8.4545C17.0076,8.6304 17.1583,8.7736 17.3432,8.7736H17.3711C17.5561,8.7736 17.7067,8.6304 17.7067,8.4545V8.3346C17.8802,8.4218 17.9981,8.5946 17.9981,8.7939V9.5303C17.9981,9.7281 17.8802,9.9025 17.7067,9.9897V9.8698C17.7067,9.6938 17.5561,9.5506 17.3711,9.5506H17.3416C17.1566,9.5506 17.006,9.6938 17.006,9.8698V9.9897C16.8357,9.9025 16.7162,9.7296 16.7162,9.5303ZM17.9997,13.1893C17.9997,13.3871 17.8819,13.5615 17.7083,13.6486V13.4727C17.7083,13.2968 17.5577,13.1535 17.3727,13.1535H17.3449C17.1599,13.1535 17.0092,13.2968 17.0092,13.4727V13.6486C16.8357,13.5615 16.7178,13.3886 16.7178,13.1893V12.4529C16.7178,12.2551 16.8357,12.0807 17.0092,11.9935V12.0574C17.0092,12.2333 17.1599,12.3766 17.3449,12.3766H17.3744C17.5593,12.3766 17.7099,12.2333 17.7099,12.0574V11.9935C17.8835,12.0807 18.0014,12.2536 18.0014,12.4529V13.1893H17.9997Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M10.1463,10.2798C10.1397,10.244 10.1316,10.2098 10.1185,10.1771C10.1234,10.1693 11.2104,8.729 10.2036,7.8384C9.1984,6.9478 8.0245,8.1094 8.018,8.1156C7.8313,8.0595 7.643,8.0253 7.4564,8.0097C7.4564,8.0097 7.4564,8.0097 7.4548,8.0097C7.0717,7.949 6.5347,8.0097 6.5347,8.0097C6.3497,8.0253 6.163,8.0595 5.978,8.114C5.9715,8.1078 4.7976,6.9463 3.7924,7.8369C2.7872,8.7275 3.8726,10.1693 3.8775,10.1755C3.8661,10.2098 3.8562,10.244 3.8497,10.2783C3.7416,10.8948 3,11.0848 3,12.1591C3,13.2537 3.7744,14.1163 5.3559,14.1163H6.0042C6.0075,14.1194 6.258,14.4589 6.7753,14.4791C6.7753,14.4791 6.8949,14.4915 7.1699,14.4822C7.7183,14.4822 7.9852,14.1225 7.9868,14.1178H8.6352C10.2166,14.1178 10.991,13.2553 10.991,12.1607C10.996,11.0879 10.2543,10.8964 10.1463,10.2798Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_calendar_20.xml
Normal file
9
core/ui/src/main/res/drawable/ic_calendar_20.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M15.417,3C17.118,3 18,3.892 18,5.611V15.389C18,17.1 17.118,18 15.417,18H4.583C2.882,18 2,17.108 2,15.389V5.611C2,3.892 2.882,3 4.583,3H15.417ZM4.559,6.896C3.905,6.897 3.543,7.242 3.543,7.949V15.373C3.543,16.072 3.905,16.425 4.559,16.425H15.425C16.086,16.425 16.457,16.072 16.457,15.373V7.949C16.457,7.242 16.086,6.896 15.425,6.896H4.559ZM6.37,13.638C6.653,13.638 6.748,13.726 6.748,14.015V14.481C6.748,14.77 6.653,14.858 6.37,14.858H5.905C5.63,14.858 5.535,14.77 5.535,14.481V14.015C5.535,13.726 5.63,13.638 5.905,13.638H6.37ZM8.945,13.638C9.228,13.638 9.323,13.726 9.323,14.015V14.481C9.323,14.77 9.228,14.858 8.945,14.858H8.48C8.205,14.858 8.11,14.77 8.11,14.481V14.015C8.11,13.726 8.205,13.638 8.48,13.638H8.945ZM11.519,13.638C11.803,13.638 11.897,13.726 11.898,14.015V14.481C11.897,14.77 11.803,14.858 11.519,14.858H11.064C10.78,14.858 10.685,14.77 10.685,14.481V14.015C10.685,13.726 10.78,13.638 11.064,13.638H11.519ZM6.37,11.051C6.653,11.051 6.748,11.139 6.748,11.428V11.894C6.748,12.183 6.654,12.271 6.37,12.271H5.905C5.63,12.271 5.535,12.183 5.535,11.894V11.428C5.535,11.139 5.63,11.051 5.905,11.051H6.37ZM8.945,11.051C9.228,11.051 9.323,11.139 9.323,11.428V11.894C9.323,12.183 9.228,12.271 8.945,12.271H8.48C8.205,12.271 8.11,12.183 8.11,11.894V11.428C8.11,11.139 8.205,11.051 8.48,11.051H8.945ZM11.519,11.051C11.803,11.051 11.897,11.139 11.898,11.428V11.894C11.898,12.183 11.803,12.271 11.519,12.271H11.064C10.78,12.271 10.685,12.183 10.685,11.894V11.428C10.685,11.139 10.78,11.051 11.064,11.051H11.519ZM14.095,11.051C14.378,11.051 14.473,11.139 14.473,11.428V11.894C14.473,12.183 14.378,12.271 14.095,12.271H13.638C13.354,12.271 13.26,12.183 13.26,11.894V11.428C13.26,11.139 13.354,11.051 13.638,11.051H14.095ZM8.945,8.463C9.228,8.463 9.323,8.552 9.323,8.841V9.307C9.323,9.596 9.228,9.684 8.945,9.685H8.48C8.205,9.685 8.11,9.596 8.11,9.307V8.841C8.11,8.552 8.205,8.463 8.48,8.463H8.945ZM11.519,8.463C11.803,8.463 11.897,8.552 11.898,8.841V9.307C11.898,9.596 11.803,9.685 11.519,9.685H11.064C10.78,9.685 10.685,9.596 10.685,9.307V8.841C10.685,8.552 10.78,8.463 11.064,8.463H11.519ZM14.095,8.463C14.378,8.463 14.473,8.552 14.473,8.841V9.307C14.473,9.596 14.378,9.685 14.095,9.685H13.638C13.354,9.685 13.26,9.596 13.26,9.307V8.841C13.26,8.552 13.354,8.463 13.638,8.463H14.095Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
12
core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml
Normal file
12
core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M11,7L15.917,11.804C16.028,11.912 16.028,12.088 15.917,12.196L11,17"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_share_new_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_share_new_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M9.5,10H7.5C6.672,10 6,10.672 6,11.5V18.5C6,19.328 6.672,20 7.5,20H16.5C17.328,20 18,19.328 18,18.5V11.5C18,10.672 17.328,10 16.5,10H14.5V8H16.5C18.433,8 20,9.567 20,11.5V18.5C20,20.433 18.433,22 16.5,22H7.5C5.567,22 4,20.433 4,18.5V11.5C4,9.567 5.567,8 7.5,8H9.5V10ZM11.374,2.225C11.75,1.965 12.249,1.965 12.625,2.225L12.704,2.285L15.641,4.731C16.065,5.085 16.122,5.716 15.769,6.141C15.415,6.565 14.784,6.622 14.359,6.268L13,5.135V14C13,14.552 12.552,15 12,15C11.448,15 11,14.552 11,14V5.135L9.641,6.268C9.216,6.622 8.585,6.565 8.231,6.141C7.878,5.716 7.935,5.085 8.359,4.731L11.296,2.285L11.374,2.225Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
5
core/ui/src/main/res/drawable/ic_shield_24.xml
Normal file
5
core/ui/src/main/res/drawable/ic_shield_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M12,21C11.922,21 11.824,20.984 11.707,20.952C11.596,20.927 11.478,20.882 11.354,20.818C9.992,20.06 8.838,19.391 7.892,18.811C6.954,18.232 6.197,17.671 5.623,17.129C5.056,16.581 4.642,15.992 4.381,15.361C4.127,14.724 4,13.975 4,13.115V5.909C4,5.418 4.108,5.065 4.323,4.848C4.538,4.625 4.848,4.434 5.252,4.275C5.48,4.185 5.793,4.068 6.191,3.921C6.588,3.768 7.025,3.606 7.501,3.434C7.984,3.255 8.463,3.083 8.939,2.918C9.421,2.745 9.861,2.593 10.259,2.459C10.657,2.319 10.97,2.21 11.198,2.134C11.328,2.096 11.459,2.064 11.589,2.038C11.726,2.013 11.863,2 12,2C12.137,2 12.274,2.013 12.411,2.038C12.548,2.064 12.681,2.096 12.812,2.134C13.04,2.21 13.35,2.319 13.741,2.459C14.139,2.593 14.575,2.745 15.051,2.918C15.534,3.09 16.013,3.262 16.489,3.434C16.972,3.606 17.412,3.765 17.809,3.911C18.207,4.058 18.52,4.179 18.748,4.275C19.159,4.44 19.469,4.631 19.677,4.848C19.892,5.065 20,5.418 20,5.909V13.115C20,13.975 19.876,14.73 19.628,15.38C19.381,16.024 18.973,16.626 18.406,17.187C17.845,17.747 17.092,18.314 16.147,18.888C15.208,19.461 14.041,20.105 12.646,20.818C12.522,20.882 12.401,20.927 12.284,20.952C12.173,20.984 12.078,21 12,21ZM7.599,11.815C7.599,11.943 7.641,12.048 7.726,12.131C7.817,12.207 7.928,12.245 8.059,12.245H11.482L9.653,17.034C9.568,17.244 9.571,17.416 9.663,17.55C9.76,17.684 9.894,17.754 10.064,17.76C10.233,17.76 10.386,17.674 10.523,17.502L16.049,10.726C16.16,10.598 16.215,10.471 16.215,10.344C16.215,10.216 16.17,10.111 16.078,10.028C15.993,9.945 15.886,9.904 15.755,9.904H12.333L14.161,5.116C14.246,4.905 14.24,4.737 14.142,4.609C14.05,4.475 13.92,4.408 13.751,4.408C13.588,4.402 13.434,4.485 13.291,4.657L7.765,11.433C7.654,11.554 7.599,11.682 7.599,11.815Z"/>
|
||||
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M7.993,1.333C11.64,1.333 14.667,4.353 14.667,8C14.667,11.641 11.647,14.667 8,14.667C4.359,14.667 1.333,11.641 1.333,8C1.333,4.353 4.353,1.333 7.993,1.333ZM8.333,4.912C8.19,4.62 7.753,4.62 7.61,4.912L6.824,6.522C6.766,6.641 6.648,6.723 6.511,6.739L4.656,6.953C4.32,6.992 4.184,7.387 4.433,7.606L5.801,8.815C5.902,8.905 5.947,9.037 5.921,9.165L5.562,10.907C5.496,11.223 5.85,11.468 6.146,11.311L7.778,10.448C7.899,10.385 8.046,10.385 8.166,10.448L9.798,11.311C10.094,11.468 10.448,11.223 10.383,10.907L10.023,9.165C9.997,9.037 10.042,8.905 10.143,8.815L11.511,7.606C11.759,7.387 11.624,6.992 11.288,6.953L9.434,6.739C9.297,6.723 9.178,6.641 9.12,6.522L8.333,4.912Z"
|
||||
android:fillColor="#656565"/>
|
||||
</vector>
|
||||
22
core/ui/src/main/res/drawable/img_berachain_22.xml
Normal file
22
core/ui/src/main/res/drawable/img_berachain_22.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
|
||||
<path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
|
||||
android:fillColor="#814626"/>
|
||||
<path
|
||||
android:pathData="M14.1523,13.2142V12.3734C14.6582,12.1959 15.0266,11.6448 15.0266,10.9924C15.0266,10.34 14.6582,9.7873 14.1523,9.6113V8.7705C14.6287,8.6039 14.984,8.1041 15.0233,7.5H14.4438V7.7569C14.4438,7.9547 14.3259,8.129 14.1523,8.2162V8.0963C14.1523,7.9204 14.0017,7.7772 13.8167,7.7772H13.7873C13.6023,7.7772 13.4516,7.9204 13.4516,8.0963V8.2162C13.2781,8.129 13.1602,7.9562 13.1602,7.7569V7.5H12.5807C12.62,8.1041 12.9752,8.6039 13.4516,8.7705V9.6113C12.9474,9.7873 12.5774,10.3384 12.5774,10.9924C12.5774,11.6463 12.9457,12.1975 13.4516,12.3734V13.2142C12.9752,13.3808 12.62,13.8807 12.5807,14.4847H13.1602V14.2278C13.1602,14.0301 13.2781,13.8557 13.4516,13.7685V13.8884C13.4516,14.0644 13.6023,14.2076 13.7873,14.2076H13.8167C14.0017,14.2076 14.1523,14.0644 14.1523,13.8884V13.7685C14.3259,13.8557 14.4438,14.0285 14.4438,14.2278V14.4847H15.0233C14.984,13.8807 14.6287,13.3808 14.1523,13.2142ZM13.1602,11.3598V10.6234C13.1602,10.4256 13.2781,10.2512 13.4516,10.164V10.2839C13.4516,10.4599 13.6023,10.6031 13.7873,10.6031H13.8167C14.0017,10.6031 14.1523,10.4599 14.1523,10.2839V10.164C14.3259,10.2512 14.4438,10.4241 14.4438,10.6234V11.3598C14.4438,11.5576 14.3259,11.732 14.1523,11.8191V11.6993C14.1523,11.5233 14.0017,11.3801 13.8167,11.3801H13.7873C13.6023,11.3801 13.4516,11.5233 13.4516,11.6993V11.8191C13.2797,11.732 13.1602,11.5591 13.1602,11.3598Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M17.7083,11.4392V10.544C18.2142,10.3665 18.5826,9.8153 18.5826,9.1629C18.5826,8.5105 18.2142,7.9578 17.7083,7.7818V7.5H17.0092V7.7818C16.5033,7.9593 16.135,8.5105 16.135,9.1629C16.135,9.8153 16.5033,10.368 17.0092,10.544V11.4392C16.5033,11.6167 16.135,12.1679 16.135,12.8203C16.135,13.4727 16.5033,14.0254 17.0092,14.2014V14.4832H17.7083V14.2014C18.2142,14.0239 18.5826,13.4727 18.5826,12.8203C18.5826,12.1679 18.2142,11.6167 17.7083,11.4392ZM16.7162,9.5303V8.7939C16.7162,8.5961 16.8341,8.4218 17.0076,8.3346V8.4545C17.0076,8.6304 17.1583,8.7736 17.3432,8.7736H17.3711C17.5561,8.7736 17.7067,8.6304 17.7067,8.4545V8.3346C17.8802,8.4218 17.9981,8.5946 17.9981,8.7939V9.5303C17.9981,9.7281 17.8802,9.9025 17.7067,9.9897V9.8698C17.7067,9.6938 17.5561,9.5506 17.3711,9.5506H17.3416C17.1566,9.5506 17.006,9.6938 17.006,9.8698V9.9897C16.8357,9.9025 16.7162,9.7296 16.7162,9.5303ZM17.9997,13.1893C17.9997,13.3871 17.8819,13.5615 17.7083,13.6486V13.4727C17.7083,13.2968 17.5577,13.1535 17.3727,13.1535H17.3449C17.1599,13.1535 17.0092,13.2968 17.0092,13.4727V13.6486C16.8357,13.5615 16.7178,13.3886 16.7178,13.1893V12.4529C16.7178,12.2551 16.8357,12.0807 17.0092,11.9935V12.0574C17.0092,12.2333 17.1599,12.3766 17.3449,12.3766H17.3744C17.5593,12.3766 17.7099,12.2333 17.7099,12.0574V11.9935C17.8835,12.0807 18.0014,12.2536 18.0014,12.4529V13.1893H17.9997Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M10.1463,10.2798C10.1397,10.244 10.1316,10.2098 10.1185,10.1771C10.1234,10.1693 11.2104,8.729 10.2036,7.8384C9.1984,6.9478 8.0245,8.1094 8.018,8.1156C7.8313,8.0595 7.643,8.0253 7.4564,8.0097C7.4564,8.0097 7.4564,8.0097 7.4548,8.0097C7.0717,7.949 6.5347,8.0097 6.5347,8.0097C6.3497,8.0253 6.163,8.0595 5.978,8.114C5.9715,8.1078 4.7976,6.9463 3.7924,7.8369C2.7872,8.7275 3.8726,10.1693 3.8775,10.1755C3.8661,10.2098 3.8562,10.244 3.8497,10.2783C3.7416,10.8948 3,11.0848 3,12.1591C3,13.2537 3.7744,14.1163 5.3559,14.1163H6.0042C6.0075,14.1194 6.258,14.4589 6.7753,14.4791C6.7753,14.4791 6.8949,14.4915 7.1699,14.4822C7.7183,14.4822 7.9852,14.1225 7.9868,14.1178H8.6352C10.2166,14.1178 10.991,13.2553 10.991,12.1607C10.996,11.0879 10.2543,10.8964 10.1463,10.2798Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
</vector>
|
||||
BIN
core/ui/src/main/res/drawable/img_nft_empty_collection.webp
Normal file
BIN
core/ui/src/main/res/drawable/img_nft_empty_collection.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
9
core/ui/src/main/res/drawable/shape_triangular.xml
Normal file
9
core/ui/src/main/res/drawable/shape_triangular.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="8dp"
|
||||
android:height="8dp"
|
||||
android:viewportWidth="8"
|
||||
android:viewportHeight="8">
|
||||
<path
|
||||
android:fillColor="#34DF12"
|
||||
android:pathData="M8,8L0,8L0,0C0,0 2,6 8,8Z" />
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue