Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-04 19:37:14 +04:00
commit 304bf0df87
928 changed files with 23076 additions and 29831 deletions

View file

@ -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,
),
),
),
)
}

View 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,
),
),
),
)
}

View file

@ -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 = {})
}
}

View file

@ -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)
},

View file

@ -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
}

View file

@ -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,
)
}
}
}

View file

@ -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),
)
}

View file

@ -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),

View file

@ -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,

View file

@ -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)

View file

@ -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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -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 })
}
}
}

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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,

View 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

View file

@ -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
}

View file

@ -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

View file

@ -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),
)
},

View file

@ -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(

View file

@ -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 semitransparent 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

View file

@ -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,
)
}
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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()

View file

@ -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),

View file

@ -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

View file

@ -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

View file

@ -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
},

View file

@ -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,
)
}
}

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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
*/

View file

@ -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)
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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,

View file

@ -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)
}
}
}
}

View file

@ -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);
}
"""
}

View file

@ -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],
)
}
}

View file

@ -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)
}
}

View file

@ -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))
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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"
}

View file

@ -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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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>