Files
radiola-android/app/src/main/java/com/radiola/ui/theme/Motion.kt
nk a614ac3764 feat(ui): дизайн-система radiOLA — палитра, тема, типографика, бренд, motion, pill таб-бар
- цветовые токены тёмно-зелёной темы + RadiolaColors (CompositionLocal)
- darkColorScheme + всегда тёмная тема, фирменные shapes
- типографика с весами/размерами под макет
- Brand: AppMark (градиентный R), RadiolaWordmark, MonoMark
- Motion: спеки движения, pressScale, живой эквалайзер
- pill-таб-бар с анимированной активной вкладкой
2026-06-02 21:13:27 +03:00

103 lines
3.7 KiB
Kotlin

package com.radiola.ui.theme
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import kotlin.math.abs
/** Стандартные длительности и кривые движения приложения. */
object Motion {
const val Fast = 120
const val Medium = 220
const val Slow = 360
fun <T> snappy() = spring<T>(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow
)
fun <T> gentle() = tween<T>(durationMillis = Medium, easing = FastOutSlowInEasing)
}
/** Лёгкое нажатие: плавное уменьшение масштаба, пока палец на элементе. */
@Composable
fun Modifier.pressScale(
pressedScale: Float = 0.94f,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
): Modifier {
val pressed by interactionSource.collectIsPressedAsState()
val scale by androidx.compose.animation.core.animateFloatAsState(
targetValue = if (pressed) pressedScale else 1f,
animationSpec = tween(Motion.Fast, easing = FastOutSlowInEasing),
label = "pressScale"
)
return this.graphicsLayer {
scaleX = scale
scaleY = scale
}
}
/**
* Живой эквалайзер для прямого эфира — декоративный, без перемотки.
* Полоски плавно «дышат» при воспроизведении.
*/
@Composable
fun LiveEqualizer(
modifier: Modifier = Modifier,
barCount: Int = 36,
color: Color = Accent,
playing: Boolean = true
) {
val transition = rememberInfiniteTransition(label = "eq")
val phase by transition.animateFloat(
initialValue = 0f,
targetValue = (Math.PI * 2).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(1400, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "eqPhase"
)
Canvas(modifier = modifier) {
val gap = 3.dp.toPx()
val barWidth = (size.width - gap * (barCount - 1)) / barCount
val maxH = size.height
for (i in 0 until barCount) {
val seed = (i * 0.7f)
val wave = if (playing) {
0.45f + 0.55f * abs(kotlin.math.sin(phase + seed))
} else 0.25f
val h = maxH * wave
val x = i * (barWidth + gap)
val y = (maxH - h) / 2f
drawRoundRect(
color = color,
topLeft = Offset(x, y),
size = Size(barWidth, h),
cornerRadius = CornerRadius(barWidth / 2f, barWidth / 2f)
)
}
}
}