Updated on 2026-08-14
This commit is contained in:
parent
c0fbd241f4
commit
af84319092
9 changed files with 821 additions and 5 deletions
|
|
@ -19,10 +19,12 @@ generation a component belongs to is essential so you don't mix tokens or pull t
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| **DS1** (legacy) | `core/ui/src/main/java/com/tangem/core/ui/components/` | `TangemTheme.colors` | `TangemTheme.typography` | `TangemTheme.dimens` | `TangemThemePreview` |
|
| **DS1** (legacy) | `core/ui/src/main/java/com/tangem/core/ui/components/` | `TangemTheme.colors` | `TangemTheme.typography` | `TangemTheme.dimens` | `TangemThemePreview` |
|
||||||
| **DS2** (redesign) | `core/ui/src/main/java/com/tangem/core/ui/ds/` | `TangemTheme.colors2` | `TangemTheme.typography2` | `TangemTheme.dimens2` | `TangemThemePreviewRedesign` |
|
| **DS2** (redesign) | `core/ui/src/main/java/com/tangem/core/ui/ds/` | `TangemTheme.colors2` | `TangemTheme.typography2` | `TangemTheme.dimens2` | `TangemThemePreviewRedesign` |
|
||||||
| **DS3** (target) | `core/ui/src/main/java/com/tangem/core/ui/ds2/` | `TangemTheme.colors3` | `TangemTheme.typography3` | `TangemTheme.dimens2` | `TangemThemePreviewRedesign` |
|
| **DS3** (target) | `core/ui/src/main/java/com/tangem/core/ui/ds2/` | `TangemTheme.colors3` | `TangemTheme.typography3` | literal `.dp` (no token) | `TangemThemePreviewRedesign` |
|
||||||
|
|
||||||
> Mind the numbering mismatch: **folder `ds` is DS2**, **folder `ds2` is DS3**.
|
> Mind the numbering mismatch: **folder `ds` is DS2**, **folder `ds2` is DS3**.
|
||||||
> The `colors2` / `typography2` tokens are `@Deprecated` (ReplaceWith `colors3` / `typography3`).
|
> The `colors2` / `typography2` tokens are `@Deprecated` (ReplaceWith `colors3` / `typography3`).
|
||||||
|
> **DS3 has no dimension token** — `dimens2` is a DS2 token and must **not** be used in `ds2/`
|
||||||
|
> components. Express dimensions as literal `.dp` values (see rule 2 below).
|
||||||
|
|
||||||
- **DS1** — the entire current app is built on it. Do **not** add new components here.
|
- **DS1** — the entire current app is built on it. Do **not** add new components here.
|
||||||
- **DS2** — redesign components. A transitional generation; don't write new components in it, only
|
- **DS2** — redesign components. A transitional generation; don't write new components in it, only
|
||||||
|
|
@ -49,9 +51,11 @@ Pattern rules:
|
||||||
|
|
||||||
1. **Package & location.** `com.tangem.core.ui.ds2.<component>`, folder
|
1. **Package & location.** `com.tangem.core.ui.ds2.<component>`, folder
|
||||||
`core/ui/.../ds2/<component>/`. The component name is `Tangem<Name>`.
|
`core/ui/.../ds2/<component>/`. The component name is `Tangem<Name>`.
|
||||||
2. **DS3 tokens only.** Colors — `TangemTheme.colors3.*`, text — `TangemTheme.typography3.*`,
|
2. **DS3 tokens only.** Colors — `TangemTheme.colors3.*`, text — `TangemTheme.typography3.*`. No
|
||||||
dimensions — `TangemTheme.dimens2.*`. No `colors` / `colors2` / hardcoded values (literal dp/colors
|
`colors` / `colors2` and no hardcoded colors outside `@Preview`. **Dimensions have no DS3 token** —
|
||||||
are acceptable only inside `@Preview`, where you add `@Suppress("MagicNumber")`).
|
do **not** use `TangemTheme.dimens2.*` (it is a DS2 token); express dimensions as literal `.dp`
|
||||||
|
values and add `@Suppress("MagicNumber")` to the composable (or a `…Ext.kt` / `…Internal.kt` token
|
||||||
|
holder, as `TangemButtonInternal.kt` and `TangemCheckmark.kt` do).
|
||||||
3. **Signature.** `modifier: Modifier = Modifier` is mandatory (defaulting to `Modifier`, placed first
|
3. **Signature.** `modifier: Modifier = Modifier` is mandatory (defaulting to `Modifier`, placed first
|
||||||
among the optional params or right after the required ones). Express variants/sizes via a nested
|
among the optional params or right after the required ones). Express variants/sizes via a nested
|
||||||
`enum` in `object Tangem<Name>` (like `TangemButton.Variant` / `TangemButton.Size`), not boolean flags.
|
`enum` in `object Tangem<Name>` (like `TangemButton.Variant` / `TangemButton.Size`), not boolean flags.
|
||||||
|
|
@ -156,7 +160,8 @@ Page layout guidelines live in
|
||||||
|
|
||||||
- [ ] Component created under `core/ui/.../ds2/<component>/`, package `com.tangem.core.ui.ds2.<component>`.
|
- [ ] Component created under `core/ui/.../ds2/<component>/`, package `com.tangem.core.ui.ds2.<component>`.
|
||||||
- [ ] Named `Tangem<Name>`; first optional parameter is `modifier: Modifier = Modifier`.
|
- [ ] Named `Tangem<Name>`; first optional parameter is `modifier: Modifier = Modifier`.
|
||||||
- [ ] Uses **only** DS3 tokens: `colors3`, `typography3`, `dimens2`. No hardcoded values outside previews.
|
- [ ] Uses **only** DS3 tokens: `colors3`, `typography3`. No hardcoded colors outside previews. Dimensions
|
||||||
|
are literal `.dp` (DS3 has no dimension token — never use `dimens2`), with `@Suppress("MagicNumber")`.
|
||||||
- [ ] Variants/sizes expressed as an `enum` inside `object Tangem<Name>` (not a set of boolean flags).
|
- [ ] Variants/sizes expressed as an `enum` inside `object Tangem<Name>` (not a set of boolean flags).
|
||||||
- [ ] All public types (enums, statuses, constants) declared inside the `object Tangem<Name>`.
|
- [ ] All public types (enums, statuses, constants) declared inside the `object Tangem<Name>`.
|
||||||
- [ ] Convenient overloads provided (simpler `@Composable` overloads and/or `object` extension presets).
|
- [ ] Convenient overloads provided (simpler `@Composable` overloads and/or `object` extension presets).
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,297 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.core.ui.ds2.glowring
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.animation.core.CubicBezierEasing
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.lerp
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.res.generated.TangemColors3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Glow Ring** — an animated angular-gradient halo that runs around a
|
||||||
|
* rounded-rect outline, like lights chasing along the border.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=4933-126&m=dev)
|
||||||
|
*
|
||||||
|
* @param modifier Modifier for the whole component; also defines its size when there is no [content].
|
||||||
|
* @param variant Color theme of the gradient — see [TangemGlowRing.Variant].
|
||||||
|
* @param cornerRadius Corner radius of the ring; should match the radius of the wrapped surface.
|
||||||
|
* @param animated When `false`, the ring is rendered static (no rotation).
|
||||||
|
* @param quality Rendering strategy; defaults to [TangemGlowRing.Quality.Auto] (device-appropriate).
|
||||||
|
* Force [TangemGlowRing.Quality.LayeredStrokes] to preview the pre-Android-12 fallback on any device.
|
||||||
|
* @param contentDescription Accessibility label; pass a value when the ring conveys state (e.g. error),
|
||||||
|
* leave `null` when it is purely decorative.
|
||||||
|
* @param content Optional content drawn inside/over the ring.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TangemGlowRing(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
variant: TangemGlowRing.Variant = TangemGlowRing.Variant.Magic,
|
||||||
|
cornerRadius: Dp = 24.dp,
|
||||||
|
animated: Boolean = true,
|
||||||
|
quality: TangemGlowRing.Quality = TangemGlowRing.Quality.Auto,
|
||||||
|
contentDescription: String? = null,
|
||||||
|
content: @Composable BoxScope.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
val resolved = remember(quality) { resolveQuality(quality) }
|
||||||
|
val stops = rememberGlowRingStops(variant, animated)
|
||||||
|
val metrics = remember {
|
||||||
|
GlowRingMetrics(coreWidth = 2.dp, ringWidth = 4.dp, blurMid = 8.dp, blurBottom = 16.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val angle = if (animated) {
|
||||||
|
val transition = rememberInfiniteTransition(label = "glowRing")
|
||||||
|
val rotation by transition.animateFloat(
|
||||||
|
initialValue = GLOW_RING_START_ANGLE,
|
||||||
|
targetValue = 270f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 24_000,
|
||||||
|
easing = CubicBezierEasing(a = 0.1f, b = 0f, c = 0.9f, d = 1f),
|
||||||
|
),
|
||||||
|
repeatMode = RepeatMode.Restart,
|
||||||
|
),
|
||||||
|
label = "angle",
|
||||||
|
)
|
||||||
|
rotation
|
||||||
|
} else {
|
||||||
|
GLOW_RING_START_ANGLE
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = if (contentDescription != null) {
|
||||||
|
modifier.semantics { this.contentDescription = contentDescription }
|
||||||
|
} else {
|
||||||
|
modifier
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val ringModifier = Modifier.matchParentSize()
|
||||||
|
when (resolved) {
|
||||||
|
ResolvedGlowRingQuality.Blur -> BlurGlowRing(
|
||||||
|
angle = angle,
|
||||||
|
stops = stops,
|
||||||
|
cornerRadius = cornerRadius,
|
||||||
|
metrics = metrics,
|
||||||
|
modifier = ringModifier,
|
||||||
|
)
|
||||||
|
ResolvedGlowRingQuality.LayeredStrokes -> LayeredStrokesGlowRing(
|
||||||
|
angle = angle,
|
||||||
|
stops = stops,
|
||||||
|
cornerRadius = cornerRadius,
|
||||||
|
metrics = metrics,
|
||||||
|
modifier = ringModifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sweep start angle, also reused as the static angle when [TangemGlowRing] is not animated (Figma: -90°). */
|
||||||
|
private const val GLOW_RING_START_ANGLE = -90f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the gradient stops for [variant] from the DS3 `colors3.glow` tokens. The
|
||||||
|
* [TangemGlowRing.Variant.Magic] variant continuously ping-pongs between gradient A (`glow.magic`) and
|
||||||
|
* gradient B (`glow.magicBlend`) while [animated] is `true`; every other variant has a single static
|
||||||
|
* gradient.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun rememberGlowRingStops(variant: TangemGlowRing.Variant, animated: Boolean): List<Pair<Float, Color>> {
|
||||||
|
val glow = TangemTheme.colors3.glow
|
||||||
|
if (variant != TangemGlowRing.Variant.Magic || !animated) {
|
||||||
|
return variant.stops(glow)
|
||||||
|
}
|
||||||
|
val morphTransition = rememberInfiniteTransition(label = "glowRingMorph")
|
||||||
|
val mix by morphTransition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
// 6s A→B half-period; Reverse makes a 12s ping-pong (Figma morphDur = 12s).
|
||||||
|
animation = tween(durationMillis = 6_000, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
),
|
||||||
|
label = "morphMix",
|
||||||
|
)
|
||||||
|
return morphedMagicStops(glow.magic.steps(), glow.magicBlend.steps(), mix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Public API surface of [TangemGlowRing]. */
|
||||||
|
object TangemGlowRing {
|
||||||
|
|
||||||
|
/** Color theme of the glow ring gradient. */
|
||||||
|
enum class Variant {
|
||||||
|
/**
|
||||||
|
* Multi-color "magic" gradient that continuously auto-morphs (ping-pongs) between separated
|
||||||
|
* orange / blue / purple arcs and a continuous fully-saturated blend.
|
||||||
|
*/
|
||||||
|
Magic,
|
||||||
|
|
||||||
|
/** Green success glow. */
|
||||||
|
Success,
|
||||||
|
|
||||||
|
/** Red error glow. */
|
||||||
|
Error,
|
||||||
|
|
||||||
|
/** Orange/amber warning glow. */
|
||||||
|
Warning,
|
||||||
|
|
||||||
|
/** Blue informational glow. */
|
||||||
|
Info,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendering strategy for the glow.
|
||||||
|
*
|
||||||
|
* [Auto] picks the best renderer for the current device — a real Gaussian blur on Android 12+
|
||||||
|
* (API 31) and a layered-stroke approximation on older versions. The explicit values force one
|
||||||
|
* renderer regardless of API level; they exist mainly for previews / Storybook so the
|
||||||
|
* pre-Android-12 fallback can be inspected on a modern device. Product code should use [Auto].
|
||||||
|
*/
|
||||||
|
enum class Quality {
|
||||||
|
/** Auto-detect the renderer from the device API level (recommended). */
|
||||||
|
Auto,
|
||||||
|
|
||||||
|
/** Force the Android 12+ real-blur renderer. */
|
||||||
|
Blur,
|
||||||
|
|
||||||
|
/** Force the pre-Android-12 layered-stroke fallback. */
|
||||||
|
LayeredStrokes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves [quality] to a concrete renderer. [TangemGlowRing.Quality.Auto] picks a real blur on
|
||||||
|
* Android 12+ (API 31) and falls back to stacked translucent strokes on older versions; the explicit
|
||||||
|
* values force their renderer regardless of API level.
|
||||||
|
*/
|
||||||
|
private fun resolveQuality(quality: TangemGlowRing.Quality): ResolvedGlowRingQuality = when (quality) {
|
||||||
|
TangemGlowRing.Quality.Blur -> ResolvedGlowRingQuality.Blur
|
||||||
|
TangemGlowRing.Quality.LayeredStrokes -> ResolvedGlowRingQuality.LayeredStrokes
|
||||||
|
TangemGlowRing.Quality.Auto -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
ResolvedGlowRingQuality.Blur
|
||||||
|
} else {
|
||||||
|
ResolvedGlowRingQuality.LayeredStrokes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Concrete rendering strategy chosen by [resolveQuality]. */
|
||||||
|
private enum class ResolvedGlowRingQuality { Blur, LayeredStrokes }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the angular-gradient stops for [this] variant from its DS3 `colors3.glow` token group. Every
|
||||||
|
* variant token exposes the same 10 [steps] — solid arcs at steps 1/4/7, a faint arc at 9 and transparent
|
||||||
|
* gaps elsewhere — which [glowStops] lays out as evenly-spaced, seamlessly-looping stops.
|
||||||
|
*/
|
||||||
|
private fun TangemGlowRing.Variant.stops(glow: TangemColors3.Glow): List<Pair<Float, Color>> = glowStops(
|
||||||
|
when (this) {
|
||||||
|
TangemGlowRing.Variant.Magic -> glow.magic.steps()
|
||||||
|
TangemGlowRing.Variant.Success -> glow.success.steps()
|
||||||
|
TangemGlowRing.Variant.Error -> glow.error.steps()
|
||||||
|
TangemGlowRing.Variant.Warning -> glow.warning.steps()
|
||||||
|
TangemGlowRing.Variant.Info -> glow.info.steps()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blends the Magic gradients A ([magic] = `glow.magic`) and B ([magicBlend] = `glow.magicBlend`) at
|
||||||
|
* [mix] (`0` = A, `1` = B). Both token groups share the same stop positions, so the morph is a direct
|
||||||
|
* per-step color lerp. Mirrors the reference rig's auto-morph (ping-pong) between gradient A and B.
|
||||||
|
*/
|
||||||
|
private fun morphedMagicStops(magic: List<Color>, magicBlend: List<Color>, mix: Float): List<Pair<Float, Color>> {
|
||||||
|
val m = mix.coerceIn(0f, 1f)
|
||||||
|
return glowStops(List(magic.size) { lerp(magic[it], magicBlend[it], m) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lays the glow [steps] out as an angular gradient: evenly spaced from `0`, with step 1 repeated at `1.0`
|
||||||
|
* so the rotation loops seamlessly. Transparent steps create the gaps between the glowing arcs.
|
||||||
|
*/
|
||||||
|
private fun glowStops(steps: List<Color>): List<Pair<Float, Color>> {
|
||||||
|
val count = steps.size
|
||||||
|
return steps.mapIndexed { index, color -> index.toFloat() / count to color } + (1f to steps.first())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TangemColors3.Glow.Magic.steps(): List<Color> =
|
||||||
|
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||||
|
|
||||||
|
private fun TangemColors3.Glow.MagicBlend.steps(): List<Color> =
|
||||||
|
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||||
|
|
||||||
|
private fun TangemColors3.Glow.Success.steps(): List<Color> =
|
||||||
|
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||||
|
|
||||||
|
private fun TangemColors3.Glow.Error.steps(): List<Color> =
|
||||||
|
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||||
|
|
||||||
|
private fun TangemColors3.Glow.Warning.steps(): List<Color> =
|
||||||
|
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||||
|
|
||||||
|
private fun TangemColors3.Glow.Info.steps(): List<Color> =
|
||||||
|
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||||
|
|
||||||
|
@Preview(name = "Light", showBackground = true)
|
||||||
|
@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun TangemGlowRingPreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(TangemTheme.colors3.bg.primary)
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||||
|
TangemGlowRing(
|
||||||
|
modifier = Modifier.size(120.dp, 72.dp),
|
||||||
|
variant = TangemGlowRing.Variant.Magic,
|
||||||
|
)
|
||||||
|
TangemGlowRing(
|
||||||
|
modifier = Modifier.size(120.dp, 72.dp),
|
||||||
|
variant = TangemGlowRing.Variant.Success,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||||
|
TangemGlowRing(
|
||||||
|
modifier = Modifier.size(120.dp, 72.dp),
|
||||||
|
variant = TangemGlowRing.Variant.Error,
|
||||||
|
)
|
||||||
|
TangemGlowRing(
|
||||||
|
modifier = Modifier.size(120.dp, 72.dp),
|
||||||
|
variant = TangemGlowRing.Variant.Warning,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||||
|
TangemGlowRing(
|
||||||
|
modifier = Modifier.size(120.dp, 72.dp),
|
||||||
|
variant = TangemGlowRing.Variant.Info,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.core.ui.ds2.glowring
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.PathFillType
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||||
|
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||||
|
import androidx.compose.ui.graphics.lerp
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token-driven measurements shared by both renderers, mirroring the Figma component anatomy:
|
||||||
|
* a crisp [coreWidth] core line plus two wider, blurred glow bands ([ringWidth] stroked, blurred by
|
||||||
|
* [blurMid] and [blurBottom]).
|
||||||
|
*/
|
||||||
|
internal data class GlowRingMetrics(
|
||||||
|
val coreWidth: Dp, // crisp core stroke (top layer)
|
||||||
|
val ringWidth: Dp, // glow band stroke (mid + bottom layers)
|
||||||
|
val blurMid: Dp, // mid glow blur radius
|
||||||
|
val blurBottom: Dp, // widest glow blur radius
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tier 1 — works on every API level, no blur or shader. Approximates the blurred glow by stacking the
|
||||||
|
* same breathing angular-gradient ring several times: progressively wider + fainter bands under a crisp
|
||||||
|
* core. Everything is clipped to the rounded box, so only the inner half of each band shows → inner glow.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun LayeredStrokesGlowRing(
|
||||||
|
angle: Float,
|
||||||
|
stops: List<Pair<Float, Color>>,
|
||||||
|
cornerRadius: Dp,
|
||||||
|
metrics: GlowRingMetrics,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier.clip(RoundedCornerShape(cornerRadius))) {
|
||||||
|
Canvas(Modifier.fillMaxSize()) {
|
||||||
|
val r = cornerRadius.toPx()
|
||||||
|
// widest & faintest first, crisp core last
|
||||||
|
drawBreathingRing(
|
||||||
|
stops = stops,
|
||||||
|
angleDeg = angle,
|
||||||
|
cornerRadiusPx = r,
|
||||||
|
strokePx = (metrics.ringWidth + metrics.blurBottom).toPx(),
|
||||||
|
alpha = 0.06f,
|
||||||
|
)
|
||||||
|
drawBreathingRing(
|
||||||
|
stops = stops,
|
||||||
|
angleDeg = angle,
|
||||||
|
cornerRadiusPx = r,
|
||||||
|
strokePx = (metrics.ringWidth + metrics.blurMid).toPx(),
|
||||||
|
alpha = 0.12f,
|
||||||
|
)
|
||||||
|
drawBreathingRing(
|
||||||
|
stops = stops,
|
||||||
|
angleDeg = angle,
|
||||||
|
cornerRadiusPx = r,
|
||||||
|
strokePx = metrics.ringWidth.toPx(),
|
||||||
|
alpha = 0.30f,
|
||||||
|
)
|
||||||
|
drawBreathingRing(
|
||||||
|
stops = stops,
|
||||||
|
angleDeg = angle,
|
||||||
|
cornerRadiusPx = r,
|
||||||
|
strokePx = metrics.coreWidth.toPx(),
|
||||||
|
alpha = 1.0f,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tier 2 — Android 12+ (API 31). Reproduces the Figma anatomy directly: three stacked breathing
|
||||||
|
* angular-gradient rings with real blur (bottom widest, mid, top crisp). Each layer bleeds with
|
||||||
|
* [BlurredEdgeTreatment.Unbounded]; the surrounding [clip] to the rounded box keeps only the inner
|
||||||
|
* bloom, producing the inner glow.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun BlurGlowRing(
|
||||||
|
angle: Float,
|
||||||
|
stops: List<Pair<Float, Color>>,
|
||||||
|
cornerRadius: Dp,
|
||||||
|
metrics: GlowRingMetrics,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier.clip(RoundedCornerShape(cornerRadius))) {
|
||||||
|
// bottom — widest halo
|
||||||
|
BreathingRing(
|
||||||
|
angle = angle,
|
||||||
|
stops = stops,
|
||||||
|
cornerRadius = cornerRadius,
|
||||||
|
strokeWidth = metrics.ringWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.blur(radius = metrics.blurBottom, edgeTreatment = BlurredEdgeTreatment.Unbounded),
|
||||||
|
)
|
||||||
|
// mid
|
||||||
|
BreathingRing(
|
||||||
|
angle = angle,
|
||||||
|
stops = stops,
|
||||||
|
cornerRadius = cornerRadius,
|
||||||
|
strokeWidth = metrics.ringWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.blur(radius = metrics.blurMid, edgeTreatment = BlurredEdgeTreatment.Unbounded),
|
||||||
|
)
|
||||||
|
// top — crisp core
|
||||||
|
BreathingRing(
|
||||||
|
angle = angle,
|
||||||
|
stops = stops,
|
||||||
|
cornerRadius = cornerRadius,
|
||||||
|
strokeWidth = metrics.coreWidth,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BreathingRing(
|
||||||
|
angle: Float,
|
||||||
|
stops: List<Pair<Float, Color>>,
|
||||||
|
cornerRadius: Dp,
|
||||||
|
strokeWidth: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Canvas(modifier) {
|
||||||
|
drawBreathingRing(
|
||||||
|
stops = stops,
|
||||||
|
angleDeg = angle,
|
||||||
|
cornerRadiusPx = cornerRadius.toPx(),
|
||||||
|
strokePx = strokeWidth.toPx(),
|
||||||
|
alpha = 1f,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws one angular-gradient ring band clipped to the rounded-rect stroke outline. The gradient is a
|
||||||
|
* sweep whose colour seam is rotated by [angleDeg] (via [rotatedStops]) and whose vertical squish
|
||||||
|
* breathes between W/2 and W/8 over the rotation (`rxM = mid + amp·cos(2φ)`), reproducing the morphing
|
||||||
|
* arcs of the reference rig.
|
||||||
|
*/
|
||||||
|
private fun DrawScope.drawBreathingRing(
|
||||||
|
stops: List<Pair<Float, Color>>,
|
||||||
|
angleDeg: Float,
|
||||||
|
cornerRadiusPx: Float,
|
||||||
|
strokePx: Float,
|
||||||
|
alpha: Float,
|
||||||
|
) {
|
||||||
|
val w = size.width
|
||||||
|
val h = size.height
|
||||||
|
if (w <= 0f || h <= 0f) return
|
||||||
|
val center = Offset(w / 2f, h / 2f)
|
||||||
|
|
||||||
|
// Breathing horizontal radius of the gradient ellipse → vertical squish of the angle sampling.
|
||||||
|
val maxRx = w / 2f
|
||||||
|
val minRx = w / 8f
|
||||||
|
val mid = (maxRx + minRx) / 2f
|
||||||
|
val amp = max((maxRx - minRx) / 2f, 0f)
|
||||||
|
val phaseRad = Math.toRadians(angleDeg.toDouble()).toFloat()
|
||||||
|
val rxM = mid + amp * cos(2f * phaseRad)
|
||||||
|
val scaleY = h / 2f / max(rxM, 1f)
|
||||||
|
|
||||||
|
val r = min(cornerRadiusPx, min(w, h) / 2f)
|
||||||
|
val o = strokePx / 2f
|
||||||
|
val ring = Path().apply {
|
||||||
|
fillType = PathFillType.EvenOdd
|
||||||
|
addRoundRect(
|
||||||
|
RoundRect(rect = Rect(Offset(-o, -o), Size(w + 2f * o, h + 2f * o)), cornerRadius = CornerRadius(r + o)),
|
||||||
|
)
|
||||||
|
addRoundRect(
|
||||||
|
RoundRect(
|
||||||
|
rect = Rect(Offset(o, o), Size(w - 2f * o, h - 2f * o)),
|
||||||
|
cornerRadius = CornerRadius(max(r - o, 0f)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val brush = Brush.sweepGradient(colorStops = rotatedStops(stops, angleDeg), center = center)
|
||||||
|
val big = max(w, h) * 4f
|
||||||
|
clipPath(ring) {
|
||||||
|
withTransform({ scale(scaleX = 1f, scaleY = scaleY, pivot = center) }) {
|
||||||
|
drawRect(
|
||||||
|
brush = brush,
|
||||||
|
topLeft = Offset(center.x - big / 2f, center.y - big / 2f),
|
||||||
|
size = Size(big, big),
|
||||||
|
alpha = alpha,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose's [Brush.sweepGradient] has no start-angle parameter, so the colour seam is rotated by
|
||||||
|
* shifting every stop position by `deg/360` (wrapping around the loop) and re-anchoring boundary stops
|
||||||
|
* at 0 and 1 with the interpolated wrap colour. Mirrors `rotatedStops` from the reference rig.
|
||||||
|
*/
|
||||||
|
private fun rotatedStops(base: List<Pair<Float, Color>>, deg: Float): Array<Pair<Float, Color>> {
|
||||||
|
val d = (deg / 360f % 1f + 1f) % 1f
|
||||||
|
val uniq = base.dropLast(1) // drop the duplicate wrap stop at 1.0
|
||||||
|
val shifted = uniq
|
||||||
|
.map { (p, c) -> ((p + d) % 1f + 1f) % 1f to c }
|
||||||
|
.sortedBy { it.first }
|
||||||
|
val first = shifted.first()
|
||||||
|
val last = shifted.last()
|
||||||
|
val span = first.first + 1f - last.first
|
||||||
|
val wrapFraction = if (span > 1e-6f) (1f - last.first) / span else 0f
|
||||||
|
val wrapColor = lerp(last.second, first.second, wrapFraction)
|
||||||
|
return (listOf(0f to wrapColor) + shifted + listOf(1f to wrapColor)).toTypedArray()
|
||||||
|
}
|
||||||
|
|
@ -190,11 +190,13 @@ object TangemTheme {
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
get() = LocalTangemTypography3.current
|
get() = LocalTangemTypography3.current
|
||||||
|
|
||||||
|
@Deprecated("Use plain dp")
|
||||||
val dimens: TangemDimens
|
val dimens: TangemDimens
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
get() = LocalTangemDimens.current
|
get() = LocalTangemDimens.current
|
||||||
|
|
||||||
|
@Deprecated("Use plain dp")
|
||||||
val dimens2: TangemDimens2
|
val dimens2: TangemDimens2
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||||
import com.tangem.core.ui.ds2.badge.TangemBadge
|
import com.tangem.core.ui.ds2.badge.TangemBadge
|
||||||
import com.tangem.core.ui.ds2.button.TangemButton
|
import com.tangem.core.ui.ds2.button.TangemButton
|
||||||
import com.tangem.core.ui.ds2.fade.TangemFade
|
import com.tangem.core.ui.ds2.fade.TangemFade
|
||||||
|
import com.tangem.core.ui.ds2.glowring.TangemGlowRing
|
||||||
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
|
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
|
||||||
import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
||||||
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
||||||
|
|
@ -351,6 +352,25 @@ internal data class TangemCheckmarkStory(
|
||||||
val onEnabledToggle: () -> Unit,
|
val onEnabledToggle: () -> Unit,
|
||||||
) : DsStoryBookPage
|
) : DsStoryBookPage
|
||||||
|
|
||||||
|
internal data class TangemGlowRingStory(
|
||||||
|
val variant: TangemGlowRing.Variant,
|
||||||
|
val quality: TangemGlowRing.Quality,
|
||||||
|
val background: Background,
|
||||||
|
val isAnimated: Boolean,
|
||||||
|
val onVariantChange: (TangemGlowRing.Variant) -> Unit,
|
||||||
|
val onQualityChange: (TangemGlowRing.Quality) -> Unit,
|
||||||
|
val onBackgroundChange: (Background) -> Unit,
|
||||||
|
val onAnimatedToggle: () -> Unit,
|
||||||
|
) : DsStoryBookPage {
|
||||||
|
|
||||||
|
/** Backdrop the glow-ring preview is rendered on top of. */
|
||||||
|
enum class Background(val label: String) {
|
||||||
|
BgPrimary("bg.primary"),
|
||||||
|
BgSecondary("bg.secondary"),
|
||||||
|
BgInverse("bg.inverse"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal data class TangemBadgeV2Story(
|
internal data class TangemBadgeV2Story(
|
||||||
val variant: TangemBadge.Variant,
|
val variant: TangemBadge.Variant,
|
||||||
val status: TangemBadge.Status,
|
val status: TangemBadge.Status,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.button.tangemBut
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.checkbox.tangemCheckboxV2StoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.checkbox.tangemCheckboxV2StoryFactory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.tangemCheckmarkStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.tangemCheckmarkStoryFactory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeStoryFactory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.page.ds.glowring.tangemGlowRingStoryFactory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.search.tangemSearchStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.search.tangemSearchStoryFactory
|
||||||
|
|
@ -39,6 +40,7 @@ private fun buildDsStories() = listOf(
|
||||||
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
|
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
|
||||||
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
|
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
|
||||||
DsStoryItem(title = "🧭 TangemTopNavigation", factory = tangemTopNavigationStoryFactory),
|
DsStoryItem(title = "🧭 TangemTopNavigation", factory = tangemTopNavigationStoryFactory),
|
||||||
|
DsStoryItem(title = "💫 TangemGlowRing", factory = tangemGlowRingStoryFactory),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.tangem.feature.tester.presentation.storybook.page.ds.glowring
|
||||||
|
|
||||||
|
import com.tangem.core.ui.ds2.glowring.TangemGlowRing
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.entity.TangemGlowRingStory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||||
|
|
||||||
|
internal fun StateUpdater<TangemGlowRingStory>.build(): TangemGlowRingStory {
|
||||||
|
return TangemGlowRingStory(
|
||||||
|
variant = TangemGlowRing.Variant.Magic,
|
||||||
|
quality = TangemGlowRing.Quality.Auto,
|
||||||
|
background = TangemGlowRingStory.Background.BgPrimary,
|
||||||
|
isAnimated = true,
|
||||||
|
onVariantChange = { variant ->
|
||||||
|
updateStory { it.copy(variant = variant) }
|
||||||
|
},
|
||||||
|
onQualityChange = { quality ->
|
||||||
|
updateStory { it.copy(quality = quality) }
|
||||||
|
},
|
||||||
|
onBackgroundChange = { background ->
|
||||||
|
updateStory { it.copy(background = background) }
|
||||||
|
},
|
||||||
|
onAnimatedToggle = {
|
||||||
|
updateStory { it.copy(isAnimated = !it.isAnimated) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val tangemGlowRingStoryFactory
|
||||||
|
get() = storyPageFactory(StateUpdater<TangemGlowRingStory>::build)
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.feature.tester.presentation.storybook.page.ds.glowring
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.ds2.glowring.TangemGlowRing
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.entity.TangemGlowRingStory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.entity.TangemGlowRingStory.Background
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TangemGlowRingStory(state: TangemGlowRingStory, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
// Preview stays pinned at the top.
|
||||||
|
ComponentPreview(state = state)
|
||||||
|
// Only the controls scroll.
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
VariantSelector(selected = state.variant, onSelect = state.onVariantChange)
|
||||||
|
QualitySelector(selected = state.quality, onSelect = state.onQualityChange)
|
||||||
|
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
|
||||||
|
Toggles(state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PreviewBackground(background: Background, modifier: Modifier = Modifier) {
|
||||||
|
when (background) {
|
||||||
|
Background.BgPrimary -> Box(modifier.background(TangemTheme.colors3.bg.primary))
|
||||||
|
Background.BgSecondary -> Box(modifier.background(TangemTheme.colors3.bg.secondary))
|
||||||
|
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ComponentPreview(state: TangemGlowRingStory) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp)),
|
||||||
|
) {
|
||||||
|
PreviewBackground(
|
||||||
|
background = state.background,
|
||||||
|
modifier = Modifier.matchParentSize(),
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 48.dp),
|
||||||
|
) {
|
||||||
|
TangemGlowRing(
|
||||||
|
modifier = Modifier.size(width = 200.dp, height = 120.dp),
|
||||||
|
variant = state.variant,
|
||||||
|
animated = state.isAnimated,
|
||||||
|
quality = state.quality,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VariantSelector(selected: TangemGlowRing.Variant, onSelect: (TangemGlowRing.Variant) -> Unit) {
|
||||||
|
Section(label = "Variant") {
|
||||||
|
ChipGrid(
|
||||||
|
items = TangemGlowRing.Variant.entries,
|
||||||
|
label = { it.name },
|
||||||
|
isSelected = { it == selected },
|
||||||
|
onSelect = onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QualitySelector(selected: TangemGlowRing.Quality, onSelect: (TangemGlowRing.Quality) -> Unit) {
|
||||||
|
Section(label = "Quality (renderer)") {
|
||||||
|
ChipGrid(
|
||||||
|
items = TangemGlowRing.Quality.entries,
|
||||||
|
label = { it.name },
|
||||||
|
isSelected = { it == selected },
|
||||||
|
onSelect = onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BackgroundSelector(selected: Background, onSelect: (Background) -> Unit) {
|
||||||
|
Section(label = "Background") {
|
||||||
|
ChipGrid(
|
||||||
|
items = Background.entries,
|
||||||
|
label = { it.label },
|
||||||
|
isSelected = { it == selected },
|
||||||
|
onSelect = onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Toggles(state: TangemGlowRingStory) {
|
||||||
|
Section(label = "Flags") {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
ToggleRow(label = "animated", checked = state.isAnimated, onToggle = state.onAnimatedToggle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Section(label: String, content: @Composable () -> Unit) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.subtitle1,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
|
||||||
|
val shape = RoundedCornerShape(50)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(shape)
|
||||||
|
.background(TangemTheme.colors2.surface.level2)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = TangemTheme.colors2.border.neutral.secondary,
|
||||||
|
shape = shape,
|
||||||
|
)
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
items.forEach { item ->
|
||||||
|
Chip(
|
||||||
|
label = label(item),
|
||||||
|
selected = isSelected(item),
|
||||||
|
onClick = { onSelect(item) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Chip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
val chipShape = RoundedCornerShape(50)
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.clip(chipShape)
|
||||||
|
.background(
|
||||||
|
if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2,
|
||||||
|
)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(TangemTheme.colors2.surface.level2)
|
||||||
|
.clickable(onClick = onToggle)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.body2,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (checked) "ON" else "OFF",
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,6 +42,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.button.TangemBut
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.checkbox.TangemCheckboxV2Story
|
import com.tangem.feature.tester.presentation.storybook.page.ds.checkbox.TangemCheckboxV2Story
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.TangemCheckmarkStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.TangemCheckmarkStory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeStory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.page.ds.glowring.TangemGlowRingStory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.search.TangemSearchStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.search.TangemSearchStory
|
||||||
|
|
@ -96,6 +97,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
|
||||||
is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState)
|
is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState)
|
||||||
is TangemCheckboxV2Story -> TangemCheckboxV2Story(state = storyState)
|
is TangemCheckboxV2Story -> TangemCheckboxV2Story(state = storyState)
|
||||||
is TangemCheckmarkStory -> TangemCheckmarkStory(state = storyState)
|
is TangemCheckmarkStory -> TangemCheckmarkStory(state = storyState)
|
||||||
|
is TangemGlowRingStory -> TangemGlowRingStory(state = storyState)
|
||||||
is TangemRowStory -> TangemRowStory(state = storyState)
|
is TangemRowStory -> TangemRowStory(state = storyState)
|
||||||
is TangemSearchStory -> TangemSearchStory(state = storyState)
|
is TangemSearchStory -> TangemSearchStory(state = storyState)
|
||||||
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
|
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue